@dxos/react-ui-form 0.8.4-main.f5c0578 → 0.8.4-main.fcc0d83b33

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 (246) hide show
  1. package/README.md +0 -1
  2. package/dist/lib/browser/index.mjs +2774 -2137
  3. package/dist/lib/browser/index.mjs.map +4 -4
  4. package/dist/lib/browser/meta.json +1 -1
  5. package/dist/lib/browser/translations.mjs +82 -0
  6. package/dist/lib/browser/translations.mjs.map +7 -0
  7. package/dist/lib/node-esm/index.mjs +2774 -2137
  8. package/dist/lib/node-esm/index.mjs.map +4 -4
  9. package/dist/lib/node-esm/meta.json +1 -1
  10. package/dist/lib/node-esm/translations.mjs +84 -0
  11. package/dist/lib/node-esm/translations.mjs.map +7 -0
  12. package/dist/types/src/components/FieldEditor/FieldEditor.d.ts +8 -8
  13. package/dist/types/src/components/FieldEditor/FieldEditor.d.ts.map +1 -1
  14. package/dist/types/src/components/FieldEditor/FieldEditor.stories.d.ts +84 -5
  15. package/dist/types/src/components/FieldEditor/FieldEditor.stories.d.ts.map +1 -1
  16. package/dist/types/src/components/Form/Form.d.ts +109 -25
  17. package/dist/types/src/components/Form/Form.d.ts.map +1 -1
  18. package/dist/types/src/components/Form/Form.stories.d.ts +130 -56
  19. package/dist/types/src/components/Form/Form.stories.d.ts.map +1 -1
  20. package/dist/types/src/components/Form/FormField.d.ts +43 -0
  21. package/dist/types/src/components/Form/FormField.d.ts.map +1 -0
  22. package/dist/types/src/components/Form/FormFieldComponent.d.ts +79 -0
  23. package/dist/types/src/components/Form/FormFieldComponent.d.ts.map +1 -0
  24. package/dist/types/src/components/Form/FormFieldComponent.stories.d.ts +9 -0
  25. package/dist/types/src/components/Form/FormFieldComponent.stories.d.ts.map +1 -0
  26. package/dist/types/src/components/Form/FormFieldSet.d.ts +15 -0
  27. package/dist/types/src/components/Form/FormFieldSet.d.ts.map +1 -0
  28. package/dist/types/src/components/Form/fields/ArrayField.d.ts +18 -0
  29. package/dist/types/src/components/Form/fields/ArrayField.d.ts.map +1 -0
  30. package/dist/types/src/components/Form/fields/BooleanField.d.ts +4 -0
  31. package/dist/types/src/components/Form/fields/BooleanField.d.ts.map +1 -0
  32. package/dist/types/src/components/Form/fields/GeoPointField.d.ts +5 -0
  33. package/dist/types/src/components/Form/fields/GeoPointField.d.ts.map +1 -0
  34. package/dist/types/src/components/Form/fields/MarkdownField.d.ts +11 -0
  35. package/dist/types/src/components/Form/fields/MarkdownField.d.ts.map +1 -0
  36. package/dist/types/src/components/Form/fields/MarkdownField.stories.d.ts +83 -0
  37. package/dist/types/src/components/Form/fields/MarkdownField.stories.d.ts.map +1 -0
  38. package/dist/types/src/components/Form/fields/NumberField.d.ts +4 -0
  39. package/dist/types/src/components/Form/fields/NumberField.d.ts.map +1 -0
  40. package/dist/types/src/components/Form/fields/RefField.d.ts +22 -0
  41. package/dist/types/src/components/Form/fields/RefField.d.ts.map +1 -0
  42. package/dist/types/src/components/Form/fields/SelectField.d.ts +10 -0
  43. package/dist/types/src/components/Form/fields/SelectField.d.ts.map +1 -0
  44. package/dist/types/src/components/Form/fields/SelectOptionField.d.ts +5 -0
  45. package/dist/types/src/components/Form/fields/SelectOptionField.d.ts.map +1 -0
  46. package/dist/types/src/components/Form/fields/SelectOptionField.stories.d.ts +84 -0
  47. package/dist/types/src/components/Form/fields/SelectOptionField.stories.d.ts.map +1 -0
  48. package/dist/types/src/components/Form/fields/TextAreaField.d.ts +4 -0
  49. package/dist/types/src/components/Form/fields/TextAreaField.d.ts.map +1 -0
  50. package/dist/types/src/components/Form/fields/TextField.d.ts +4 -0
  51. package/dist/types/src/components/Form/fields/TextField.d.ts.map +1 -0
  52. package/dist/types/src/components/Form/fields/TupleField.d.ts +6 -0
  53. package/dist/types/src/components/Form/fields/TupleField.d.ts.map +1 -0
  54. package/dist/types/src/components/Form/fields/index.d.ts +12 -0
  55. package/dist/types/src/components/Form/fields/index.d.ts.map +1 -0
  56. package/dist/types/src/components/Form/index.d.ts +2 -4
  57. package/dist/types/src/components/Form/index.d.ts.map +1 -1
  58. package/dist/types/src/components/ObjectForm/ObjectForm.d.ts +9 -0
  59. package/dist/types/src/components/ObjectForm/ObjectForm.d.ts.map +1 -0
  60. package/dist/types/src/components/ObjectForm/index.d.ts +2 -0
  61. package/dist/types/src/components/ObjectForm/index.d.ts.map +1 -0
  62. package/dist/types/src/components/ObjectPicker/ObjectPicker.d.ts +68 -0
  63. package/dist/types/src/components/ObjectPicker/ObjectPicker.d.ts.map +1 -0
  64. package/dist/types/src/components/ObjectPicker/ObjectPicker.stories.d.ts +87 -0
  65. package/dist/types/src/components/ObjectPicker/ObjectPicker.stories.d.ts.map +1 -0
  66. package/dist/types/src/components/ObjectPicker/index.d.ts +2 -0
  67. package/dist/types/src/components/ObjectPicker/index.d.ts.map +1 -0
  68. package/dist/types/src/components/ObjectProperties/ObjectProperties.d.ts +9 -0
  69. package/dist/types/src/components/ObjectProperties/ObjectProperties.d.ts.map +1 -0
  70. package/dist/types/src/components/ObjectProperties/ObjectProperties.stories.d.ts +127 -0
  71. package/dist/types/src/components/ObjectProperties/ObjectProperties.stories.d.ts.map +1 -0
  72. package/dist/types/src/components/ObjectProperties/ObjectProperties.test.d.ts +2 -0
  73. package/dist/types/src/components/ObjectProperties/ObjectProperties.test.d.ts.map +1 -0
  74. package/dist/types/src/components/ObjectProperties/index.d.ts +2 -0
  75. package/dist/types/src/components/ObjectProperties/index.d.ts.map +1 -0
  76. package/dist/types/src/components/Settings/Settings.d.ts +51 -0
  77. package/dist/types/src/components/Settings/Settings.d.ts.map +1 -0
  78. package/dist/types/src/components/Settings/Settings.stories.d.ts +84 -0
  79. package/dist/types/src/components/Settings/Settings.stories.d.ts.map +1 -0
  80. package/dist/types/src/components/Settings/SettingsFieldSet.d.ts +43 -0
  81. package/dist/types/src/components/Settings/SettingsFieldSet.d.ts.map +1 -0
  82. package/dist/types/src/components/Settings/SettingsItem.d.ts +16 -0
  83. package/dist/types/src/components/Settings/SettingsItem.d.ts.map +1 -0
  84. package/dist/types/src/components/Settings/SettingsSelectField.d.ts +18 -0
  85. package/dist/types/src/components/Settings/SettingsSelectField.d.ts.map +1 -0
  86. package/dist/types/src/components/Settings/index.d.ts +4 -0
  87. package/dist/types/src/components/Settings/index.d.ts.map +1 -0
  88. package/dist/types/src/components/ViewEditor/ViewEditor.d.ts +22 -10
  89. package/dist/types/src/components/ViewEditor/ViewEditor.d.ts.map +1 -1
  90. package/dist/types/src/components/ViewEditor/ViewEditor.stories.d.ts +86 -8
  91. package/dist/types/src/components/ViewEditor/ViewEditor.stories.d.ts.map +1 -1
  92. package/dist/types/src/components/index.d.ts +4 -2
  93. package/dist/types/src/components/index.d.ts.map +1 -1
  94. package/dist/types/src/components/testing.d.ts +5 -4
  95. package/dist/types/src/components/testing.d.ts.map +1 -1
  96. package/dist/types/src/hooks/index.d.ts +2 -3
  97. package/dist/types/src/hooks/index.d.ts.map +1 -1
  98. package/dist/types/src/hooks/useFormHandler.d.ts +82 -0
  99. package/dist/types/src/hooks/useFormHandler.d.ts.map +1 -0
  100. package/dist/types/src/hooks/useKeyHandler.d.ts +7 -0
  101. package/dist/types/src/hooks/useKeyHandler.d.ts.map +1 -0
  102. package/dist/types/src/index.d.ts +0 -2
  103. package/dist/types/src/index.d.ts.map +1 -1
  104. package/dist/types/src/translations.d.ts +65 -55
  105. package/dist/types/src/translations.d.ts.map +1 -1
  106. package/dist/types/src/util/field-type.d.ts +19 -0
  107. package/dist/types/src/util/field-type.d.ts.map +1 -0
  108. package/dist/types/src/util/field.d.ts +3 -3
  109. package/dist/types/src/util/field.d.ts.map +1 -1
  110. package/dist/types/src/util/formatting.d.ts +2 -2
  111. package/dist/types/src/util/formatting.d.ts.map +1 -1
  112. package/dist/types/src/util/index.d.ts +2 -1
  113. package/dist/types/src/util/index.d.ts.map +1 -1
  114. package/dist/types/src/util/properties.d.ts +7 -0
  115. package/dist/types/src/util/properties.d.ts.map +1 -0
  116. package/dist/types/src/util/refs.d.ts +3 -3
  117. package/dist/types/src/util/refs.d.ts.map +1 -1
  118. package/dist/types/tsconfig.tsbuildinfo +1 -1
  119. package/package.json +56 -46
  120. package/src/components/FieldEditor/FieldEditor.stories.tsx +49 -31
  121. package/src/components/FieldEditor/FieldEditor.test.tsx +0 -1
  122. package/src/components/FieldEditor/FieldEditor.tsx +86 -91
  123. package/src/components/Form/AUDIT.md +17 -0
  124. package/src/components/Form/Form.stories.tsx +147 -224
  125. package/src/components/Form/Form.tsx +376 -96
  126. package/src/components/Form/FormField.tsx +306 -0
  127. package/src/components/Form/FormFieldComponent.stories.tsx +65 -0
  128. package/src/components/Form/FormFieldComponent.tsx +174 -0
  129. package/src/components/Form/FormFieldSet.tsx +144 -0
  130. package/src/components/Form/README.md +3 -0
  131. package/src/components/Form/fields/ArrayField.tsx +159 -0
  132. package/src/components/Form/fields/BooleanField.tsx +22 -0
  133. package/src/components/Form/fields/GeoPointField.tsx +121 -0
  134. package/src/components/Form/fields/MarkdownField.stories.tsx +152 -0
  135. package/src/components/Form/fields/MarkdownField.tsx +168 -0
  136. package/src/components/Form/fields/NumberField.tsx +39 -0
  137. package/src/components/Form/fields/RefField.tsx +229 -0
  138. package/src/components/Form/fields/SelectField.tsx +65 -0
  139. package/src/components/Form/fields/SelectOptionField.stories.tsx +66 -0
  140. package/src/components/Form/{custom/SelectOptionsInput.tsx → fields/SelectOptionField.tsx} +37 -31
  141. package/src/components/Form/fields/TextAreaField.tsx +38 -0
  142. package/src/components/Form/fields/TextField.tsx +38 -0
  143. package/src/components/Form/{custom/TupleInput.tsx → fields/TupleField.tsx} +17 -10
  144. package/src/components/Form/fields/index.ts +15 -0
  145. package/src/components/Form/index.ts +3 -4
  146. package/src/components/ObjectForm/ObjectForm.tsx +114 -0
  147. package/src/components/{ControlSection → ObjectForm}/index.ts +1 -1
  148. package/src/components/ObjectPicker/ObjectPicker.stories.tsx +212 -0
  149. package/src/components/ObjectPicker/ObjectPicker.tsx +194 -0
  150. package/src/components/ObjectPicker/index.ts +5 -0
  151. package/src/components/ObjectProperties/ObjectProperties.stories.tsx +362 -0
  152. package/src/components/ObjectProperties/ObjectProperties.test.tsx +101 -0
  153. package/src/components/ObjectProperties/ObjectProperties.tsx +141 -0
  154. package/src/components/ObjectProperties/index.ts +5 -0
  155. package/src/components/Settings/AUDIT.md +52 -0
  156. package/src/components/Settings/Settings.stories.tsx +104 -0
  157. package/src/components/Settings/Settings.tsx +82 -0
  158. package/src/components/Settings/SettingsFieldSet.tsx +257 -0
  159. package/src/components/Settings/SettingsItem.tsx +62 -0
  160. package/src/components/Settings/SettingsSelectField.tsx +59 -0
  161. package/src/components/Settings/index.ts +7 -0
  162. package/src/components/ViewEditor/ViewEditor.stories.tsx +140 -63
  163. package/src/components/ViewEditor/ViewEditor.test.tsx +12 -15
  164. package/src/components/ViewEditor/ViewEditor.tsx +348 -212
  165. package/src/components/index.ts +4 -3
  166. package/src/components/testing.tsx +31 -12
  167. package/src/hooks/index.ts +2 -3
  168. package/src/hooks/useFormHandler.ts +380 -0
  169. package/src/hooks/useKeyHandler.tsx +37 -0
  170. package/src/index.ts +0 -4
  171. package/src/translations.ts +70 -59
  172. package/src/util/field-type.ts +60 -0
  173. package/src/util/field.ts +33 -43
  174. package/src/util/formatting.ts +22 -22
  175. package/src/util/index.ts +2 -1
  176. package/src/util/properties.ts +18 -0
  177. package/src/util/refs.ts +17 -19
  178. package/dist/types/src/components/ControlSection/ControlSection.d.ts +0 -23
  179. package/dist/types/src/components/ControlSection/ControlSection.d.ts.map +0 -1
  180. package/dist/types/src/components/ControlSection/ControlSection.stories.d.ts +0 -8
  181. package/dist/types/src/components/ControlSection/ControlSection.stories.d.ts.map +0 -1
  182. package/dist/types/src/components/ControlSection/index.d.ts +0 -2
  183. package/dist/types/src/components/ControlSection/index.d.ts.map +0 -1
  184. package/dist/types/src/components/Form/ArrayField.d.ts +0 -16
  185. package/dist/types/src/components/Form/ArrayField.d.ts.map +0 -1
  186. package/dist/types/src/components/Form/Defaults.d.ts +0 -14
  187. package/dist/types/src/components/Form/Defaults.d.ts.map +0 -1
  188. package/dist/types/src/components/Form/FormActions.d.ts +0 -9
  189. package/dist/types/src/components/Form/FormActions.d.ts.map +0 -1
  190. package/dist/types/src/components/Form/FormContent.d.ts +0 -57
  191. package/dist/types/src/components/Form/FormContent.d.ts.map +0 -1
  192. package/dist/types/src/components/Form/FormContext.d.ts +0 -23
  193. package/dist/types/src/components/Form/FormContext.d.ts.map +0 -1
  194. package/dist/types/src/components/Form/Input.d.ts +0 -26
  195. package/dist/types/src/components/Form/Input.d.ts.map +0 -1
  196. package/dist/types/src/components/Form/RefField.d.ts +0 -12
  197. package/dist/types/src/components/Form/RefField.d.ts.map +0 -1
  198. package/dist/types/src/components/Form/custom/GeoPointInput.d.ts +0 -4
  199. package/dist/types/src/components/Form/custom/GeoPointInput.d.ts.map +0 -1
  200. package/dist/types/src/components/Form/custom/SelectOptionsInput.d.ts +0 -4
  201. package/dist/types/src/components/Form/custom/SelectOptionsInput.d.ts.map +0 -1
  202. package/dist/types/src/components/Form/custom/SelectOptionsInput.stories.d.ts +0 -7
  203. package/dist/types/src/components/Form/custom/SelectOptionsInput.stories.d.ts.map +0 -1
  204. package/dist/types/src/components/Form/custom/TupleInput.d.ts +0 -6
  205. package/dist/types/src/components/Form/custom/TupleInput.d.ts.map +0 -1
  206. package/dist/types/src/components/Form/custom/index.d.ts +0 -4
  207. package/dist/types/src/components/Form/custom/index.d.ts.map +0 -1
  208. package/dist/types/src/components/Form/factory.d.ts +0 -8
  209. package/dist/types/src/components/Form/factory.d.ts.map +0 -1
  210. package/dist/types/src/components/deprecated/Form.d.ts +0 -15
  211. package/dist/types/src/components/deprecated/Form.d.ts.map +0 -1
  212. package/dist/types/src/components/deprecated/Form.stories.d.ts +0 -13
  213. package/dist/types/src/components/deprecated/Form.stories.d.ts.map +0 -1
  214. package/dist/types/src/components/deprecated/FormInput.d.ts +0 -16
  215. package/dist/types/src/components/deprecated/FormInput.d.ts.map +0 -1
  216. package/dist/types/src/components/deprecated/index.d.ts +0 -2
  217. package/dist/types/src/components/deprecated/index.d.ts.map +0 -1
  218. package/dist/types/src/hooks/useForm.d.ts +0 -67
  219. package/dist/types/src/hooks/useForm.d.ts.map +0 -1
  220. package/dist/types/src/hooks/useQueryRefOptions.d.ts +0 -23
  221. package/dist/types/src/hooks/useQueryRefOptions.d.ts.map +0 -1
  222. package/dist/types/src/hooks/useRefQueryLookupHandler.d.ts +0 -8
  223. package/dist/types/src/hooks/useRefQueryLookupHandler.d.ts.map +0 -1
  224. package/dist/types/src/util/schema.d.ts +0 -3
  225. package/dist/types/src/util/schema.d.ts.map +0 -1
  226. package/src/components/ControlSection/ControlSection.stories.tsx +0 -42
  227. package/src/components/ControlSection/ControlSection.tsx +0 -139
  228. package/src/components/Form/ArrayField.tsx +0 -125
  229. package/src/components/Form/Defaults.tsx +0 -204
  230. package/src/components/Form/FormActions.tsx +0 -54
  231. package/src/components/Form/FormContent.tsx +0 -241
  232. package/src/components/Form/FormContext.tsx +0 -110
  233. package/src/components/Form/Input.tsx +0 -51
  234. package/src/components/Form/RefField.tsx +0 -214
  235. package/src/components/Form/custom/GeoPointInput.tsx +0 -72
  236. package/src/components/Form/custom/SelectOptionsInput.stories.tsx +0 -62
  237. package/src/components/Form/custom/index.ts +0 -7
  238. package/src/components/Form/factory.tsx +0 -31
  239. package/src/components/deprecated/Form.stories.tsx +0 -63
  240. package/src/components/deprecated/Form.tsx +0 -119
  241. package/src/components/deprecated/FormInput.tsx +0 -71
  242. package/src/components/deprecated/index.ts +0 -5
  243. package/src/hooks/useForm.ts +0 -293
  244. package/src/hooks/useQueryRefOptions.ts +0 -53
  245. package/src/hooks/useRefQueryLookupHandler.ts +0 -39
  246. package/src/util/schema.ts +0 -14
