@dxos/react-ui-form 0.8.4-main.fffef41 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (301) hide show
  1. package/LICENSE +102 -5
  2. package/README.md +0 -1
  3. package/dist/lib/browser/index.mjs +4005 -2402
  4. package/dist/lib/browser/index.mjs.map +4 -4
  5. package/dist/lib/browser/meta.json +1 -1
  6. package/dist/lib/browser/translations.mjs +87 -0
  7. package/dist/lib/browser/translations.mjs.map +7 -0
  8. package/dist/lib/node-esm/index.mjs +4005 -2402
  9. package/dist/lib/node-esm/index.mjs.map +4 -4
  10. package/dist/lib/node-esm/meta.json +1 -1
  11. package/dist/lib/node-esm/translations.mjs +89 -0
  12. package/dist/lib/node-esm/translations.mjs.map +7 -0
  13. package/dist/types/src/components/FieldEditor/FieldEditor.d.ts +8 -7
  14. package/dist/types/src/components/FieldEditor/FieldEditor.d.ts.map +1 -1
  15. package/dist/types/src/components/FieldEditor/FieldEditor.stories.d.ts +73 -59
  16. package/dist/types/src/components/FieldEditor/FieldEditor.stories.d.ts.map +1 -1
  17. package/dist/types/src/components/Form/Form.d.ts +135 -27
  18. package/dist/types/src/components/Form/Form.d.ts.map +1 -1
  19. package/dist/types/src/components/Form/Form.stories.d.ts +192 -137
  20. package/dist/types/src/components/Form/Form.stories.d.ts.map +1 -1
  21. package/dist/types/src/components/Form/FormField.d.ts +52 -0
  22. package/dist/types/src/components/Form/FormField.d.ts.map +1 -0
  23. package/dist/types/src/components/Form/FormFieldComponent.d.ts +98 -0
  24. package/dist/types/src/components/Form/FormFieldComponent.d.ts.map +1 -0
  25. package/dist/types/src/components/Form/FormFieldComponent.stories.d.ts +9 -0
  26. package/dist/types/src/components/Form/FormFieldComponent.stories.d.ts.map +1 -0
  27. package/dist/types/src/components/Form/FormFieldSet.d.ts +26 -0
  28. package/dist/types/src/components/Form/FormFieldSet.d.ts.map +1 -0
  29. package/dist/types/src/components/Form/FormTooltipsContext.d.ts +19 -0
  30. package/dist/types/src/components/Form/FormTooltipsContext.d.ts.map +1 -0
  31. package/dist/types/src/components/Form/Layout/FormLayout.d.ts +50 -0
  32. package/dist/types/src/components/Form/Layout/FormLayout.d.ts.map +1 -0
  33. package/dist/types/src/components/Form/Layout/FormLayout.stories.d.ts +123 -0
  34. package/dist/types/src/components/Form/Layout/FormLayout.stories.d.ts.map +1 -0
  35. package/dist/types/src/components/Form/Layout/FormLayout.stories.test.d.ts +2 -0
  36. package/dist/types/src/components/Form/Layout/FormLayout.stories.test.d.ts.map +1 -0
  37. package/dist/types/src/components/Form/Layout/FormLayout.test.d.ts +2 -0
  38. package/dist/types/src/components/Form/Layout/FormLayout.test.d.ts.map +1 -0
  39. package/dist/types/src/components/Form/Layout/annotation.d.ts +30 -0
  40. package/dist/types/src/components/Form/Layout/annotation.d.ts.map +1 -0
  41. package/dist/types/src/components/Form/Layout/index.d.ts +4 -0
  42. package/dist/types/src/components/Form/Layout/index.d.ts.map +1 -0
  43. package/dist/types/src/components/Form/Layout/parser.d.ts +31 -0
  44. package/dist/types/src/components/Form/Layout/parser.d.ts.map +1 -0
  45. package/dist/types/src/components/Form/Layout/parser.test.d.ts +2 -0
  46. package/dist/types/src/components/Form/Layout/parser.test.d.ts.map +1 -0
  47. package/dist/types/src/components/Form/fields/ArrayField.d.ts +18 -0
  48. package/dist/types/src/components/Form/fields/ArrayField.d.ts.map +1 -0
  49. package/dist/types/src/components/Form/fields/BooleanField.d.ts +4 -0
  50. package/dist/types/src/components/Form/fields/BooleanField.d.ts.map +1 -0
  51. package/dist/types/src/components/Form/fields/DateField.d.ts +4 -0
  52. package/dist/types/src/components/Form/fields/DateField.d.ts.map +1 -0
  53. package/dist/types/src/components/Form/fields/GeoPointField.d.ts +5 -0
  54. package/dist/types/src/components/Form/fields/GeoPointField.d.ts.map +1 -0
  55. package/dist/types/src/components/Form/fields/InlineRefField.d.ts +13 -0
  56. package/dist/types/src/components/Form/fields/InlineRefField.d.ts.map +1 -0
  57. package/dist/types/src/components/Form/fields/MarkdownField.d.ts +11 -0
  58. package/dist/types/src/components/Form/fields/MarkdownField.d.ts.map +1 -0
  59. package/dist/types/src/components/Form/fields/MarkdownField.stories.d.ts +89 -0
  60. package/dist/types/src/components/Form/fields/MarkdownField.stories.d.ts.map +1 -0
  61. package/dist/types/src/components/Form/fields/NumberField.d.ts +4 -0
  62. package/dist/types/src/components/Form/fields/NumberField.d.ts.map +1 -0
  63. package/dist/types/src/components/Form/fields/RefField.d.ts +34 -0
  64. package/dist/types/src/components/Form/fields/RefField.d.ts.map +1 -0
  65. package/dist/types/src/components/Form/fields/RefField.test.d.ts +2 -0
  66. package/dist/types/src/components/Form/fields/RefField.test.d.ts.map +1 -0
  67. package/dist/types/src/components/Form/fields/SelectField.d.ts +13 -0
  68. package/dist/types/src/components/Form/fields/SelectField.d.ts.map +1 -0
  69. package/dist/types/src/components/Form/fields/SelectOptionField.d.ts +5 -0
  70. package/dist/types/src/components/Form/fields/SelectOptionField.d.ts.map +1 -0
  71. package/dist/types/src/components/Form/fields/SelectOptionField.stories.d.ts +89 -0
  72. package/dist/types/src/components/Form/fields/SelectOptionField.stories.d.ts.map +1 -0
  73. package/dist/types/src/components/Form/fields/TextAreaField.d.ts +4 -0
  74. package/dist/types/src/components/Form/fields/TextAreaField.d.ts.map +1 -0
  75. package/dist/types/src/components/Form/fields/TextField.d.ts +4 -0
  76. package/dist/types/src/components/Form/fields/TextField.d.ts.map +1 -0
  77. package/dist/types/src/components/Form/fields/TupleField.d.ts +6 -0
  78. package/dist/types/src/components/Form/fields/TupleField.d.ts.map +1 -0
  79. package/dist/types/src/components/Form/fields/index.d.ts +14 -0
  80. package/dist/types/src/components/Form/fields/index.d.ts.map +1 -0
  81. package/dist/types/src/components/Form/index.d.ts +5 -4
  82. package/dist/types/src/components/Form/index.d.ts.map +1 -1
  83. package/dist/types/src/components/Form/meta-tags.d.ts +30 -0
  84. package/dist/types/src/components/Form/meta-tags.d.ts.map +1 -0
  85. package/dist/types/src/components/Form/meta-tags.test.d.ts +2 -0
  86. package/dist/types/src/components/Form/meta-tags.test.d.ts.map +1 -0
  87. package/dist/types/src/components/ObjectForm/ObjectForm.d.ts +8 -0
  88. package/dist/types/src/components/ObjectForm/ObjectForm.d.ts.map +1 -0
  89. package/dist/types/src/components/ObjectForm/ObjectForm.stories.d.ts +87 -0
  90. package/dist/types/src/components/ObjectForm/ObjectForm.stories.d.ts.map +1 -0
  91. package/dist/types/src/components/ObjectForm/index.d.ts +2 -0
  92. package/dist/types/src/components/ObjectForm/index.d.ts.map +1 -0
  93. package/dist/types/src/components/ObjectPicker/ObjectPicker.d.ts +39 -12
  94. package/dist/types/src/components/ObjectPicker/ObjectPicker.d.ts.map +1 -1
  95. package/dist/types/src/components/ObjectPicker/ObjectPicker.stories.d.ts +69 -58
  96. package/dist/types/src/components/ObjectPicker/ObjectPicker.stories.d.ts.map +1 -1
  97. package/dist/types/src/components/ObjectProperties/ObjectProperties.d.ts +9 -0
  98. package/dist/types/src/components/ObjectProperties/ObjectProperties.d.ts.map +1 -0
  99. package/dist/types/src/components/ObjectProperties/ObjectProperties.stories.d.ts +132 -0
  100. package/dist/types/src/components/ObjectProperties/ObjectProperties.stories.d.ts.map +1 -0
  101. package/dist/types/src/components/ObjectProperties/ObjectProperties.test.d.ts +2 -0
  102. package/dist/types/src/components/ObjectProperties/ObjectProperties.test.d.ts.map +1 -0
  103. package/dist/types/src/components/ObjectProperties/index.d.ts +2 -0
  104. package/dist/types/src/components/ObjectProperties/index.d.ts.map +1 -0
  105. package/dist/types/src/components/ObjectTree/ObjectTree.d.ts +9 -0
  106. package/dist/types/src/components/ObjectTree/ObjectTree.d.ts.map +1 -0
  107. package/dist/types/src/components/ObjectTree/ObjectTree.stories.d.ts +91 -0
  108. package/dist/types/src/components/ObjectTree/ObjectTree.stories.d.ts.map +1 -0
  109. package/dist/types/src/components/ObjectTree/index.d.ts +2 -0
  110. package/dist/types/src/components/ObjectTree/index.d.ts.map +1 -0
  111. package/dist/types/src/components/RefEditor/RefEditor.d.ts +73 -0
  112. package/dist/types/src/components/RefEditor/RefEditor.d.ts.map +1 -0
  113. package/dist/types/src/components/RefEditor/RefEditor.stories.d.ts +92 -0
  114. package/dist/types/src/components/RefEditor/RefEditor.stories.d.ts.map +1 -0
  115. package/dist/types/src/components/RefEditor/index.d.ts +3 -0
  116. package/dist/types/src/components/RefEditor/index.d.ts.map +1 -0
  117. package/dist/types/src/components/RefEditor/ref-editor-extension.d.ts +47 -0
  118. package/dist/types/src/components/RefEditor/ref-editor-extension.d.ts.map +1 -0
  119. package/dist/types/src/components/Settings/Settings.d.ts +51 -0
  120. package/dist/types/src/components/Settings/Settings.d.ts.map +1 -0
  121. package/dist/types/src/components/Settings/Settings.stories.d.ts +89 -0
  122. package/dist/types/src/components/Settings/Settings.stories.d.ts.map +1 -0
  123. package/dist/types/src/components/Settings/SettingsFieldSet.d.ts +43 -0
  124. package/dist/types/src/components/Settings/SettingsFieldSet.d.ts.map +1 -0
  125. package/dist/types/src/components/Settings/SettingsItem.d.ts +16 -0
  126. package/dist/types/src/components/Settings/SettingsItem.d.ts.map +1 -0
  127. package/dist/types/src/components/Settings/SettingsSelectField.d.ts +18 -0
  128. package/dist/types/src/components/Settings/SettingsSelectField.d.ts.map +1 -0
  129. package/dist/types/src/components/Settings/index.d.ts +4 -0
  130. package/dist/types/src/components/Settings/index.d.ts.map +1 -0
  131. package/dist/types/src/components/ViewEditor/ViewEditor.d.ts +15 -19
  132. package/dist/types/src/components/ViewEditor/ViewEditor.d.ts.map +1 -1
  133. package/dist/types/src/components/ViewEditor/ViewEditor.stories.d.ts +75 -64
  134. package/dist/types/src/components/ViewEditor/ViewEditor.stories.d.ts.map +1 -1
  135. package/dist/types/src/components/index.d.ts +5 -1
  136. package/dist/types/src/components/index.d.ts.map +1 -1
  137. package/dist/types/src/components/testing.d.ts +13 -6
  138. package/dist/types/src/components/testing.d.ts.map +1 -1
  139. package/dist/types/src/hooks/index.d.ts +2 -3
  140. package/dist/types/src/hooks/index.d.ts.map +1 -1
  141. package/dist/types/src/hooks/useFormHandler.d.ts +82 -0
  142. package/dist/types/src/hooks/useFormHandler.d.ts.map +1 -0
  143. package/dist/types/src/hooks/useKeyHandler.d.ts +7 -0
  144. package/dist/types/src/hooks/useKeyHandler.d.ts.map +1 -0
  145. package/dist/types/src/index.d.ts +0 -2
  146. package/dist/types/src/index.d.ts.map +1 -1
  147. package/dist/types/src/translations.d.ts +70 -59
  148. package/dist/types/src/translations.d.ts.map +1 -1
  149. package/dist/types/src/util/field-type.d.ts +19 -0
  150. package/dist/types/src/util/field-type.d.ts.map +1 -0
  151. package/dist/types/src/util/field.d.ts +3 -3
  152. package/dist/types/src/util/field.d.ts.map +1 -1
  153. package/dist/types/src/util/formatting.d.ts +2 -2
  154. package/dist/types/src/util/formatting.d.ts.map +1 -1
  155. package/dist/types/src/util/index.d.ts +2 -1
  156. package/dist/types/src/util/index.d.ts.map +1 -1
  157. package/dist/types/src/util/properties.d.ts +35 -0
  158. package/dist/types/src/util/properties.d.ts.map +1 -0
  159. package/dist/types/src/util/properties.test.d.ts +2 -0
  160. package/dist/types/src/util/properties.test.d.ts.map +1 -0
  161. package/dist/types/src/util/refs.d.ts +2 -2
  162. package/dist/types/src/util/refs.d.ts.map +1 -1
  163. package/dist/types/tsconfig.tsbuildinfo +1 -1
  164. package/package.json +64 -52
  165. package/src/components/FieldEditor/FieldEditor.stories.tsx +44 -25
  166. package/src/components/FieldEditor/FieldEditor.test.tsx +4 -2
  167. package/src/components/FieldEditor/FieldEditor.tsx +91 -86
  168. package/src/components/Form/AUDIT.md +17 -0
  169. package/src/components/Form/Form.stories.tsx +147 -252
  170. package/src/components/Form/Form.tsx +461 -108
  171. package/src/components/Form/FormField.tsx +345 -0
  172. package/src/components/Form/FormFieldComponent.stories.tsx +65 -0
  173. package/src/components/Form/FormFieldComponent.tsx +253 -0
  174. package/src/components/Form/FormFieldSet.tsx +213 -0
  175. package/src/components/Form/FormTooltipsContext.ts +25 -0
  176. package/src/components/Form/Layout/FormLayout.stories.test.tsx +51 -0
  177. package/src/components/Form/Layout/FormLayout.stories.tsx +422 -0
  178. package/src/components/Form/Layout/FormLayout.test.ts +59 -0
  179. package/src/components/Form/Layout/FormLayout.tsx +251 -0
  180. package/src/components/Form/Layout/annotation.ts +38 -0
  181. package/src/components/Form/Layout/index.ts +7 -0
  182. package/src/components/Form/Layout/parser.test.ts +120 -0
  183. package/src/components/Form/Layout/parser.ts +167 -0
  184. package/src/components/Form/README.md +3 -0
  185. package/src/components/Form/fields/ArrayField.tsx +173 -0
  186. package/src/components/Form/fields/BooleanField.tsx +22 -0
  187. package/src/components/Form/fields/DateField.tsx +101 -0
  188. package/src/components/Form/fields/GeoPointField.tsx +124 -0
  189. package/src/components/Form/fields/InlineRefField.tsx +138 -0
  190. package/src/components/Form/fields/MarkdownField.stories.tsx +206 -0
  191. package/src/components/Form/fields/MarkdownField.tsx +167 -0
  192. package/src/components/Form/fields/NumberField.tsx +76 -0
  193. package/src/components/Form/fields/RefField.test.ts +45 -0
  194. package/src/components/Form/fields/RefField.tsx +266 -0
  195. package/src/components/Form/fields/SelectField.tsx +77 -0
  196. package/src/components/Form/{custom/SelectOptionsInput.stories.tsx → fields/SelectOptionField.stories.tsx} +23 -18
  197. package/src/components/Form/{custom/SelectOptionsInput.tsx → fields/SelectOptionField.tsx} +33 -26
  198. package/src/components/Form/fields/TextAreaField.tsx +38 -0
  199. package/src/components/Form/fields/TextField.tsx +38 -0
  200. package/src/components/Form/{custom/TupleInput.tsx → fields/TupleField.tsx} +17 -9
  201. package/src/components/Form/fields/index.ts +17 -0
  202. package/src/components/Form/index.ts +6 -4
  203. package/src/components/Form/meta-tags.test.ts +47 -0
  204. package/src/components/Form/meta-tags.ts +33 -0
  205. package/src/components/ObjectForm/ObjectForm.stories.tsx +62 -0
  206. package/src/components/ObjectForm/ObjectForm.tsx +111 -0
  207. package/src/components/{ControlSection → ObjectForm}/index.ts +1 -1
  208. package/src/components/ObjectPicker/ObjectPicker.stories.tsx +26 -21
  209. package/src/components/ObjectPicker/ObjectPicker.tsx +123 -82
  210. package/src/components/ObjectProperties/ObjectProperties.stories.tsx +350 -0
  211. package/src/components/ObjectProperties/ObjectProperties.test.tsx +111 -0
  212. package/src/components/ObjectProperties/ObjectProperties.tsx +146 -0
  213. package/src/components/ObjectProperties/index.ts +5 -0
  214. package/src/components/ObjectTree/ObjectTree.stories.tsx +134 -0
  215. package/src/components/ObjectTree/ObjectTree.tsx +190 -0
  216. package/src/components/ObjectTree/index.ts +5 -0
  217. package/src/components/RefEditor/RefEditor.stories.tsx +148 -0
  218. package/src/components/RefEditor/RefEditor.tsx +275 -0
  219. package/src/components/RefEditor/index.ts +6 -0
  220. package/src/components/RefEditor/ref-editor-extension.ts +254 -0
  221. package/src/components/Settings/AUDIT.md +51 -0
  222. package/src/components/Settings/Settings.stories.tsx +104 -0
  223. package/src/components/Settings/Settings.tsx +81 -0
  224. package/src/components/Settings/SettingsFieldSet.tsx +257 -0
  225. package/src/components/Settings/SettingsItem.tsx +58 -0
  226. package/src/components/Settings/SettingsSelectField.tsx +59 -0
  227. package/src/components/Settings/index.ts +7 -0
  228. package/src/components/ViewEditor/ViewEditor.stories.tsx +70 -76
  229. package/src/components/ViewEditor/ViewEditor.test.tsx +17 -13
  230. package/src/components/ViewEditor/ViewEditor.tsx +340 -226
  231. package/src/components/index.ts +5 -1
  232. package/src/components/testing.tsx +24 -11
  233. package/src/hooks/index.ts +2 -3
  234. package/src/hooks/useFormHandler.ts +380 -0
  235. package/src/hooks/useKeyHandler.tsx +37 -0
  236. package/src/index.ts +0 -4
  237. package/src/translations.ts +76 -64
  238. package/src/util/field-type.ts +60 -0
  239. package/src/util/field.ts +25 -25
  240. package/src/util/formatting.ts +22 -22
  241. package/src/util/index.ts +2 -1
  242. package/src/util/properties.test.ts +153 -0
  243. package/src/util/properties.ts +100 -0
  244. package/src/util/refs.ts +16 -15
  245. package/src/vite-env.d.ts +10 -0
  246. package/dist/types/src/components/ControlSection/ControlSection.d.ts +0 -23
  247. package/dist/types/src/components/ControlSection/ControlSection.d.ts.map +0 -1
  248. package/dist/types/src/components/ControlSection/ControlSection.stories.d.ts +0 -83
  249. package/dist/types/src/components/ControlSection/ControlSection.stories.d.ts.map +0 -1
  250. package/dist/types/src/components/ControlSection/index.d.ts +0 -2
  251. package/dist/types/src/components/ControlSection/index.d.ts.map +0 -1
  252. package/dist/types/src/components/Form/ArrayField.d.ts +0 -16
  253. package/dist/types/src/components/Form/ArrayField.d.ts.map +0 -1
  254. package/dist/types/src/components/Form/Defaults.d.ts +0 -14
  255. package/dist/types/src/components/Form/Defaults.d.ts.map +0 -1
  256. package/dist/types/src/components/Form/FormActions.d.ts +0 -9
  257. package/dist/types/src/components/Form/FormActions.d.ts.map +0 -1
  258. package/dist/types/src/components/Form/FormContent.d.ts +0 -63
  259. package/dist/types/src/components/Form/FormContent.d.ts.map +0 -1
  260. package/dist/types/src/components/Form/FormContext.d.ts +0 -23
  261. package/dist/types/src/components/Form/FormContext.d.ts.map +0 -1
  262. package/dist/types/src/components/Form/Input.d.ts +0 -29
  263. package/dist/types/src/components/Form/Input.d.ts.map +0 -1
  264. package/dist/types/src/components/Form/RefField.d.ts +0 -22
  265. package/dist/types/src/components/Form/RefField.d.ts.map +0 -1
  266. package/dist/types/src/components/Form/custom/GeoPointInput.d.ts +0 -4
  267. package/dist/types/src/components/Form/custom/GeoPointInput.d.ts.map +0 -1
  268. package/dist/types/src/components/Form/custom/SelectOptionsInput.d.ts +0 -4
  269. package/dist/types/src/components/Form/custom/SelectOptionsInput.d.ts.map +0 -1
  270. package/dist/types/src/components/Form/custom/SelectOptionsInput.stories.d.ts +0 -78
  271. package/dist/types/src/components/Form/custom/SelectOptionsInput.stories.d.ts.map +0 -1
  272. package/dist/types/src/components/Form/custom/TupleInput.d.ts +0 -6
  273. package/dist/types/src/components/Form/custom/TupleInput.d.ts.map +0 -1
  274. package/dist/types/src/components/Form/custom/index.d.ts +0 -4
  275. package/dist/types/src/components/Form/custom/index.d.ts.map +0 -1
  276. package/dist/types/src/components/Form/factory.d.ts +0 -8
  277. package/dist/types/src/components/Form/factory.d.ts.map +0 -1
  278. package/dist/types/src/hooks/useForm.d.ts +0 -67
  279. package/dist/types/src/hooks/useForm.d.ts.map +0 -1
  280. package/dist/types/src/hooks/useQueryRefOptions.d.ts +0 -29
  281. package/dist/types/src/hooks/useQueryRefOptions.d.ts.map +0 -1
  282. package/dist/types/src/hooks/useRefQueryLookupHandler.d.ts +0 -8
  283. package/dist/types/src/hooks/useRefQueryLookupHandler.d.ts.map +0 -1
  284. package/dist/types/src/util/schema.d.ts +0 -3
  285. package/dist/types/src/util/schema.d.ts.map +0 -1
  286. package/src/components/ControlSection/ControlSection.stories.tsx +0 -38
  287. package/src/components/ControlSection/ControlSection.tsx +0 -153
  288. package/src/components/Form/ArrayField.tsx +0 -125
  289. package/src/components/Form/Defaults.tsx +0 -205
  290. package/src/components/Form/FormActions.tsx +0 -54
  291. package/src/components/Form/FormContent.tsx +0 -362
  292. package/src/components/Form/FormContext.tsx +0 -110
  293. package/src/components/Form/Input.tsx +0 -54
  294. package/src/components/Form/RefField.tsx +0 -283
  295. package/src/components/Form/custom/GeoPointInput.tsx +0 -80
  296. package/src/components/Form/custom/index.ts +0 -7
  297. package/src/components/Form/factory.tsx +0 -31
  298. package/src/hooks/useForm.ts +0 -296
  299. package/src/hooks/useQueryRefOptions.ts +0 -62
  300. package/src/hooks/useRefQueryLookupHandler.ts +0 -39
  301. package/src/util/schema.ts +0 -12
