@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,257 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ import * as SchemaAST from 'effect/SchemaAST';
6
+ import * as String from 'effect/String';
7
+ import React, { useCallback, useMemo } from 'react';
8
+
9
+ import { getAnnotation, type SchemaProperty } from '@dxos/effect';
10
+ import { Input, Select } from '@dxos/react-ui';
11
+
12
+ import { getFormProperties, type SelectOption, detectFieldType, getSelectOptionsFromAst } from '../../util';
13
+ import { SettingsItem } from './SettingsItem';
14
+
15
+ //
16
+ // Types
17
+ //
18
+
19
+ /**
20
+ * Props for a custom field renderer.
21
+ * The renderer provides the control only; SettingsItem wrapper is handled by FieldSet.
22
+ */
23
+ export type SettingsFieldProps<T = any> = {
24
+ value: T;
25
+ onChange: (value: T) => void;
26
+ readonly?: boolean;
27
+ };
28
+
29
+ /**
30
+ * Map of property names to either a custom field renderer or a nested field map for struct fields.
31
+ * When a value type `T` is provided, the map shape is checked against the schema's value type.
32
+ */
33
+ export type SettingsFieldMap<T extends Record<string, any> = Record<string, any>> = {
34
+ [K in keyof T]?: NonNullable<T[K]> extends Record<string, any>
35
+ ? SettingsFieldMap<NonNullable<T[K]>>
36
+ : React.FC<SettingsFieldProps<T[K]>>;
37
+ };
38
+
39
+ export type SettingsFieldSetProps<T extends Record<string, any> = Record<string, any>> = {
40
+ /** Effect Schema for the settings object. */
41
+ schema: { ast: SchemaAST.AST };
42
+
43
+ /** Current settings values. */
44
+ values: T;
45
+
46
+ /** Callback when any value changes. Receives a new complete object. */
47
+ onValuesChanged?: (values: T) => void;
48
+
49
+ /** When true, all controls are disabled. */
50
+ readonly?: boolean;
51
+
52
+ /** Map of property names to custom field renderers. Nested objects map to struct sub-fields. */
53
+ fieldMap?: SettingsFieldMap<T>;
54
+
55
+ /** Control field visibility. Receives the dotted path (e.g. `'modelDefaults.edge'`) at every level. */
56
+ visible?: (path: string, values: T) => boolean;
57
+
58
+ /** Override the order of fields. Fields not listed are appended in schema order. Applied per scope. */
59
+ sort?: string[];
60
+
61
+ /** Internal: dot-path prefix used by recursive struct rendering. */
62
+ pathPrefix?: string;
63
+ };
64
+
65
+ //
66
+ // Component
67
+ //
68
+
69
+ export const SettingsFieldSet = <T extends Record<string, any>>({
70
+ schema,
71
+ values,
72
+ onValuesChanged,
73
+ readonly,
74
+ fieldMap,
75
+ visible,
76
+ sort,
77
+ pathPrefix = '',
78
+ }: SettingsFieldSetProps<T>) => {
79
+ const properties = useMemo(() => {
80
+ const props = getFormProperties(schema.ast);
81
+ if (!sort) {
82
+ return props;
83
+ }
84
+ const rank = (name: string) => {
85
+ const idx = sort.indexOf(name);
86
+ return idx === -1 ? Number.POSITIVE_INFINITY : idx;
87
+ };
88
+ return [...props].sort(({ name: a }, { name: b }) => rank(a.toString()) - rank(b.toString()));
89
+ }, [schema, sort]);
90
+
91
+ const handleChange = useCallback(
92
+ (name: string, value: unknown) => {
93
+ onValuesChanged?.({ ...values, [name]: value } as T);
94
+ },
95
+ [values, onValuesChanged],
96
+ );
97
+
98
+ return (
99
+ <>
100
+ {properties.map((property) => {
101
+ const name = property.name.toString();
102
+ const path = pathPrefix ? `${pathPrefix}.${name}` : name;
103
+ if (visible && !visible(path, values)) {
104
+ return null;
105
+ }
106
+
107
+ const entry = fieldMap?.[name as keyof typeof fieldMap] as
108
+ | React.FC<SettingsFieldProps>
109
+ | SettingsFieldMap
110
+ | undefined;
111
+ return (
112
+ <SettingsFieldItem
113
+ key={name}
114
+ path={path}
115
+ property={property}
116
+ value={values[name]}
117
+ onChange={(value) => handleChange(name, value)}
118
+ readonly={readonly}
119
+ customField={typeof entry === 'function' ? entry : undefined}
120
+ nestedFieldMap={typeof entry === 'object' ? entry : undefined}
121
+ visible={visible as (path: string, values: any) => boolean}
122
+ sort={sort}
123
+ />
124
+ );
125
+ })}
126
+ </>
127
+ );
128
+ };
129
+
130
+ SettingsFieldSet.displayName = 'Settings.FieldSet';
131
+
132
+ //
133
+ // Internal: render a single settings field.
134
+ //
135
+
136
+ type SettingsFieldItemProps = {
137
+ property: SchemaProperty;
138
+ path: string;
139
+ value: any;
140
+ onChange: (value: any) => void;
141
+ readonly?: boolean;
142
+ customField?: React.FC<SettingsFieldProps>;
143
+ nestedFieldMap?: SettingsFieldMap;
144
+ visible?: (path: string, values: any) => boolean;
145
+ sort?: string[];
146
+ };
147
+
148
+ const SettingsFieldItem = ({
149
+ property,
150
+ path,
151
+ value,
152
+ onChange,
153
+ readonly,
154
+ customField,
155
+ nestedFieldMap,
156
+ visible,
157
+ sort,
158
+ }: SettingsFieldItemProps) => {
159
+ const { type } = property;
160
+ const name = property.name.toString();
161
+ const title = getAnnotation<string>(SchemaAST.TitleAnnotationId)(type) ?? String.capitalize(name);
162
+ const description = getAnnotation<string>(SchemaAST.DescriptionAnnotationId)(type);
163
+
164
+ // Custom field renderer.
165
+ if (customField) {
166
+ const CustomField = customField;
167
+ return (
168
+ <SettingsItem title={title} description={description}>
169
+ <CustomField value={value} onChange={onChange} readonly={readonly} />
170
+ </SettingsItem>
171
+ );
172
+ }
173
+
174
+ const fieldType = detectFieldType(type);
175
+
176
+ switch (fieldType) {
177
+ case 'struct':
178
+ return (
179
+ <SettingsFieldSet
180
+ schema={{ ast: type }}
181
+ values={value ?? {}}
182
+ onValuesChanged={onChange}
183
+ readonly={readonly}
184
+ fieldMap={nestedFieldMap}
185
+ visible={visible}
186
+ sort={sort}
187
+ pathPrefix={path}
188
+ />
189
+ );
190
+
191
+ case 'boolean':
192
+ return (
193
+ <SettingsItem title={title} description={description}>
194
+ <Input.Switch disabled={readonly} checked={!!value} onCheckedChange={(checked) => onChange(!!checked)} />
195
+ </SettingsItem>
196
+ );
197
+
198
+ case 'select': {
199
+ const options = getSelectOptionsFromAst(type) ?? [];
200
+ return (
201
+ <SettingsItem title={title} description={description}>
202
+ <Select.Root
203
+ disabled={readonly}
204
+ value={value == null ? '' : globalThis.String(value)}
205
+ onValueChange={(selected) => {
206
+ const matched = options.find((option) => globalThis.String(option.value) === selected);
207
+ onChange(matched?.value ?? selected);
208
+ }}
209
+ >
210
+ <Select.TriggerButton disabled={readonly} />
211
+ <Select.Portal>
212
+ <Select.Content>
213
+ <Select.Viewport>
214
+ {options.map((option: SelectOption) => (
215
+ <Select.Option key={globalThis.String(option.value)} value={globalThis.String(option.value)}>
216
+ {option.label ?? globalThis.String(option.value)}
217
+ </Select.Option>
218
+ ))}
219
+ </Select.Viewport>
220
+ <Select.Arrow />
221
+ </Select.Content>
222
+ </Select.Portal>
223
+ </Select.Root>
224
+ </SettingsItem>
225
+ );
226
+ }
227
+
228
+ case 'string':
229
+ return (
230
+ <SettingsItem title={title} description={description}>
231
+ <Input.TextInput disabled={readonly} value={value ?? ''} onChange={(event) => onChange(event.target.value)} />
232
+ </SettingsItem>
233
+ );
234
+
235
+ case 'number':
236
+ return (
237
+ <SettingsItem title={title} description={description}>
238
+ <Input.TextInput
239
+ disabled={readonly}
240
+ type='number'
241
+ value={value ?? ''}
242
+ onChange={(event) => {
243
+ if (event.target.value === '') {
244
+ onChange(undefined);
245
+ return;
246
+ }
247
+ const parsed = Number(event.target.value);
248
+ onChange(Number.isNaN(parsed) ? undefined : parsed);
249
+ }}
250
+ />
251
+ </SettingsItem>
252
+ );
253
+
254
+ default:
255
+ return null;
256
+ }
257
+ };
@@ -0,0 +1,58 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ import React, { type PropsWithChildren } from 'react';
6
+
7
+ import { Input, type Label, type ThemedClassName, toLocalizedString, useTranslation } from '@dxos/react-ui';
8
+ import { mx } from '@dxos/ui-theme';
9
+
10
+ import { translationKey } from '#translations';
11
+
12
+ const SETTINGS_PANEL_NAME = 'Settings.Panel';
13
+ const SETTINGS_ITEM_NAME = 'Settings.Item';
14
+
15
+ //
16
+ // Panel (bordered card container).
17
+ //
18
+
19
+ type SettingsPanelProps = ThemedClassName<PropsWithChildren>;
20
+
21
+ export const SettingsPanel = ({ classNames, children }: SettingsPanelProps) => {
22
+ return (
23
+ <div className={mx('flex flex-col gap-3 p-trim-md border border-separator rounded-sm', classNames)}>{children}</div>
24
+ );
25
+ };
26
+
27
+ SettingsPanel.displayName = SETTINGS_PANEL_NAME;
28
+
29
+ //
30
+ // Item (label + optional description + control, proper label semantics).
31
+ //
32
+
33
+ type SettingsItemProps = PropsWithChildren<{
34
+ title: Label;
35
+ description?: Label;
36
+ }>;
37
+
38
+ export const SettingsItem = ({ title, description = '', children }: SettingsItemProps) => {
39
+ const { t } = useTranslation(translationKey);
40
+
41
+ return (
42
+ <Input.Root>
43
+ <SettingsPanel classNames='grid grid-cols-1 md:grid-cols-[1fr_1fr] gap-x-trim-lg gap-y-0'>
44
+ <Input.Label classNames='pb-trim-md text-base-foreground text-lg md:col-span-2'>
45
+ {toLocalizedString(title, t)}
46
+ </Input.Label>
47
+ <Input.DescriptionAndValidation>
48
+ <Input.Description classNames='text-base text-description'>
49
+ {toLocalizedString(description, t)}
50
+ </Input.Description>
51
+ </Input.DescriptionAndValidation>
52
+ <div className='text-end py-1'>{children}</div>
53
+ </SettingsPanel>
54
+ </Input.Root>
55
+ );
56
+ };
57
+
58
+ SettingsItem.displayName = SETTINGS_ITEM_NAME;
@@ -0,0 +1,59 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ import React from 'react';
6
+
7
+ import { Select } from '@dxos/react-ui';
8
+
9
+ import { type SettingsFieldProps } from './SettingsFieldSet';
10
+
11
+ export type SelectFieldOption = {
12
+ value: string;
13
+ label?: string;
14
+ };
15
+
16
+ export type CreateSelectFieldOptions = {
17
+ /** Options to display. Strings are used as both value and label. */
18
+ options: ReadonlyArray<string | SelectFieldOption>;
19
+ /** Label for the sentinel option that maps to `undefined`. Defaults to `'Default'`. Pass `null` to omit. */
20
+ defaultLabel?: string | null;
21
+ };
22
+
23
+ /**
24
+ * Factory for a `SettingsFieldSet` `fieldMap` entry that renders a Select control
25
+ * over a fixed list of options, with an optional sentinel for `undefined`.
26
+ */
27
+ export const createSelectField = ({
28
+ options,
29
+ defaultLabel = 'Default',
30
+ }: CreateSelectFieldOptions): React.FC<SettingsFieldProps<string | undefined>> => {
31
+ const normalized = options.map((option) => (typeof option === 'string' ? { value: option, label: option } : option));
32
+ const hasDefault = defaultLabel !== null;
33
+ // Per-instance sentinel avoids collisions with real option values; empty string when the
34
+ // sentinel is omitted so `undefined` maps to a value Select.Root treats as "no selection".
35
+ const sentinel = hasDefault ? `__default__${Math.random().toString(36).slice(2)}` : '';
36
+
37
+ return ({ value, onChange, readonly }) => (
38
+ <Select.Root
39
+ disabled={readonly}
40
+ value={value ?? sentinel}
41
+ onValueChange={(next) => onChange(hasDefault && next === sentinel ? undefined : next)}
42
+ >
43
+ <Select.TriggerButton disabled={readonly} />
44
+ <Select.Portal>
45
+ <Select.Content>
46
+ <Select.Viewport>
47
+ {hasDefault && <Select.Option value={sentinel}>{defaultLabel}</Select.Option>}
48
+ {normalized.map((option) => (
49
+ <Select.Option key={option.value} value={option.value}>
50
+ {option.label ?? option.value}
51
+ </Select.Option>
52
+ ))}
53
+ </Select.Viewport>
54
+ <Select.Arrow />
55
+ </Select.Content>
56
+ </Select.Portal>
57
+ </Select.Root>
58
+ );
59
+ };
@@ -0,0 +1,7 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ export * from './Settings';
6
+ export { type SettingsFieldMap, type SettingsFieldProps, type SettingsFieldSetProps } from './SettingsFieldSet';
7
+ export { createSelectField, type CreateSelectFieldOptions, type SelectFieldOption } from './SettingsSelectField';
@@ -6,44 +6,44 @@ import { type Meta, type StoryObj } from '@storybook/react-vite';
6
6
  import * as Schema from 'effect/Schema';
