@dxos/react-ui-form 0.8.4-main.e8ec1fe → 0.8.4-main.effb148878

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 (268) hide show
  1. package/LICENSE +102 -5
  2. package/README.md +0 -1
  3. package/dist/lib/browser/index.mjs +3240 -2345
  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 +83 -0
  7. package/dist/lib/browser/translations.mjs.map +7 -0
  8. package/dist/lib/node-esm/index.mjs +3240 -2345
  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 +85 -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 +69 -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 +123 -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 +175 -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 +48 -0
  22. package/dist/types/src/components/Form/FormField.d.ts.map +1 -0
  23. package/dist/types/src/components/Form/FormFieldComponent.d.ts +95 -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 +21 -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 +25 -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 +116 -0
  34. package/dist/types/src/components/Form/Layout/FormLayout.stories.d.ts.map +1 -0
  35. package/dist/types/src/components/Form/Layout/annotation.d.ts +30 -0
  36. package/dist/types/src/components/Form/Layout/annotation.d.ts.map +1 -0
  37. package/dist/types/src/components/Form/Layout/index.d.ts +4 -0
  38. package/dist/types/src/components/Form/Layout/index.d.ts.map +1 -0
  39. package/dist/types/src/components/Form/Layout/parser.d.ts +31 -0
  40. package/dist/types/src/components/Form/Layout/parser.d.ts.map +1 -0
  41. package/dist/types/src/components/Form/Layout/parser.test.d.ts +2 -0
  42. package/dist/types/src/components/Form/Layout/parser.test.d.ts.map +1 -0
  43. package/dist/types/src/components/Form/fields/ArrayField.d.ts +18 -0
  44. package/dist/types/src/components/Form/fields/ArrayField.d.ts.map +1 -0
  45. package/dist/types/src/components/Form/fields/BooleanField.d.ts +4 -0
  46. package/dist/types/src/components/Form/fields/BooleanField.d.ts.map +1 -0
  47. package/dist/types/src/components/Form/fields/DateField.d.ts +4 -0
  48. package/dist/types/src/components/Form/fields/DateField.d.ts.map +1 -0
  49. package/dist/types/src/components/Form/fields/GeoPointField.d.ts +5 -0
  50. package/dist/types/src/components/Form/fields/GeoPointField.d.ts.map +1 -0
  51. package/dist/types/src/components/Form/fields/MarkdownField.d.ts +11 -0
  52. package/dist/types/src/components/Form/fields/MarkdownField.d.ts.map +1 -0
  53. package/dist/types/src/components/Form/fields/MarkdownField.stories.d.ts +85 -0
  54. package/dist/types/src/components/Form/fields/MarkdownField.stories.d.ts.map +1 -0
  55. package/dist/types/src/components/Form/fields/NumberField.d.ts +4 -0
  56. package/dist/types/src/components/Form/fields/NumberField.d.ts.map +1 -0
  57. package/dist/types/src/components/Form/fields/RefField.d.ts +22 -0
  58. package/dist/types/src/components/Form/fields/RefField.d.ts.map +1 -0
  59. package/dist/types/src/components/Form/fields/SelectField.d.ts +13 -0
  60. package/dist/types/src/components/Form/fields/SelectField.d.ts.map +1 -0
  61. package/dist/types/src/components/Form/fields/SelectOptionField.d.ts +5 -0
  62. package/dist/types/src/components/Form/fields/SelectOptionField.d.ts.map +1 -0
  63. package/dist/types/src/components/Form/fields/SelectOptionField.stories.d.ts +85 -0
  64. package/dist/types/src/components/Form/fields/SelectOptionField.stories.d.ts.map +1 -0
  65. package/dist/types/src/components/Form/fields/TextAreaField.d.ts +4 -0
  66. package/dist/types/src/components/Form/fields/TextAreaField.d.ts.map +1 -0
  67. package/dist/types/src/components/Form/fields/TextField.d.ts +4 -0
  68. package/dist/types/src/components/Form/fields/TextField.d.ts.map +1 -0
  69. package/dist/types/src/components/Form/fields/TupleField.d.ts +6 -0
  70. package/dist/types/src/components/Form/fields/TupleField.d.ts.map +1 -0
  71. package/dist/types/src/components/Form/fields/index.d.ts +13 -0
  72. package/dist/types/src/components/Form/fields/index.d.ts.map +1 -0
  73. package/dist/types/src/components/Form/index.d.ts +4 -4
  74. package/dist/types/src/components/Form/index.d.ts.map +1 -1
  75. package/dist/types/src/components/ObjectForm/ObjectForm.d.ts +8 -0
  76. package/dist/types/src/components/ObjectForm/ObjectForm.d.ts.map +1 -0
  77. package/dist/types/src/components/ObjectForm/index.d.ts +2 -0
  78. package/dist/types/src/components/ObjectForm/index.d.ts.map +1 -0
  79. package/dist/types/src/components/ObjectPicker/ObjectPicker.d.ts +37 -10
  80. package/dist/types/src/components/ObjectPicker/ObjectPicker.d.ts.map +1 -1
  81. package/dist/types/src/components/ObjectPicker/ObjectPicker.stories.d.ts +65 -58
  82. package/dist/types/src/components/ObjectPicker/ObjectPicker.stories.d.ts.map +1 -1
  83. package/dist/types/src/components/ObjectProperties/ObjectProperties.d.ts +9 -0
  84. package/dist/types/src/components/ObjectProperties/ObjectProperties.d.ts.map +1 -0
  85. package/dist/types/src/components/ObjectProperties/ObjectProperties.stories.d.ts +128 -0
  86. package/dist/types/src/components/ObjectProperties/ObjectProperties.stories.d.ts.map +1 -0
  87. package/dist/types/src/components/ObjectProperties/ObjectProperties.test.d.ts +2 -0
  88. package/dist/types/src/components/ObjectProperties/ObjectProperties.test.d.ts.map +1 -0
  89. package/dist/types/src/components/ObjectProperties/index.d.ts +2 -0
  90. package/dist/types/src/components/ObjectProperties/index.d.ts.map +1 -0
  91. package/dist/types/src/components/ObjectTree/ObjectTree.d.ts +9 -0
  92. package/dist/types/src/components/ObjectTree/ObjectTree.d.ts.map +1 -0
  93. package/dist/types/src/components/ObjectTree/ObjectTree.stories.d.ts +87 -0
  94. package/dist/types/src/components/ObjectTree/ObjectTree.stories.d.ts.map +1 -0
  95. package/dist/types/src/components/ObjectTree/index.d.ts +2 -0
  96. package/dist/types/src/components/ObjectTree/index.d.ts.map +1 -0
  97. package/dist/types/src/components/Settings/Settings.d.ts +51 -0
  98. package/dist/types/src/components/Settings/Settings.d.ts.map +1 -0
  99. package/dist/types/src/components/Settings/Settings.stories.d.ts +85 -0
  100. package/dist/types/src/components/Settings/Settings.stories.d.ts.map +1 -0
  101. package/dist/types/src/components/Settings/SettingsFieldSet.d.ts +43 -0
  102. package/dist/types/src/components/Settings/SettingsFieldSet.d.ts.map +1 -0
  103. package/dist/types/src/components/Settings/SettingsItem.d.ts +16 -0
  104. package/dist/types/src/components/Settings/SettingsItem.d.ts.map +1 -0
  105. package/dist/types/src/components/Settings/SettingsSelectField.d.ts +18 -0
  106. package/dist/types/src/components/Settings/SettingsSelectField.d.ts.map +1 -0
  107. package/dist/types/src/components/Settings/index.d.ts +4 -0
  108. package/dist/types/src/components/Settings/index.d.ts.map +1 -0
  109. package/dist/types/src/components/ViewEditor/ViewEditor.d.ts +14 -18
  110. package/dist/types/src/components/ViewEditor/ViewEditor.d.ts.map +1 -1
  111. package/dist/types/src/components/ViewEditor/ViewEditor.stories.d.ts +71 -64
  112. package/dist/types/src/components/ViewEditor/ViewEditor.stories.d.ts.map +1 -1
  113. package/dist/types/src/components/index.d.ts +4 -1
  114. package/dist/types/src/components/index.d.ts.map +1 -1
  115. package/dist/types/src/components/testing.d.ts +13 -6
  116. package/dist/types/src/components/testing.d.ts.map +1 -1
  117. package/dist/types/src/hooks/index.d.ts +2 -3
  118. package/dist/types/src/hooks/index.d.ts.map +1 -1
  119. package/dist/types/src/hooks/useFormHandler.d.ts +82 -0
  120. package/dist/types/src/hooks/useFormHandler.d.ts.map +1 -0
  121. package/dist/types/src/hooks/useKeyHandler.d.ts +7 -0
  122. package/dist/types/src/hooks/useKeyHandler.d.ts.map +1 -0
  123. package/dist/types/src/index.d.ts +0 -2
  124. package/dist/types/src/index.d.ts.map +1 -1
  125. package/dist/types/src/translations.d.ts +66 -59
  126. package/dist/types/src/translations.d.ts.map +1 -1
  127. package/dist/types/src/util/field-type.d.ts +19 -0
  128. package/dist/types/src/util/field-type.d.ts.map +1 -0
  129. package/dist/types/src/util/field.d.ts +3 -3
  130. package/dist/types/src/util/field.d.ts.map +1 -1
  131. package/dist/types/src/util/formatting.d.ts +2 -2
  132. package/dist/types/src/util/formatting.d.ts.map +1 -1
  133. package/dist/types/src/util/index.d.ts +2 -1
  134. package/dist/types/src/util/index.d.ts.map +1 -1
  135. package/dist/types/src/util/properties.d.ts +17 -0
  136. package/dist/types/src/util/properties.d.ts.map +1 -0
  137. package/dist/types/src/util/properties.test.d.ts +2 -0
  138. package/dist/types/src/util/properties.test.d.ts.map +1 -0
  139. package/dist/types/src/util/refs.d.ts +2 -2
  140. package/dist/types/src/util/refs.d.ts.map +1 -1
  141. package/dist/types/tsconfig.tsbuildinfo +1 -1
  142. package/package.json +61 -52
  143. package/src/components/FieldEditor/FieldEditor.stories.tsx +44 -25
  144. package/src/components/FieldEditor/FieldEditor.test.tsx +0 -1
  145. package/src/components/FieldEditor/FieldEditor.tsx +84 -84
  146. package/src/components/Form/AUDIT.md +17 -0
  147. package/src/components/Form/Form.stories.tsx +134 -252
  148. package/src/components/Form/Form.tsx +417 -109
  149. package/src/components/Form/FormField.tsx +332 -0
  150. package/src/components/Form/FormFieldComponent.stories.tsx +65 -0
  151. package/src/components/Form/FormFieldComponent.tsx +206 -0
  152. package/src/components/Form/FormFieldSet.tsx +173 -0
  153. package/src/components/Form/FormTooltipsContext.ts +25 -0
  154. package/src/components/Form/Layout/FormLayout.stories.tsx +339 -0
  155. package/src/components/Form/Layout/FormLayout.tsx +123 -0
  156. package/src/components/Form/Layout/annotation.ts +38 -0
  157. package/src/components/Form/Layout/index.ts +7 -0
  158. package/src/components/Form/Layout/parser.test.ts +120 -0
  159. package/src/components/Form/Layout/parser.ts +167 -0
  160. package/src/components/Form/README.md +3 -0
  161. package/src/components/Form/fields/ArrayField.tsx +185 -0
  162. package/src/components/Form/fields/BooleanField.tsx +22 -0
  163. package/src/components/Form/fields/DateField.tsx +95 -0
  164. package/src/components/Form/fields/GeoPointField.tsx +124 -0
  165. package/src/components/Form/fields/MarkdownField.stories.tsx +206 -0
  166. package/src/components/Form/fields/MarkdownField.tsx +168 -0
  167. package/src/components/Form/fields/NumberField.tsx +39 -0
  168. package/src/components/Form/fields/RefField.tsx +232 -0
  169. package/src/components/Form/fields/SelectField.tsx +77 -0
  170. package/src/components/Form/{custom/SelectOptionsInput.stories.tsx → fields/SelectOptionField.stories.tsx} +23 -18
  171. package/src/components/Form/{custom/SelectOptionsInput.tsx → fields/SelectOptionField.tsx} +32 -25
  172. package/src/components/Form/fields/TextAreaField.tsx +38 -0
  173. package/src/components/Form/fields/TextField.tsx +38 -0
  174. package/src/components/Form/{custom/TupleInput.tsx → fields/TupleField.tsx} +17 -9
  175. package/src/components/Form/fields/index.ts +16 -0
  176. package/src/components/Form/index.ts +5 -4
  177. package/src/components/ObjectForm/ObjectForm.tsx +115 -0
  178. package/src/components/{ControlSection → ObjectForm}/index.ts +1 -1
  179. package/src/components/ObjectPicker/ObjectPicker.stories.tsx +26 -21
  180. package/src/components/ObjectPicker/ObjectPicker.tsx +122 -81
  181. package/src/components/ObjectProperties/ObjectProperties.stories.tsx +350 -0
  182. package/src/components/ObjectProperties/ObjectProperties.test.tsx +108 -0
  183. package/src/components/ObjectProperties/ObjectProperties.tsx +147 -0
  184. package/src/components/ObjectProperties/index.ts +5 -0
  185. package/src/components/ObjectTree/ObjectTree.stories.tsx +134 -0
  186. package/src/components/ObjectTree/ObjectTree.tsx +190 -0
  187. package/src/components/ObjectTree/index.ts +5 -0
  188. package/src/components/Settings/AUDIT.md +51 -0
  189. package/src/components/Settings/Settings.stories.tsx +104 -0
  190. package/src/components/Settings/Settings.tsx +81 -0
  191. package/src/components/Settings/SettingsFieldSet.tsx +257 -0
  192. package/src/components/Settings/SettingsItem.tsx +58 -0
  193. package/src/components/Settings/SettingsSelectField.tsx +59 -0
  194. package/src/components/Settings/index.ts +7 -0
  195. package/src/components/ViewEditor/ViewEditor.stories.tsx +62 -71
  196. package/src/components/ViewEditor/ViewEditor.test.tsx +10 -10
  197. package/src/components/ViewEditor/ViewEditor.tsx +308 -217
  198. package/src/components/index.ts +4 -1
  199. package/src/components/testing.tsx +24 -11
  200. package/src/hooks/index.ts +2 -3
  201. package/src/hooks/useFormHandler.ts +380 -0
  202. package/src/hooks/useKeyHandler.tsx +37 -0
  203. package/src/index.ts +0 -4
  204. package/src/translations.ts +71 -64
  205. package/src/util/field-type.ts +60 -0
  206. package/src/util/field.ts +25 -25
  207. package/src/util/formatting.ts +22 -22
  208. package/src/util/index.ts +2 -1
  209. package/src/util/properties.test.ts +78 -0
  210. package/src/util/properties.ts +38 -0
  211. package/src/util/refs.ts +13 -15
  212. package/src/vite-env.d.ts +10 -0
  213. package/dist/types/src/components/ControlSection/ControlSection.d.ts +0 -23
  214. package/dist/types/src/components/ControlSection/ControlSection.d.ts.map +0 -1
  215. package/dist/types/src/components/ControlSection/ControlSection.stories.d.ts +0 -83
  216. package/dist/types/src/components/ControlSection/ControlSection.stories.d.ts.map +0 -1
  217. package/dist/types/src/components/ControlSection/index.d.ts +0 -2
  218. package/dist/types/src/components/ControlSection/index.d.ts.map +0 -1
  219. package/dist/types/src/components/Form/ArrayField.d.ts +0 -16
  220. package/dist/types/src/components/Form/ArrayField.d.ts.map +0 -1
  221. package/dist/types/src/components/Form/Defaults.d.ts +0 -14
  222. package/dist/types/src/components/Form/Defaults.d.ts.map +0 -1
  223. package/dist/types/src/components/Form/FormActions.d.ts +0 -9
  224. package/dist/types/src/components/Form/FormActions.d.ts.map +0 -1
  225. package/dist/types/src/components/Form/FormContent.d.ts +0 -63
  226. package/dist/types/src/components/Form/FormContent.d.ts.map +0 -1
  227. package/dist/types/src/components/Form/FormContext.d.ts +0 -23
  228. package/dist/types/src/components/Form/FormContext.d.ts.map +0 -1
  229. package/dist/types/src/components/Form/Input.d.ts +0 -29
  230. package/dist/types/src/components/Form/Input.d.ts.map +0 -1
  231. package/dist/types/src/components/Form/RefField.d.ts +0 -22
  232. package/dist/types/src/components/Form/RefField.d.ts.map +0 -1
  233. package/dist/types/src/components/Form/custom/GeoPointInput.d.ts +0 -4
  234. package/dist/types/src/components/Form/custom/GeoPointInput.d.ts.map +0 -1
  235. package/dist/types/src/components/Form/custom/SelectOptionsInput.d.ts +0 -4
  236. package/dist/types/src/components/Form/custom/SelectOptionsInput.d.ts.map +0 -1
  237. package/dist/types/src/components/Form/custom/SelectOptionsInput.stories.d.ts +0 -78
  238. package/dist/types/src/components/Form/custom/SelectOptionsInput.stories.d.ts.map +0 -1
  239. package/dist/types/src/components/Form/custom/TupleInput.d.ts +0 -6
  240. package/dist/types/src/components/Form/custom/TupleInput.d.ts.map +0 -1
  241. package/dist/types/src/components/Form/custom/index.d.ts +0 -4
  242. package/dist/types/src/components/Form/custom/index.d.ts.map +0 -1
  243. package/dist/types/src/components/Form/factory.d.ts +0 -8
  244. package/dist/types/src/components/Form/factory.d.ts.map +0 -1
  245. package/dist/types/src/hooks/useForm.d.ts +0 -67
  246. package/dist/types/src/hooks/useForm.d.ts.map +0 -1
  247. package/dist/types/src/hooks/useQueryRefOptions.d.ts +0 -29
  248. package/dist/types/src/hooks/useQueryRefOptions.d.ts.map +0 -1
  249. package/dist/types/src/hooks/useRefQueryLookupHandler.d.ts +0 -8
  250. package/dist/types/src/hooks/useRefQueryLookupHandler.d.ts.map +0 -1
  251. package/dist/types/src/util/schema.d.ts +0 -3
  252. package/dist/types/src/util/schema.d.ts.map +0 -1
  253. package/src/components/ControlSection/ControlSection.stories.tsx +0 -38
  254. package/src/components/ControlSection/ControlSection.tsx +0 -153
  255. package/src/components/Form/ArrayField.tsx +0 -125
  256. package/src/components/Form/Defaults.tsx +0 -205
  257. package/src/components/Form/FormActions.tsx +0 -54
  258. package/src/components/Form/FormContent.tsx +0 -362
  259. package/src/components/Form/FormContext.tsx +0 -110
  260. package/src/components/Form/Input.tsx +0 -54
  261. package/src/components/Form/RefField.tsx +0 -283
  262. package/src/components/Form/custom/GeoPointInput.tsx +0 -80
  263. package/src/components/Form/custom/index.ts +0 -7
  264. package/src/components/Form/factory.tsx +0 -31
  265. package/src/hooks/useForm.ts +0 -296
  266. package/src/hooks/useQueryRefOptions.ts +0 -62
  267. package/src/hooks/useRefQueryLookupHandler.ts +0 -39
  268. package/src/util/schema.ts +0 -12
