@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,158 @@
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
+ import { getFormProperties } from '../../../util';
14
+ import { useFormValues } from '../Form';
15
+ import { FormField, type FormFieldProps } from '../FormField';
16
+ import { FormFieldLabel, type FormFieldStateProps } from '../FormFieldComponent';
17
+
18
+ export type ArrayFieldProps = {
19
+ label: string;
20
+ fieldProps: FormFieldStateProps;
21
+ } & FormFieldProps;
22
+
23
+ export const ArrayField = ({
24
+ type,
25
+ path,
26
+ label,
27
+ readonly,
28
+ layout,
29
+ fieldProps: inputProps,
30
+ ...props
31
+ }: ArrayFieldProps) => {
32
+ const { t } = useTranslation(translationKey);
33
+ const elementType = getArrayElementType(type);
34
+ const { onValueChange } = inputProps;
35
+ const values = useFormValues(ArrayField.displayName, path, () => []);
36
+
37
+ const getDefaultObjectValue = (typeNode: SchemaAST.AST): any => {
38
+ const baseNode = findNode(typeNode, isDiscriminatedUnion);
39
+ const typeLiteral = baseNode ? getDiscriminatedType(baseNode, {}) : findNode(typeNode, SchemaAST.isTypeLiteral);
40
+ if (!typeLiteral) {
41
+ return {};
42
+ }
43
+
44
+ return Object.fromEntries(
45
+ getFormProperties(typeLiteral).map((prop) => {
46
+ const defaultValue = SchemaAST.getDefaultAnnotation(prop.type).pipe((annotation) =>
47
+ Option.getOrUndefined(annotation),
48
+ );
49
+ return [prop.name, defaultValue];
50
+ }),
51
+ );
52
+ };
53
+
54
+ const handleAdd = useCallback(() => {
55
+ const defaultValue =
56
+ isNestedType(type) && elementType ? getDefaultObjectValue(elementType) : getDefaultValue(elementType);
57
+ values && onValueChange(type, [...values, defaultValue]);
58
+ }, [onValueChange, type, elementType, values]);
59
+
60
+ const handleDelete = useCallback(
61
+ (idx: number) => {
62
+ onValueChange(
63
+ type,
64
+ values?.filter((_, i) => i !== idx),
65
+ );
66
+ },
67
+ [onValueChange, type, values],
68
+ );
69
+
70
+ if (!elementType || ((readonly || layout === 'static') && values && values.length < 1)) {
71
+ return null;
72
+ }
73
+
74
+ return (
75
+ <>
76
+ {(layout !== 'static' || (values && values.length > 0)) && (
77
+ <FormFieldLabel readonly={readonly} label={label} asChild />
78
+ )}
79
+
80
+ <div role='none' className='flex flex-col gap-form-gap'>
81
+ {values?.map((_, index) => {
82
+ return (
83
+ <div
84
+ role='none'
85
+ key={index}
86
+ className='grid grid-cols-[1fr_min-content] gap-form-gap last:mb-form-gap items-center'
87
+ >
88
+ <FormField
89
+ autoFocus={index === values.length - 1}
90
+ type={elementType}
91
+ path={[...(path ?? []), index]}
92
+ readonly={readonly || layout === 'static'}
93
+ layout='inline'
94
+ {...props}
95
+ />
96
+
97
+ {!readonly && layout !== 'static' && (
98
+ <div role='none' className='flex flex-col h-full justify-end'>
99
+ {/* NOTE: Aligns with center of last field if multi-field object. */}
100
+ <div role='none' className='flex items-center h-[2rem]'>
101
+ <IconButton
102
+ icon='ph--x--regular'
103
+ iconOnly
104
+ label={t('remove.button')}
105
+ onClick={() => handleDelete(index)}
106
+ classNames='self-center'
107
+ />
108
+ </div>
109
+ </div>
110
+ )}
111
+ </div>
112
+ );
113
+ })}
114
+ </div>
115
+
116
+ {/* TODO(burdon): Get label from schema. */}
117
+ {!readonly && layout !== 'static' && (
118
+ <IconButton
119
+ classNames='flex w-full'
120
+ icon='ph--plus--regular'
121
+ label={t('add-item.button')}
122
+ onClick={handleAdd}
123
+ />
124
+ )}
125
+ </>
126
+ );
127
+ };
128
+
129
+ ArrayField.displayName = 'Form.ArrayField';
130
+
131
+ /**
132
+ * Returns the default empty value for a given AST.
133
+ * Used for initializing new array values etc.
134
+ */
135
+ // TODO(wittjosiah): Factor out?
136
+ export const getDefaultValue = (ast?: SchemaAST.AST): any => {
137
+ switch (ast?._tag) {
138
+ case 'StringKeyword': {
139
+ return '';
140
+ }
141
+ case 'NumberKeyword': {
142
+ return 0;
143
+ }
144
+ case 'BooleanKeyword': {
145
+ return false;
146
+ }
147
+ case 'Suspend': {
148
+ return getDefaultValue(ast.f());
149
+ }
150
+ default: {
151
+ if (ast && isNestedType(ast)) {
152
+ return {};
153
+ } else {
154
+ throw new Error(`Unsupported type: ${ast?._tag}`);
155
+ }
156
+ }
157
+ }
158
+ };
@@ -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,120 @@
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
+ import { type FormFieldComponentProps, FormFieldLabel } from '../FormFieldComponent';
13
+
14
+ export const GeoPointField = ({
15
+ type,
16
+ label,
17
+ readonly,
18
+ layout,
19
+ getStatus,
20
+ getValue,
21
+ onValueChange,
22
+ onBlur,
23
+ }: FormFieldComponentProps<GeoPoint>) => {
24
+ const { t } = useTranslation(translationKey);
25
+ const { status, error } = getStatus();
26
+ const geoPoint = useMemo<GeoPoint>(() => getValue() ?? [0, 0], [getValue]);
27
+ const value = useMemo(() => GeoLocation.fromGeoPoint(geoPoint), [geoPoint]);
28
+
29
+ const [longitudeText, setLongitudeText] = useState(value.longitude?.toString());
30
+ const [latitudeText, setLatitudeText] = useState(value.latitude?.toString());
31
+ useEffect(() => {
32
+ const location = GeoLocation.fromGeoPoint(geoPoint);
33
+ setLongitudeText(location.longitude?.toString());
34
+ setLatitudeText(location.latitude?.toString());
35
+ }, [geoPoint]);
36
+
37
+ const handleChange = useCallback(
38
+ (coordinateType: keyof Pick<GeoLocation, 'longitude' | 'latitude'>, setText: (text: string) => void) =>
39
+ (event: ChangeEvent<HTMLInputElement>) => {
40
+ const inputText = event.target.value;
41
+ setText(inputText);
42
+ if (inputText !== '' && inputText !== '-') {
43
+ const coord = safeParseFloat(inputText);
44
+ if (coord !== undefined && !isNaN(coord)) {
45
+ const currentLocation = GeoLocation.fromGeoPoint(getValue() ?? [0, 0]);
46
+ const newLocation = { ...currentLocation, [coordinateType]: coord };
47
+ const newValue = GeoLocation.toGeoPoint(newLocation);
48
+ onValueChange(type, newValue);
49
+ }
50
+ }
51
+ },
52
+ [type, getValue, onValueChange],
53
+ );
54
+
55
+ if ((readonly || layout === 'static') && !value?.latitude && !value?.longitude) {
56
+ return null;
57
+ }
58
+
59
+ return (
60
+ <Input.Root validationValence={status}>
61
+ {layout !== 'inline' && <FormFieldLabel error={error} readonly={readonly} label={label} asChild />}
62
+ {layout === 'static' ? (
63
+ <LatLng {...value} />
64
+ ) : (
65
+ <div role='none' className='grid grid-cols-2 gap-form-gap'>
66
+ <div>
67
+ <Input.Root>
68
+ {layout !== 'inline' && <Input.Label>{t('latitude.label')}</Input.Label>}
69
+ <Input.TextInput
70
+ type='number'
71
+ step='0.00001'
72
+ min='-90'
73
+ max='90'
74
+ disabled={!!readonly}
75
+ placeholder={t('latitude.placeholder')}
76
+ value={latitudeText ?? ''}
77
+ onChange={handleChange('latitude', setLatitudeText)}
78
+ onBlur={onBlur}
79
+ />
80
+ </Input.Root>
81
+ </div>
82
+ <div>
83
+ <Input.Root>
84
+ {layout !== 'inline' && <Input.Label>{t('longitude.label')}</Input.Label>}
85
+ <Input.TextInput
86
+ type='number'
87
+ step='0.00001'
88
+ min='-180'
89
+ max='180'
90
+ disabled={!!readonly}
91
+ placeholder={t('longitude.placeholder')}
92
+ value={longitudeText ?? ''}
93
+ onChange={handleChange('longitude', setLongitudeText)}
94
+ onBlur={onBlur}
95
+ />
96
+ </Input.Root>
97
+ </div>
98
+ </div>
99
+ )}
100
+ </Input.Root>
101
+ );
102
+ };
103
+
104
+ const LatLng = ({ latitude = 0, longitude = 0 }: GeoLocation) => {
105
+ const latHem = latitude >= 0 ? 'N' : 'S';
106
+ const lngHem = longitude >= 0 ? 'E' : 'W';
107
+
108
+ return (
109
+ <span className='inline-flex items-center gap-form-gap'>
110
+ <span>
111
+ <span>{Math.abs(latitude).toFixed(5)}</span>
112
+ <span className='text-subdued'>°{latHem}</span>
113
+ </span>
114
+ <span>
115
+ <span>{Math.abs(longitude).toFixed(5)}</span>
116
+ <span className='text-subdued'>°{lngHem}</span>
117
+ </span>
118
+ </span>
119
+ );
120
+ };
@@ -0,0 +1,151 @@
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
+ import { TestLayout } from '../../testing';
18
+ import { Form } from '../Form';
19
+
20
+ //
21
+ // String-backed markdown.
22
+ //
23
+
24
+ const StringSchema = Schema.Struct({
25
+ notes: Schema.String.pipe(Format.FormatAnnotation.set(Format.TypeFormat.Markdown)).annotations({ title: 'Notes' }),
26
+ });
27
+
28
+ const StringStory = () => (
29
+ <TestLayout>
30
+ <Form.Root schema={StringSchema} defaultValues={{ notes: '# Hello\n\nMarkdown text *here*.' }}>
31
+ <Form.Viewport>
32
+ <Form.Content>
33
+ <Form.FieldSet />
34
+ </Form.Content>
35
+ </Form.Viewport>
36
+ </Form.Root>
37
+ </TestLayout>
38
+ );
39
+
40
+ //
41
+ // Ref<Text>-backed markdown.
42
+ //
43
+
44
+ const TextNoteHolder = Schema.Struct({
45
+ instructions: Ref.Ref(Text.Text)
46
+ .pipe(Format.FormatAnnotation.set(Format.TypeFormat.Markdown))
47
+ .annotations({ title: 'Instructions' }),
48
+ });
49
+
50
+ const RefStory = () => {
51
+ const { space } = useClientStory();
52
+ const [text] = useQuery(space?.db, Filter.type(Text.Text));
53
+ const values = useMemo(() => (text ? { instructions: Ref.make(text) } : undefined), [text]);
54
+
55
+ if (!space || !values) {
56
+ return null;
57
+ }
58
+
59
+ return (
60
+ <TestLayout>
61
+ <Panel.Root>
62
+ <Panel.Content asChild>
63
+ <Form.Root schema={TextNoteHolder} values={values as any} db={space.db}>
64
+ <Form.Viewport>
65
+ <Form.Content>
66
+ <Form.FieldSet />
67
+ </Form.Content>
68
+ </Form.Viewport>
69
+ </Form.Root>
70
+ </Panel.Content>
71
+ </Panel.Root>
72
+ </TestLayout>
73
+ );
74
+ };
75
+
76
+ //
77
+ // Ref<Text> empty — shows the create button.
78
+ //
79
+
80
+ const EmptyRefSchema = Schema.Struct({
81
+ instructions: Schema.optional(
82
+ Ref.Ref(Text.Text)
83
+ .pipe(Format.FormatAnnotation.set(Format.TypeFormat.Markdown))
84
+ .annotations({ title: 'Instructions' }),
85
+ ),
86
+ });
87
+
88
+ const EmptyRefStory = () => {
89
+ const { space } = useClientStory();
90
+ if (!space) {
91
+ return null;
92
+ }
93
+
94
+ return (
95
+ <TestLayout>
96
+ <Panel.Root>
97
+ <Panel.Content asChild>
98
+ <Form.Root schema={EmptyRefSchema} defaultValues={{}} db={space.db}>
99
+ <Form.Viewport>
100
+ <Form.Content>
101
+ <Form.FieldSet />
102
+ </Form.Content>
103
+ </Form.Viewport>
104
+ </Form.Root>
105
+ </Panel.Content>
106
+ </Panel.Root>
107
+ </TestLayout>
108
+ );
109
+ };
110
+
111
+ const meta = {
112
+ title: 'ui/react-ui-form/MarkdownField',
113
+ decorators: [withTheme(), withLayout({ layout: 'column' })],
114
+ parameters: {
115
+ layout: 'fullscreen',
116
+ translations,
117
+ },
118
+ } satisfies Meta;
119
+
120
+ export default meta;
121
+
122
+ type Story = StoryObj<typeof meta>;
123
+
124
+ export const StringBacked: Story = {
125
+ render: () => <StringStory />,
126
+ };
127
+
128
+ export const RefPopulated: Story = {
129
+ render: () => <RefStory />,
130
+ decorators: [
131
+ withClientProvider({
132
+ createIdentity: true,
133
+ createSpace: true,
134
+ types: [Text.Text],
135
+ onCreateSpace: async ({ space }) => {
136
+ space.db.add(Obj.make(Text.Text, { content: '# Magazine brief\n\nLong-form text describing what to gather.' }));
137
+ },
138
+ }),
139
+ ],
140
+ };
141
+
142
+ export const RefEmpty: Story = {
143
+ render: () => <EmptyRefStory />,
144
+ decorators: [
145
+ withClientProvider({
146
+ createIdentity: true,
147
+ createSpace: true,
148
+ types: [Text.Text],
149
+ }),
150
+ ],
151
+ };
@@ -0,0 +1,167 @@
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
+ import { type FormFieldComponentProps, FormFieldLabel } from '../FormFieldComponent';
18
+
19
+ const editorClassNames =
20
+ 'transition-colors bg-input-surface focus-within:bg-focus-surface border border-separator rounded-xs p-1 px-2';
21
+
22
+ /**
23
+ * Form field that edits a markdown value in a CodeMirror editor.
24
+ * Supports two backings:
25
+ * - `Schema.String` annotated with `Format.TypeFormat.Markdown`: edited as plain text via `getValue`/`onValueChange`.
26
+ * - `Ref<Text>` annotated with `Format.TypeFormat.Markdown`: edited in-place via an Automerge doc accessor.
27
+ * When the ref is empty and a database is available, a create button is shown to allocate a new Text.
28
+ */
29
+ export const MarkdownField = ({
30
+ type,
31
+ readonly,
32
+ label,
33
+ placeholder,
34
+ layout,
35
+ db,
36
+ getStatus,
37
+ getValue,
38
+ onValueChange,
39
+ }: FormFieldComponentProps) => {
40
+ const { status, error } = getStatus();
41
+ const isRef = Ref.isRefType(type);
42
+ const value = getValue();
43
+
44
+ if ((readonly || layout === 'static') && value == null) {
45
+ return null;
46
+ }
47
+
48
+ const renderEditor = () => {
49
+ if (isRef) {
50
+ const reference = value as Ref.Ref<any> | undefined;
51
+ if (reference) {
52
+ return <RefMarkdownEditor reference={reference} placeholder={placeholder} />;
53
+ }
54
+
55
+ if (readonly) {
56
+ return null;
57
+ }
58
+
59
+ return <CreateTextButton db={db} disabled={!db} onCreate={(ref) => onValueChange(type, ref)} />;
60
+ }
61
+
62
+ return (
63
+ <StringMarkdownEditor
64
+ value={(value as string | undefined) ?? ''}
65
+ placeholder={placeholder}
66
+ readonly={!!readonly}
67
+ onChange={(next) => onValueChange(type, next)}
68
+ />
69
+ );
70
+ };
71
+
72
+ const renderStatic = () => {
73
+ if (isRef) {
74
+ const reference = value as Ref.Ref<any> | undefined;
75
+ return reference ? <RefStaticText reference={reference} /> : null;
76
+ }
77
+ return <p className='whitespace-pre-wrap'>{(value as string | undefined) ?? ''}</p>;
78
+ };
79
+
80
+ return (
81
+ <Input.Root validationValence={status}>
82
+ {layout !== 'inline' && <FormFieldLabel error={error} readonly={readonly} label={label} />}
83
+ {layout === 'static' ? renderStatic() : renderEditor()}
84
+ {layout === 'full' && <Input.Validation>{error}</Input.Validation>}
85
+ </Input.Root>
86
+ );
87
+ };
88
+
89
+ /** Read-only static rendering for a `Ref<Text>` value: resolve the ref and print its content. */
90
+ const RefStaticText = ({ reference }: { reference: Ref.Ref<any> }) => {
91
+ const text = useAtomValue(useMemo(() => AtomRef.make(reference), [reference]));
92
+ if (!text?.content) {
93
+ return null;
94
+ }
95
+ return <p className='whitespace-pre-wrap'>{text.content}</p>;
96
+ };
97
+
98
+ type RefMarkdownEditorProps = {
99
+ reference: Ref.Ref<any>;
100
+ placeholder?: string;
101
+ };
102
+
103
+ const RefMarkdownEditor = ({ reference, placeholder }: RefMarkdownEditorProps) => {
104
+ const text = useAtomValue(useMemo(() => AtomRef.make(reference), [reference]));
105
+ const dataExtensions = useMemo(
106
+ () =>
107
+ text ? [createDataExtensions({ id: reference.dxn.toString(), text: createDocAccessor(text, ['content']) })] : [],
108
+ [text, reference],
109
+ );
110
+ const extensions = useBasicMarkdownExtensions({ placeholder, extensions: dataExtensions });
111
+ if (!text) {
112
+ return null;
113
+ }
114
+
115
+ return (
116
+ <Editor.Root>
117
+ <Editor.View classNames={editorClassNames} extensions={extensions} />
118
+ </Editor.Root>
119
+ );
120
+ };
121
+
122
+ type StringMarkdownEditorProps = {
123
+ value: string;
124
+ placeholder?: string;
125
+ readonly?: boolean;
126
+ onChange: (value: string) => void;
127
+ };
128
+
129
+ const StringMarkdownEditor = ({ value, placeholder, readonly, onChange }: StringMarkdownEditorProps) => {
130
+ const extensions = useBasicMarkdownExtensions({ placeholder });
131
+ const handleChange = useCallback((next: string) => onChange(next), [onChange]);
132
+
133
+ return (
134
+ <Editor.Root>
135
+ <Editor.View
136
+ classNames={editorClassNames}
137
+ extensions={extensions}
138
+ value={value}
139
+ onChange={readonly ? undefined : handleChange}
140
+ />
141
+ </Editor.Root>
142
+ );
143
+ };
144
+
145
+ type CreateTextButtonProps = {
146
+ db?: Database.Database;
147
+ disabled?: boolean;
148
+ onCreate: (ref: Ref.Ref<Text.Text>) => void;
149
+ };
150
+
151
+ const CreateTextButton = ({ db, disabled, onCreate }: CreateTextButtonProps) => {
152
+ const { t } = useTranslation(translationKey);
153
+ const handleClick = useCallback(() => {
154
+ if (!db) {
155
+ return;
156
+ }
157
+ const text = db.add(Obj.make(Text.Text, { content: '' }));
158
+ onCreate(Ref.make(text));
159
+ }, [db, onCreate]);
160
+
161
+ return (
162
+ <Button onClick={handleClick} disabled={disabled}>
163
+ <Icon icon='ph--plus--regular' size={4} />
164
+ <span>{t('create-text.label')}</span>
165
+ </Button>
166
+ );
167
+ };
@@ -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
+ };