@@ -0,0 +1,422 @@
1
+ //
2
+ // Copyright 2026 DXOS.org
3
+ //
4
+
5
+ import { xml } from '@codemirror/lang-xml';
6
+ import { type Meta, type StoryObj } from '@storybook/react-vite';
7
+ import * as Schema from 'effect/Schema';
8
+ import React, { useCallback, useEffect, useMemo, useState } from 'react';
9
+
10
+ import { Annotation, DXN, Format, Type } from '@dxos/echo';
11
+ import { withClientProvider } from '@dxos/react-client/testing';
12
+ import { Card, Tooltip, useThemeContext } from '@dxos/react-ui';
13
+ import { Editor } from '@dxos/react-ui-editor';
14
+ import { Syntax } from '@dxos/react-ui-syntax-highlighter';
15
+ import { withLayout, withTheme } from '@dxos/react-ui/testing';
16
+ import { createBasicExtensions, createThemeExtensions } from '@dxos/ui-editor';
17
+ import { mx } from '@dxos/ui-theme';
18
+ import { trim } from '@dxos/util';
19
+
20
+ import { translations } from '#translations';
21
+
22
+ import { TestLayout, TestPanel } from '../../testing';
23
+ import { Form, type FormRootProps, omitId } from '../Form';
24
+ import { FormLayoutAnnotation } from './annotation';
25
+ import { parseLayout } from './parser';
26
+
27
+ /**
28
+ * Sample schema: a Flight booking carries airline, flight number, route, dates,
29
+ * cabin class. Without an annotation `<Form.FieldSet/>` renders one field per
30
+ * row (see the `Linear` story). The `FormLayoutAnnotation` on this schema
31
+ * arranges the same fields in a 2-column grid with selective spans.
32
+ */
33
+ const Flight = Schema.Struct({
34
+ airline: Schema.optional(Schema.String.annotations({ title: 'Airline' })),
35
+ flightNumber: Schema.optional(Schema.String.annotations({ title: 'Flight #' })),
36
+ origin: Schema.optional(Schema.String.annotations({ title: 'From' })),
37
+ destination: Schema.optional(Schema.String.annotations({ title: 'To' })),
38
+ departAt: Schema.optional(Format.DateTime.annotations({ title: 'Depart' })),
39
+ arriveAt: Schema.optional(Format.DateTime.annotations({ title: 'Arrive' })),
40
+ cabin: Schema.optional(Schema.Literal('economy', 'premium', 'business', 'first').annotations({ title: 'Cabin' })),
41
+ notes: Schema.optional(Format.Text.annotations({ title: 'Notes' })),
42
+ }).pipe(Type.makeObject(DXN.make('com.example.type.flight', '0.1.0')));
43
+
44
+ export type Flight = Type.InstanceType<typeof Flight>;
45
+
46
+ type FlightValues = Omit<Type.InstanceType<typeof Flight>, 'id'>;
47
+
48
+ const flight = {
49
+ airline: 'Air France',
50
+ flightNumber: 'AF-1',
51
+ origin: 'JFK',
52
+ destination: 'CDG',
53
+ departAt: '2026-06-01T15:30:00.000Z',
54
+ arriveAt: '2026-06-02T09:30:00.000Z',
55
+ cabin: 'economy',
56
+ } satisfies Partial<FlightValues>;
57
+
58
+ const FLIGHT_LAYOUT = trim`
59
+ <grid cols="2">
60
+ <field name="airline"/>
61
+ <field name="flightNumber"/>
62
+ <field name="origin"/>
63
+ <field name="destination"/>
64
+ <field name="departAt"/>
65
+ <field name="arriveAt"/>
66
+ <field name="cabin" span="2"/>
67
+ <field name="notes" span="2"/>
68
+ </grid>
69
+ `;
70
+
71
+ /** Compact variant — single column, only the headline fields. */
72
+ const FLIGHT_LAYOUT_COMPACT = trim`
73
+ <grid cols="1">
74
+ <field name="airline"/>
75
+ <field name="flightNumber"/>
76
+ <field name="departAt"/>
77
+ </grid>
78
+ `;
79
+
80
+ /**
81
+ * Same schema annotated with two named layouts. `Form.Layout name="…"`
82
+ * (or `Form.FieldSet layoutName="…"`) picks the variant; without a name
83
+ * the `'default'` entry is used.
84
+ */
85
+ const AnnotatedFlight = Type.getSchema(Flight)
86
+ .annotations({})
87
+ .pipe(FormLayoutAnnotation.set({ default: FLIGHT_LAYOUT, compact: FLIGHT_LAYOUT_COMPACT }));
88
+
89
+ /**
90
+ * Sample schema with nested `Place` structs carrying a `LabelAnnotation`.
91
+ * Demonstrates the two nested-field DSL behaviors: `<field name="origin"/>`
92
+ * auto-converts to the place's computed label, while `<field name="origin.code"/>`
93
+ * drills into a leaf sub-field.
94
+ */
95
+ const Place = Schema.Struct({
96
+ name: Schema.optional(Schema.String.annotations({ title: 'Name' })),
97
+ code: Schema.optional(Schema.String.annotations({ title: 'Code' })),
98
+ city: Schema.optional(Schema.String.annotations({ title: 'City' })),
99
+ }).pipe(Annotation.LabelAnnotation.set(['name']));
100
+
101
+ const Journey = Schema.Struct({
102
+ flightNumber: Schema.optional(Schema.String.annotations({ title: 'Flight #' })),
103
+ departAt: Schema.optional(Format.DateTime.annotations({ title: 'Depart' })),
104
+ origin: Schema.optional(Place),
105
+ destination: Schema.optional(Place),
106
+ }).pipe(Type.makeObject(DXN.make('com.example.type.journey', '0.1.0')));
107
+
108
+ type JourneyValues = Omit<Type.InstanceType<typeof Journey>, 'id'>;
109
+
110
+ const journey = {
111
+ flightNumber: 'AF-1',
112
+ departAt: '2026-06-01T15:30:00.000Z',
113
+ origin: { name: 'John F. Kennedy Intl', code: 'JFK', city: 'New York' },
114
+ destination: { name: 'Charles de Gaulle', code: 'CDG', city: 'Paris' },
115
+ } satisfies Partial<JourneyValues>;
116
+
117
+ const JOURNEY_LAYOUT = trim`
118
+ <grid cols="2">
119
+ <field name="flightNumber" span="2"/>
120
+ <field name="departAt" span="2"/>
121
+ <field name="origin"/>
122
+ <field name="destination"/>
123
+ <field name="origin.code"/>
124
+ <field name="destination.code"/>
125
+ </grid>
126
+ `;
127
+
128
+ type StoryProps = {
129
+ schema: Schema.Schema<any>;
130
+ template?: string;
131
+ };
132
+
133
+ const DefaultStory = ({ schema, template }: StoryProps) => {
134
+ const [values, setValues] = useState<Partial<FlightValues>>(flight);
135
+
136
+ const handleSave = useCallback<NonNullable<FormRootProps<any>['onSave']>>((next) => {
137
+ setValues(next as Partial<FlightValues>);
138
+ }, []);
139
+
140
+ return (
141
+ <Tooltip.Provider>
142
+ <TestLayout json={{ values }}>
143
+ <Form.Root schema={schema} defaultValues={values} onSave={handleSave} autoSave>
144
+ <Form.Viewport>
145
+ <Form.Content>
146
+ {template !== undefined ? <Form.Layout schema={schema} template={template} /> : <Form.FieldSet />}
147
+ </Form.Content>
148
+ </Form.Viewport>
149
+ </Form.Root>
150
+ </TestLayout>
151
+ </Tooltip.Provider>
152
+ );
153
+ };
154
+
155
+ const meta = {
156
+ title: 'ui/react-ui-form/FormLayout',
157
+ render: DefaultStory,
158
+ decorators: [
159
+ withTheme(),
160
+ withLayout({ layout: 'fullscreen' }),
161
+ withClientProvider({ createIdentity: true, createSpace: true }),
162
+ ],
163
+ parameters: {
164
+ layout: 'fullscreen',
165
+ translations,
166
+ },
167
+ } satisfies Meta<StoryProps>;
168
+
169
+ export default meta;
170
+
171
+ type Story = StoryObj<StoryProps>;
172
+
173
+ /** Baseline: no annotation, no override — `Form.FieldSet` renders linearly (one field per row). */
174
+ export const Linear: Story = {
175
+ args: {
176
+ schema: omitId(Type.getSchema(Flight)),
177
+ },
178
+ };
179
+
180
+ /** `Form.Layout` invoked directly with a `template` prop, bypassing any annotation. */
181
+ export const TemplateProp: Story = {
182
+ args: {
183
+ schema: omitId(Type.getSchema(Flight)),
184
+ template: FLIGHT_LAYOUT,
185
+ },
186
+ };
187
+
188
+ /** Schema carries `FormLayoutAnnotation`; `Form.FieldSet` auto-detects and delegates. */
189
+ export const SchemaAnnotation: Story = {
190
+ args: {
191
+ schema: omitId(AnnotatedFlight),
192
+ },
193
+ };
194
+
195
+ /**
196
+ * Same annotated schema, but the story switches between the two named
197
+ * layouts (`default` and `compact`) via a radio above the form.
198
+ */
199
+ const NamedAnnotationStory = () => {
200
+ const schema = useMemo(() => omitId(AnnotatedFlight) as unknown as Schema.Schema<any>, []);
201
+ const [layoutName, setLayoutName] = useState<'default' | 'compact'>('default');
202
+ const [values, setValues] = useState<Partial<FlightValues>>(flight);
203
+
204
+ const handleSave = useCallback<NonNullable<FormRootProps<any>['onSave']>>((next) => {
205
+ setValues(next as Flight);
206
+ }, []);
207
+
208
+ return (
209
+ <Tooltip.Provider>
210
+ <TestLayout json={{ layoutName, values }}>
211
+ <div className='flex flex-col gap-2'>
212
+ <div className='flex gap-2 text-sm'>
213
+ {(['default', 'compact'] as const).map((name) => (
214
+ <label key={name} className='flex items-center gap-1'>
215
+ <input type='radio' name='layout' checked={layoutName === name} onChange={() => setLayoutName(name)} />
216
+ {name}
217
+ </label>
218
+ ))}
219
+ </div>
220
+ <Form.Root schema={schema} defaultValues={values} onSave={handleSave} autoSave>
221
+ <Form.Viewport>
222
+ <Form.Content>
223
+ <Form.FieldSet layoutName={layoutName} />
224
+ </Form.Content>
225
+ </Form.Viewport>
226
+ </Form.Root>
227
+ </div>
228
+ </TestLayout>
229
+ </Tooltip.Provider>
230
+ );
231
+ };
232
+
233
+ export const NamedAnnotation: Story = {
234
+ render: () => <NamedAnnotationStory />,
235
+ };
236
+
237
+ /**
238
+ * Nested struct fields. `origin`/`destination` are `Place` structs with a
239
+ * `LabelAnnotation` — referenced bare they collapse to the place's name, while
240
+ * `origin.code`/`destination.code` drill into the leaf sub-field.
241
+ */
242
+ const NestedLabelStory = ({ readonly = false }: { readonly?: boolean }) => {
243
+ const schema = useMemo(() => omitId(Type.getSchema(Journey)) as unknown as Schema.Schema<any>, []);
244
+ const [values, setValues] = useState<Partial<JourneyValues>>(journey);
245
+
246
+ const handleSave = useCallback<NonNullable<FormRootProps<any>['onSave']>>((next) => {
247
+ setValues(next as Partial<JourneyValues>);
248
+ }, []);
249
+
250
+ return (
251
+ <Tooltip.Provider>
252
+ <TestLayout json={{ values }}>
253
+ <Form.Root
254
+ schema={schema}
255
+ defaultValues={values}
256
+ layout={readonly ? 'static' : 'full'}
257
+ readonly={readonly}
258
+ onSave={handleSave}
259
+ autoSave
260
+ >
261
+ <Form.Viewport>
262
+ <Form.Content>
263
+ <Form.Layout schema={schema} template={JOURNEY_LAYOUT} />
264
+ </Form.Content>
265
+ </Form.Viewport>
266
+ </Form.Root>
267
+ </TestLayout>
268
+ </Tooltip.Provider>
269
+ );
270
+ };
271
+
272
+ export const NestedLabel: Story = {
273
+ render: () => <NestedLabelStory />,
274
+ };
275
+
276
+ /** Read-only static presentation: dates render human-readable; nested structs as labels. */
277
+ export const NestedLabelStatic: Story = {
278
+ render: () => <NestedLabelStory readonly />,
279
+ };
280
+
281
+ /**
282
+ * Live playground: the layout template lives in a code editor on the right.
283
+ * The form on the left re-renders as you type. If the template fails to
284
+ * parse, the previous valid layout stays on screen and an error banner
285
+ * surfaces under the editor.
286
+ */
287
+ type PlaygroundStoryProps = {
288
+ /** Wrap the rendered form in `Card.Root` / `Card.Body` chrome. */
289
+ card?: boolean;
290
+ };
291
+
292
+ const PlaygroundStory = ({ card = false }: PlaygroundStoryProps) => {
293
+ const schema = useMemo(() => omitId(Type.getSchema(Flight)) as unknown as Schema.Schema<any>, []);
294
+ const [template, setTemplate] = useState(FLIGHT_LAYOUT);
295
+ const [lastValid, setLastValid] = useState(FLIGHT_LAYOUT);
296
+ const [error, setError] = useState<string | undefined>();
297
+
298
+ useEffect(() => {
299
+ try {
300
+ parseLayout(template);
301
+ setLastValid(template);
302
+ setError(undefined);
303
+ } catch (err) {
304
+ setError(err instanceof Error ? err.message : String(err));
305
+ }
306
+ }, [template]);
307
+
308
+ const [values, setValues] = useState<Partial<FlightValues>>(flight);
309
+
310
+ const handleSave = useCallback<NonNullable<FormRootProps<any>['onSave']>>((next) => {
311
+ setValues(next as Partial<FlightValues>);
312
+ }, []);
313
+
314
+ const { themeMode } = useThemeContext();
315
+ const extensions = useMemo(
316
+ () => [
317
+ xml(),
318
+ createBasicExtensions({ placeholder: 'Edit layout DSL…' }),
319
+ createThemeExtensions({ themeMode, syntaxHighlighting: true }),
320
+ ],
321
+ [themeMode],
322
+ );
323
+
324
+ const renderForm = (readonly = false) => (
325
+ <Form.Root
326
+ schema={schema}
327
+ defaultValues={values}
328
+ layout={readonly ? 'static' : 'full'}
329
+ readonly={readonly}
330
+ autoSave
331
+ onSave={handleSave}
332
+ >
333
+ <Form.Viewport>
334
+ <Form.Content>
335
+ <Form.Layout schema={schema} template={lastValid} />
336
+ </Form.Content>
337
+ </Form.Viewport>
338
+ </Form.Root>
339
+ );
340
+
341
+ return (
342
+ <Tooltip.Provider>
343
+ <div
344
+ className={mx(
345
+ 'dx-container grid grid-rows-1 p-4 gap-4 overflow-hidden',
346
+ card ? 'grid-cols-[var(--spacing-card-min-width)_var(--spacing-card-min-width)_1fr]' : 'grid-cols-2',
347
+ )}
348
+ >
349
+ {card ? (
350
+ <>
351
+ <div>
352
+ <Card.Root fullWidth>
353
+ <Card.Header>
354
+ <Card.DragHandle />
355
+ <Card.Title>Read-only</Card.Title>
356
+ <Card.ActionIconButton action='close' onClick={() => console.log('close')} />
357
+ </Card.Header>
358
+ <Card.Body>{renderForm(true)}</Card.Body>
359
+ </Card.Root>
360
+ </div>
361
+ <div>
362
+ <Card.Root fullWidth>
363
+ <Card.Header>
364
+ <Card.DragHandle />
365
+ <Card.Title>Editable</Card.Title>
366
+ <Card.ActionIconButton action='close' onClick={() => console.log('close')} />
367
+ </Card.Header>
368
+ <Card.Body>{renderForm(false)}</Card.Body>
369
+ </Card.Root>
370
+ </div>
371
+ </>
372
+ ) : (
373
+ <TestPanel>{renderForm(false)}</TestPanel>
374
+ )}
375
+ <div className='grid grid-rows-3 gap-4 overflow-hidden'>
376
+ <TestPanel>
377
+ <div className='flex flex-col h-full overflow-hidden'>
378
+ <Editor.Root>
379
+ <Editor.View
380
+ classNames='flex-1 overflow-auto font-mono text-sm p-2'
381
+ extensions={extensions}
382
+ value={template}
383
+ onChange={setTemplate}
384
+ />
385
+ </Editor.Root>
386
+ {error && <div className='border-t border-separator p-2 text-sm text-error-text font-mono'>{error}</div>}
387
+ </div>
388
+ </TestPanel>
389
+ <TestPanel>
390
+ <Syntax.Root data={schema.ast}>
391
+ <Syntax.Content>
392
+ <Syntax.Filter />
393
+ <Syntax.Viewport>
394
+ <Syntax.Code testId='schema' classNames='text-sm' />
395
+ </Syntax.Viewport>
396
+ </Syntax.Content>
397
+ </Syntax.Root>
398
+ </TestPanel>
399
+ <TestPanel>
400
+ <Syntax.Root data={values}>
401
+ <Syntax.Content>
402
+ <Syntax.Filter />
403
+ <Syntax.Viewport>
404
+ <Syntax.Code testId='data' classNames='text-sm' />
405
+ </Syntax.Viewport>
406
+ </Syntax.Content>
407
+ </Syntax.Root>
408
+ </TestPanel>
409
+ </div>
410
+ </div>
411
+ </Tooltip.Provider>
412
+ );
413
+ };
414
+
415
+ export const FormPlayground: Story = {
416
+ render: () => <PlaygroundStory />,
417
+ };
418
+
419
+ /** Same playground, but the form is rendered inside a `Card.Root` / `Card.Body`. */
420
+ export const CardPlayground: Story = {
421
+ render: () => <PlaygroundStory card />,
422
+ };
@@ -0,0 +1,59 @@
1
+ //
2
+ // Copyright 2026 DXOS.org
3
+ //
4
+
5
+ import * as Schema from 'effect/Schema';
6
+ import * as SchemaAST from 'effect/SchemaAST';
7
+ import { describe, test } from 'vitest';
8
+
9
+ import { Annotation } from '@dxos/echo';
10
+
11
+ import { resolveLayoutField } from './FormLayout';
12
+
13
+ const Place = Schema.Struct({
14
+ name: Schema.optional(Schema.String),
15
+ code: Schema.optional(Schema.String),
16
+ city: Schema.optional(Schema.String),
17
+ }).pipe(Annotation.LabelAnnotation.set(['name']));
18
+
19
+ const Journey = Schema.Struct({
20
+ number: Schema.optional(Schema.String),
21
+ origin: Schema.optional(Place),
22
+ destination: Schema.optional(Place),
23
+ });
24
+
25
+ describe('resolveLayoutField', () => {
26
+ test('resolves a top-level scalar field', ({ expect }) => {
27
+ const resolved = resolveLayoutField(Journey, 'number');
28
+ expect(resolved).toBeDefined();
29
+ expect(resolved!.segments).toEqual(['number']);
30
+ expect(resolved!.leafName).toBe('number');
31
+ expect(resolved!.labelType).toBeUndefined();
32
+ });
33
+
34
+ test('auto-labels a nested struct carrying a LabelAnnotation', ({ expect }) => {
35
+ const resolved = resolveLayoutField(Journey, 'origin');
36
+ expect(resolved).toBeDefined();
37
+ expect(resolved!.segments).toEqual(['origin']);
38
+ expect(resolved!.leafName).toBe('origin');
39
+ expect(resolved!.labelType).toBeDefined();
40
+ // The flagged type literal computes the place's label.
41
+ const label = Annotation.getLabelWithSchema(Schema.make(resolved!.labelType!), { name: 'JFK', code: 'JFK' } as any);
42
+ expect(label).toBe('JFK');
43
+ });
44
+
45
+ test('drills into a nested sub-field via a dotted name', ({ expect }) => {
46
+ const resolved = resolveLayoutField(Journey, 'origin.code');
47
+ expect(resolved).toBeDefined();
48
+ expect(resolved!.segments).toEqual(['origin', 'code']);
49
+ expect(resolved!.leafName).toBe('code');
50
+ // The leaf is a scalar, not an auto-labelled struct.
51
+ expect(resolved!.labelType).toBeUndefined();
52
+ expect(SchemaAST.isStringKeyword(resolved!.type) || SchemaAST.isUnion(resolved!.type)).toBe(true);
53
+ });
54
+
55
+ test('returns undefined for an unknown field', ({ expect }) => {
56
+ expect(resolveLayoutField(Journey, 'missing')).toBeUndefined();
57
+ expect(resolveLayoutField(Journey, 'origin.missing')).toBeUndefined();
58
+ });
59
+ });