@dxos/react-ui-form 0.8.4-main.fffef41 → 0.8.4-staging.60fe92afc8

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,251 @@
1
+ //
2
+ // Copyright 2026 DXOS.org
3
+ //
4
+
5
+ import * as Option from 'effect/Option';
6
+ import * as Schema from 'effect/Schema';
7
+ import * as SchemaAST from 'effect/SchemaAST';
8
+ import * as String from 'effect/String';
9
+ import React, { Fragment, useMemo } from 'react';
10
+
11
+ import { Annotation } from '@dxos/echo';
12
+ import { type AnyProperties } from '@dxos/echo/internal';
13
+ import { SchemaEx } from '@dxos/effect';
14
+ import { Input } from '@dxos/react-ui';
15
+
16
+ import { useFormFieldState } from '../Form';
17
+ import { FormField, type FormFieldProps } from '../FormField';
18
+ import { FormFieldErrorBoundary, FormFieldLabel, type Presentation } from '../FormFieldComponent';
19
+ import { DEFAULT_LAYOUT_NAME, FormLayoutAnnotation } from './annotation';
20
+ import { LayoutParseError, type LayoutNode, parseLayout } from './parser';
21
+
22
+ const FORM_LAYOUT_NAME = 'Form.Layout';
23
+
24
+ export type FormLayoutProps = {
25
+ /**
26
+ * Template string in the FormLayout DSL (`<grid cols=…><field name=…/></grid>`).
27
+ * If omitted, the layout is read from the schema's `FormLayoutAnnotation` —
28
+ * the entry named by `name` (default: `'default'`).
29
+ */
30
+ template?: string;
31
+ /**
32
+ * Picks a named layout out of `FormLayoutAnnotation`'s map when no
33
+ * explicit `template` is supplied. Defaults to `'default'`.
34
+ */
35
+ name?: string;
36
+ } & Pick<FormFieldSetSubset, 'schema' | 'path' | 'readonly' | 'layout' | 'projection' | 'fieldMap' | 'fieldProvider'>;
37
+
38
+ type FormFieldSetSubset = Pick<
39
+ FormFieldProps,
40
+ 'path' | 'readonly' | 'layout' | 'projection' | 'fieldMap' | 'fieldProvider'
41
+ > & {
42
+ schema: Schema.Schema<AnyProperties>;
43
+ };
44
+
45
+ /**
46
+ * Lays out schema fields according to a `FormLayout` DSL template. The template
47
+ * is either passed via the `template` prop or read from the schema's
48
+ * `FormLayoutAnnotation`. Fields not referenced in the template are hidden —
49
+ * the template controls exactly what renders.
50
+ *
51
+ * A `<field name=…/>` resolves against the schema:
52
+ * - A dotted name (`origin.code`) drills into a nested struct and renders the leaf field.
53
+ * - A name that resolves to a nested struct carrying a `LabelAnnotation` auto-converts
54
+ * to the struct's computed label, rendered as a single read-only text (rather than
55
+ * expanding the struct's sub-fields).
56
+ */
57
+ export const FormLayout = ({
58
+ schema,
59
+ template,
60
+ name = DEFAULT_LAYOUT_NAME,
61
+ path,
62
+ readonly,
63
+ layout,
64
+ projection,
65
+ ...props
66
+ }: FormLayoutProps) => {
67
+ const annotated = Option.getOrUndefined(FormLayoutAnnotation.get(schema));
68
+ const source = template ?? annotated?.[name];
69
+ if (source === undefined) {
70
+ throw new LayoutParseError(
71
+ annotated
72
+ ? `no layout named "${name}" on schema (available: ${Object.keys(annotated).join(', ') || '∅'})`
73
+ : `no template provided and schema has no FormLayoutAnnotation`,
74
+ );
75
+ }
76
+
77
+ const tree = useMemo(() => parseLayout(source), [source]);
78
+
79
+ return (
80
+ <RenderNode
81
+ node={tree}
82
+ schema={schema}
83
+ basePath={path ?? []}
84
+ readonly={readonly}
85
+ layout={layout}
86
+ projection={projection}
87
+ {...props}
88
+ />
89
+ );
90
+ };
91
+
92
+ FormLayout.displayName = FORM_LAYOUT_NAME;
93
+
94
+ type RenderNodeProps = Omit<FormLayoutProps, 'template' | 'path'> & {
95
+ node: LayoutNode;
96
+ basePath: (string | number)[];
97
+ };
98
+
99
+ const RenderNode = ({ node, schema, basePath, ...props }: RenderNodeProps) => {
100
+ if (node.kind === 'grid') {
101
+ return (
102
+ <div className='grid gap-x-form-gap' style={{ gridTemplateColumns: `repeat(${node.cols}, minmax(0, 1fr))` }}>
103
+ {node.children.map((child, index) => (
104
+ <Fragment key={index}>
105
+ <RenderNode node={child} schema={schema} basePath={basePath} {...props} />
106
+ </Fragment>
107
+ ))}
108
+ </div>
109
+ );
110
+ }
111
+
112
+ const resolved = resolveLayoutField(schema, node.name);
113
+ if (!resolved) {
114
+ throw new LayoutParseError(`field "${node.name}" not found on schema`);
115
+ }
116
+
117
+ const { type, segments, leafName, title, labelType } = resolved;
118
+ const path = [...basePath, ...segments];
119
+ const span = node.span ? { gridColumn: `span ${node.span} / span ${node.span}` } : undefined;
120
+
121
+ return (
122
+ <div className='min-w-0' style={span}>
123
+ <FormFieldErrorBoundary path={path}>
124
+ {labelType ? (
125
+ <LabelField
126
+ schema={Schema.make(labelType)}
127
+ label={title ?? String.capitalize(leafName)}
128
+ path={path}
129
+ layout={props.layout}
130
+ />
131
+ ) : (
132
+ <FormField type={type} name={leafName} path={path} {...props} />
133
+ )}
134
+ </FormFieldErrorBoundary>
135
+ </div>
136
+ );
137
+ };
138
+
139
+ export type ResolvedLayoutField = {
140
+ /** Resolved leaf AST node for the field. */
141
+ type: SchemaAST.AST;
142
+ /** Path segments (dotted names split on `.`). */
143
+ segments: string[];
144
+ /** Last path segment; used to derive the field label when there is no `title` annotation. */
145
+ leafName: string;
146
+ /** Resolved `title` annotation for the field, if any. */
147
+ title?: string;
148
+ /**
149
+ * Set when the field resolves to a nested struct carrying a `LabelAnnotation` —
150
+ * the type literal whose computed label should render in place of the struct's
151
+ * sub-fields. Undefined for ordinary (non-auto-labelled) fields.
152
+ */
153
+ labelType?: SchemaAST.AST;
154
+ };
155
+
156
+ /**
157
+ * Walks a dotted path of property names through nested struct ASTs, returning the
158
+ * leaf `PropertySignature` (or `undefined` if any segment is missing).
159
+ */
160
+ const resolvePropertySignature = (ast: SchemaAST.AST, segments: string[]): SchemaAST.PropertySignature | undefined => {
161
+ let node: SchemaAST.AST = ast;
162
+ for (let index = 0; index < segments.length; index++) {
163
+ const typeLiteral = SchemaEx.findNode(node, SchemaAST.isTypeLiteral);
164
+ if (!typeLiteral) {
165
+ return undefined;
166
+ }
167
+ const prop = SchemaAST.getPropertySignatures(typeLiteral).find(
168
+ (p) => globalThis.String(p.name) === segments[index],
169
+ );
170
+ if (!prop) {
171
+ return undefined;
172
+ }
173
+ if (index === segments.length - 1) {
174
+ return prop;
175
+ }
176
+ node = prop.type;
177
+ }
178
+
179
+ return undefined;
180
+ };
181
+
182
+ /**
183
+ * Resolves a (possibly dotted) layout field name against a schema.
184
+ * - `origin.code` drills through nested structs to the leaf field.
185
+ * - A name resolving to a nested struct with a `LabelAnnotation` is flagged via
186
+ * `labelType` so the layout can auto-convert it to a single read-only label.
187
+ * Returns `undefined` when the name does not resolve to a property.
188
+ */
189
+ export const resolveLayoutField = (schema: Schema.Schema<any>, name: string): ResolvedLayoutField | undefined => {
190
+ const segments = name.split('.');
191
+ const prop = resolvePropertySignature(schema.ast, segments);
192
+ if (!prop) {
193
+ return undefined;
194
+ }
195
+
196
+ // Normalized leaf type (optional unwrapped, refinements stripped, signature-level
197
+ // annotations merged) — matches how `getProperties` feeds `FormField`.
198
+ const { type: baseType } = SchemaEx.getBaseType(prop);
199
+ const type =
200
+ prop.annotations && Reflect.ownKeys(prop.annotations).length > 0
201
+ ? ({ ...baseType, annotations: { ...baseType.annotations, ...prop.annotations } } as SchemaAST.AST)
202
+ : baseType;
203
+
204
+ const title = SchemaEx.getAnnotation<string>(SchemaAST.TitleAnnotationId)(type);
205
+
206
+ // Label detection reads the *raw* property type: `SchemaEx.getBaseType`'s `encodedBoundAST`
207
+ // strips annotations from non-keyword inner types (e.g. nested structs), which would
208
+ // drop the `LabelAnnotation` we rely on here.
209
+ const labelType = SchemaEx.findNode(prop.type, SchemaAST.isTypeLiteral);
210
+ const labelled = labelType != null && Option.isSome(Annotation.LabelAnnotation.getFromAst(labelType));
211
+
212
+ return {
213
+ type,
214
+ segments,
215
+ leafName: segments[segments.length - 1],
216
+ title,
217
+ labelType: labelled ? labelType : undefined,
218
+ };
219
+ };
220
+
221
+ type LabelFieldProps = {
222
+ schema: Schema.Schema<any>;
223
+ label: string;
224
+ path: (string | number)[];
225
+ layout?: Presentation;
226
+ };
227
+
228
+ /**
229
+ * Renders a nested struct value as its computed label (via `LabelAnnotation`),
230
+ * as a single read-only text. Empty values are omitted, mirroring the static
231
+ * presentation of regular fields.
232
+ */
233
+ const LabelField = ({ schema, label, path, layout }: LabelFieldProps) => {
234
+ const { getValue } = useFormFieldState(FORM_LAYOUT_NAME, path);
235
+ const value = getValue();
236
+ const text = value == null ? undefined : Annotation.getLabelWithSchema(schema, value);
237
+ if (text == null || text.trim().length === 0) {
238
+ return null;
239
+ }
240
+
241
+ return (
242
+ <div className='contents'>
243
+ <Input.Root>
244
+ {layout !== 'inline' && <FormFieldLabel readonly label={label} path={SchemaEx.createJsonPath(path)} />}
245
+ <p className='truncate min-w-0' title={text}>
246
+ {text}
247
+ </p>
248
+ </Input.Root>
249
+ </div>
250
+ );
251
+ };
@@ -0,0 +1,38 @@
1
+ //
2
+ // Copyright 2026 DXOS.org
3
+ //
4
+
5
+ import { createAnnotationHelper } from '@dxos/echo/internal';
6
+
7
+ /**
8
+ * Annotation carrying one or more named `FormLayout` DSL templates that
9
+ * control how `Form.FieldSet` / `Form.Layout` arrange a schema's fields.
10
+ * Callers select a variant via the `name` prop; the implicit name is
11
+ * `DEFAULT_LAYOUT_NAME` (`'default'`).
12
+ *
13
+ * Templates use a minimal XML grammar — see `parser.ts`. Example:
14
+ *
15
+ * FormLayoutAnnotation.set({
16
+ * default: `
17
+ * <grid cols="2">
18
+ * <field name="origin"/>
19
+ * <field name="destination"/>
20
+ * <field name="provider" span="2"/>
21
+ * </grid>
22
+ * `,
23
+ * card: `
24
+ * <grid cols="1">
25
+ * <field name="provider"/>
26
+ * <field name="number"/>
27
+ * </grid>
28
+ * `,
29
+ * })
30
+ */
31
+ export const FormLayoutAnnotationId = Symbol.for('@dxos/react-ui-form/annotation/Layout');
32
+
33
+ export type FormLayoutMap = Record<string, string>;
34
+
35
+ export const FormLayoutAnnotation = createAnnotationHelper<FormLayoutMap>(FormLayoutAnnotationId);
36
+
37
+ /** Name used when no explicit variant is requested. */
38
+ export const DEFAULT_LAYOUT_NAME = 'default';
@@ -0,0 +1,7 @@
1
+ //
2
+ // Copyright 2026 DXOS.org
3
+ //
4
+
5
+ export * from './annotation';
6
+ export { FormLayout } from './FormLayout';
7
+ export { LayoutParseError, parseLayout, type LayoutNode } from './parser';
@@ -0,0 +1,120 @@
1
+ //
2
+ // Copyright 2026 DXOS.org
3
+ //
4
+
5
+ import { describe, test } from 'vitest';
6
+
7
+ import { LayoutParseError, parseLayout } from './parser';
8
+
9
+ describe('parseLayout', () => {
10
+ test('grid with two fields', ({ expect }) => {
11
+ const tree = parseLayout(`<grid cols="2"><field name="a"/><field name="b"/></grid>`);
12
+ expect(tree).toEqual({
13
+ kind: 'grid',
14
+ cols: 2,
15
+ children: [
16
+ { kind: 'field', name: 'a', span: undefined },
17
+ { kind: 'field', name: 'b', span: undefined },
18
+ ],
19
+ });
20
+ });
21
+
22
+ test('field with span attribute', ({ expect }) => {
23
+ const tree = parseLayout(`<grid cols="3"><field name="a" span="2"/><field name="b"/></grid>`);
24
+ expect(tree).toEqual({
25
+ kind: 'grid',
26
+ cols: 3,
27
+ children: [
28
+ { kind: 'field', name: 'a', span: 2 },
29
+ { kind: 'field', name: 'b', span: undefined },
30
+ ],
31
+ });
32
+ });
33
+
34
+ test('dotted field name', ({ expect }) => {
35
+ const tree = parseLayout(`<grid cols="1"><field name="details.airline"/></grid>`);
36
+ expect(tree).toEqual({
37
+ kind: 'grid',
38
+ cols: 1,
39
+ children: [{ kind: 'field', name: 'details.airline', span: undefined }],
40
+ });
41
+ });
42
+
43
+ test('nested grids', ({ expect }) => {
44
+ const tree = parseLayout(`
45
+ <grid cols="2">
46
+ <field name="a"/>
47
+ <grid cols="2">
48
+ <field name="b"/>
49
+ <field name="c"/>
50
+ </grid>
51
+ </grid>
52
+ `);
53
+ expect(tree.kind).toBe('grid');
54
+ if (tree.kind !== 'grid') {
55
+ return;
56
+ }
57
+ expect(tree.cols).toBe(2);
58
+ expect(tree.children).toHaveLength(2);
59
+ const inner = tree.children[1];
60
+ expect(inner.kind).toBe('grid');
61
+ if (inner.kind === 'grid') {
62
+ expect(inner.cols).toBe(2);
63
+ expect(inner.children).toHaveLength(2);
64
+ }
65
+ });
66
+
67
+ test('accepts single-quoted and unquoted attributes', ({ expect }) => {
68
+ const tree = parseLayout(`<grid cols=2><field name='x' span=3/></grid>`);
69
+ expect(tree).toEqual({
70
+ kind: 'grid',
71
+ cols: 2,
72
+ children: [{ kind: 'field', name: 'x', span: 3 }],
73
+ });
74
+ });
75
+
76
+ test('ignores whitespace between tags', ({ expect }) => {
77
+ const tree = parseLayout(`
78
+ <grid cols="1">
79
+ <field name="x"/>
80
+ </grid>
81
+ `);
82
+ expect(tree.kind).toBe('grid');
83
+ });
84
+
85
+ test('empty template throws', ({ expect }) => {
86
+ expect(() => parseLayout('')).toThrow(LayoutParseError);
87
+ });
88
+
89
+ test('text content between tags throws', ({ expect }) => {
90
+ expect(() => parseLayout(`<grid cols="1">hello<field name="x"/></grid>`)).toThrow(/unexpected text content/);
91
+ });
92
+
93
+ test('missing required cols attribute throws', ({ expect }) => {
94
+ expect(() => parseLayout(`<grid><field name="x"/></grid>`)).toThrow(/missing required attribute "cols"/);
95
+ });
96
+
97
+ test('missing required field name throws', ({ expect }) => {
98
+ expect(() => parseLayout(`<grid cols="1"><field/></grid>`)).toThrow(/missing required attribute "name"/);
99
+ });
100
+
101
+ test('non-integer cols throws', ({ expect }) => {
102
+ expect(() => parseLayout(`<grid cols="auto"><field name="x"/></grid>`)).toThrow(/expects an integer/);
103
+ });
104
+
105
+ test('non-grid root throws', ({ expect }) => {
106
+ expect(() => parseLayout(`<field name="x"/>`)).toThrow(/root element must be <grid>/);
107
+ });
108
+
109
+ test('unclosed grid throws', ({ expect }) => {
110
+ expect(() => parseLayout(`<grid cols="1"><field name="x"/>`)).toThrow(/<grid> not closed/);
111
+ });
112
+
113
+ test('non-self-closing field throws', ({ expect }) => {
114
+ expect(() => parseLayout(`<grid cols="1"><field name="x"></field></grid>`)).toThrow(/must be self-closing/);
115
+ });
116
+
117
+ test('unknown element throws', ({ expect }) => {
118
+ expect(() => parseLayout(`<grid cols="1"><row name="x"/></grid>`)).toThrow(/unknown element <row>/);
119
+ });
120
+ });
@@ -0,0 +1,167 @@
1
+ //
2
+ // Copyright 2026 DXOS.org
3
+ //
4
+
5
+ /**
6
+ * Minimal XML-style DSL for laying out schema fields in a grid.
7
+ *
8
+ * Grammar:
9
+ * <grid cols="N"> ...children... </grid>
10
+ * <field name="x" span="N"/>
11
+ *
12
+ * - Attributes accept `"…"`, `'…'`, or unquoted integers.
13
+ * - Whitespace between tags is ignored. Text content (between tags) is forbidden.
14
+ * - Self-closing form (`<field … />`) is required for `field`; `grid` must use a closing tag.
15
+ *
16
+ * The parser is hand-rolled (no HTML parser) because the grammar is tiny.
17
+ */
18
+
19
+ /** Node type for the parsed layout tree. */
20
+ export type LayoutNode =
21
+ | { readonly kind: 'grid'; readonly cols: number; readonly children: readonly LayoutNode[] }
22
+ | { readonly kind: 'field'; readonly name: string; readonly span?: number };
23
+
24
+ export class LayoutParseError extends Error {
25
+ readonly _tag = 'LayoutParseError';
26
+ constructor(
27
+ message: string,
28
+ readonly position?: number,
29
+ ) {
30
+ super(position !== undefined ? `${message} (at position ${position})` : message);
31
+ }
32
+ }
33
+
34
+ const TAG_RE = /<\s*(\/?)\s*(\w+)([^>]*?)(\/?)\s*>/g;
35
+ const ATTR_RE = /([\w-]+)\s*=\s*(?:"([^"]*)"|'([^']*)'|(\d+))/g;
36
+
37
+ type Token =
38
+ | { type: 'open'; name: string; attrs: Record<string, string>; selfClosing: boolean; position: number }
39
+ | { type: 'close'; name: string; position: number };
40
+
41
+ const parseAttrs = (raw: string): Record<string, string> => {
42
+ const attrs: Record<string, string> = {};
43
+ ATTR_RE.lastIndex = 0;
44
+ let match: RegExpExecArray | null;
45
+ while ((match = ATTR_RE.exec(raw)) !== null) {
46
+ const [, key, doubleQuoted, singleQuoted, bare] = match;
47
+ attrs[key] = doubleQuoted ?? singleQuoted ?? bare;
48
+ }
49
+ return attrs;
50
+ };
51
+
52
+ const tokenize = (input: string): Token[] => {
53
+ const tokens: Token[] = [];
54
+ TAG_RE.lastIndex = 0;
55
+ let match: RegExpExecArray | null;
56
+ let cursor = 0;
57
+ while ((match = TAG_RE.exec(input)) !== null) {
58
+ // Validate that any text between the previous tag and this one is whitespace-only.
59
+ const between = input.slice(cursor, match.index);
60
+ if (between.trim().length > 0) {
61
+ throw new LayoutParseError(`unexpected text content: ${JSON.stringify(between.trim())}`, cursor);
62
+ }
63
+ const [, slash, name, attrsRaw, selfSlash] = match;
64
+ if (slash) {
65
+ tokens.push({ type: 'close', name, position: match.index });
66
+ } else {
67
+ tokens.push({
68
+ type: 'open',
69
+ name,
70
+ attrs: parseAttrs(attrsRaw),
71
+ selfClosing: selfSlash === '/',
72
+ position: match.index,
73
+ });
74
+ }
75
+ cursor = match.index + match[0].length;
76
+ }
77
+ // Any trailing text after the last tag must also be whitespace.
78
+ const trailing = input.slice(cursor);
79
+ if (trailing.trim().length > 0) {
80
+ throw new LayoutParseError(`unexpected text content: ${JSON.stringify(trailing.trim())}`, cursor);
81
+ }
82
+ return tokens;
83
+ };
84
+
85
+ const requireAttr = (attrs: Record<string, string>, key: string, tag: string, position: number): string => {
86
+ const value = attrs[key];
87
+ if (value === undefined) {
88
+ throw new LayoutParseError(`<${tag}> missing required attribute "${key}"`, position);
89
+ }
90
+ return value;
91
+ };
92
+
93
+ const parseInteger = (value: string, attr: string, tag: string, position: number): number => {
94
+ if (!/^\d+$/.test(value)) {
95
+ throw new LayoutParseError(
96
+ `<${tag}> attribute "${attr}" expects an integer, got ${JSON.stringify(value)}`,
97
+ position,
98
+ );
99
+ }
100
+ return Number.parseInt(value, 10);
101
+ };
102
+
103
+ /** Parses a layout template string into a single root node. The root must be `<grid>`. */
104
+ export const parseLayout = (template: string): LayoutNode => {
105
+ const tokens = tokenize(template);
106
+ if (tokens.length === 0) {
107
+ throw new LayoutParseError('empty template');
108
+ }
109
+
110
+ let index = 0;
111
+
112
+ const parseNode = (): LayoutNode => {
113
+ const token = tokens[index];
114
+ if (!token || token.type !== 'open') {
115
+ throw new LayoutParseError(`expected an opening tag, got ${JSON.stringify(token)}`, token?.position);
116
+ }
117
+
118
+ if (token.name === 'field') {
119
+ if (!token.selfClosing) {
120
+ throw new LayoutParseError('<field> must be self-closing (e.g. <field name="x"/>)', token.position);
121
+ }
122
+ const name = requireAttr(token.attrs, 'name', 'field', token.position);
123
+ const spanRaw = token.attrs.span;
124
+ const span = spanRaw !== undefined ? parseInteger(spanRaw, 'span', 'field', token.position) : undefined;
125
+ index += 1;
126
+ return { kind: 'field', name, span };
127
+ }
128
+
129
+ if (token.name === 'grid') {
130
+ if (token.selfClosing) {
131
+ throw new LayoutParseError('<grid> must wrap children (cannot self-close)', token.position);
132
+ }
133
+ const cols = parseInteger(
134
+ requireAttr(token.attrs, 'cols', 'grid', token.position),
135
+ 'cols',
136
+ 'grid',
137
+ token.position,
138
+ );
139
+ index += 1;
140
+ const children: LayoutNode[] = [];
141
+ while (index < tokens.length && !(tokens[index].type === 'close' && tokens[index].name === 'grid')) {
142
+ children.push(parseNode());
143
+ }
144
+ const closing = tokens[index];
145
+ if (!closing || closing.type !== 'close' || closing.name !== 'grid') {
146
+ throw new LayoutParseError('<grid> not closed', token.position);
147
+ }
148
+ index += 1;
149
+ return { kind: 'grid', cols, children };
150
+ }
151
+
152
+ throw new LayoutParseError(`unknown element <${token.name}>`, token.position);
153
+ };
154
+
155
+ const root = parseNode();
156
+ if (index !== tokens.length) {
157
+ const trailing = tokens[index];
158
+ throw new LayoutParseError(
159
+ `unexpected trailing tag <${trailing.type === 'open' ? '' : '/'}${trailing.name}>`,
160
+ trailing.position,
161
+ );
162
+ }
163
+ if (root.kind !== 'grid') {
164
+ throw new LayoutParseError('root element must be <grid>');
165
+ }
166
+ return root;
167
+ };
@@ -0,0 +1,3 @@
1
+ # Form
2
+
3
+ - [ ] Layout options (e.g., label on the side.)