7
7
  import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
8
8
 
9
- import { DXN, Filter, Obj, Query, type QueryAST, Tag, Type } from '@dxos/echo';
10
- import { type EchoSchema, Format } from '@dxos/echo/internal';
9
+ import { DXN, EchoURI, Filter, JsonSchema, Obj, Query, type QueryAST, Tag, Type, type View } from '@dxos/echo';
10
+ import { Format, type Mutable } from '@dxos/echo/internal';
11
11
  import { useQuery } from '@dxos/react-client/echo';
12
- import { useClientProvider, withClientProvider } from '@dxos/react-client/testing';
12
+ import { useClientStory, withClientProvider } from '@dxos/react-client/testing';
13
13
  import { useAsyncEffect } from '@dxos/react-ui';
14
- import { withTheme } from '@dxos/react-ui/testing';
15
- import { type ProjectionModel, View, getTypenameFromQuery } from '@dxos/schema';
16
- import { Employer, Organization, Person, Project } from '@dxos/types';
14
+ import { withLayout, withTheme } from '@dxos/react-ui/testing';
15
+ import { type ProjectionModel, ViewModel, getTypenameFromQuery } from '@dxos/schema';
16
+ import { Employer, Organization, Person, Pipeline } from '@dxos/types';
17
17
 
18
- import { translations } from '../../translations';
19
- import { TestLayout, TestPanel, VIEW_EDITOR_DEBUG_SYMBOL } from '../testing';
18
+ import { translations } from '#translations';
20
19
 
