@dxos/react-ui-form 0.9.0 → 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 (372) hide show
  1. package/dist/lib/browser/index.mjs +2337 -1919
  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 +2337 -1919
  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 +3 -3
  10. package/dist/types/src/components/FieldEditor/FieldEditor.d.ts.map +1 -1
  11. package/dist/types/src/components/Form/Form.d.ts +23 -110
  12. package/dist/types/src/components/Form/Form.d.ts.map +1 -1
  13. package/dist/types/src/components/Form/Form.stories.d.ts +48 -101
  14. package/dist/types/src/components/Form/Form.stories.d.ts.map +1 -1
  15. package/dist/types/src/components/Form/Form.theme.d.ts +252 -0
  16. package/dist/types/src/components/Form/Form.theme.d.ts.map +1 -0
  17. package/dist/types/src/components/Form/Form.theme.test.d.ts +2 -0
  18. package/dist/types/src/components/Form/Form.theme.test.d.ts.map +1 -0
  19. package/dist/types/src/components/Form/FormControls.d.ts +75 -0
  20. package/dist/types/src/components/Form/FormControls.d.ts.map +1 -0
  21. package/dist/types/src/components/Form/FormField/FormField.d.ts +44 -0
  22. package/dist/types/src/components/Form/FormField/FormField.d.ts.map +1 -0
  23. package/dist/types/src/components/Form/FormField/FormFieldHeader.d.ts +27 -0
  24. package/dist/types/src/components/Form/FormField/FormFieldHeader.d.ts.map +1 -0
  25. package/dist/types/src/components/Form/FormField/FormRow.d.ts +85 -0
  26. package/dist/types/src/components/Form/FormField/FormRow.d.ts.map +1 -0
  27. package/dist/types/src/components/Form/{FormFieldComponent.stories.d.ts → FormField/FormRow.stories.d.ts} +6 -3
  28. package/dist/types/src/components/Form/FormField/FormRow.stories.d.ts.map +1 -0
  29. package/dist/types/src/components/Form/{fields → FormField/fields/ArrayField}/ArrayField.d.ts +2 -2
  30. package/dist/types/src/components/Form/FormField/fields/ArrayField/ArrayField.d.ts.map +1 -0
  31. package/dist/types/src/components/Form/FormField/fields/ArrayField/ArrayField.stories.d.ts +88 -0
  32. package/dist/types/src/components/Form/FormField/fields/ArrayField/ArrayField.stories.d.ts.map +1 -0
  33. package/dist/types/src/components/Form/FormField/fields/ArrayField/index.d.ts +2 -0
  34. package/dist/types/src/components/Form/FormField/fields/ArrayField/index.d.ts.map +1 -0
  35. package/dist/types/src/components/Form/FormField/fields/AsyncSelectField/AsyncSelectField.d.ts +12 -0
  36. package/dist/types/src/components/Form/FormField/fields/AsyncSelectField/AsyncSelectField.d.ts.map +1 -0
  37. package/dist/types/src/components/Form/FormField/fields/AsyncSelectField/index.d.ts +2 -0
  38. package/dist/types/src/components/Form/FormField/fields/AsyncSelectField/index.d.ts.map +1 -0
  39. package/dist/types/src/components/Form/FormField/fields/AutofillField/AutofillField.d.ts +12 -0
  40. package/dist/types/src/components/Form/FormField/fields/AutofillField/AutofillField.d.ts.map +1 -0
  41. package/dist/types/src/components/Form/FormField/fields/AutofillField/index.d.ts +2 -0
  42. package/dist/types/src/components/Form/FormField/fields/AutofillField/index.d.ts.map +1 -0
  43. package/dist/types/src/components/Form/FormField/fields/BooleanField/BooleanField.d.ts +4 -0
  44. package/dist/types/src/components/Form/FormField/fields/BooleanField/BooleanField.d.ts.map +1 -0
  45. package/dist/types/src/components/{Settings/Settings.stories.d.ts → Form/FormField/fields/BooleanField/BooleanField.stories.d.ts} +1 -2
  46. package/dist/types/src/components/Form/FormField/fields/BooleanField/BooleanField.stories.d.ts.map +1 -0
  47. package/dist/types/src/components/Form/FormField/fields/BooleanField/index.d.ts +2 -0
  48. package/dist/types/src/components/Form/FormField/fields/BooleanField/index.d.ts.map +1 -0
  49. package/dist/types/src/components/Form/FormField/fields/ComboboxField/ComboboxField.d.ts +12 -0
  50. package/dist/types/src/components/Form/FormField/fields/ComboboxField/ComboboxField.d.ts.map +1 -0
  51. package/dist/types/src/components/Form/FormField/fields/ComboboxField/index.d.ts +2 -0
  52. package/dist/types/src/components/Form/FormField/fields/ComboboxField/index.d.ts.map +1 -0
  53. package/dist/types/src/components/Form/FormField/fields/DateField/DateField.d.ts +4 -0
  54. package/dist/types/src/components/Form/FormField/fields/DateField/DateField.d.ts.map +1 -0
  55. package/dist/types/src/components/Form/FormField/fields/DateField/DateField.stories.d.ts +88 -0
  56. package/dist/types/src/components/Form/FormField/fields/DateField/DateField.stories.d.ts.map +1 -0
  57. package/dist/types/src/components/Form/FormField/fields/DateField/index.d.ts +2 -0
  58. package/dist/types/src/components/Form/FormField/fields/DateField/index.d.ts.map +1 -0
  59. package/dist/types/src/components/Form/FormField/fields/GeoPointField/GeoPointField.d.ts +5 -0
  60. package/dist/types/src/components/Form/FormField/fields/GeoPointField/GeoPointField.d.ts.map +1 -0
  61. package/dist/types/src/components/Form/FormField/fields/GeoPointField/GeoPointField.stories.d.ts +88 -0
  62. package/dist/types/src/components/Form/FormField/fields/GeoPointField/GeoPointField.stories.d.ts.map +1 -0
  63. package/dist/types/src/components/Form/FormField/fields/GeoPointField/index.d.ts +2 -0
  64. package/dist/types/src/components/Form/FormField/fields/GeoPointField/index.d.ts.map +1 -0
  65. package/dist/types/src/components/Form/{fields → FormField/fields/MarkdownField}/MarkdownField.d.ts +2 -2
  66. package/dist/types/src/components/Form/FormField/fields/MarkdownField/MarkdownField.d.ts.map +1 -0
  67. package/dist/types/src/components/Form/FormField/fields/MarkdownField/MarkdownField.stories.d.ts.map +1 -0
  68. package/dist/types/src/components/Form/FormField/fields/MarkdownField/index.d.ts +2 -0
  69. package/dist/types/src/components/Form/FormField/fields/MarkdownField/index.d.ts.map +1 -0
  70. package/dist/types/src/components/Form/FormField/fields/NumberField/NumberField.d.ts +15 -0
  71. package/dist/types/src/components/Form/FormField/fields/NumberField/NumberField.d.ts.map +1 -0
  72. package/dist/types/src/components/Form/FormField/fields/NumberField/NumberField.stories.d.ts +88 -0
  73. package/dist/types/src/components/Form/FormField/fields/NumberField/NumberField.stories.d.ts.map +1 -0
  74. package/dist/types/src/components/Form/FormField/fields/NumberField/NumberField.test.d.ts +2 -0
  75. package/dist/types/src/components/Form/FormField/fields/NumberField/NumberField.test.d.ts.map +1 -0
  76. package/dist/types/src/components/Form/FormField/fields/NumberField/index.d.ts +2 -0
  77. package/dist/types/src/components/Form/FormField/fields/NumberField/index.d.ts.map +1 -0
  78. package/dist/types/src/components/Form/FormField/fields/PasswordField/PasswordField.d.ts +4 -0
  79. package/dist/types/src/components/Form/FormField/fields/PasswordField/PasswordField.d.ts.map +1 -0
  80. package/dist/types/src/components/Form/FormField/fields/PasswordField/PasswordField.stories.d.ts +88 -0
  81. package/dist/types/src/components/Form/FormField/fields/PasswordField/PasswordField.stories.d.ts.map +1 -0
  82. package/dist/types/src/components/Form/FormField/fields/PasswordField/index.d.ts +2 -0
  83. package/dist/types/src/components/Form/FormField/fields/PasswordField/index.d.ts.map +1 -0
  84. package/dist/types/src/components/Form/FormField/fields/RefField/InlineRefField.d.ts.map +1 -0
  85. package/dist/types/src/components/Form/FormField/fields/RefField/RefField.d.ts +16 -0
  86. package/dist/types/src/components/Form/FormField/fields/RefField/RefField.d.ts.map +1 -0
  87. package/dist/types/src/components/Form/FormField/fields/RefField/RefField.stories.d.ts +92 -0
  88. package/dist/types/src/components/Form/FormField/fields/RefField/RefField.stories.d.ts.map +1 -0
  89. package/dist/types/src/components/Form/FormField/fields/RefField/RefField.test.d.ts.map +1 -0
  90. package/dist/types/src/components/Form/FormField/fields/RefField/index.d.ts +3 -0
  91. package/dist/types/src/components/Form/FormField/fields/RefField/index.d.ts.map +1 -0
  92. package/dist/types/src/components/Form/FormField/fields/SelectField/SelectField.d.ts +29 -0
  93. package/dist/types/src/components/Form/FormField/fields/SelectField/SelectField.d.ts.map +1 -0
  94. package/dist/types/src/components/Form/FormField/fields/SelectField/SelectField.stories.d.ts +88 -0
  95. package/dist/types/src/components/Form/FormField/fields/SelectField/SelectField.stories.d.ts.map +1 -0
  96. package/dist/types/src/components/Form/FormField/fields/SelectField/index.d.ts +2 -0
  97. package/dist/types/src/components/Form/FormField/fields/SelectField/index.d.ts.map +1 -0
  98. package/dist/types/src/components/Form/FormField/fields/SelectOptionField/SelectOptionField.d.ts +5 -0
  99. package/dist/types/src/components/Form/FormField/fields/SelectOptionField/SelectOptionField.d.ts.map +1 -0
  100. package/dist/types/src/components/Form/FormField/fields/SelectOptionField/SelectOptionField.stories.d.ts.map +1 -0
  101. package/dist/types/src/components/Form/FormField/fields/SelectOptionField/index.d.ts +2 -0
  102. package/dist/types/src/components/Form/FormField/fields/SelectOptionField/index.d.ts.map +1 -0
  103. package/dist/types/src/components/Form/FormField/fields/TextAreaField/TextAreaField.d.ts +4 -0
  104. package/dist/types/src/components/Form/FormField/fields/TextAreaField/TextAreaField.d.ts.map +1 -0
  105. package/dist/types/src/components/Form/FormField/fields/TextAreaField/TextAreaField.stories.d.ts +88 -0
  106. package/dist/types/src/components/Form/FormField/fields/TextAreaField/TextAreaField.stories.d.ts.map +1 -0
  107. package/dist/types/src/components/Form/FormField/fields/TextAreaField/index.d.ts +2 -0
  108. package/dist/types/src/components/Form/FormField/fields/TextAreaField/index.d.ts.map +1 -0
  109. package/dist/types/src/components/Form/FormField/fields/TextField/TextField.d.ts +4 -0
  110. package/dist/types/src/components/Form/FormField/fields/TextField/TextField.d.ts.map +1 -0
  111. package/dist/types/src/components/Form/FormField/fields/TextField/TextField.stories.d.ts +88 -0
  112. package/dist/types/src/components/Form/FormField/fields/TextField/TextField.stories.d.ts.map +1 -0
  113. package/dist/types/src/components/Form/FormField/fields/TextField/index.d.ts +2 -0
  114. package/dist/types/src/components/Form/FormField/fields/TextField/index.d.ts.map +1 -0
  115. package/dist/types/src/components/Form/{fields → FormField/fields/TupleField}/TupleField.d.ts +2 -2
  116. package/dist/types/src/components/Form/FormField/fields/TupleField/TupleField.d.ts.map +1 -0
  117. package/dist/types/src/components/Form/FormField/fields/TupleField/TupleField.stories.d.ts +88 -0
  118. package/dist/types/src/components/Form/FormField/fields/TupleField/TupleField.stories.d.ts.map +1 -0
  119. package/dist/types/src/components/Form/FormField/fields/TupleField/index.d.ts +2 -0
  120. package/dist/types/src/components/Form/FormField/fields/TupleField/index.d.ts.map +1 -0
  121. package/dist/types/src/components/Form/{fields → FormField/fields}/index.d.ts +4 -1
  122. package/dist/types/src/components/Form/FormField/fields/index.d.ts.map +1 -0
  123. package/dist/types/src/components/Form/FormField/index.d.ts +6 -0
  124. package/dist/types/src/components/Form/FormField/index.d.ts.map +1 -0
  125. package/dist/types/src/components/Form/FormField/presentation.d.ts +22 -0
  126. package/dist/types/src/components/Form/FormField/presentation.d.ts.map +1 -0
  127. package/dist/types/src/components/Form/FormFieldSet/FormFieldSet.d.ts +30 -0
  128. package/dist/types/src/components/Form/FormFieldSet/FormFieldSet.d.ts.map +1 -0
  129. package/dist/types/src/components/Form/FormFieldSet/FormFieldSetContainer.d.ts +22 -0
  130. package/dist/types/src/components/Form/FormFieldSet/FormFieldSetContainer.d.ts.map +1 -0
  131. package/dist/types/src/components/Form/FormFieldSet/index.d.ts +3 -0
  132. package/dist/types/src/components/Form/FormFieldSet/index.d.ts.map +1 -0
  133. package/dist/types/src/components/Form/{Layout → FormLayout}/FormLayout.d.ts +2 -0
  134. package/dist/types/src/components/Form/FormLayout/FormLayout.d.ts.map +1 -0
  135. package/dist/types/src/components/Form/{Layout → FormLayout}/FormLayout.stories.d.ts +5 -5
  136. package/dist/types/src/components/Form/FormLayout/FormLayout.stories.d.ts.map +1 -0
  137. package/dist/types/src/components/Form/FormLayout/FormLayout.stories.test.d.ts.map +1 -0
  138. package/dist/types/src/components/Form/FormLayout/FormLayout.test.d.ts.map +1 -0
  139. package/dist/types/src/components/Form/FormLayout/index.d.ts +3 -0
  140. package/dist/types/src/components/Form/FormLayout/index.d.ts.map +1 -0
  141. package/dist/types/src/components/Form/FormLayout/parser.d.ts.map +1 -0
  142. package/dist/types/src/components/Form/FormLayout/parser.test.d.ts.map +1 -0
  143. package/dist/types/src/components/Form/index.d.ts +3 -5
  144. package/dist/types/src/components/Form/index.d.ts.map +1 -1
  145. package/dist/types/src/components/Form/meta-tags.d.ts +1 -12
  146. package/dist/types/src/components/Form/meta-tags.d.ts.map +1 -1
  147. package/dist/types/src/components/ObjectForm/ObjectForm.d.ts.map +1 -1
  148. package/dist/types/src/components/ObjectPicker/ObjectPicker.d.ts +6 -23
  149. package/dist/types/src/components/ObjectPicker/ObjectPicker.d.ts.map +1 -1
  150. package/dist/types/src/components/ObjectProperties/ObjectProperties.d.ts +2 -1
  151. package/dist/types/src/components/ObjectProperties/ObjectProperties.d.ts.map +1 -1
  152. package/dist/types/src/components/RefEditor/RefEditor.stories.d.ts +2 -2
  153. package/dist/types/src/components/RefEditor/RefEditor.stories.d.ts.map +1 -1
  154. package/dist/types/src/components/ViewEditor/ViewEditor.d.ts +1 -1
  155. package/dist/types/src/components/ViewEditor/ViewEditor.d.ts.map +1 -1
  156. package/dist/types/src/components/ViewEditor/ViewEditor.stories.d.ts +2 -2
  157. package/dist/types/src/components/ViewEditor/ViewEditor.stories.d.ts.map +1 -1
  158. package/dist/types/src/components/index.d.ts +0 -1
  159. package/dist/types/src/components/index.d.ts.map +1 -1
  160. package/dist/types/src/hooks/index.d.ts +2 -0
  161. package/dist/types/src/hooks/index.d.ts.map +1 -1
  162. package/dist/types/src/hooks/useAsyncFieldEffect.d.ts +24 -0
  163. package/dist/types/src/hooks/useAsyncFieldEffect.d.ts.map +1 -0
  164. package/dist/types/src/hooks/useFormContext.d.ts +42 -0
  165. package/dist/types/src/hooks/useFormContext.d.ts.map +1 -0
  166. package/dist/types/src/hooks/useFormHandler.d.ts +9 -6
  167. package/dist/types/src/hooks/useFormHandler.d.ts.map +1 -1
  168. package/dist/types/src/index.d.ts +2 -0
  169. package/dist/types/src/index.d.ts.map +1 -1
  170. package/dist/types/src/{components/testing.d.ts → testing/TestLayout.d.ts} +2 -2
  171. package/dist/types/src/testing/TestLayout.d.ts.map +1 -0
  172. package/dist/types/src/testing/index.d.ts +3 -0
  173. package/dist/types/src/testing/index.d.ts.map +1 -0
  174. package/dist/types/src/testing/schema.d.ts +40 -0
  175. package/dist/types/src/testing/schema.d.ts.map +1 -0
  176. package/dist/types/src/types.d.ts +161 -0
  177. package/dist/types/src/types.d.ts.map +1 -0
  178. package/dist/types/src/util/index.d.ts +1 -1
  179. package/dist/types/src/util/index.d.ts.map +1 -1
  180. package/dist/types/src/util/omit.d.ts +12 -0
  181. package/dist/types/src/util/omit.d.ts.map +1 -0
  182. package/dist/types/tsconfig.tsbuildinfo +1 -1
  183. package/package.json +43 -41
  184. package/src/annotations.ts +63 -0
  185. package/src/components/FieldEditor/FieldEditor.stories.tsx +1 -1
  186. package/src/components/FieldEditor/FieldEditor.test.tsx +1 -1
  187. package/src/components/FieldEditor/FieldEditor.tsx +10 -6
  188. package/src/components/Form/DESIGN.md +24 -0
  189. package/src/components/Form/Form.stories.tsx +260 -61
  190. package/src/components/Form/Form.theme.test.ts +33 -0
  191. package/src/components/Form/Form.theme.ts +86 -0
  192. package/src/components/Form/Form.tsx +30 -453
  193. package/src/components/Form/FormControls.tsx +377 -0
  194. package/src/components/Form/{FormField.tsx → FormField/FormField.tsx} +98 -71
  195. package/src/components/Form/FormField/FormFieldHeader.tsx +68 -0
  196. package/src/components/Form/FormField/FormRow.stories.tsx +97 -0
  197. package/src/components/Form/FormField/FormRow.tsx +298 -0
  198. package/src/components/Form/FormField/fields/ArrayField/ArrayField.stories.tsx +119 -0
  199. package/src/components/Form/FormField/fields/ArrayField/ArrayField.tsx +328 -0
  200. package/src/components/Form/FormField/fields/ArrayField/index.ts +5 -0
  201. package/src/components/Form/FormField/fields/AsyncSelectField/AsyncSelectField.tsx +59 -0
  202. package/src/components/Form/FormField/fields/AsyncSelectField/index.ts +5 -0
  203. package/src/components/Form/FormField/fields/AutofillField/AutofillField.tsx +67 -0
  204. package/src/components/Form/FormField/fields/AutofillField/index.ts +5 -0
  205. package/src/components/Form/FormField/fields/BooleanField/BooleanField.stories.tsx +52 -0
  206. package/src/components/Form/FormField/fields/BooleanField/BooleanField.tsx +28 -0
  207. package/src/components/Form/FormField/fields/BooleanField/index.ts +5 -0
  208. package/src/components/Form/FormField/fields/ComboboxField/ComboboxField.tsx +104 -0
  209. package/src/components/Form/FormField/fields/ComboboxField/index.ts +5 -0
  210. package/src/components/Form/FormField/fields/DateField/DateField.stories.tsx +60 -0
  211. package/src/components/Form/{fields → FormField/fields/DateField}/DateField.tsx +12 -10
  212. package/src/components/Form/FormField/fields/DateField/index.ts +5 -0
  213. package/src/components/Form/FormField/fields/GeoPointField/GeoPointField.stories.tsx +53 -0
  214. package/src/components/Form/{fields → FormField/fields/GeoPointField}/GeoPointField.tsx +19 -21
  215. package/src/components/Form/FormField/fields/GeoPointField/index.ts +5 -0
  216. package/src/components/Form/{fields → FormField/fields/MarkdownField}/MarkdownField.stories.tsx +8 -8
  217. package/src/components/Form/{fields → FormField/fields/MarkdownField}/MarkdownField.tsx +41 -41
  218. package/src/components/Form/FormField/fields/MarkdownField/index.ts +5 -0
  219. package/src/components/Form/FormField/fields/NumberField/NumberField.stories.tsx +52 -0
  220. package/src/components/Form/FormField/fields/NumberField/NumberField.test.ts +29 -0
  221. package/src/components/Form/FormField/fields/NumberField/NumberField.tsx +140 -0
  222. package/src/components/Form/FormField/fields/NumberField/index.ts +5 -0
  223. package/src/components/Form/FormField/fields/PasswordField/PasswordField.stories.tsx +53 -0
  224. package/src/components/Form/FormField/fields/PasswordField/PasswordField.tsx +41 -0
  225. package/src/components/Form/FormField/fields/PasswordField/index.ts +5 -0
  226. package/src/components/Form/{fields → FormField/fields/RefField}/InlineRefField.tsx +21 -14
  227. package/src/components/Form/FormField/fields/RefField/RefField.stories.tsx +97 -0
  228. package/src/components/Form/{fields → FormField/fields/RefField}/RefField.test.ts +10 -1
  229. package/src/components/Form/{fields → FormField/fields/RefField}/RefField.tsx +55 -47
  230. package/src/components/Form/FormField/fields/RefField/index.ts +6 -0
  231. package/src/components/Form/FormField/fields/SelectField/SelectField.stories.tsx +53 -0
  232. package/src/components/Form/FormField/fields/SelectField/SelectField.tsx +136 -0
  233. package/src/components/Form/FormField/fields/SelectField/index.ts +5 -0
  234. package/src/components/Form/{fields → FormField/fields/SelectOptionField}/SelectOptionField.stories.tsx +5 -5
  235. package/src/components/Form/{fields → FormField/fields/SelectOptionField}/SelectOptionField.tsx +56 -54
  236. package/src/components/Form/FormField/fields/SelectOptionField/index.ts +5 -0
  237. package/src/components/Form/FormField/fields/TextAreaField/TextAreaField.stories.tsx +53 -0
  238. package/src/components/Form/{fields → FormField/fields/TextAreaField}/TextAreaField.tsx +6 -4
  239. package/src/components/Form/FormField/fields/TextAreaField/index.ts +5 -0
  240. package/src/components/Form/FormField/fields/TextField/TextField.stories.tsx +52 -0
  241. package/src/components/Form/{fields → FormField/fields/TextField}/TextField.tsx +6 -4
  242. package/src/components/Form/FormField/fields/TextField/index.ts +5 -0
  243. package/src/components/Form/FormField/fields/TupleField/TupleField.stories.tsx +55 -0
  244. package/src/components/Form/{fields → FormField/fields/TupleField}/TupleField.tsx +4 -2
  245. package/src/components/Form/FormField/fields/TupleField/index.ts +5 -0
  246. package/src/components/Form/{fields → FormField/fields}/index.ts +4 -1
  247. package/src/components/Form/FormField/index.ts +10 -0
  248. package/src/components/Form/FormField/presentation.tsx +32 -0
  249. package/src/components/Form/{FormFieldSet.tsx → FormFieldSet/FormFieldSet.tsx} +53 -90
  250. package/src/components/Form/FormFieldSet/FormFieldSetContainer.tsx +93 -0
  251. package/src/components/Form/FormFieldSet/index.ts +6 -0
  252. package/src/components/Form/{Layout → FormLayout}/FormLayout.stories.tsx +11 -9
  253. package/src/components/Form/{Layout → FormLayout}/FormLayout.tsx +29 -18
  254. package/src/components/Form/FormLayout/index.ts +6 -0
  255. package/src/components/Form/index.ts +3 -5
  256. package/src/components/Form/meta-tags.test.ts +1 -1
  257. package/src/components/Form/meta-tags.ts +1 -1
  258. package/src/components/ObjectForm/ObjectForm.tsx +15 -14
  259. package/src/components/ObjectPicker/ObjectPicker.tsx +20 -27
  260. package/src/components/ObjectProperties/ObjectProperties.stories.tsx +2 -2
  261. package/src/components/ObjectProperties/ObjectProperties.test.tsx +1 -1
  262. package/src/components/ObjectProperties/ObjectProperties.tsx +13 -8
  263. package/src/components/RefEditor/RefEditor.stories.tsx +2 -3
  264. package/src/components/RefEditor/RefEditor.tsx +1 -1
  265. package/src/components/ViewEditor/ViewEditor.stories.tsx +6 -7
  266. package/src/components/ViewEditor/ViewEditor.test.tsx +9 -3
  267. package/src/components/ViewEditor/ViewEditor.tsx +98 -128
  268. package/src/components/index.ts +0 -1
  269. package/src/hooks/index.ts +2 -0
  270. package/src/hooks/useAsyncFieldEffect.ts +81 -0
  271. package/src/hooks/useFormContext.ts +89 -0
  272. package/src/hooks/useFormHandler.ts +12 -10
  273. package/src/index.ts +2 -0
  274. package/src/{components/testing.tsx → testing/TestLayout.tsx} +16 -13
  275. package/src/testing/index.ts +7 -0
  276. package/src/testing/schema.ts +59 -0
  277. package/src/types.ts +186 -0
  278. package/src/util/index.ts +1 -1
  279. package/src/util/omit.ts +61 -0
  280. package/src/util/properties.test.ts +110 -28
  281. package/src/util/refs.ts +1 -1
  282. package/src/vitest-setup.ts +40 -0
  283. package/dist/types/src/components/Form/FormField.d.ts +0 -52
  284. package/dist/types/src/components/Form/FormField.d.ts.map +0 -1
  285. package/dist/types/src/components/Form/FormFieldComponent.d.ts +0 -98
  286. package/dist/types/src/components/Form/FormFieldComponent.d.ts.map +0 -1
  287. package/dist/types/src/components/Form/FormFieldComponent.stories.d.ts.map +0 -1
  288. package/dist/types/src/components/Form/FormFieldSet.d.ts +0 -26
  289. package/dist/types/src/components/Form/FormFieldSet.d.ts.map +0 -1
  290. package/dist/types/src/components/Form/FormTooltipsContext.d.ts +0 -19
  291. package/dist/types/src/components/Form/FormTooltipsContext.d.ts.map +0 -1
  292. package/dist/types/src/components/Form/Layout/FormLayout.d.ts.map +0 -1
  293. package/dist/types/src/components/Form/Layout/FormLayout.stories.d.ts.map +0 -1
  294. package/dist/types/src/components/Form/Layout/FormLayout.stories.test.d.ts.map +0 -1
  295. package/dist/types/src/components/Form/Layout/FormLayout.test.d.ts.map +0 -1
  296. package/dist/types/src/components/Form/Layout/annotation.d.ts +0 -30
  297. package/dist/types/src/components/Form/Layout/annotation.d.ts.map +0 -1
  298. package/dist/types/src/components/Form/Layout/index.d.ts +0 -4
  299. package/dist/types/src/components/Form/Layout/index.d.ts.map +0 -1
  300. package/dist/types/src/components/Form/Layout/parser.d.ts.map +0 -1
  301. package/dist/types/src/components/Form/Layout/parser.test.d.ts.map +0 -1
  302. package/dist/types/src/components/Form/fields/ArrayField.d.ts.map +0 -1
  303. package/dist/types/src/components/Form/fields/BooleanField.d.ts +0 -4
  304. package/dist/types/src/components/Form/fields/BooleanField.d.ts.map +0 -1
  305. package/dist/types/src/components/Form/fields/DateField.d.ts +0 -4
  306. package/dist/types/src/components/Form/fields/DateField.d.ts.map +0 -1
  307. package/dist/types/src/components/Form/fields/GeoPointField.d.ts +0 -5
  308. package/dist/types/src/components/Form/fields/GeoPointField.d.ts.map +0 -1
  309. package/dist/types/src/components/Form/fields/InlineRefField.d.ts.map +0 -1
  310. package/dist/types/src/components/Form/fields/MarkdownField.d.ts.map +0 -1
  311. package/dist/types/src/components/Form/fields/MarkdownField.stories.d.ts.map +0 -1
  312. package/dist/types/src/components/Form/fields/NumberField.d.ts +0 -4
  313. package/dist/types/src/components/Form/fields/NumberField.d.ts.map +0 -1
  314. package/dist/types/src/components/Form/fields/RefField.d.ts +0 -34
  315. package/dist/types/src/components/Form/fields/RefField.d.ts.map +0 -1
  316. package/dist/types/src/components/Form/fields/RefField.test.d.ts.map +0 -1
  317. package/dist/types/src/components/Form/fields/SelectField.d.ts +0 -13
  318. package/dist/types/src/components/Form/fields/SelectField.d.ts.map +0 -1
  319. package/dist/types/src/components/Form/fields/SelectOptionField.d.ts +0 -5
  320. package/dist/types/src/components/Form/fields/SelectOptionField.d.ts.map +0 -1
  321. package/dist/types/src/components/Form/fields/SelectOptionField.stories.d.ts.map +0 -1
  322. package/dist/types/src/components/Form/fields/TextAreaField.d.ts +0 -4
  323. package/dist/types/src/components/Form/fields/TextAreaField.d.ts.map +0 -1
  324. package/dist/types/src/components/Form/fields/TextField.d.ts +0 -4
  325. package/dist/types/src/components/Form/fields/TextField.d.ts.map +0 -1
  326. package/dist/types/src/components/Form/fields/TupleField.d.ts.map +0 -1
  327. package/dist/types/src/components/Form/fields/index.d.ts.map +0 -1
  328. package/dist/types/src/components/Settings/Settings.d.ts +0 -51
  329. package/dist/types/src/components/Settings/Settings.d.ts.map +0 -1
  330. package/dist/types/src/components/Settings/Settings.stories.d.ts.map +0 -1
  331. package/dist/types/src/components/Settings/SettingsFieldSet.d.ts +0 -43
  332. package/dist/types/src/components/Settings/SettingsFieldSet.d.ts.map +0 -1
  333. package/dist/types/src/components/Settings/SettingsItem.d.ts +0 -16
  334. package/dist/types/src/components/Settings/SettingsItem.d.ts.map +0 -1
  335. package/dist/types/src/components/Settings/SettingsSelectField.d.ts +0 -18
  336. package/dist/types/src/components/Settings/SettingsSelectField.d.ts.map +0 -1
  337. package/dist/types/src/components/Settings/index.d.ts +0 -4
  338. package/dist/types/src/components/Settings/index.d.ts.map +0 -1
  339. package/dist/types/src/components/testing.d.ts.map +0 -1
  340. package/dist/types/src/util/field-type.d.ts +0 -19
  341. package/dist/types/src/util/field-type.d.ts.map +0 -1
  342. package/src/components/Form/AUDIT.md +0 -17
  343. package/src/components/Form/FormFieldComponent.stories.tsx +0 -65
  344. package/src/components/Form/FormFieldComponent.tsx +0 -253
  345. package/src/components/Form/FormTooltipsContext.ts +0 -25
  346. package/src/components/Form/Layout/annotation.ts +0 -38
  347. package/src/components/Form/Layout/index.ts +0 -7
  348. package/src/components/Form/README.md +0 -3
  349. package/src/components/Form/fields/ArrayField.tsx +0 -173
  350. package/src/components/Form/fields/BooleanField.tsx +0 -22
  351. package/src/components/Form/fields/NumberField.tsx +0 -76
  352. package/src/components/Form/fields/SelectField.tsx +0 -77
  353. package/src/components/Settings/AUDIT.md +0 -51
  354. package/src/components/Settings/Settings.stories.tsx +0 -104
  355. package/src/components/Settings/Settings.tsx +0 -81
  356. package/src/components/Settings/SettingsFieldSet.tsx +0 -257
  357. package/src/components/Settings/SettingsItem.tsx +0 -58
  358. package/src/components/Settings/SettingsSelectField.tsx +0 -59
  359. package/src/components/Settings/index.ts +0 -7
  360. package/src/util/field-type.ts +0 -60
  361. /package/dist/types/src/components/Form/{fields → FormField/fields/MarkdownField}/MarkdownField.stories.d.ts +0 -0
  362. /package/dist/types/src/components/Form/{fields → FormField/fields/RefField}/InlineRefField.d.ts +0 -0
  363. /package/dist/types/src/components/Form/{fields → FormField/fields/RefField}/RefField.test.d.ts +0 -0
  364. /package/dist/types/src/components/Form/{fields → FormField/fields/SelectOptionField}/SelectOptionField.stories.d.ts +0 -0
  365. /package/dist/types/src/components/Form/{Layout → FormLayout}/FormLayout.stories.test.d.ts +0 -0
  366. /package/dist/types/src/components/Form/{Layout → FormLayout}/FormLayout.test.d.ts +0 -0
  367. /package/dist/types/src/components/Form/{Layout → FormLayout}/parser.d.ts +0 -0
  368. /package/dist/types/src/components/Form/{Layout → FormLayout}/parser.test.d.ts +0 -0
  369. /package/src/components/Form/{Layout → FormLayout}/FormLayout.stories.test.tsx +0 -0
  370. /package/src/components/Form/{Layout → FormLayout}/FormLayout.test.ts +0 -0
  371. /package/src/components/Form/{Layout → FormLayout}/parser.test.ts +0 -0
  372. /package/src/components/Form/{Layout → FormLayout}/parser.ts +0 -0
