@dxos/react-ui-form 0.8.4-main.406dc2a → 0.8.4-main.422d1c7879

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 (219) hide show
  1. package/README.md +0 -1
  2. package/dist/lib/browser/index.mjs +2856 -2309
  3. package/dist/lib/browser/index.mjs.map +4 -4
  4. package/dist/lib/browser/meta.json +1 -1
  5. package/dist/lib/node-esm/index.mjs +2856 -2309
  6. package/dist/lib/node-esm/index.mjs.map +4 -4
  7. package/dist/lib/node-esm/meta.json +1 -1
  8. package/dist/types/src/components/FieldEditor/FieldEditor.d.ts +8 -7
  9. package/dist/types/src/components/FieldEditor/FieldEditor.d.ts.map +1 -1
  10. package/dist/types/src/components/FieldEditor/FieldEditor.stories.d.ts +68 -60
  11. package/dist/types/src/components/FieldEditor/FieldEditor.stories.d.ts.map +1 -1
  12. package/dist/types/src/components/Form/Form.d.ts +93 -26
  13. package/dist/types/src/components/Form/Form.d.ts.map +1 -1
  14. package/dist/types/src/components/Form/Form.stories.d.ts +114 -137
  15. package/dist/types/src/components/Form/Form.stories.d.ts.map +1 -1
  16. package/dist/types/src/components/Form/FormField.d.ts +43 -0
  17. package/dist/types/src/components/Form/FormField.d.ts.map +1 -0
  18. package/dist/types/src/components/Form/FormFieldComponent.d.ts +80 -0
  19. package/dist/types/src/components/Form/FormFieldComponent.d.ts.map +1 -0
  20. package/dist/types/src/components/Form/FormFieldComponent.stories.d.ts +9 -0
  21. package/dist/types/src/components/Form/FormFieldComponent.stories.d.ts.map +1 -0
  22. package/dist/types/src/components/Form/FormFieldSet.d.ts +18 -0
  23. package/dist/types/src/components/Form/FormFieldSet.d.ts.map +1 -0
  24. package/dist/types/src/components/Form/fields/ArrayField.d.ts +18 -0
  25. package/dist/types/src/components/Form/fields/ArrayField.d.ts.map +1 -0
  26. package/dist/types/src/components/Form/fields/BooleanField.d.ts +4 -0
  27. package/dist/types/src/components/Form/fields/BooleanField.d.ts.map +1 -0
  28. package/dist/types/src/components/Form/fields/GeoPointField.d.ts +5 -0
  29. package/dist/types/src/components/Form/fields/GeoPointField.d.ts.map +1 -0
  30. package/dist/types/src/components/Form/fields/MarkdownField.d.ts +11 -0
  31. package/dist/types/src/components/Form/fields/MarkdownField.d.ts.map +1 -0
  32. package/dist/types/src/components/Form/fields/MarkdownField.stories.d.ts +83 -0
  33. package/dist/types/src/components/Form/fields/MarkdownField.stories.d.ts.map +1 -0
  34. package/dist/types/src/components/Form/fields/NumberField.d.ts +4 -0
  35. package/dist/types/src/components/Form/fields/NumberField.d.ts.map +1 -0
  36. package/dist/types/src/components/Form/fields/RefField.d.ts +22 -0
  37. package/dist/types/src/components/Form/fields/RefField.d.ts.map +1 -0
  38. package/dist/types/src/components/Form/fields/SelectField.d.ts +10 -0
  39. package/dist/types/src/components/Form/fields/SelectField.d.ts.map +1 -0
  40. package/dist/types/src/components/Form/fields/SelectOptionField.d.ts +5 -0
  41. package/dist/types/src/components/Form/fields/SelectOptionField.d.ts.map +1 -0
  42. package/dist/types/src/components/Form/fields/SelectOptionField.stories.d.ts +84 -0
  43. package/dist/types/src/components/Form/fields/SelectOptionField.stories.d.ts.map +1 -0
  44. package/dist/types/src/components/Form/fields/TextAreaField.d.ts +4 -0
  45. package/dist/types/src/components/Form/fields/TextAreaField.d.ts.map +1 -0
  46. package/dist/types/src/components/Form/fields/TextField.d.ts +4 -0
  47. package/dist/types/src/components/Form/fields/TextField.d.ts.map +1 -0
  48. package/dist/types/src/components/Form/fields/TupleField.d.ts +6 -0
  49. package/dist/types/src/components/Form/fields/TupleField.d.ts.map +1 -0
  50. package/dist/types/src/components/Form/fields/index.d.ts +12 -0
  51. package/dist/types/src/components/Form/fields/index.d.ts.map +1 -0
  52. package/dist/types/src/components/Form/index.d.ts +2 -4
  53. package/dist/types/src/components/Form/index.d.ts.map +1 -1
  54. package/dist/types/src/components/ObjectForm/ObjectForm.d.ts +9 -0
  55. package/dist/types/src/components/ObjectForm/ObjectForm.d.ts.map +1 -0
  56. package/dist/types/src/components/ObjectForm/index.d.ts +2 -0
  57. package/dist/types/src/components/ObjectForm/index.d.ts.map +1 -0
  58. package/dist/types/src/components/ObjectPicker/ObjectPicker.d.ts +37 -10
  59. package/dist/types/src/components/ObjectPicker/ObjectPicker.d.ts.map +1 -1
  60. package/dist/types/src/components/ObjectPicker/ObjectPicker.stories.d.ts +64 -59
  61. package/dist/types/src/components/ObjectPicker/ObjectPicker.stories.d.ts.map +1 -1
  62. package/dist/types/src/components/ObjectProperties/ObjectProperties.d.ts +9 -0
  63. package/dist/types/src/components/ObjectProperties/ObjectProperties.d.ts.map +1 -0
  64. package/dist/types/src/components/ObjectProperties/ObjectProperties.stories.d.ts +127 -0
  65. package/dist/types/src/components/ObjectProperties/ObjectProperties.stories.d.ts.map +1 -0
  66. package/dist/types/src/components/ObjectProperties/ObjectProperties.test.d.ts +2 -0
  67. package/dist/types/src/components/ObjectProperties/ObjectProperties.test.d.ts.map +1 -0
  68. package/dist/types/src/components/ObjectProperties/index.d.ts +2 -0
  69. package/dist/types/src/components/ObjectProperties/index.d.ts.map +1 -0
  70. package/dist/types/src/components/Settings/Settings.d.ts +43 -0
  71. package/dist/types/src/components/Settings/Settings.d.ts.map +1 -0
  72. package/dist/types/src/components/Settings/Settings.stories.d.ts +84 -0
  73. package/dist/types/src/components/Settings/Settings.stories.d.ts.map +1 -0
  74. package/dist/types/src/components/Settings/SettingsFieldSet.d.ts +38 -0
  75. package/dist/types/src/components/Settings/SettingsFieldSet.d.ts.map +1 -0
  76. package/dist/types/src/components/Settings/SettingsItem.d.ts +17 -0
  77. package/dist/types/src/components/Settings/SettingsItem.d.ts.map +1 -0
  78. package/dist/types/src/components/Settings/index.d.ts +3 -0
  79. package/dist/types/src/components/Settings/index.d.ts.map +1 -0
  80. package/dist/types/src/components/ViewEditor/ViewEditor.d.ts +13 -15
  81. package/dist/types/src/components/ViewEditor/ViewEditor.d.ts.map +1 -1
  82. package/dist/types/src/components/ViewEditor/ViewEditor.stories.d.ts +70 -64
  83. package/dist/types/src/components/ViewEditor/ViewEditor.stories.d.ts.map +1 -1
  84. package/dist/types/src/components/index.d.ts +3 -1
  85. package/dist/types/src/components/index.d.ts.map +1 -1
  86. package/dist/types/src/components/testing.d.ts +4 -6
  87. package/dist/types/src/components/testing.d.ts.map +1 -1
  88. package/dist/types/src/hooks/index.d.ts +2 -3
  89. package/dist/types/src/hooks/index.d.ts.map +1 -1
  90. package/dist/types/src/hooks/useFormHandler.d.ts +82 -0
  91. package/dist/types/src/hooks/useFormHandler.d.ts.map +1 -0
  92. package/dist/types/src/hooks/useKeyHandler.d.ts +7 -0
  93. package/dist/types/src/hooks/useKeyHandler.d.ts.map +1 -0
  94. package/dist/types/src/translations.d.ts +65 -60
  95. package/dist/types/src/translations.d.ts.map +1 -1
  96. package/dist/types/src/util/field-type.d.ts +19 -0
  97. package/dist/types/src/util/field-type.d.ts.map +1 -0
  98. package/dist/types/src/util/field.d.ts +3 -3
  99. package/dist/types/src/util/field.d.ts.map +1 -1
  100. package/dist/types/src/util/formatting.d.ts +2 -2
  101. package/dist/types/src/util/formatting.d.ts.map +1 -1
  102. package/dist/types/src/util/index.d.ts +2 -1
  103. package/dist/types/src/util/index.d.ts.map +1 -1
  104. package/dist/types/src/util/properties.d.ts +7 -0
  105. package/dist/types/src/util/properties.d.ts.map +1 -0
  106. package/dist/types/src/util/refs.d.ts +2 -2
  107. package/dist/types/src/util/refs.d.ts.map +1 -1
  108. package/dist/types/tsconfig.tsbuildinfo +1 -1
  109. package/package.json +47 -50
  110. package/src/components/FieldEditor/FieldEditor.stories.tsx +43 -25
  111. package/src/components/FieldEditor/FieldEditor.test.tsx +0 -1
  112. package/src/components/FieldEditor/FieldEditor.tsx +74 -78
  113. package/src/components/Form/AUDIT.md +17 -0
  114. package/src/components/Form/Form.stories.tsx +137 -250
  115. package/src/components/Form/Form.tsx +375 -107
  116. package/src/components/Form/FormField.tsx +305 -0
  117. package/src/components/Form/FormFieldComponent.stories.tsx +65 -0
  118. package/src/components/Form/FormFieldComponent.tsx +174 -0
  119. package/src/components/Form/FormFieldSet.tsx +144 -0
  120. package/src/components/Form/README.md +3 -0
  121. package/src/components/Form/fields/ArrayField.tsx +158 -0
  122. package/src/components/Form/fields/BooleanField.tsx +22 -0
  123. package/src/components/Form/fields/GeoPointField.tsx +120 -0
  124. package/src/components/Form/fields/MarkdownField.stories.tsx +151 -0
  125. package/src/components/Form/fields/MarkdownField.tsx +167 -0
  126. package/src/components/Form/fields/NumberField.tsx +39 -0
  127. package/src/components/Form/fields/RefField.tsx +228 -0
  128. package/src/components/Form/fields/SelectField.tsx +65 -0
  129. package/src/components/Form/{custom/SelectOptionsInput.stories.tsx → fields/SelectOptionField.stories.tsx} +21 -17
  130. package/src/components/Form/{custom/SelectOptionsInput.tsx → fields/SelectOptionField.tsx} +28 -23
  131. package/src/components/Form/fields/TextAreaField.tsx +38 -0
  132. package/src/components/Form/fields/TextField.tsx +38 -0
  133. package/src/components/Form/{custom/TupleInput.tsx → fields/TupleField.tsx} +16 -9
  134. package/src/components/Form/fields/index.ts +15 -0
  135. package/src/components/Form/index.ts +3 -4
  136. package/src/components/ObjectForm/ObjectForm.tsx +113 -0
  137. package/src/components/{ControlSection → ObjectForm}/index.ts +1 -1
  138. package/src/components/ObjectPicker/ObjectPicker.stories.tsx +23 -21
  139. package/src/components/ObjectPicker/ObjectPicker.tsx +112 -80
  140. package/src/components/ObjectProperties/ObjectProperties.stories.tsx +361 -0
  141. package/src/components/ObjectProperties/ObjectProperties.test.tsx +101 -0
  142. package/src/components/ObjectProperties/ObjectProperties.tsx +140 -0
  143. package/src/components/ObjectProperties/index.ts +5 -0
  144. package/src/components/Settings/Settings.stories.tsx +103 -0
  145. package/src/components/Settings/Settings.tsx +81 -0
  146. package/src/components/Settings/SettingsFieldSet.tsx +211 -0
  147. package/src/components/Settings/SettingsItem.tsx +62 -0
  148. package/src/components/Settings/index.ts +6 -0
  149. package/src/components/ViewEditor/ViewEditor.stories.tsx +53 -50
  150. package/src/components/ViewEditor/ViewEditor.test.tsx +6 -6
  151. package/src/components/ViewEditor/ViewEditor.tsx +296 -206
  152. package/src/components/index.ts +3 -1
  153. package/src/components/testing.tsx +24 -11
  154. package/src/hooks/index.ts +2 -3
  155. package/src/hooks/useFormHandler.ts +380 -0
  156. package/src/hooks/useKeyHandler.tsx +37 -0
  157. package/src/translations.ts +70 -65
  158. package/src/util/field-type.ts +57 -0
  159. package/src/util/field.ts +25 -25
  160. package/src/util/formatting.ts +22 -22
  161. package/src/util/index.ts +2 -1
  162. package/src/util/properties.ts +18 -0
  163. package/src/util/refs.ts +16 -18
  164. package/dist/types/src/components/ControlSection/ControlSection.d.ts +0 -23
  165. package/dist/types/src/components/ControlSection/ControlSection.d.ts.map +0 -1
  166. package/dist/types/src/components/ControlSection/ControlSection.stories.d.ts +0 -84
  167. package/dist/types/src/components/ControlSection/ControlSection.stories.d.ts.map +0 -1
  168. package/dist/types/src/components/ControlSection/index.d.ts +0 -2
  169. package/dist/types/src/components/ControlSection/index.d.ts.map +0 -1
  170. package/dist/types/src/components/Form/ArrayField.d.ts +0 -16
  171. package/dist/types/src/components/Form/ArrayField.d.ts.map +0 -1
  172. package/dist/types/src/components/Form/Defaults.d.ts +0 -14
  173. package/dist/types/src/components/Form/Defaults.d.ts.map +0 -1
  174. package/dist/types/src/components/Form/FormActions.d.ts +0 -9
  175. package/dist/types/src/components/Form/FormActions.d.ts.map +0 -1
  176. package/dist/types/src/components/Form/FormContent.d.ts +0 -63
  177. package/dist/types/src/components/Form/FormContent.d.ts.map +0 -1
  178. package/dist/types/src/components/Form/FormContext.d.ts +0 -23
  179. package/dist/types/src/components/Form/FormContext.d.ts.map +0 -1
  180. package/dist/types/src/components/Form/Input.d.ts +0 -29
  181. package/dist/types/src/components/Form/Input.d.ts.map +0 -1
  182. package/dist/types/src/components/Form/RefField.d.ts +0 -22
  183. package/dist/types/src/components/Form/RefField.d.ts.map +0 -1
  184. package/dist/types/src/components/Form/custom/GeoPointInput.d.ts +0 -4
  185. package/dist/types/src/components/Form/custom/GeoPointInput.d.ts.map +0 -1
  186. package/dist/types/src/components/Form/custom/SelectOptionsInput.d.ts +0 -4
  187. package/dist/types/src/components/Form/custom/SelectOptionsInput.d.ts.map +0 -1
  188. package/dist/types/src/components/Form/custom/SelectOptionsInput.stories.d.ts +0 -79
  189. package/dist/types/src/components/Form/custom/SelectOptionsInput.stories.d.ts.map +0 -1
  190. package/dist/types/src/components/Form/custom/TupleInput.d.ts +0 -6
  191. package/dist/types/src/components/Form/custom/TupleInput.d.ts.map +0 -1
  192. package/dist/types/src/components/Form/custom/index.d.ts +0 -4
  193. package/dist/types/src/components/Form/custom/index.d.ts.map +0 -1
  194. package/dist/types/src/components/Form/factory.d.ts +0 -8
  195. package/dist/types/src/components/Form/factory.d.ts.map +0 -1
  196. package/dist/types/src/hooks/useForm.d.ts +0 -67
  197. package/dist/types/src/hooks/useForm.d.ts.map +0 -1
  198. package/dist/types/src/hooks/useQueryRefOptions.d.ts +0 -29
  199. package/dist/types/src/hooks/useQueryRefOptions.d.ts.map +0 -1
  200. package/dist/types/src/hooks/useRefQueryLookupHandler.d.ts +0 -8
  201. package/dist/types/src/hooks/useRefQueryLookupHandler.d.ts.map +0 -1
  202. package/dist/types/src/util/schema.d.ts +0 -3
  203. package/dist/types/src/util/schema.d.ts.map +0 -1
  204. package/src/components/ControlSection/ControlSection.stories.tsx +0 -38
  205. package/src/components/ControlSection/ControlSection.tsx +0 -153
  206. package/src/components/Form/ArrayField.tsx +0 -125
  207. package/src/components/Form/Defaults.tsx +0 -205
  208. package/src/components/Form/FormActions.tsx +0 -54
  209. package/src/components/Form/FormContent.tsx +0 -322
  210. package/src/components/Form/FormContext.tsx +0 -110
  211. package/src/components/Form/Input.tsx +0 -54
  212. package/src/components/Form/RefField.tsx +0 -283
  213. package/src/components/Form/custom/GeoPointInput.tsx +0 -80
  214. package/src/components/Form/custom/index.ts +0 -7
  215. package/src/components/Form/factory.tsx +0 -31
  216. package/src/hooks/useForm.ts +0 -296
  217. package/src/hooks/useQueryRefOptions.ts +0 -62
  218. package/src/hooks/useRefQueryLookupHandler.ts +0 -39
  219. package/src/util/schema.ts +0 -12