@@ -0,0 +1,159 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ import * as Option from 'effect/Option';
6
+ import * as SchemaAST from 'effect/SchemaAST';
7
+ import React, { useCallback } from 'react';
8
+
9
+ import { findNode, getArrayElementType, getDiscriminatedType, isDiscriminatedUnion, isNestedType } from '@dxos/effect';
10
+ import { IconButton, useTranslation } from '@dxos/react-ui';
11
+
12
+ import { translationKey } from '#translations';
13
+
14
+ import { getFormProperties } from '../../../util';
15
+ import { useFormValues } from '../Form';
16
+ import { FormField, type FormFieldProps } from '../FormField';
17
+ import { FormFieldLabel, type FormFieldStateProps } from '../FormFieldComponent';
18
+
19
+ export type ArrayFieldProps = {
20
+ label: string;
21
+ fieldProps: FormFieldStateProps;
22
+ } & FormFieldProps;
23
+
24
+ export const ArrayField = ({
25
+ type,
26
+ path,
27
+ label,
28
+ readonly,
29
+ layout,
30
+ fieldProps: inputProps,
31
+ ...props
32
+ }: ArrayFieldProps) => {
33
+ const { t } = useTranslation(translationKey);
34
+ const elementType = getArrayElementType(type);
35
+ const { onValueChange } = inputProps;
36
+ const values = useFormValues(ArrayField.displayName, path, () => []);
37
+
38
+ const getDefaultObjectValue = (typeNode: SchemaAST.AST): any => {
39
+ const baseNode = findNode(typeNode, isDiscriminatedUnion);
40
+ const typeLiteral = baseNode ? getDiscriminatedType(baseNode, {}) : findNode(typeNode, SchemaAST.isTypeLiteral);
41
+ if (!typeLiteral) {
42
+ return {};
43
+ }
44
+
45
+ return Object.fromEntries(
46
+ getFormProperties(typeLiteral).map((prop) => {
47
+ const defaultValue = SchemaAST.getDefaultAnnotation(prop.type).pipe((annotation) =>
48
+ Option.getOrUndefined(annotation),
49
+ );
50
+ return [prop.name, defaultValue];
51
+ }),
52
+ );
53
+ };
54
+
55
+ const handleAdd = useCallback(() => {
56
+ const defaultValue =
57
+ isNestedType(type) && elementType ? getDefaultObjectValue(elementType) : getDefaultValue(elementType);
58
+ values && onValueChange(type, [...values, defaultValue]);
59
+ }, [onValueChange, type, elementType, values]);
60
+
61
+ const handleDelete = useCallback(
62
+ (idx: number) => {
63
+ onValueChange(
64
+ type,
65
+ values?.filter((_, i) => i !== idx),
66
+ );
67
+ },
68
+ [onValueChange, type, values],
69
+ );
70
+
71
+ if (!elementType || ((readonly || layout === 'static') && values && values.length < 1)) {
72
+ return null;
73
+ }
74
+
75
+ return (
76
+ <>
77
+ {(layout !== 'static' || (values && values.length > 0)) && (
78
+ <FormFieldLabel readonly={readonly} label={label} asChild />
79
+ )}
80
+
81
+ <div role='none' className='flex flex-col gap-form-gap'>
82
+ {values?.map((_, index) => {
83
+ return (
84
+ <div
85
+ role='none'
86
+ key={index}
87
+ className='grid grid-cols-[1fr_min-content] gap-form-gap last:mb-form-gap items-center'
88
+ >
89
+ <FormField
90
+ autoFocus={index === values.length - 1}
91
+ type={elementType}
92
+ path={[...(path ?? []), index]}
93
+ readonly={readonly || layout === 'static'}
94
+ layout='inline'
95
+ {...props}
96
+ />
97
+
98
+ {!readonly && layout !== 'static' && (
99
+ <div role='none' className='flex flex-col h-full justify-end'>
100
+ {/* NOTE: Aligns with center of last field if multi-field object. */}
101
+ <div role='none' className='flex items-center h-[2rem]'>
102
+ <IconButton
103
+ icon='ph--x--regular'
104
+ iconOnly
105
+ label={t('remove.button')}
106
+ onClick={() => handleDelete(index)}
107
+ classNames='self-center'
108
+ />
109
+ </div>
110
+ </div>
111
+ )}
112
+ </div>
113
+ );
114
+ })}
115
+ </div>
116
+
117
+ {/* TODO(burdon): Get label from schema. */}
118
+ {!readonly && layout !== 'static' && (
119
+ <IconButton
120
+ classNames='flex w-full'
121
+ icon='ph--plus--regular'
122
+ label={t('add-item.button')}
123
+ onClick={handleAdd}
124
+ />
125
+ )}
126
+ </>
127
+ );
128
+ };
129
+
130
+ ArrayField.displayName = 'Form.ArrayField';
131
+
132
+ /**
133
+ * Returns the default empty value for a given AST.
134
+ * Used for initializing new array values etc.
135
+ */
136
+ // TODO(wittjosiah): Factor out?
137
+ export const getDefaultValue = (ast?: SchemaAST.AST): any => {
138
+ switch (ast?._tag) {
139
+ case 'StringKeyword': {
140
+ return '';
141
+ }
142
+ case 'NumberKeyword': {
143
+ return 0;
144
+ }
145
+ case 'BooleanKeyword': {
146
+ return false;
147
+ }
148
+ case 'Suspend': {
149
+ return getDefaultValue(ast.f());
150
+ }
151
+ default: {
152
+ if (ast && isNestedType(ast)) {
153
+ return {};
154
+ } else {
155
+ throw new Error(`Unsupported type: ${ast?._tag}`);
156
+ }
157
+ }
158
+ }
159
+ };
@@ -0,0 +1,22 @@
1
+ //
2
+ // Copyright 2024 DXOS.org
3
+ //
4
+
5
+ import React, { useCallback } from 'react';
6
+
7
+ import { Input, type SwitchProps } from '@dxos/react-ui';
8
+
9
+ import { type FormFieldComponentProps, FormFieldWrapper } from '../FormFieldComponent';
10
+
11
+ export const BooleanField = ({ type, readonly, onValueChange, ...props }: FormFieldComponentProps<boolean>) => {
12
+ const handleChange = useCallback<NonNullable<SwitchProps['onCheckedChange']>>(
13
+ (value) => onValueChange?.(type, value),
14
+ [type, onValueChange],
15
+ );
16
+
17
+ return (
18
+ <FormFieldWrapper<boolean> readonly={readonly} {...props}>
19
+ {({ value }) => <Input.Switch disabled={!!readonly} checked={value} onCheckedChange={handleChange} />}
20
+ </FormFieldWrapper>
21
+ );
22
+ };
@@ -0,0 +1,121 @@
1
+ //
2
+ // Copyright 2024 DXOS.org
3
+ //
4
+
5
+ import React, { type ChangeEvent, useCallback, useEffect, useMemo, useState } from 'react';
6
+
7
+ import { GeoLocation, type GeoPoint } from '@dxos/echo/internal';
8
+ import { Input, useTranslation } from '@dxos/react-ui';
9
+ import { safeParseFloat } from '@dxos/util';
10
+
11
+ import { translationKey } from '#translations';
12
+
13
+ import { type FormFieldComponentProps, FormFieldLabel } from '../FormFieldComponent';
14
+
15
+ export const GeoPointField = ({
16
+ type,
17
+ label,
18
+ readonly,
19
+ layout,
20
+ getStatus,
21
+ getValue,
22
+ onValueChange,
23
+ onBlur,
24
+ }: FormFieldComponentProps<GeoPoint>) => {
25
+ const { t } = useTranslation(translationKey);
26
+ const { status, error } = getStatus();
27
+ const geoPoint = useMemo<GeoPoint>(() => getValue() ?? [0, 0], [getValue]);
28
+ const value = useMemo(() => GeoLocation.fromGeoPoint(geoPoint), [geoPoint]);
29
+
30
+ const [longitudeText, setLongitudeText] = useState(value.longitude?.toString());
31
+ const [latitudeText, setLatitudeText] = useState(value.latitude?.toString());
32
+ useEffect(() => {
33
+ const location = GeoLocation.fromGeoPoint(geoPoint);
34
+ setLongitudeText(location.longitude?.toString());
35
+ setLatitudeText(location.latitude?.toString());
36
+ }, [geoPoint]);
37
+
38
+ const handleChange = useCallback(
39
+ (coordinateType: keyof Pick<GeoLocation, 'longitude' | 'latitude'>, setText: (text: string) => void) =>
40
+ (event: ChangeEvent<HTMLInputElement>) => {
41
+ const inputText = event.target.value;
42
+ setText(inputText);
43
+ if (inputText !== '' && inputText !== '-') {
44
+ const coord = safeParseFloat(inputText);
45
+ if (coord !== undefined && !isNaN(coord)) {
46
+ const currentLocation = GeoLocation.fromGeoPoint(getValue() ?? [0, 0]);
47
+ const newLocation = { ...currentLocation, [coordinateType]: coord };
48
+ const newValue = GeoLocation.toGeoPoint(newLocation);
49
+ onValueChange(type, newValue);
50
+ }
51
+ }
52
+ },
53
+ [type, getValue, onValueChange],
54
+ );
55
+
56
+ if ((readonly || layout === 'static') && !value?.latitude && !value?.longitude) {
57
+ return null;
58
+ }
59
+
60
+ return (
61
+ <Input.Root validationValence={status}>
62
+ {layout !== 'inline' && <FormFieldLabel error={error} readonly={readonly} label={label} asChild />}
63
+ {layout === 'static' ? (
64
+ <LatLng {...value} />
65
+ ) : (
66
+ <div role='none' className='grid grid-cols-2 gap-form-gap'>
67
+ <div>
68
+ <Input.Root>
69
+ {layout !== 'inline' && <Input.Label>{t('latitude.label')}</Input.Label>}
70
+ <Input.TextInput
71
+ type='number'
72
+ step='0.00001'
73
+ min='-90'
74
+ max='90'
75
+ disabled={!!readonly}
76
+ placeholder={t('latitude.placeholder')}
77
+ value={latitudeText ?? ''}
78
+ onChange={handleChange('latitude', setLatitudeText)}
79
+ onBlur={onBlur}
80
+ />
81
+ </Input.Root>
82
+ </div>
83
+ <div>
84
+ <Input.Root>
85
+ {layout !== 'inline' && <Input.Label>{t('longitude.label')}</Input.Label>}
86
+ <Input.TextInput
87
+ type='number'
88
+ step='0.00001'
89
+ min='-180'
90
+ max='180'
91
+ disabled={!!readonly}
92
+ placeholder={t('longitude.placeholder')}
93
+ value={longitudeText ?? ''}
94
+ onChange={handleChange('longitude', setLongitudeText)}
95
+ onBlur={onBlur}
96
+ />
97
+ </Input.Root>
98
+ </div>
99
+ </div>
100
+ )}
101
+ </Input.Root>
102
+ );
103
+ };
104
+
105
+ const LatLng = ({ latitude = 0, longitude = 0 }: GeoLocation) => {
106
+ const latHem = latitude >= 0 ? 'N' : 'S';
107
+ const lngHem = longitude >= 0 ? 'E' : 'W';
108
+
109
+ return (
110
+ <span className='inline-flex items-center gap-form-gap'>
111
+ <span>
112
+ <span>{Math.abs(latitude).toFixed(5)}</span>
113
+ <span className='text-subdued'>°{latHem}</span>
114
+ </span>
115
+ <span>
116
+ <span>{Math.abs(longitude).toFixed(5)}</span>
117
+ <span className='text-subdued'>°{lngHem}</span>
118
+ </span>
119
+ </span>
120
+ );
121
+ };
@@ -0,0 +1,152 @@
1
+ //
2
+ // Copyright 2026 DXOS.org
3
+ //
4
+
5
+ import { type Meta, type StoryObj } from '@storybook/react-vite';
6
+ import * as Schema from 'effect/Schema';
7
+ import React, { useMemo } from 'react';
8
+
9
+ import { Filter, Format, Obj, Ref } from '@dxos/echo';
10
+ import { useQuery } from '@dxos/react-client/echo';
11
+ import { useClientStory, withClientProvider } from '@dxos/react-client/testing';
12
+ import { Panel } from '@dxos/react-ui';
13
+ import { withLayout, withTheme } from '@dxos/react-ui/testing';
14
+ import { Text } from '@dxos/schema';
15
+
16
+ import { translations } from '#translations';
17
+
18
+ import { TestLayout } from '../../testing';
19
+ import { Form } from '../Form';
20
+
21
+ //
22
+ // String-backed markdown.
23
+ //
24
+
25
+ const StringSchema = Schema.Struct({
26
+ notes: Schema.String.pipe(Format.FormatAnnotation.set(Format.TypeFormat.Markdown)).annotations({ title: 'Notes' }),
27
+ });
28
+
29
+ const StringStory = () => (
30
+ <TestLayout>
31
+ <Form.Root schema={StringSchema} defaultValues={{ notes: '# Hello\n\nMarkdown text *here*.' }}>
32
+ <Form.Viewport>
33
+ <Form.Content>
34
+ <Form.FieldSet />
35
+ </Form.Content>
36
+ </Form.Viewport>
37
+ </Form.Root>
38
+ </TestLayout>
39
+ );
40
+
41
+ //
42
+ // Ref<Text>-backed markdown.
43
+ //
44
+
45
+ const TextNoteHolder = Schema.Struct({
46
+ instructions: Ref.Ref(Text.Text)
47
+ .pipe(Format.FormatAnnotation.set(Format.TypeFormat.Markdown))
48
+ .annotations({ title: 'Instructions' }),
49
+ });
50
+
51
+ const RefStory = () => {
52
+ const { space } = useClientStory();
53
+ const [text] = useQuery(space?.db, Filter.type(Text.Text));
54
+ const values = useMemo(() => (text ? { instructions: Ref.make(text) } : undefined), [text]);
55
+
56
+ if (!space || !values) {
57
+ return null;
58
+ }
59
+
60
+ return (
61
+ <TestLayout>
62
+ <Panel.Root>
63
+ <Panel.Content asChild>
64
+ <Form.Root schema={TextNoteHolder} values={values as any} db={space.db}>
65
+ <Form.Viewport>
66
+ <Form.Content>
67
+ <Form.FieldSet />
68
+ </Form.Content>
69
+ </Form.Viewport>
70
+ </Form.Root>
71
+ </Panel.Content>
72
+ </Panel.Root>
73
+ </TestLayout>
74
+ );
75
+ };
76
+
77
+ //
78
+ // Ref<Text> empty — shows the create button.
79
+ //
80
+
81
+ const EmptyRefSchema = Schema.Struct({
82
+ instructions: Schema.optional(
83
+ Ref.Ref(Text.Text)
84
+ .pipe(Format.FormatAnnotation.set(Format.TypeFormat.Markdown))
85
+ .annotations({ title: 'Instructions' }),
86
+ ),
87
+ });
88
+
89
+ const EmptyRefStory = () => {
90
+ const { space } = useClientStory();
91
+ if (!space) {
92
+ return null;
93
+ }
94
+
95
+ return (
96
+ <TestLayout>
97
+ <Panel.Root>
98
+ <Panel.Content asChild>
99
+ <Form.Root schema={EmptyRefSchema} defaultValues={{}} db={space.db}>
100
+ <Form.Viewport>
101
+ <Form.Content>
102
+ <Form.FieldSet />
103
+ </Form.Content>
104
+ </Form.Viewport>
105
+ </Form.Root>
106
+ </Panel.Content>
107
+ </Panel.Root>
108
+ </TestLayout>
109
+ );
110
+ };
111
+
112
+ const meta = {
113
+ title: 'ui/react-ui-form/MarkdownField',
114
+ decorators: [withTheme(), withLayout({ layout: 'column' })],
115
+ parameters: {
116
+ layout: 'fullscreen',
117
+ translations,
118
+ },
119
+ } satisfies Meta;
120
+
121
+ export default meta;
122
+
123
+ type Story = StoryObj<typeof meta>;
124
+
125
+ export const StringBacked: Story = {
126
+ render: () => <StringStory />,
127
+ };
128
+
129
+ export const RefPopulated: Story = {
130
+ render: () => <RefStory />,
131
+ decorators: [
132
+ withClientProvider({
133
+ createIdentity: true,
134
+ createSpace: true,
135
+ types: [Text.Text],
136
+ onCreateSpace: async ({ space }) => {
137
+ space.db.add(Obj.make(Text.Text, { content: '# Magazine brief\n\nLong-form text describing what to gather.' }));
138
+ },
139
+ }),
140
+ ],
141
+ };
142
+
143
+ export const RefEmpty: Story = {
144
+ render: () => <EmptyRefStory />,
145
+ decorators: [
146
+ withClientProvider({
147
+ createIdentity: true,
148
+ createSpace: true,
149
+ types: [Text.Text],
150
+ }),
151
+ ],
152
+ };
@@ -0,0 +1,168 @@
1
+ //
2
+ // Copyright 2024 DXOS.org
3
+ //
4
+
5
+ import { useAtomValue } from '@effect-atom/atom-react';
6
+ import React, { useCallback, useMemo } from 'react';
7
+
8
+ import { type Database, Obj, Ref } from '@dxos/echo';
9
+ import { AtomRef } from '@dxos/echo-atom';
10
+ import { createDocAccessor } from '@dxos/echo-db';
11
+ import { Button, Icon, Input, useTranslation } from '@dxos/react-ui';
12
+ import { Editor, useBasicMarkdownExtensions } from '@dxos/react-ui-editor';
13
+ import { Text } from '@dxos/schema';
14
+ import { createDataExtensions } from '@dxos/ui-editor';
15
+
16
+ import { translationKey } from '#translations';
17
+
18
+ import { type FormFieldComponentProps, FormFieldLabel } from '../FormFieldComponent';
19
+
20
+ const editorClassNames =
21
+ 'transition-colors bg-input-surface focus-within:bg-focus-surface border border-separator rounded-xs p-1 px-2';
22
+
23
+ /**
24
+ * Form field that edits a markdown value in a CodeMirror editor.
25
+ * Supports two backings:
26
+ * - `Schema.String` annotated with `Format.TypeFormat.Markdown`: edited as plain text via `getValue`/`onValueChange`.
27
+ * - `Ref<Text>` annotated with `Format.TypeFormat.Markdown`: edited in-place via an Automerge doc accessor.
28
+ * When the ref is empty and a database is available, a create button is shown to allocate a new Text.
29
+ */
30
+ export const MarkdownField = ({
31
+ type,
32
+ readonly,
33
+ label,
34
+ placeholder,
35
+ layout,
36
+ db,
37
+ getStatus,
38
+ getValue,
39
+ onValueChange,
40
+ }: FormFieldComponentProps) => {
41
+ const { status, error } = getStatus();
42
+ const isRef = Ref.isRefType(type);
43
+ const value = getValue();
44
+
45
+ if ((readonly || layout === 'static') && value == null) {
46
+ return null;
47
+ }
48
+
49
+ const renderEditor = () => {
50
+ if (isRef) {
51
+ const reference = value as Ref.Ref<any> | undefined;
52
+ if (reference) {
53
+ return <RefMarkdownEditor reference={reference} placeholder={placeholder} />;
54
+ }
55
+
56
+ if (readonly) {
57
+ return null;
58
+ }
59
+
60
+ return <CreateTextButton db={db} disabled={!db} onCreate={(ref) => onValueChange(type, ref)} />;
61
+ }
62
+
63
+ return (
64
+ <StringMarkdownEditor
65
+ value={(value as string | undefined) ?? ''}
66
+ placeholder={placeholder}
67
+ readonly={!!readonly}
68
+ onChange={(next) => onValueChange(type, next)}
69
+ />
70
+ );
71
+ };
72
+
73
+ const renderStatic = () => {
74
+ if (isRef) {
75
+ const reference = value as Ref.Ref<any> | undefined;
76
+ return reference ? <RefStaticText reference={reference} /> : null;
77
+ }
78
+ return <p className='whitespace-pre-wrap'>{(value as string | undefined) ?? ''}</p>;
79
+ };
80
+
81
+ return (
82
+ <Input.Root validationValence={status}>
83
+ {layout !== 'inline' && <FormFieldLabel error={error} readonly={readonly} label={label} />}
84
+ {layout === 'static' ? renderStatic() : renderEditor()}
85
+ {layout === 'full' && <Input.Validation>{error}</Input.Validation>}
86
+ </Input.Root>
87
+ );
88
+ };
89
+
90
+ /** Read-only static rendering for a `Ref<Text>` value: resolve the ref and print its content. */
91
+ const RefStaticText = ({ reference }: { reference: Ref.Ref<any> }) => {
92
+ const text = useAtomValue(useMemo(() => AtomRef.make(reference), [reference]));
93
+ if (!text?.content) {
94
+ return null;
95
+ }
96
+ return <p className='whitespace-pre-wrap'>{text.content}</p>;
97
+ };
98
+
99
+ type RefMarkdownEditorProps = {
100
+ reference: Ref.Ref<any>;
101
+ placeholder?: string;
102
+ };
103
+
104
+ const RefMarkdownEditor = ({ reference, placeholder }: RefMarkdownEditorProps) => {
105
+ const text = useAtomValue(useMemo(() => AtomRef.make(reference), [reference]));
106
+ const dataExtensions = useMemo(
107
+ () =>
108
+ text ? [createDataExtensions({ id: reference.dxn.toString(), text: createDocAccessor(text, ['content']) })] : [],
109
+ [text, reference],
110
+ );
111
+ const extensions = useBasicMarkdownExtensions({ placeholder, extensions: dataExtensions });
112
+ if (!text) {
113
+ return null;
114
+ }
115
+
116
+ return (
117
+ <Editor.Root>
118
+ <Editor.View classNames={editorClassNames} extensions={extensions} />
119
+ </Editor.Root>
120
+ );
121
+ };
122
+
123
+ type StringMarkdownEditorProps = {
124
+ value: string;
125
+ placeholder?: string;
126
+ readonly?: boolean;
127
+ onChange: (value: string) => void;
128
+ };
129
+
130
+ const StringMarkdownEditor = ({ value, placeholder, readonly, onChange }: StringMarkdownEditorProps) => {
131
+ const extensions = useBasicMarkdownExtensions({ placeholder });
132
+ const handleChange = useCallback((next: string) => onChange(next), [onChange]);
133
+
134
+ return (
135
+ <Editor.Root>
136
+ <Editor.View
137
+ classNames={editorClassNames}
138
+ extensions={extensions}
139
+ value={value}
140
+ onChange={readonly ? undefined : handleChange}
141
+ />
142
+ </Editor.Root>
143
+ );
144
+ };
145
+
146
+ type CreateTextButtonProps = {
147
+ db?: Database.Database;
148
+ disabled?: boolean;
149
+ onCreate: (ref: Ref.Ref<Text.Text>) => void;
150
+ };
151
+
152
+ const CreateTextButton = ({ db, disabled, onCreate }: CreateTextButtonProps) => {
153
+ const { t } = useTranslation(translationKey);
154
+ const handleClick = useCallback(() => {
155
+ if (!db) {
156
+ return;
157
+ }
158
+ const text = db.add(Obj.make(Text.Text, { content: '' }));
159
+ onCreate(Ref.make(text));
160
+ }, [db, onCreate]);
161
+
162
+ return (
163
+ <Button onClick={handleClick} disabled={disabled}>
164
+ <Icon icon='ph--plus--regular' size={4} />
165
+ <span>{t('create-text.label')}</span>
166
+ </Button>
167
+ );
168
+ };
@@ -0,0 +1,39 @@
1
+ //
2
+ // Copyright 2024 DXOS.org
3
+ //
4
+
5
+ import React, { useCallback } from 'react';
6
+
7
+ import { Input, type TextInputProps } from '@dxos/react-ui';
8
+ import { safeParseFloat } from '@dxos/util';
9
+
10
+ import { type FormFieldComponentProps, FormFieldWrapper } from '../FormFieldComponent';
11
+
12
+ export const NumberField = ({
13
+ type,
14
+ readonly,
15
+ placeholder,
16
+ onValueChange,
17
+ onBlur,
18
+ ...props
19
+ }: FormFieldComponentProps<number>) => {
20
+ const handleChange = useCallback<NonNullable<TextInputProps['onChange']>>(
21
+ (event) => onValueChange(type, safeParseFloat(event.target.value) || 0),
22
+ [type, onValueChange],
23
+ );
24
+
25
+ return (
26
+ <FormFieldWrapper<number> readonly={readonly} {...props}>
27
+ {({ value = '' }) => (
28
+ <Input.TextInput
29
+ type='number'
30
+ disabled={!!readonly}
31
+ placeholder={placeholder}
32
+ value={value}
33
+ onChange={handleChange}
34
+ onBlur={onBlur}
35
+ />
36
+ )}
37
+ </FormFieldWrapper>
38
+ );
39
+ };