20
+ import { TestLayout, VIEW_EDITOR_DEBUG_SYMBOL } from '../testing';
21
21
  import { ViewEditor, type ViewEditorProps } from './ViewEditor';
22
22
 
23
23
  const types = [
24
24
  // TODO(burdon): Get label from annotation.
25
- { value: Organization.Organization.typename, label: 'Organization' },
26
- { value: Person.Person.typename, label: 'Person' },
27
- { value: Type.getTypename(Project.Project), label: 'Project' },
28
- { value: Employer.Employer.typename, label: 'Employer' },
25
+ { value: Type.getTypename(Organization.Organization), label: 'Organization' },
26
+ { value: Type.getTypename(Person.Person), label: 'Person' },
27
+ { value: Type.getTypename(Pipeline.Pipeline), label: 'Project' },
28
+ { value: Type.getTypename(Employer.Employer), label: 'Employer' },
29
29
  ];
30
30
 
31
31
  // Type definition for debug objects exposed to tests.
32
32
  export type ViewEditorDebugObjects = {
33
- schema: EchoSchema;
33
+ type: Type.AnyEntity;
34
34
  view: View.View;
35
35
  projection: ProjectionModel;
36
36
  };
37
37
 
38
- type StoryProps = Pick<ViewEditorProps, 'readonly' | 'mode'>;
38
+ type DefaultStoryProps = Pick<ViewEditorProps, 'readonly' | 'mode'>;
39
39
 