@@ -0,0 +1,103 @@
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
+ import { Settings } from './Settings';
15
+
16
+ random.seed(132);
17
+
18
+ const DefaultStory = () => {
19
+ return (
20
+ <Settings.Viewport>
21
+ <Settings.Section title='Settings' description={random.lorem.paragraphs(1)}>
22
+ <Settings.Item title={random.lorem.sentence(2)} description={random.lorem.paragraphs(1)}>
23
+ <Input.TextInput placeholder='Input' />
24
+ </Settings.Item>
25
+ <Settings.Item title={random.lorem.sentence(2)} description={random.lorem.paragraphs(2)}>
26
+ <Input.Switch />
27
+ </Settings.Item>
28
+ <Settings.Item title={random.lorem.sentence(3)} description={random.lorem.paragraphs(2)}>
29
+ <Button>Test</Button>
30
+ </Settings.Item>
31
+ </Settings.Section>
32
+ <Settings.Section title='Panel Example'>
33
+ <Settings.Panel>
34
+ <h3 className='text-lg mb-2'>Members</h3>
35
+ <p className='text-description'>Content inside a panel.</p>
36
+ </Settings.Panel>
37
+ </Settings.Section>
38
+ </Settings.Viewport>
39
+ );
40
+ };
41
+
42
+ const meta = {
43
+ title: 'ui/react-ui-form/Settings',
44
+ render: DefaultStory,
45
+ decorators: [withTheme(), withLayout({ layout: 'fullscreen' })],
46
+ parameters: {
47
+ layout: 'fullscreen',
48
+ translations,
49
+ },
50
+ } satisfies Meta;
51
+
52
+ export default meta;
53
+
54
+ type Story = StoryObj<typeof meta>;
55
+
56
+ export const Default: Story = {};
57
+
58
+ const TestViewModes = ['preview', 'readonly', 'source'] as const;
59
+ const TestViewMode = Schema.Union(...TestViewModes.map((mode) => Schema.Literal(mode)));
60
+
61
+ const TestSettings = Schema.mutable(
62
+ Schema.Struct({
63
+ viewMode: TestViewMode.annotations({ title: 'View mode', description: 'Default document view mode.' }),
64
+ toolbar: Schema.optional(
65
+ Schema.Boolean.annotations({ title: 'Show toolbar', description: 'Display formatting toolbar.' }),
66
+ ),
67
+ fontSize: Schema.optional(
68
+ Schema.Number.annotations({ title: 'Font size', description: 'Editor font size in pixels.' }),
69
+ ),
70
+ placeholder: Schema.optional(
71
+ Schema.String.annotations({ title: 'Placeholder', description: 'Default placeholder text.' }),
72
+ ),
73
+ debug: Schema.optional(Schema.Boolean.annotations({ title: 'Debug mode', description: 'Enable debug features.' })),
74
+ }),
75
+ );
76
+
77
+ type TestSettings = Schema.Schema.Type<typeof TestSettings>;
78
+
79
+ const FieldSetStory = () => {
80
+ const [values, setValues] = React.useState<TestSettings>({
81
+ viewMode: 'preview',
82
+ toolbar: true,
83
+ fontSize: 14,
84
+ debug: false,
85
+ });
86
+
87
+ return (
88
+ <Settings.Viewport>
89
+ <Settings.Section title='Plugin Settings (Auto-generated)'>
90
+ <Settings.FieldSet
91
+ schema={TestSettings}
92
+ values={values}
93
+ onValuesChanged={setValues}
94
+ visible={(path, values) => path !== 'placeholder' || !!values.debug}
95
+ />
96
+ </Settings.Section>
97
+ </Settings.Viewport>
98
+ );
99
+ };
100
+
101
+ export const FieldSet: Story = {
102
+ render: FieldSetStory,
103
+ };
@@ -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, mx } from '@dxos/ui-theme';
9
+
10
+ import { translationKey } from '../../translations';
11
+ import { SettingsFieldSet } from './SettingsFieldSet';
12
+ import { SettingsItem, SettingsPanel } from './SettingsItem';
13
+
14
+ const SETTINGS_ROOT_NAME = 'Settings.Root';
15
+ const SETTINGS_VIEWPORT_NAME = 'Settings.Viewport';
16
+ const SETTINGS_SECTION_NAME = 'Settings.Section';
17
+
18
+ //
19
+ // Root (headless — no styling, no scroll).
20
+ //
21
+
22
+ type SettingsRootProps = ThemedClassName<PropsWithChildren>;
23
+
24
+ const SettingsRoot = ({ children, classNames }: SettingsRootProps) => {
25
+ return <div className={mx('flex flex-col gap-2', classNames)}>{children}</div>;
26
+ };
27
+
28
+ SettingsRoot.displayName = SETTINGS_ROOT_NAME;
29
+
30
+ //
31
+ // Viewport (scroll area + padding + document layout).
32
+ //
33
+
34
+ const SettingsViewport = composable<HTMLDivElement>(({ children, ...props }, forwardedRef) => {
35
+ return (
36
+ <ScrollArea.Root {...composableProps(props)} orientation='vertical' centered thin ref={forwardedRef}>
37
+ <ScrollArea.Viewport classNames='px-4 pointer-coarse:px-2'>
38
+ <div role='none' className='dx-document flex flex-col gap-2 py-4'>
39
+ {children}
40
+ </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
+ };
@@ -0,0 +1,211 @@
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 custom field renderers.
31
+ */
32
+ export type SettingsFieldMap = Record<string, React.FC<SettingsFieldProps>>;
33
+
34
+ export type SettingsFieldSetProps<T extends Record<string, any> = Record<string, any>> = {
35
+ /** Effect Schema for the settings object. */
36
+ schema: { ast: SchemaAST.AST };
37
+
38
+ /** Current settings values. */
39
+ values: T;
40
+
41
+ /** Callback when any value changes. Receives a new complete object. */
42
+ onValuesChanged?: (values: T) => void;
43
+
44
+ /** When true, all controls are disabled. */
45
+ readonly?: boolean;
46
+
47
+ /** Map of property names to custom field renderers. */
48
+ fieldMap?: SettingsFieldMap;
49
+
50
+ /** Control field visibility. Return false to hide a field. */
51
+ visible?: (path: string, values: T) => boolean;
52
+
53
+ /** Override the order of fields. Fields not listed are appended in schema order. */
54
+ sort?: string[];
55
+ };
56
+
57
+ //
58
+ // Component
59
+ //
60
+
61
+ export const SettingsFieldSet = <T extends Record<string, any>>({
62
+ schema,
63
+ values,
64
+ onValuesChanged,
65
+ readonly,
66
+ fieldMap,
67
+ visible,
68
+ sort,
69
+ }: SettingsFieldSetProps<T>) => {
70
+ const properties = useMemo(() => {
71
+ const props = getFormProperties(schema.ast);
72
+ if (!sort) {
73
+ return props;
74
+ }
75
+ const rank = (name: string) => {
76
+ const idx = sort.indexOf(name);
77
+ return idx === -1 ? Number.POSITIVE_INFINITY : idx;
78
+ };
79
+ return [...props].sort(({ name: a }, { name: b }) => rank(a.toString()) - rank(b.toString()));
80
+ }, [schema, sort]);
81
+
82
+ const handleChange = useCallback(
83
+ (name: string, value: unknown) => {
84
+ onValuesChanged?.({ ...values, [name]: value } as T);
85
+ },
86
+ [values, onValuesChanged],
87
+ );
88
+
89
+ return (
90
+ <>
91
+ {properties.map((property) => {
92
+ const name = property.name.toString();
93
+ if (visible && !visible(name, values)) {
94
+ return null;
95
+ }
96
+
97
+ return (
98
+ <SettingsFieldItem
99
+ key={name}
100
+ property={property}
101
+ value={values[name]}
102
+ onChange={(value) => handleChange(name, value)}
103
+ readonly={readonly}
104
+ customField={fieldMap?.[name]}
105
+ />
106
+ );
107
+ })}
108
+ </>
109
+ );
110
+ };
111
+
112
+ SettingsFieldSet.displayName = 'Settings.FieldSet';
113
+
114
+ //
115
+ // Internal: render a single settings field.
116
+ //
117
+
118
+ type SettingsFieldItemProps = {
119
+ property: SchemaProperty;
120
+ value: any;
121
+ onChange: (value: any) => void;
122
+ readonly?: boolean;
123
+ customField?: React.FC<SettingsFieldProps>;
124
+ };
125
+
126
+ const SettingsFieldItem = ({ property, value, onChange, readonly, customField }: SettingsFieldItemProps) => {
127
+ const { type } = property;
128
+ const name = property.name.toString();
129
+ const title = getAnnotation<string>(SchemaAST.TitleAnnotationId)(type) ?? String.capitalize(name);
130
+ const description = getAnnotation<string>(SchemaAST.DescriptionAnnotationId)(type);
131
+
132
+ // Custom field renderer.
133
+ if (customField) {
134
+ const CustomField = customField;
135
+ return (
136
+ <SettingsItem title={title} description={description}>
137
+ <CustomField value={value} onChange={onChange} readonly={readonly} />
138
+ </SettingsItem>
139
+ );
140
+ }
141
+
142
+ const fieldType = detectFieldType(type);
143
+
144
+ switch (fieldType) {
145
+ case 'boolean':
146
+ return (
147
+ <SettingsItem title={title} description={description}>
148
+ <Input.Switch disabled={readonly} checked={!!value} onCheckedChange={(checked) => onChange(!!checked)} />
149
+ </SettingsItem>
150
+ );
151
+
152
+ case 'select': {
153
+ const options = getSelectOptionsFromAst(type) ?? [];
154
+ return (
155
+ <SettingsItem title={title} description={description}>
156
+ <Select.Root
157
+ disabled={readonly}
158
+ value={value == null ? '' : globalThis.String(value)}
159
+ onValueChange={(selected) => {
160
+ const matched = options.find((option) => globalThis.String(option.value) === selected);
161
+ onChange(matched?.value ?? selected);
162
+ }}
163
+ >
164
+ <Select.TriggerButton disabled={readonly} />
165
+ <Select.Portal>
166
+ <Select.Content>
167
+ <Select.Viewport>
168
+ {options.map((option: SelectOption) => (
169
+ <Select.Option key={globalThis.String(option.value)} value={globalThis.String(option.value)}>
170
+ {option.label ?? globalThis.String(option.value)}
171
+ </Select.Option>
172
+ ))}
173
+ </Select.Viewport>
174
+ <Select.Arrow />
175
+ </Select.Content>
176
+ </Select.Portal>
177
+ </Select.Root>
178
+ </SettingsItem>
179
+ );
180
+ }
181
+
182
+ case 'string':
183
+ return (
184
+ <SettingsItem title={title} description={description}>
185
+ <Input.TextInput disabled={readonly} value={value ?? ''} onChange={(event) => onChange(event.target.value)} />
186
+ </SettingsItem>
187
+ );
188
+
189
+ case 'number':
190
+ return (
191
+ <SettingsItem title={title} description={description}>
192
+ <Input.TextInput
193
+ disabled={readonly}
194
+ type='number'
195
+ value={value ?? ''}
196
+ onChange={(event) => {
197
+ if (event.target.value === '') {
198
+ onChange(undefined);
199
+ return;
200
+ }
201
+ const parsed = Number(event.target.value);
202
+ onChange(Number.isNaN(parsed) ? undefined : parsed);
203
+ }}
204
+ />
205
+ </SettingsItem>
206
+ );
207
+
208
+ default:
209
+ return null;
210
+ }
211
+ };
@@ -0,0 +1,62 @@
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 role='none' className={mx('flex flex-col gap-3 p-trim-md border border-separator rounded-sm', classNames)}>
24
+ {children}
25
+ </div>
26
+ );
27
+ };
28
+
29
+ SettingsPanel.displayName = SETTINGS_PANEL_NAME;
30
+
31
+ //
32
+ // Item (label + optional description + control, proper label semantics).
33
+ //
34
+
35
+ type SettingsItemProps = PropsWithChildren<{
36
+ title: Label;
37
+ description?: Label;
38
+ }>;
39
+
40
+ export const SettingsItem = ({ title, description = '', children }: SettingsItemProps) => {
41
+ const { t } = useTranslation(translationKey);
42
+
43
+ return (
44
+ <Input.Root>
45
+ <SettingsPanel classNames='grid grid-cols-1 md:grid-cols-[1fr_1fr] gap-x-trim-lg gap-y-0'>
46
+ <Input.Label classNames='pb-trim-md text-base-surface-text text-lg md:col-span-2'>
47
+ {toLocalizedString(title, t)}
48
+ </Input.Label>
49
+ <Input.DescriptionAndValidation>
50
+ <Input.Description classNames='text-base text-description'>
51
+ {toLocalizedString(description, t)}
52
+ </Input.Description>
53
+ </Input.DescriptionAndValidation>
54
+ <div role='none' className='text-end py-1'>
55
+ {children}
56
+ </div>
57
+ </SettingsPanel>
58
+ </Input.Root>
59
+ );
60
+ };
61
+
62
+ SettingsItem.displayName = SETTINGS_ITEM_NAME;
@@ -0,0 +1,6 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ export * from './Settings';
6
+ export { type SettingsFieldMap, type SettingsFieldProps, type SettingsFieldSetProps } from './SettingsFieldSet';
@@ -6,43 +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, Filter, JsonSchema, Obj, Query, type QueryAST, Tag, Type } from '@dxos/echo';
10
+ import { type View } from '@dxos/echo';
11
+ import { type EchoSchema, Format, type Mutable } from '@dxos/echo/internal';
11
12
  import { useQuery } from '@dxos/react-client/echo';