@@ -0,0 +1,328 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ import * as Option from 'effect/Option';
6
+ import * as SchemaAST from 'effect/SchemaAST';
7
+ import React, { type ReactNode, useCallback, useRef } from 'react';
8
+
9
+ import { Annotation, Ref } from '@dxos/echo';
10
+ import { useType as defaultUseType } from '@dxos/echo-react';
11
+ import { SchemaEx } from '@dxos/effect';
12
+ import { DXN } from '@dxos/keys';
13
+ import { log } from '@dxos/log';
14
+ import { useTranslation } from '@dxos/react-ui';
15
+ import { OrderedList } from '@dxos/react-ui-list';
16
+ import { mx } from '@dxos/ui-theme';
17
+ import { arrayMove } from '@dxos/util';
18
+
19
+ import { translationKey } from '#translations';
20
+ import { type FormFieldStateProps } from '#types';
21
+
22
+ import { useFormValues } from '../../../../../hooks';
23
+ import { getFormProperties } from '../../../../../util';
24
+ import { CompactIconButton, FormField, type FormFieldProps } from '../../FormField';
25
+ import { FormFieldHeader } from '../../FormFieldHeader';
26
+
27
+ // Synthetic id assigned to each row when rendering an ordered list. Plain form
28
+ // values have no stable identity, so drag-and-drop (which requires a stable key
29
+ // surviving the `onValuesChanged` round-trip) is given one here and kept in
30
+ // lockstep with the array via the add/delete/move handlers.
31
+ const DND_ID = '__dndId';
32
+
33
+ type OrderedItem = {
34
+ [DND_ID]: string;
35
+ value: unknown;
36
+ index: number;
37
+ };
38
+
39
+ const isOrderedItem = (item: any): item is OrderedItem =>
40
+ !!item && typeof item === 'object' && typeof item[DND_ID] === 'string';
41
+
42
+ const getOrderedId = (item: OrderedItem): string => item[DND_ID];
43
+
44
+ export type ArrayFieldProps = {
45
+ label: string;
46
+ fieldProps: FormFieldStateProps;
47
+ } & FormFieldProps;
48
+
49
+ export const ArrayField = ({
50
+ type,
51
+ path,
52
+ label,
53
+ readonly,
54
+ layout,
55
+ fieldProps: inputProps,
56
+ ...props
57
+ }: ArrayFieldProps) => {
58
+ const { t } = useTranslation(translationKey);
59
+ const elementType = SchemaEx.getArrayElementType(type);
60
+ const { onValueChange } = inputProps;
61
+ const values = useFormValues(ArrayField.displayName, path, () => []);
62
+
63
+ // Order is meaningful and user-controllable -> render as a drag-to-reorder list.
64
+ const ordered = Annotation.FormOrderedAnnotation.getFromAst(type).pipe(Option.getOrElse(() => false));
65
+
66
+ // Owned-ref array (`FormCreateAnnotation`): "add" creates a new target object of the annotation's typename,
67
+ // and each row renders that target inline (no existing-object picker).
68
+ const { db, onCreate, getCreateDefaults, useType = defaultUseType } = props;
69
+ const createTypename = Annotation.FormCreateAnnotation.getFromAst(type).pipe(Option.getOrUndefined);
70
+ const createSchema = useType(db, createTypename ? DXN.make(createTypename) : undefined);
71
+ const createInline =
72
+ !!createTypename && !!elementType && Ref.isRefType(elementType) && !readonly && layout !== 'static';
73
+
74
+ // Synthetic identity map (see `DND_ID`). The counter only advances when the
75
+ // array grows; reconciliation here covers the initial mount and any external
76
+ // length change, while the handlers below keep ids aligned on mutation.
77
+ const idsRef = useRef<string[]>([]);
78
+ const counterRef = useRef(0);
79
+ const nextId = useCallback(() => `${DND_ID}-${counterRef.current++}`, []);
80
+
81
+ // Create a new owned target object and append its ref (see `createInline`).
82
+ const handleAddCreate = useCallback(async () => {
83
+ if (!createSchema || !onCreate) {
84
+ return;
85
+ }
86
+ // Let the container pre-populate the new object (e.g. a back-reference to the parent).
87
+ const defaults = getCreateDefaults?.({ jsonPath: SchemaEx.createJsonPath(path ?? []), schema: createSchema }) ?? {};
88
+ const created = await onCreate(createSchema, defaults);
89
+ if (!created) {
90
+ return;
91
+ }
92
+ idsRef.current.push(nextId());
93
+ onValueChange(type, [...(values ?? []), Ref.make(created)]);
94
+ }, [createSchema, onCreate, getCreateDefaults, onValueChange, type, path, values, nextId]);
95
+
96
+ const getDefaultObjectValue = (typeNode: SchemaAST.AST): any => {
97
+ const baseNode = SchemaEx.findNode(typeNode, SchemaEx.isDiscriminatedUnion);
98
+ const typeLiteral = baseNode
99
+ ? SchemaEx.getDiscriminatedType(baseNode, {})
100
+ : SchemaEx.findNode(typeNode, SchemaAST.isTypeLiteral);
101
+ if (!typeLiteral) {
102
+ return {};
103
+ }
104
+
105
+ return Object.fromEntries(
106
+ getFormProperties(typeLiteral).map((prop) => {
107
+ const defaultValue = SchemaAST.getDefaultAnnotation(prop.type).pipe((annotation) =>
108
+ Option.getOrUndefined(annotation),
109
+ );
110
+ return [prop.name, defaultValue];
111
+ }),
112
+ );
113
+ };
114
+
115
+ const handleAdd = useCallback(() => {
116
+ let defaultValue;
117
+ try {
118
+ defaultValue =
119
+ elementType && SchemaEx.isNestedType(elementType)
120
+ ? getDefaultObjectValue(elementType)
121
+ : getDefaultValue(elementType);
122
+ } catch (err) {
123
+ // `getDefaultValue` throws on element types it can't seed (runs in this click handler, so an
124
+ // unguarded throw would be uncaught). Skip the add rather than crash the form.
125
+ log.catch(err);
126
+ return;
127
+ }
128
+ idsRef.current.push(nextId());
129
+ // `values` is `undefined` on first render for arrays whose parent path
130
+ // hasn't been materialised in the form values yet (e.g. `package.repos`
131
+ // when the form starts with no `package`). `useFormValues`'s default-
132
+ // value effect would eventually backfill it to `[]`, but until then the
133
+ // old `values && ...` guard silently swallowed the click. Treat a
134
+ // missing array as empty so Add always produces `[defaultValue]`.
135
+ onValueChange(type, [...(values ?? []), defaultValue]);
136
+ }, [onValueChange, type, elementType, values, nextId]);
137
+
138
+ const handleDelete = useCallback(
139
+ (idx: number) => {
140
+ idsRef.current.splice(idx, 1);
141
+ onValueChange(
142
+ type,
143
+ values?.filter((_, i) => i !== idx),
144
+ );
145
+ },
146
+ [onValueChange, type, values],
147
+ );
148
+
149
+ const handleMove = useCallback(
150
+ (fromIndex: number, toIndex: number) => {
151
+ const next = [...(values ?? [])];
152
+ arrayMove(next, fromIndex, toIndex);
153
+ arrayMove(idsRef.current, fromIndex, toIndex);
154
+ onValueChange(type, next);
155
+ },
156
+ [onValueChange, type, values],
157
+ );
158
+
159
+ if (!elementType || ((readonly || layout === 'static') && values && values.length < 1)) {
160
+ return null;
161
+ }
162
+
163
+ const renderItemAsObject = elementType && SchemaEx.isNestedType(elementType) && !Ref.isRefType(elementType);
164
+
165
+ // Object items: a recursively-rendered FormField (multiple sub-rows for the
166
+ // object's fields). Scalar items (refs, primitives): a single inline FormField.
167
+ const renderField = (index: number, isLast: boolean): ReactNode => {
168
+ const field = (
169
+ <FormField
170
+ {...props}
171
+ autoFocus={isLast}
172
+ type={elementType}
173
+ // Suppress the per-item header for object items and owned-ref items (both render their own
174
+ // sub-field labels). Scalar items (refs, primitives) take the array's resolved label (e.g. its
175
+ // `title` annotation, `Tags`) so inline-layout children (e.g. RefField) have a real label to use
176
+ // as a fallback placeholder, rather than re-deriving it from the raw array property name (`_tags`).
177
+ {...(renderItemAsObject || createInline ? { name: null } : { label })}
178
+ path={[...(path ?? []), index]}
179
+ readonly={readonly || layout === 'static'}
180
+ layout={renderItemAsObject ? (layout === 'static' ? 'static' : undefined) : 'inline'}
181
+ refInline={createInline || undefined}
182
+ />
183
+ );
184
+
185
+ // TODO(burdon): Hacky.
186
+ // An owned inline object form must establish its own column context: otherwise it inherits the parent
187
+ // form's `--dx-col` (center) placement and collapses into the array row's narrow second track.
188
+ return createInline ? (
189
+ <div role='none' className='w-full min-w-0 [--dx-col:auto]'>
190
+ {field}
191
+ </div>
192
+ ) : (
193
+ field
194
+ );
195
+ };
196
+
197
+ const header = (layout !== 'static' || (values && values.length > 0)) && (
198
+ <FormFieldHeader
199
+ label={label}
200
+ path={SchemaEx.createJsonPath(path ?? [])}
201
+ readonly={readonly}
202
+ add={
203
+ layout !== 'static'
204
+ ? { label: t('add-item.button'), onClick: createInline ? () => void handleAddCreate() : handleAdd }
205
+ : undefined
206
+ }
207
+ />
208
+ );
209
+
210
+ //
211
+ // Ordered (drag-to-reorder) list.
212
+ //
213
+
214
+ if (ordered && !readonly && layout !== 'static') {
215
+ // Reconcile synthetic ids with the current values (initial mount / external change).
216
+ const ids = idsRef.current;
217
+ const count = values?.length ?? 0;
218
+ while (ids.length < count) {
219
+ ids.push(nextId());
220
+ }
221
+ if (ids.length > count) {
222
+ ids.length = count;
223
+ }
224
+
225
+ const items: OrderedItem[] = (values ?? []).map((value, index) => ({ value, index, [DND_ID]: ids[index] }));
226
+
227
+ return (
228
+ <>
229
+ {header}
230
+ <OrderedList.Root<OrderedItem> items={items} isItem={isOrderedItem} getId={getOrderedId} onMove={handleMove}>
231
+ {({ items }) => (
232
+ <OrderedList.Content>
233
+ {items.map((item) => (
234
+ <OrderedList.Item<OrderedItem>
235
+ key={getOrderedId(item)}
236
+ id={getOrderedId(item)}
237
+ item={item}
238
+ classNames='grid grid-cols-[min-content_1fr_min-content] items-start gap-1 p-0 pb-1'
239
+ >
240
+ <OrderedList.DragHandle />
241
+ {renderField(item.index, item.index === items.length - 1)}
242
+ <CompactIconButton
243
+ icon='ph--x--regular'
244
+ label={t('remove-item.button')}
245
+ onClick={() => handleDelete(item.index)}
246
+ />
247
+ </OrderedList.Item>
248
+ ))}
249
+ </OrderedList.Content>
250
+ )}
251
+ </OrderedList.Root>
252
+ </>
253
+ );
254
+ }
255
+
256
+ //
257
+ // Static list.
258
+ //
259
+
260
+ return (
261
+ <>
262
+ {header}
263
+ <div className='flex flex-col gap-2'>
264
+ {values?.map((_, index) => {
265
+ const isLast = index === values.length - 1;
266
+ return (
267
+ <div
268
+ key={index}
269
+ className={mx(
270
+ 'grid grid-cols-[minmax(0,1fr)_min-content] gap-1',
271
+ renderItemAsObject || createInline ? 'items-start' : 'items-center',
272
+ )}
273
+ >
274
+ {renderField(index, isLast)}
275
+ {!readonly && layout !== 'static' && (
276
+ <CompactIconButton
277
+ icon='ph--x--regular'
278
+ label={t('remove-item.button')}
279
+ onClick={() => handleDelete(index)}
280
+ />
281
+ )}
282
+ </div>
283
+ );
284
+ })}
285
+ </div>
286
+ </>
287
+ );
288
+ };
289
+
290
+ ArrayField.displayName = 'Form.ArrayField';
291
+
292
+ /**
293
+ * Returns the default empty value for a given AST.
294
+ * Used for initializing new array values etc.
295
+ */
296
+ // TODO(wittjosiah): Factor out?
297
+ export const getDefaultValue = (ast?: SchemaAST.AST): any => {
298
+ switch (ast?._tag) {
299
+ case 'StringKeyword': {
300
+ return '';
301
+ }
302
+ case 'NumberKeyword': {
303
+ return 0;
304
+ }
305
+ case 'BooleanKeyword': {
306
+ return false;
307
+ }
308
+ case 'Suspend': {
309
+ return getDefaultValue(ast.f());
310
+ }
311
+ case 'Refinement': {
312
+ // Use minimum from JSON schema annotation (e.g. Schema.between(1, 31)) as the default
313
+ // so new array items start within the valid range.
314
+ const jsonSchema = Option.getOrUndefined(SchemaAST.getJSONSchemaAnnotation(ast));
315
+ if (jsonSchema != null && 'minimum' in jsonSchema && typeof jsonSchema.minimum === 'number') {
316
+ return jsonSchema.minimum;
317
+ }
318
+ return getDefaultValue(ast.from);
319
+ }
320
+ default: {
321
+ if (ast && SchemaEx.isNestedType(ast)) {
322
+ return {};
323
+ } else {
324
+ throw new Error(`Unsupported type: ${ast?._tag}`);
325
+ }
326
+ }
327
+ }
328
+ };
@@ -0,0 +1,5 @@
1
+ //
2
+ // Copyright 2024 DXOS.org
3
+ //
4
+
5
+ export * from './ArrayField';
@@ -0,0 +1,59 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ import React, { useEffect, useMemo, useRef } from 'react';
6
+
7
+ import { type AnyProperties } from '@dxos/echo/internal';
8
+
9
+ import { type FormFieldRendererProps } from '#types';
10
+
11
+ import { type OptionsLookup, type OptionsLookupEntry } from '../../../../../annotations';
12
+ import { pickValues, useAsyncFieldEffect, useFormValues } from '../../../../../hooks';
13
+ import { SelectField } from '../SelectField';
14
+
15
+ export type AsyncSelectFieldProps = FormFieldRendererProps<string> & {
16
+ /** Loads the selectable options from the lookup's declared dependency fields. */
17
+ lookup: OptionsLookup;
18
+ };
19
+
20
+ /**
21
+ * A select whose options are loaded dynamically (debounced) from the fields the lookup declares in its
22
+ * `deps`, via an {@link Annotation.OptionsLookup}. Re-runs only when one of those values changes.
23
+ * Auto-selects the sole option when exactly one resolves and nothing is chosen yet.
24
+ */
25
+ export const AsyncSelectField = ({ lookup, ...fieldProps }: AsyncSelectFieldProps) => {
26
+ const values = useFormValues<AnyProperties>(AsyncSelectField.displayName);
27
+ const subset = useMemo(() => pickValues(values, lookup.deps), [values, lookup.deps]);
28
+ const key = useMemo(() => JSON.stringify(subset), [subset]);
29
+ const { loading, data } = useAsyncFieldEffect<readonly OptionsLookupEntry[]>(() => lookup.load(subset), key);
30
+ const options = data ?? [];
31
+
32
+ const { type, placeholder } = fieldProps;
33
+ const current = fieldProps.getValue();
34
+ // `onValueChange` changes identity on every edit; hold it in a ref so auto-select depends only on the
35
+ // resolved `data` and current value (avoids an effect-refire loop).
36
+ const onValueChangeRef = useRef(fieldProps.onValueChange);
37
+ onValueChangeRef.current = fieldProps.onValueChange;
38
+ // Track which `data` reference has already been auto-selected so clearing the field does not trigger
39
+ // repeated re-selection of the same single option.
40
+ const autoSelectedDataRef = useRef<typeof data>(undefined);
41
+ useEffect(() => {
42
+ if ((current == null || current === '') && data?.length === 1 && data !== autoSelectedDataRef.current) {
43
+ autoSelectedDataRef.current = data;
44
+ onValueChangeRef.current(type, data[0].value);
45
+ }
46
+ }, [current, data, type]);
47
+
48
+ const resolvedPlaceholder = loading ? 'Loading…' : options.length === 0 ? 'No options available' : placeholder;
49
+
50
+ return (
51
+ <SelectField
52
+ {...fieldProps}
53
+ placeholder={resolvedPlaceholder}
54
+ options={options.map(({ value, label, secondaryLabel, icon }) => ({ value, label, secondaryLabel, icon }))}
55
+ />
56
+ );
57
+ };
58
+
59
+ AsyncSelectField.displayName = 'Form.AsyncSelectField';
@@ -0,0 +1,5 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ export * from './AsyncSelectField';
@@ -0,0 +1,67 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ import React, { useEffect, useMemo, useRef } from 'react';
6
+
7
+ import { type AnyProperties } from '@dxos/echo/internal';
8
+
9
+ import { type FormFieldRendererProps } from '#types';
10
+
11
+ import { type Autofill } from '../../../../../annotations';
12
+ import { pickValues, useAsyncFieldEffect, useFormValues } from '../../../../../hooks';
13
+ import { TextField } from '../TextField';
14
+
15
+ export type AutofillFieldProps = FormFieldRendererProps<string> & {
16
+ /** Derives this field's value from the autofill's declared dependency fields (e.g. a sibling URL). */
17
+ autofill: Autofill;
18
+ };
19
+
20
+ /**
21
+ * An editable text field that pre-fills its value (debounced) from the fields the autofill declares in
22
+ * its `deps`, via an {@link Autofill}. Re-runs only when one of those values changes. The
23
+ * derived value is written only while the user has not typed their own (the field is empty or still
24
+ * holds the last auto-filled value), so a manual edit is never clobbered.
25
+ */
26
+ export const AutofillField = ({ autofill, ...fieldProps }: AutofillFieldProps) => {
27
+ const values = useFormValues<AnyProperties>(AutofillField.displayName);
28
+ const subset = useMemo(() => pickValues(values, autofill.deps), [values, autofill.deps]);
29
+ const key = useMemo(() => JSON.stringify(subset), [subset]);
30
+ const { data } = useAsyncFieldEffect<string | undefined>(() => autofill.derive(subset), key);
31
+
32
+ // Read field state through refs so the write effect depends only on the derived `data` — `getValue`
33
+ // and `onValueChange` change identity on every value edit, and depending on them would re-fire the
34
+ // effect after it writes, re-applying the same value in an infinite loop.
35
+ const { type } = fieldProps;
36
+ const getValueRef = useRef(fieldProps.getValue);
37
+ getValueRef.current = fieldProps.getValue;
38
+ const onValueChangeRef = useRef(fieldProps.onValueChange);
39
+ onValueChangeRef.current = fieldProps.onValueChange;
40
+ const lastFilledRef = useRef<string | undefined>(undefined);
41
+
42
+ useEffect(() => {
43
+ const current = getValueRef.current();
44
+ if (typeof data !== 'string' || data.length === 0) {
45
+ // Derivation stopped returning a value — clear the field if it still holds our last fill so stale
46
+ // auto-filled text doesn't persist after the source field is cleared.
47
+ if (lastFilledRef.current != null && current === lastFilledRef.current) {
48
+ lastFilledRef.current = undefined;
49
+ onValueChangeRef.current(type, '');
50
+ }
51
+ return;
52
+ }
53
+ if (current === data) {
54
+ lastFilledRef.current = data;
55
+ return;
56
+ }
57
+ // Only fill while the user has not typed their own value (empty, or still our last fill).
58
+ if (current == null || current === '' || current === lastFilledRef.current) {
59
+ lastFilledRef.current = data;
60
+ onValueChangeRef.current(type, data);
61
+ }
62
+ }, [data, type]);
63
+
64
+ return <TextField {...fieldProps} />;
65
+ };
66
+
67
+ AutofillField.displayName = 'Form.AutofillField';
@@ -0,0 +1,5 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ export * from './AutofillField';
@@ -0,0 +1,52 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ import { type Meta, type StoryObj } from '@storybook/react-vite';
6
+ import * as Schema from 'effect/Schema';
7
+ import React, { useState } from 'react';
8
+
9
+ import { withLayout, withTheme } from '@dxos/react-ui/testing';
10
+
11
+ import { translations } from '#translations';
12
+
13
+ import { TestLayout } from '../../../../../testing';
14
+ import { Form } from '../../../Form';
15
+
16
+ const schema = Schema.Struct({
17
+ active: Schema.Boolean.annotations({ title: 'Active' }),
18
+ }).pipe(Schema.mutable);
19
+
20
+ type Values = Schema.Schema.Type<typeof schema>;
21
+
22
+ const DefaultStory = () => {
23
+ const [values, setValues] = useState<Values>({ active: true });
24
+ return (
25
+ <TestLayout json={values}>
26
+ <Form.Root
27
+ schema={schema}
28
+ values={values}
29
+ onValuesChanged={(values) => setValues((prev) => ({ ...prev, ...values }))}
30
+ >
31
+ <Form.Viewport>
32
+ <Form.Content>
33
+ <Form.FieldSet />
34
+ </Form.Content>
35
+ </Form.Viewport>
36
+ </Form.Root>
37
+ </TestLayout>
38
+ );
39
+ };
40
+
41
+ const meta = {
42
+ title: 'ui/react-ui-form/FormField/BooleanField',
43
+ render: DefaultStory,
44
+ decorators: [withTheme(), withLayout({ layout: 'fullscreen' })],
45
+ parameters: { layout: 'fullscreen', translations },
46
+ } satisfies Meta<typeof DefaultStory>;
47
+
48
+ export default meta;
49
+
50
+ type Story = StoryObj<typeof meta>;
51
+
52
+ export const Default: Story = {};
@@ -0,0 +1,28 @@
1
+ //
2
+ // Copyright 2024 DXOS.org
3
+ //
4
+
5
+ import React, { useCallback } from 'react';
6
+
7
+ import { Input, type SwitchProps } from '@dxos/react-ui';
8
+
9
+ import { type FormFieldRendererProps } from '#types';
10
+
11
+ import { FormRow } from '../../FormRow';
12
+
13
+ export const BooleanField = ({ type, readonly, onValueChange, ...props }: FormFieldRendererProps<boolean>) => {
14
+ const handleChange = useCallback<NonNullable<SwitchProps['onCheckedChange']>>(
15
+ (value) => onValueChange?.(type, value),
16
+ [type, onValueChange],
17
+ );
18
+
19
+ return (
20
+ <FormRow<boolean> readonly={readonly} {...props}>
21
+ {({ value }) => (
22
+ <Input.Block>
23
+ <Input.Switch disabled={!!readonly} checked={value} onCheckedChange={handleChange} />
24
+ </Input.Block>
25
+ )}
26
+ </FormRow>
27
+ );
28
+ };
@@ -0,0 +1,5 @@
1
+ //
2
+ // Copyright 2024 DXOS.org
3
+ //
4
+
5
+ export * from './BooleanField';
@@ -0,0 +1,104 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ import React, { useCallback, useMemo, useState } from 'react';
6
+
7
+ import { type AnyProperties } from '@dxos/echo/internal';
8
+ import { Combobox } from '@dxos/react-ui-list';
9
+
10
+ import { type FormFieldRendererProps } from '#types';
11
+
12
+ import { type OptionsLookup, type OptionsLookupEntry } from '../../../../../annotations';
13
+ import { pickValues, useAsyncFieldEffect, useFormValues } from '../../../../../hooks';
14
+ import { FormRow } from '../../FormRow';
15
+
16
+ export type ComboboxFieldProps = FormFieldRendererProps<string> & {
17
+ /** Loads suggestions from the lookup's declared dependency fields (typically the field's own value). */
18
+ lookup: OptionsLookup;
19
+ };
20
+
21
+ /**
22
+ * An editable combobox built on the shared {@link Combobox} primitive (trigger + popover search list,
23
+ * the same family as `ObjectPicker`/`SearchList`). The trigger shows the current value; opening it reveals
24
+ * a search input that loads suggestions (debounced) via an {@link OptionsLookup} and filters
25
+ * them as you type. The literal typed text is offered as a fallback option at the bottom (deduped), so a
26
+ * value not among the suggestions can still be selected. No options are shown until something is typed.
27
+ */
28
+ export const ComboboxField = ({ lookup, type, readonly, placeholder, onValueChange, ...props }: ComboboxFieldProps) => {
29
+ const values = useFormValues<AnyProperties>(ComboboxField.displayName);
30
+ const ownKey = props.jsonPath;
31
+ const [query, setQuery] = useState('');
32
+
33
+ // When the combobox searches on its own field (its name is among `deps`), feed the live query to the
34
+ // loader so remote suggestions track typing; otherwise the loader reads the committed sibling values.
35
+ const lookupValues = useMemo(() => {
36
+ const subset = pickValues(values, lookup.deps);
37
+ if (ownKey && lookup.deps.includes(ownKey)) {
38
+ subset[ownKey] = query;
39
+ }
40
+ return subset;
41
+ }, [values, lookup.deps, ownKey, query]);
42
+ const key = useMemo(() => JSON.stringify(lookupValues), [lookupValues]);
43
+ const { data } = useAsyncFieldEffect<readonly OptionsLookupEntry[]>(() => lookup.load(lookupValues), key);
44
+
45
+ const trimmed = query.trim();
46
+ const normalized = trimmed.toLowerCase();
47
+ // No options until something is typed; then filter the loaded suggestions by the input text.
48
+ const results = useMemo(
49
+ () =>
50
+ normalized.length === 0
51
+ ? []
52
+ : (data ?? []).filter((option) => (option.label ?? option.value).toLowerCase().includes(normalized)),
53
+ [data, normalized],
54
+ );
55
+ const hasExact = results.some((option) => option.value === trimmed);
56
+
57
+ const handleValueChange = useCallback((next: string) => onValueChange(type, next), [onValueChange, type]);
58
+ const handleOpenChange = useCallback((open: boolean) => {
59
+ if (!open) {
60
+ setQuery('');
61
+ }
62
+ }, []);
63
+
64
+ return (
65
+ <FormRow<string>
66
+ readonly={readonly}
67
+ renderStatic={(value) => <p className='truncate min-w-0'>{value ?? ''}</p>}
68
+ {...props}
69
+ >
70
+ {({ value = '' }) => (
71
+ <Combobox.Root
72
+ value={value}
73
+ onValueChange={handleValueChange}
74
+ onOpenChange={handleOpenChange}
75
+ placeholder={placeholder}
76
+ >
77
+ {/* Full-width trigger (default renders value/placeholder + caret) to match the other fields. */}
78
+ <Combobox.Trigger disabled={!!readonly} classNames='w-full' />
79
+ <Combobox.Portal>
80
+ {/* Keep the first result highlighted as the list changes while typing. */}
81
+ <Combobox.Content resetSelectionOnChange>
82
+ <Combobox.Input autoFocus value={query} onValueChange={setQuery} placeholder={placeholder} />
83
+ <Combobox.List>
84
+ {results.map((option) => (
85
+ <Combobox.Item
86
+ key={option.value}
87
+ value={option.value}
88
+ label={option.label ?? option.value}
89
+ suffix={option.secondaryLabel}
90
+ />
91
+ ))}
92
+ {/* The literal typed text as a fallback option at the bottom, unless a suggestion already is it. */}
93
+ {normalized.length > 0 && !hasExact && <Combobox.Item value={trimmed} label={trimmed} />}
94
+ </Combobox.List>
95
+ <Combobox.Arrow />
96
+ </Combobox.Content>
97
+ </Combobox.Portal>
98
+ </Combobox.Root>
99
+ )}
100
+ </FormRow>
101
+ );
102
+ };
103
+
104
+ ComboboxField.displayName = 'Form.ComboboxField';
@@ -0,0 +1,5 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ export * from './ComboboxField';