40
- const DefaultStory = (props: StoryProps) => {
41
- const { space } = useClientProvider();
42
- const [schema, setSchema] = useState<EchoSchema>();
40
+ const DefaultStory = (props: DefaultStoryProps) => {
41
+ const { space } = useClientStory();
42
+ const [type, setType] = useState<Type.AnyEntity>();
43
43
  const [view, setView] = useState<View.View>();
44
44
  const projectionRef = useRef<ProjectionModel>(null);
45
45
 
46
- const tags = useQuery(space, Filter.type(Tag.Tag));
46
+ const tags = useQuery(space?.db, Filter.type(Tag.Tag));
47
47
 
48
48
  useAsyncEffect(async () => {
49
49
  if (space) {
@@ -51,47 +51,38 @@ const DefaultStory = (props: StoryProps) => {
51
51
  name: Schema.String,
52
52
  email: Format.Email,
53
53
  salary: Format.Currency(),
54
- }).pipe(
55
- Type.Obj({
56
- typename: 'example.com/type/Test',
57
- version: '0.1.0',
58
- }),
59
- );
54
+ }).pipe(Type.makeObject(DXN.make('com.example.type.test', '0.1.0')));
60
55
 
61
56
  const AlternateSchema = Schema.Struct({
62
57
  title: Schema.String,
63
58
  description: Schema.String,
64
59
  completed: Schema.Boolean,
65
- }).pipe(
66
- Type.Obj({
67
- typename: 'example.com/type/Alternate',
68
- version: '0.1.0',
69
- }),
70
- );
60
+ }).pipe(Type.makeObject(DXN.make('com.example.type.alternate', '0.1.0')));
71
61
 