12
- import { useClientProvider, withClientProvider } from '@dxos/react-client/testing';
13
+ import { useClientStory, withClientProvider } from '@dxos/react-client/testing';
13
14
  import { useAsyncEffect } from '@dxos/react-ui';
14
- import { withTheme } from '@dxos/react-ui/testing';
15
- import { DataType, type ProjectionModel, createView, getTypenameFromQuery } from '@dxos/schema';
15
+ import { withLayout, withTheme } from '@dxos/react-ui/testing';
16
+ import { type ProjectionModel, ViewModel, getTypenameFromQuery } from '@dxos/schema';
17
+ import { Employer, Organization, Person, Pipeline } from '@dxos/types';
16
18
 
17
19
  import { translations } from '../../translations';
18
- import { TestLayout, TestPanel, VIEW_EDITOR_DEBUG_SYMBOL } from '../testing';
19
-
20
+ import { TestLayout, VIEW_EDITOR_DEBUG_SYMBOL } from '../testing';
20
21
  import { ViewEditor, type ViewEditorProps } from './ViewEditor';
21
22
 
22
23
  const types = [
23
24
  // TODO(burdon): Get label from annotation.
24
- { value: Type.getTypename(DataType.Organization), label: 'Organization' },
25
- { value: Type.getTypename(DataType.Person), label: 'Person' },
26
- { value: Type.getTypename(DataType.Project), label: 'Project' },
27
- { value: Type.getTypename(DataType.Employer), label: 'Employer' },
25
+ { value: Organization.Organization.typename, label: 'Organization' },
26
+ { value: Person.Person.typename, label: 'Person' },
27
+ { value: Pipeline.Pipeline.typename, label: 'Project' },
28
+ { value: Employer.Employer.typename, label: 'Employer' },
28
29
  ];