@@ -0,0 +1,134 @@
1
+ //
2
+ // Copyright 2026 DXOS.org
3
+ //
4
+
5
+ import { type Meta, type StoryObj } from '@storybook/react-vite';
6
+ import React from 'react';
7
+
8
+ import { parseProto } from '@dxos/effect-proto';
9
+ import { log } from '@dxos/log';
10
+ import { type Config as ConfigProto, Runtime } from '@dxos/protocols/proto/dxos/config';
11
+ // Raw .proto source resolved through `@dxos/protocols`'s `./proto/dxos/*.proto`
12
+ // exports entry. Vite's `?raw` query suffix returns the file as a string so we
13
+ // can hand it straight to `parseProto` -- the same flow the unit test uses.
14
+ import configProto from '@dxos/protocols/proto/dxos/config.proto?raw';
15
+ import { Syntax } from '@dxos/react-ui-syntax-highlighter';
16
+ import { withLayout, withTheme } from '@dxos/react-ui/testing';
17
+
18
+ import { translations } from '#translations';
19
+
20
+ import { Form } from '../Form';
21
+ import { ObjectTree } from './ObjectTree';
22
+
23
+ const registry = parseProto(configProto);
24
+ const ConfigSchema = registry.get('dxos.config.Config');
25
+
26
+ // A representative Config value that exercises nested messages, arrays, enums
27
+ // (rendered as their string names by the proto codec), and `google.protobuf.Any`.
28
+ const value: ConfigProto = {
29
+ version: 1,
30
+ runtime: {
31
+ client: {
32
+ log: { filter: 'info', prefix: 'app' },
33
+ storage: {
34
+ persistent: true,
35
+ sqliteMode: Runtime.Client.Storage.SqliteMode.OPFS,
36
+ dataRoot: '/var/lib/dxos',
37
+ },
38
+ edgeFeatures: {
39
+ echoReplicator: true,
40
+ subductionReplicator: true,
41
+ signaling: true,
42
+ },
43
+ servicesMode: Runtime.Client.ServicesMode.SHARED_WORKER,
44
+ enableSnapshots: false,
45
+ snapshotInterval: 60_000,
46
+ },
47
+ app: {
48
+ org: 'DXOS',
49
+ theme: 'dark',
50
+ website: 'https://dxos.org',
51
+ build: {
52
+ timestamp: '2026-05-22T12:00:00Z',
53
+ commitHash: 'abc1234',
54
+ version: '0.8.3',
55
+ branch: 'main',
56
+ },
57
+ },
58
+ services: {
59
+ edge: { url: 'wss://edge.dxos.org' },
60
+ signaling: [
61
+ { server: 'wss://signal-1.dxos.org', api: 'v1' },
62
+ { server: 'wss://signal-2.dxos.org', api: 'v1' },
63
+ ],
64
+ iceProviders: [{ urls: 'turn:turn.dxos.org' }],
65
+ },
66
+ keys: [
67
+ { name: 'OPENAI_API_KEY', value: 'sk-...' },
68
+ { name: 'ANTHROPIC_API_KEY', value: 'ant-...' },
69
+ ],
70
+ },
71
+ };
72
+
73
+ const meta = {
74
+ title: 'ui/react-ui-form/ObjectTree',
75
+ component: ObjectTree,
76
+ decorators: [withTheme(), withLayout({ layout: 'column', scroll: true })],
77
+ parameters: {
78
+ layout: 'fullscreen',
79
+ translations,
80
+ },
81
+ } satisfies Meta<typeof ObjectTree>;
82
+
83
+ export default meta;
84
+
85
+ type Story = StoryObj<typeof meta>;
86
+
87
+ export const Default: Story = {
88
+ args: {
89
+ schema: ConfigSchema,
90
+ value: value,
91
+ },
92
+ };
93
+
94
+ export const JSON = () => (
95
+ <Syntax.Root data={value}>
96
+ <Syntax.Content>
97
+ <Syntax.Filter />
98
+ <Syntax.Viewport>
99
+ <Syntax.Code />
100
+ </Syntax.Viewport>
101
+ </Syntax.Content>
102
+ </Syntax.Root>
103
+ );
104
+
105
+ // Renders the same proto-derived `ConfigSchema` + `value` through the
106
+ // interactive `Form` component, for side-by-side comparison with the
107
+ // read-only ObjectTree above. `db` is intentionally omitted -- the proto
108
+ // schema is not an ECHO type, so there's no space/database in play.
109
+ export const WithForm = () => (
110
+ <Form.Root
111
+ schema={ConfigSchema as any}
112
+ defaultValues={value as any}
113
+ onSave={(next) => log.info('save', { next })}
114
+ onCancel={() => log.info('cancel')}
115
+ >
116
+ <Form.Viewport>
117
+ <Form.Content>
118
+ <Form.FieldSet />
119
+ <Form.Actions />
120
+ </Form.Content>
121
+ </Form.Viewport>
122
+ </Form.Root>
123
+ );
124
+
125
+ export const WithReadOnlyForm = () => (
126
+ <Form.Root schema={ConfigSchema as any} defaultValues={value as any} readonly={true}>
127
+ <Form.Viewport>
128
+ <Form.Content>
129
+ <Form.FieldSet />
130
+ <Form.Actions />
131
+ </Form.Content>
132
+ </Form.Viewport>
133
+ </Form.Root>
134
+ );
@@ -0,0 +1,190 @@
1
+ //
2
+ // Copyright 2026 DXOS.org
3
+ //
4
+
5
+ // A minimal, read-only schema-driven viewer. Walks an Effect Schema and a
6
+ // matching value and produces a flat list of indented key/value rows -- one
7
+ // per leaf field, with nested Struct fields contributing a header row plus
8
+ // recursively-indented children, and arrays expanded as `[index]` rows.
9
+ //
10
+ // Designed to drive a hierarchical editor over schemas produced by
11
+ // `@dxos/effect-proto` (e.g. the proto-generated `dxos.config.Config`
12
+ // schema), but works for any Effect Struct schema.
13
+
14
+ import * as Schema from 'effect/Schema';
15
+ import * as SchemaAST from 'effect/SchemaAST';
16
+ import React, { type ReactElement, type ReactNode, type RefAttributes } from 'react';
17
+
18
+ import { composable, composableProps } from '@dxos/react-ui';
19
+ import { mx } from '@dxos/ui-theme';
20
+ import { type ComposableProps } from '@dxos/ui-types';
21
+
22
+ export type ObjectTreeProps<T> = {
23
+ schema: Schema.Schema<T, any, never>;
24
+ value: T;
25
+ };
26
+
27
+ /**
28
+ * Render a value as a sequence of key/value rows whose shape and order are
29
+ * driven by `schema`. Object-typed properties become a header row whose
30
+ * children are indented under it; arrays expand element-by-element.
31
+ *
32
+ * Composable: spreads unknown props (incl. `className` / `classNames` /
33
+ * `style` / `role`) onto its root `<div>` and forwards its ref, so it can
34
+ * be slotted into themed parents like `Panel.Content`.
35
+ */
36
+ const ObjectTreeImpl = composable<HTMLDivElement, ObjectTreeProps<any>>(({ schema, value, ...props }, forwardedRef) => (
37
+ <div {...composableProps(props, { role: 'table', classNames: 'font-mono text-sm w-full' })} ref={forwardedRef}>
38
+ <Node ast={schema.ast} value={value} label={null} depth={0} />
39
+ </div>
40
+ ));
41
+
42
+ ObjectTreeImpl.displayName = 'ObjectTree';
43
+
44
+ // `composable<HTMLDivElement, ObjectTreeProps<any>>` erases the `T` type
45
+ // parameter inside the factory; the cast restores the generic signature for
46
+ // consumers without changing the underlying component (the COMPOSABLE marker
47
+ // stays attached). This is the pattern documented on `composable()` itself.
48
+ export const ObjectTree = ObjectTreeImpl as unknown as <T>(
49
+ props: ComposableProps<ObjectTreeProps<T>> & RefAttributes<HTMLDivElement>,
50
+ ) => ReactElement | null;
51
+
52
+ /**
53
+ * Peel wrappers that don't affect the runtime shape: Refinements pass their
54
+ * `from`, Suspends resolve via `f()`, and Transformations expose the encoded
55
+ * (input) side. Repeated unwrapping handles chains like `Schema.suspend ->
56
+ * Refinement -> TypeLiteral`.
57
+ */
58
+ const unwrap = (ast: SchemaAST.AST): SchemaAST.AST => {
59
+ if (SchemaAST.isRefinement(ast)) {
60
+ return unwrap(ast.from);
61
+ }
62
+ if (SchemaAST.isSuspend(ast)) {
63
+ return unwrap(ast.f());
64
+ }
65
+ if (SchemaAST.isTransformation(ast)) {
66
+ return unwrap(ast.from);
67
+ }
68
+ return ast;
69
+ };
70
+
71
+ /**
72
+ * Effect models `Schema.optional(X)` as a property whose type AST is
73
+ * `Union(X, UndefinedKeyword)`. For walking purposes we want X -- the
74
+ * undefined branch carries no schema information for the viewer.
75
+ */
76
+ const stripOptional = (ast: SchemaAST.AST): SchemaAST.AST => {
77
+ if (!SchemaAST.isUnion(ast)) {
78
+ return ast;
79
+ }
80
+ const nonUndef = ast.types.filter((t) => !SchemaAST.isUndefinedKeyword(t));
81
+ return nonUndef.length === 1 ? nonUndef[0] : ast;
82
+ };
83
+
84
+ type NodeProps = {
85
+ ast: SchemaAST.AST;
86
+ value: unknown;
87
+ /** Row label; `null` means render children directly without a header row. */
88
+ label: ReactNode | null;
89
+ depth: number;
90
+ };
91
+
92
+ const Node = ({ ast, value, label, depth }: NodeProps) => {
93
+ const inner = unwrap(stripOptional(ast));
94
+
95
+ // Struct: header row + indented children. Skipping the header at depth 0
96
+ // keeps the root flush-left.
97
+ if (SchemaAST.isTypeLiteral(inner) && isPlainObject(value)) {
98
+ const childDepth = label === null ? depth : depth + 1;
99
+ return (
100
+ <>
101
+ {label !== null && <Row label={label} value={null} depth={depth} kind='group' />}
102
+ {inner.propertySignatures.map((prop) => (
103
+ <Node
104
+ key={String(prop.name)}
105
+ ast={prop.type}
106
+ value={(value as Record<string, unknown>)[String(prop.name)]}
107
+ label={String(prop.name)}
108
+ depth={childDepth}
109
+ />
110
+ ))}
111
+ </>
112
+ );
113
+ }
114
+
115
+ // Array (`Schema.Array(X)` -> TupleType with single rest element).
116
+ if (SchemaAST.isTupleType(inner) && Array.isArray(value)) {
117
+ const elemType = inner.rest[0]?.type;
118
+ return (
119
+ <>
120
+ {label !== null && <Row label={label} value={`Array(${value.length})`} depth={depth} kind='group' />}
121
+ {value.map((item, index) => (
122
+ <Node
123
+ key={index}
124
+ ast={elemType ?? SchemaAST.unknownKeyword}
125
+ value={item}
126
+ label={`[${index}]`}
127
+ depth={depth + 1}
128
+ />
129
+ ))}
130
+ </>
131
+ );
132
+ }
133
+
134
+ // Leaf (scalars, unions of literals, unknown, etc.).
135
+ return <Row label={label ?? ''} value={formatLeaf(value)} depth={depth} kind='leaf' />;
136
+ };
137
+
138
+ type RowProps = {
139
+ label: ReactNode;
140
+ value: ReactNode;
141
+ depth: number;
142
+ kind: 'group' | 'leaf';
143
+ };
144
+
145
+ const Row = ({ label, value, depth }: RowProps) => (
146
+ <div role='row' className={mx('flex items-baseline gap-3 py-1')} style={{ paddingInlineStart: 8 + depth * 16 }}>
147
+ <div role='cell' className='text-subdued'>
148
+ {label}
149
+ </div>
150
+ <div role='cell' className={mx('break-all text-base-foreground')}>
151
+ {value}
152
+ </div>
153
+ </div>
154
+ );
155
+
156
+ const isPlainObject = (value: unknown): value is Record<string, unknown> =>
157
+ typeof value === 'object' && value !== null && !Array.isArray(value);
158
+
159
+ /**
160
+ * Format a leaf value for display. Strings are quoted so empty strings are
161
+ * visible; missing values surface as an em-dash so the row stays present.
162
+ */
163
+ const formatLeaf = (value: unknown): ReactNode => {
164
+ if (value === undefined) {
165
+ return null;
166
+ }
167
+
168
+ if (value === null) {
169
+ return <span className='text-description'>null</span>;
170
+ }
171
+
172
+ switch (typeof value) {
173
+ case 'string':
174
+ // Quote strings so empty strings remain visible (`""` rather than blank).
175
+ return <span className='text-blue-text'>{JSON.stringify(value)}</span>;
176
+ case 'number':
177
+ case 'bigint':
178
+ return <span className='text-green-text'>{String(value)}</span>;
179
+ case 'boolean':
180
+ return <span className='text-purple-text'>{String(value)}</span>;
181
+ default:
182
+ // Objects we couldn't expand via schema (e.g. `google.protobuf.Any` -> Unknown)
183
+ // are rendered as compact JSON.
184
+ try {
185
+ return JSON.stringify(value);
186
+ } catch {
187
+ return String(value);
188
+ }
189
+ }
190
+ };
@@ -0,0 +1,5 @@
1
+ //
2
+ // Copyright 2026 DXOS.org
3
+ //
4
+
5
+ export * from './ObjectTree';
@@ -0,0 +1,51 @@
1
+ # Plugin Settings Audit
2
+
3
+ Tracks which plugin settings panels use schema-driven `Settings.FieldSet` and which still hand-roll their controls.
4
+
5
+ The target shape for a plugin settings panel is:
6
+
7
+ ```tsx
8
+ <SettingsForm.Viewport>
9
+ <SettingsForm.Section title={t('settings.title', { ns: meta.id })}>
10
+ <SettingsForm.FieldSet
11
+ readonly={!onSettingsChange}
12
+ schema={Settings.Settings}
13
+ values={settings}
14
+ onValuesChanged={(values) => onSettingsChange?.(() => values)}
15
+ />
16
+ </SettingsForm.Section>
17
+ </SettingsForm.Viewport>
18
+ ```
19
+
20
+ Field labels and descriptions come from `.annotations({ title, description })` on each schema field. Use `fieldMap` for runtime-driven option lists (model pickers, etc.). Use the `visible` callback to gate fields on environment or sibling values.
21
+
22
+ ## FieldSet adopters
23
+
24
+ | Plugin | Notes |
25
+ | ---------------------- | -------------------------------------------------------------------------------------------------------- |
26
+ | `plugin-assistant` | Nested `modelDefaults` struct; `fieldMap` provides per-provider model selectors via `createSelectField`. |
27
+ | `plugin-crx` | Two booleans. |
28
+ | `plugin-deck` | Five booleans; `enableNativeRedirect` hidden via `visible` when `isSocket`. |
29
+ | `plugin-inbox` | Single boolean. |
30
+ | `plugin-markdown` | Original reference adopter. `snippets` text field gated via `visible`/`fieldMap`. |
31
+ | `plugin-meeting` | Single boolean. |
32
+ | `plugin-observability` | Boolean; info `Message.Root` banner kept above `FieldSet`. |
33
+ | `plugin-presenter` | Single boolean. |
34
+ | `plugin-sample` | Single boolean. |
35
+ | `plugin-script` | Mixed: auth `Button` retained as a manual `SettingsForm.Item`; `editorInputMode` rendered by `FieldSet`. |
36
+ | `plugin-sketch` | Boolean + `gridType` literal union (rendered as a Select — was previously a Switch). |
37
+
38
+ ## Skipped
39
+
40
+ | Plugin | Reason |
41
+ | ------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
42
+ | `plugin-debug` | Action buttons (download diagnostics, download logs, repair), `Toast`, and a storage-adapter selector wired to `client.config` rather than the plugin settings atom. |
43
+ | `plugin-space` | Renders a list of spaces with per-space "open settings" buttons — not bound to any schema field. |
44
+ | `plugin-spacetime` | Section is currently empty; nothing to render. |
45
+ | `plugin-thread` | Settings schema is `Schema.Struct({})` — nothing to render. |
46
+
47
+ ## When to extend `SettingsFieldSet` vs. add a custom field
48
+
49
+ - **Extend `SettingsFieldSet`** when the rendering rule applies generally to a schema construct (e.g. nested structs recursing into sibling fields, literal unions becoming Selects). Keep auto-detection in `detectFieldType`.
50
+ - **Use `fieldMap`** when the schema can't fully describe the rendering — runtime-driven option lists, custom inputs, or override of the auto-rendered control. Use the typed form `SettingsFieldMap<T>` (or `satisfies SettingsFieldMap<Settings>`) so the entries are checked against the values shape.
51
+ - **Drop back to manual `SettingsForm.Item`** when the panel needs UI that isn't bound to a settings field at all (action buttons, info banners, lists of external entities). These coexist as siblings of `FieldSet` inside the same `Section`.
@@ -0,0 +1,104 @@
1
+ //
2
+ // Copyright 2024 DXOS.org
3
+ //
4
+
5
+ import { type Meta, type StoryObj } from '@storybook/react-vite';
6
+ import * as Schema from 'effect/Schema';
7
+ import React from 'react';
8
+
9
+ import { random } from '@dxos/random';
10
+ import { Button, Input } from '@dxos/react-ui';
11
+ import { withLayout, withTheme } from '@dxos/react-ui/testing';
12
+
13
+ import { translations } from '#translations';
14
+
15
+ import { Settings } from './Settings';
16
+
17
+ random.seed(132);
18
+
19
+ const DefaultStory = () => {
20
+ return (
21
+ <Settings.Viewport>
22
+ <Settings.Section title='Settings' description={random.lorem.paragraphs(1)}>
23
+ <Settings.Item title={random.lorem.sentence(2)} description={random.lorem.paragraphs(1)}>
24
+ <Input.TextInput placeholder='Input' />
25
+ </Settings.Item>
26
+ <Settings.Item title={random.lorem.sentence(2)} description={random.lorem.paragraphs(2)}>
27
+ <Input.Switch />
28
+ </Settings.Item>
29
+ <Settings.Item title={random.lorem.sentence(3)} description={random.lorem.paragraphs(2)}>
30
+ <Button>Test</Button>
31
+ </Settings.Item>
32
+ </Settings.Section>
33
+ <Settings.Section title='Panel Example'>
34
+ <Settings.Panel>
35
+ <h3 className='text-lg mb-2'>Members</h3>
36
+ <p className='text-description'>Content inside a panel.</p>
37
+ </Settings.Panel>
38
+ </Settings.Section>
39
+ </Settings.Viewport>
40
+ );
41
+ };
42
+
43
+ const meta = {
44
+ title: 'ui/react-ui-form/Settings',
45
+ render: DefaultStory,
46
+ decorators: [withTheme(), withLayout({ layout: 'fullscreen' })],
47
+ parameters: {
48
+ layout: 'fullscreen',
49
+ translations,
50
+ },
51
+ } satisfies Meta;
52
+
53
+ export default meta;
54
+
55
+ type Story = StoryObj<typeof meta>;
56
+
57
+ export const Default: Story = {};
58
+
59
+ const TestViewModes = ['preview', 'readonly', 'source'] as const;
60
+ const TestViewMode = Schema.Union(...TestViewModes.map((mode) => Schema.Literal(mode)));
61
+
62
+ const TestSettings = Schema.mutable(
63
+ Schema.Struct({
64
+ viewMode: TestViewMode.annotations({ title: 'View mode', description: 'Default document view mode.' }),
65
+ toolbar: Schema.optional(
66
+ Schema.Boolean.annotations({ title: 'Show toolbar', description: 'Display formatting toolbar.' }),
67
+ ),
68
+ fontSize: Schema.optional(
69
+ Schema.Number.annotations({ title: 'Font size', description: 'Editor font size in pixels.' }),
70
+ ),
71
+ placeholder: Schema.optional(
72
+ Schema.String.annotations({ title: 'Placeholder', description: 'Default placeholder text.' }),
73
+ ),
74
+ debug: Schema.optional(Schema.Boolean.annotations({ title: 'Debug mode', description: 'Enable debug features.' })),
75
+ }),
76
+ );
77
+
78
+ type TestSettings = Schema.Schema.Type<typeof TestSettings>;
79
+
80
+ const FieldSetStory = () => {
81
+ const [values, setValues] = React.useState<TestSettings>({
82
+ viewMode: 'preview',
83
+ toolbar: true,
84
+ fontSize: 14,
85
+ debug: false,
86
+ });
87
+
88
+ return (
89
+ <Settings.Viewport>
90
+ <Settings.Section title='Plugin Settings (Auto-generated)'>
91
+ <Settings.FieldSet
92
+ schema={TestSettings}
93
+ values={values}
94
+ onValuesChanged={setValues}
95
+ visible={(path, values) => path !== 'placeholder' || !!values.debug}
96
+ />
97
+ </Settings.Section>
98
+ </Settings.Viewport>
99
+ );
100
+ };
101
+
102
+ export const FieldSet: Story = {
103
+ render: FieldSetStory,
104
+ };
@@ -0,0 +1,81 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ import React, { type PropsWithChildren } from 'react';
6
+
7
+ import { type Label, ScrollArea, type ThemedClassName, toLocalizedString, useTranslation } from '@dxos/react-ui';
8
+ import { composable, composableProps } from '@dxos/react-ui';
9
+ import { mx } from '@dxos/ui-theme';
10
+
11
+ import { translationKey } from '#translations';
12
+
13
+ import { SettingsFieldSet } from './SettingsFieldSet';
14
+ import { SettingsItem, SettingsPanel } from './SettingsItem';
15
+
16
+ const SETTINGS_ROOT_NAME = 'Settings.Root';
17
+ const SETTINGS_VIEWPORT_NAME = 'Settings.Viewport';
18
+ const SETTINGS_SECTION_NAME = 'Settings.Section';
19
+
20
+ //
21
+ // Root (headless — no styling, no scroll).
22
+ //
23
+
24
+ type SettingsRootProps = ThemedClassName<PropsWithChildren>;
25
+
26
+ const SettingsRoot = ({ children, classNames }: SettingsRootProps) => {
27
+ return <div className={mx('flex flex-col gap-2', classNames)}>{children}</div>;
28
+ };
29
+
30
+ SettingsRoot.displayName = SETTINGS_ROOT_NAME;
31
+
32
+ //
33
+ // Viewport (scroll area + padding + document layout).
34
+ //
35
+
36
+ const SettingsViewport = composable<HTMLDivElement>(({ children, ...props }, forwardedRef) => {
37
+ return (
38
+ <ScrollArea.Root {...composableProps(props)} orientation='vertical' centered thin ref={forwardedRef}>
39
+ <ScrollArea.Viewport classNames='px-4 pointer-coarse:px-2'>
40
+ <div className='dx-document flex flex-col gap-2 py-4'>{children}</div>
41
+ </ScrollArea.Viewport>
42
+ </ScrollArea.Root>
43
+ );
44
+ });
45
+
46
+ SettingsViewport.displayName = SETTINGS_VIEWPORT_NAME;
47
+
48
+ //
49
+ // Section (title + optional description + children with spacing).
50
+ //
51
+
52
+ type SettingsSectionProps = PropsWithChildren<{
53
+ title: Label;
54
+ description?: Label;
55
+ }>;
56
+
57
+ const SettingsSection = ({ title, description, children }: SettingsSectionProps) => {
58
+ const { t } = useTranslation(translationKey);
59
+ return (
60
+ <>
61
+ <h2 className='px-trim-md mt-trim-md text-xl'>{toLocalizedString(title, t)}</h2>
62
+ {description && <p className='px-trim-md text-description'>{toLocalizedString(description, t)}</p>}
63
+ <div className='w-full pt-trim-md space-y-trim-md'>{children}</div>
64
+ </>
65
+ );
66
+ };
67
+
68
+ SettingsSection.displayName = SETTINGS_SECTION_NAME;
69
+
70
+ //
71
+ // Settings
72
+ //
73
+
74
+ export const Settings = {
75
+ Root: SettingsRoot,
76
+ Viewport: SettingsViewport,
77
+ Section: SettingsSection,
78
+ Panel: SettingsPanel,
79
+ Item: SettingsItem,
80
+ FieldSet: SettingsFieldSet,
81
+ };