72
62
  const [testSchema] = await space.db.schemaRegistry.register([TestSchema, AlternateSchema]);
73
- const view = View.make({
63
+ const view = ViewModel.make({
74
64
  name: 'Test',
75
65
  query: Query.select(Filter.type(TestSchema)),
76
- jsonSchema: Type.toJsonSchema(TestSchema),
77
- presentation: Obj.make(Type.Expando, {}),
66
+ jsonSchema: JsonSchema.toJsonSchema(TestSchema),
78
67
  });
79
68
 
80
- setSchema(testSchema);
69
+ setType(testSchema);
81
70
  setView(view);
82
71
  }
83
72
  }, [space]);
84
73
 
85
74
  const updateViewQuery = useCallback(
86
- async (newQuery: QueryAST.Query, target?: string) => {
87
- if (!schema || !view || !space) {
75
+ async (newQuery: QueryAST.Query, target?: EchoURI.EchoURI) => {
76
+ if (!type || !view || !space) {
88
77
  return;
89
78
  }
90
79
 
91
- if (props.mode === 'query') {
92
- const queue = target && DXN.tryParse(target) ? target : undefined;
93
- const query = queue ? Query.fromAst(newQuery).options({ queues: [queue] }) : Query.fromAst(newQuery);
94
- view.query.ast = query.ast;
80
+ if (props.mode === 'tag') {
81
+ const queue = target;
82
+ const query = queue ? Query.fromAst(newQuery).from({ feeds: [queue] }) : Query.fromAst(newQuery);
83
+ Obj.update(view, (view) => {
84
+ view.query.ast = query.ast as Mutable<typeof query.ast>;
85
+ });
95
86
 
96
87
  const typename = getTypenameFromQuery(query.ast);
97
88
  const [newSchema] = await space.db.schemaRegistry.query({ typename }).run();
@@ -99,20 +90,21 @@ const DefaultStory = (props: StoryProps) => {
99
90
  return;
100
91
  }
101
92
 
102
- const newView = View.make({
93
+ const newView = ViewModel.make({
103
94
  query,
104
95
  jsonSchema: newSchema.jsonSchema,
105
- presentation: Obj.make(Type.Expando, {}),
106
96
  });
107
- view.projection = Obj.getSnapshot(newView).projection;
108
- setSchema(() => newSchema);
97
+ Obj.update(view, (view) => {
98
+ view.projection = Obj.getSnapshot(newView).projection as Mutable<typeof view.projection>;
99
+ });
100
+ setType(() => newSchema);
109
101
  } else {
110
- console.log('updateViewQuery', { newQuery });
111
- view.query.ast = newQuery;
112
- schema.updateTypename(getTypenameFromQuery(newQuery));
102
+ Obj.update(view, (view) => {
103
+ view.query.ast = newQuery as Mutable<typeof newQuery>;
104
+ });
113
105
  }
114
106
  },
115
- [view, schema],
107
+ [view, type],
116
108
  );
117
109
 
118
110
  const handleDelete = useCallback(
@@ -122,41 +114,39 @@ const DefaultStory = (props: StoryProps) => {
122
114
 
123
115
  // Expose objects on window for test access.
124
116
  useEffect(() => {
125
- if (typeof window !== 'undefined' && schema && view && projectionRef.current) {
117
+ if (typeof window !== 'undefined' && type && view && projectionRef.current) {
126
118
  (window as any)[VIEW_EDITOR_DEBUG_SYMBOL] = {
127
- schema,
119
+ type,
128
120
  view,
129
121
  projection: projectionRef.current,
130
122
  } satisfies ViewEditorDebugObjects;
131
123
  }
132
- }, [schema, view]);
124
+ }, [type, view]);
133
125
 
134
126
  // NOTE(ZaymonFC): This looks awkward but it resolves an infinite parsing issue with sb.
135
127
  const json = useMemo(
136
- () => JSON.parse(JSON.stringify({ schema, view, projection: projectionRef.current })),
137
- [JSON.stringify(schema), JSON.stringify(view)],
128
+ () => JSON.parse(JSON.stringify({ schema: type, view, projection: projectionRef.current })),
129
+ [JSON.stringify(type), JSON.stringify(view)],
138
130
  );
139
131
 
140
- if (!schema || !view) {
132
+ if (!type || !view) {
141
133
  return <div />;
142
134
  }
143
135
 
144
136
  return (
145
137
  <TestLayout json={json}>
146
- <TestPanel>
147
- <ViewEditor
148
- ref={projectionRef}
149
- schema={schema}
150
- view={view}
151
- registry={space?.db.schemaRegistry}
152
- mode={props.mode}
153
- readonly={props.readonly}
154
- types={types}
155
- tags={tags}
156
- onQueryChanged={updateViewQuery}
157
- onDelete={handleDelete}
158
- />
159
- </TestPanel>
138
+ <ViewEditor
139
+ ref={projectionRef}
140
+ type={type}
141
+ view={view}
142
+ registry={space?.db.schemaRegistry}
143
+ mode={props.mode}
144
+ readonly={props.readonly}
145
+ types={types}
146
+ tags={tags}
147
+ onQueryChanged={updateViewQuery}
148
+ onDelete={handleDelete}
149
+ />
160
150
  </TestLayout>
161
151
  );
162
152
  };
@@ -174,7 +164,8 @@ const meta = {
174
164
  space.db.add(Tag.make({ label: 'New' }));
175
165
  },
176
166
  }),
177
- withTheme,
167
+ withTheme(),
168
+ withLayout({ layout: 'fullscreen' }),
178
169
  ],
179
170
  parameters: {
180
171
  layout: 'fullscreen',
@@ -194,8 +185,8 @@ export const Readonly: Story = {
194
185
  },
195
186
  };
196
187
 
197
- export const QueryMode: Story = {
188
+ export const TagQueryMode: Story = {
198
189
  args: {
199
- mode: 'query',
190
+ mode: 'tag',
200
191
  },
201
192
  };