29
30
 
30
31
  // Type definition for debug objects exposed to tests.
31
32
  export type ViewEditorDebugObjects = {
32
33
  schema: EchoSchema;
33
- view: DataType.View;
34
+ view: View.View;
34
35
  projection: ProjectionModel;
35
36
  };
36
37
 
37
- type StoryProps = Pick<ViewEditorProps, 'readonly' | 'mode'>;
38
+ type DefaultStoryProps = Pick<ViewEditorProps, 'readonly' | 'mode'>;
38
39
 
39
- const DefaultStory = (props: StoryProps) => {
40
- const { space } = useClientProvider();
40
+ const DefaultStory = (props: DefaultStoryProps) => {
41
+ const { space } = useClientStory();
41
42
  const [schema, setSchema] = useState<EchoSchema>();
42
- const [view, setView] = useState<DataType.View>();
43
+ const [view, setView] = useState<View.View>();
43
44
  const projectionRef = useRef<ProjectionModel>(null);
44
45
 
45
- const tags = useQuery(space, Filter.type(Tag.Tag));
46
+ const tags = useQuery(space?.db, Filter.type(Tag.Tag));
46
47
 
47
48
  useAsyncEffect(async () => {
48
49
  if (space) {
@@ -51,8 +52,8 @@ const DefaultStory = (props: StoryProps) => {
51
52
  email: Format.Email,
52
53
  salary: Format.Currency(),
53
54
  }).pipe(
54
- Type.Obj({
55
- typename: 'example.com/type/Test',
55
+ Type.object({
56
+ typename: 'com.example.type.test',
56
57
  version: '0.1.0',
57
58
  }),
58
59
  );
@@ -62,18 +63,17 @@ const DefaultStory = (props: StoryProps) => {
62
63
  description: Schema.String,
63
64
  completed: Schema.Boolean,
64
65
  }).pipe(
65
- Type.Obj({
66
- typename: 'example.com/type/Alternate',
66
+ Type.object({
67
+ typename: 'com.example.type.alternate',
67
68
  version: '0.1.0',
68
69
  }),
69
70
  );
70
71
 
71
72
  const [testSchema] = await space.db.schemaRegistry.register([TestSchema, AlternateSchema]);
72
- const view = createView({
73
+ const view = ViewModel.make({
73
74
  name: 'Test',
74
- query: Query.select(Filter.typename(TestSchema.typename)),
75
- jsonSchema: Type.toJsonSchema(TestSchema),
76
- presentation: Obj.make(Type.Expando, {}),
75
+ query: Query.select(Filter.type(TestSchema)),
76
+ jsonSchema: JsonSchema.toJsonSchema(TestSchema),
77
77
  });
78
78
 
79
79
  setSchema(testSchema);
@@ -87,10 +87,12 @@ const DefaultStory = (props: StoryProps) => {
87
87
  return;
88
88
  }
89
89
 
90
- if (props.mode === 'query') {
90
+ if (props.mode === 'tag') {
91
91
  const queue = target && DXN.tryParse(target) ? target : undefined;
92
- const query = queue ? Query.fromAst(newQuery).options({ queues: [queue] }) : Query.fromAst(newQuery);
93
- view.query.ast = query.ast;
92
+ const query = queue ? Query.fromAst(newQuery).from({ queues: [queue] }) : Query.fromAst(newQuery);
93
+ Obj.change(view, (view) => {
94
+ view.query.ast = query.ast as Mutable<typeof query.ast>;
95
+ });
94
96
 
95
97
  const typename = getTypenameFromQuery(query.ast);
96
98
  const [newSchema] = await space.db.schemaRegistry.query({ typename }).run();
@@ -98,16 +100,18 @@ const DefaultStory = (props: StoryProps) => {
98
100
  return;
99
101
  }
100
102
 
101
- const newView = createView({
103
+ const newView = ViewModel.make({
102
104
  query,
103
105
  jsonSchema: newSchema.jsonSchema,
104
- presentation: Obj.make(Type.Expando, {}),
105
106
  });
106
- view.projection = Obj.getSnapshot(newView).projection;
107
+ Obj.change(view, (view) => {
108
+ view.projection = Obj.getSnapshot(newView).projection as Mutable<typeof view.projection>;
109
+ });
107
110
  setSchema(() => newSchema);
108
111
  } else {
109
- console.log('updateViewQuery', { newQuery });
110
- view.query.ast = newQuery;
112
+ Obj.change(view, (view) => {
113
+ view.query.ast = newQuery as Mutable<typeof newQuery>;
114
+ });
111
115
  schema.updateTypename(getTypenameFromQuery(newQuery));
112
116
  }
113
117
  },
@@ -142,20 +146,18 @@ const DefaultStory = (props: StoryProps) => {
142
146
 
143
147
  return (
144
148
  <TestLayout json={json}>
145
- <TestPanel>
146
- <ViewEditor
147
- ref={projectionRef}
148
- schema={schema}
149
- view={view}
150
- registry={space?.db.schemaRegistry}
151
- mode={props.mode}
152
- readonly={props.readonly}
153
- types={types}
154
- tags={tags}
155
- onQueryChanged={updateViewQuery}
156
- onDelete={handleDelete}
157
- />
158
- </TestPanel>
149
+ <ViewEditor
150
+ ref={projectionRef}
151
+ schema={schema}
152
+ view={view}
153
+ registry={space?.db.schemaRegistry}
154
+ mode={props.mode}
155
+ readonly={props.readonly}
156
+ types={types}
157
+ tags={tags}
158
+ onQueryChanged={updateViewQuery}
159
+ onDelete={handleDelete}
160
+ />
159
161
  </TestLayout>
160
162
  );
161
163
  };
@@ -173,7 +175,8 @@ const meta = {
173
175
  space.db.add(Tag.make({ label: 'New' }));
174
176
  },
175
177
  }),
176
- withTheme,
178
+ withTheme(),
179
+ withLayout({ layout: 'fullscreen' }),
177
180
  ],
178
181
  parameters: {
179
182
  layout: 'fullscreen',
@@ -193,8 +196,8 @@ export const Readonly: Story = {
193
196
  },
194
197
  };
195
198
 
196
- export const QueryMode: Story = {
199
+ export const TagQueryMode: Story = {
197
200
  args: {
198
- mode: 'query',
201
+ mode: 'tag',
199
202
  },
200
203
  };