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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (268) hide show
  1. package/LICENSE +102 -5
  2. package/README.md +0 -1
  3. package/dist/lib/browser/index.mjs +3240 -2345
  4. package/dist/lib/browser/index.mjs.map +4 -4
  5. package/dist/lib/browser/meta.json +1 -1
  6. package/dist/lib/browser/translations.mjs +83 -0
  7. package/dist/lib/browser/translations.mjs.map +7 -0
  8. package/dist/lib/node-esm/index.mjs +3240 -2345
  9. package/dist/lib/node-esm/index.mjs.map +4 -4
  10. package/dist/lib/node-esm/meta.json +1 -1
  11. package/dist/lib/node-esm/translations.mjs +85 -0
  12. package/dist/lib/node-esm/translations.mjs.map +7 -0
  13. package/dist/types/src/components/FieldEditor/FieldEditor.d.ts +8 -7
  14. package/dist/types/src/components/FieldEditor/FieldEditor.d.ts.map +1 -1
  15. package/dist/types/src/components/FieldEditor/FieldEditor.stories.d.ts +69 -59
  16. package/dist/types/src/components/FieldEditor/FieldEditor.stories.d.ts.map +1 -1
  17. package/dist/types/src/components/Form/Form.d.ts +123 -27
  18. package/dist/types/src/components/Form/Form.d.ts.map +1 -1
  19. package/dist/types/src/components/Form/Form.stories.d.ts +175 -137
  20. package/dist/types/src/components/Form/Form.stories.d.ts.map +1 -1
  21. package/dist/types/src/components/Form/FormField.d.ts +48 -0
  22. package/dist/types/src/components/Form/FormField.d.ts.map +1 -0
  23. package/dist/types/src/components/Form/FormFieldComponent.d.ts +95 -0
  24. package/dist/types/src/components/Form/FormFieldComponent.d.ts.map +1 -0
  25. package/dist/types/src/components/Form/FormFieldComponent.stories.d.ts +9 -0
  26. package/dist/types/src/components/Form/FormFieldComponent.stories.d.ts.map +1 -0
  27. package/dist/types/src/components/Form/FormFieldSet.d.ts +21 -0
  28. package/dist/types/src/components/Form/FormFieldSet.d.ts.map +1 -0
  29. package/dist/types/src/components/Form/FormTooltipsContext.d.ts +19 -0
  30. package/dist/types/src/components/Form/FormTooltipsContext.d.ts.map +1 -0
  31. package/dist/types/src/components/Form/Layout/FormLayout.d.ts +25 -0
  32. package/dist/types/src/components/Form/Layout/FormLayout.d.ts.map +1 -0
  33. package/dist/types/src/components/Form/Layout/FormLayout.stories.d.ts +116 -0
  34. package/dist/types/src/components/Form/Layout/FormLayout.stories.d.ts.map +1 -0
  35. package/dist/types/src/components/Form/Layout/annotation.d.ts +30 -0
  36. package/dist/types/src/components/Form/Layout/annotation.d.ts.map +1 -0
  37. package/dist/types/src/components/Form/Layout/index.d.ts +4 -0
  38. package/dist/types/src/components/Form/Layout/index.d.ts.map +1 -0
  39. package/dist/types/src/components/Form/Layout/parser.d.ts +31 -0
  40. package/dist/types/src/components/Form/Layout/parser.d.ts.map +1 -0
  41. package/dist/types/src/components/Form/Layout/parser.test.d.ts +2 -0
  42. package/dist/types/src/components/Form/Layout/parser.test.d.ts.map +1 -0
  43. package/dist/types/src/components/Form/fields/ArrayField.d.ts +18 -0
  44. package/dist/types/src/components/Form/fields/ArrayField.d.ts.map +1 -0
  45. package/dist/types/src/components/Form/fields/BooleanField.d.ts +4 -0
  46. package/dist/types/src/components/Form/fields/BooleanField.d.ts.map +1 -0
  47. package/dist/types/src/components/Form/fields/DateField.d.ts +4 -0
  48. package/dist/types/src/components/Form/fields/DateField.d.ts.map +1 -0
  49. package/dist/types/src/components/Form/fields/GeoPointField.d.ts +5 -0
  50. package/dist/types/src/components/Form/fields/GeoPointField.d.ts.map +1 -0
  51. package/dist/types/src/components/Form/fields/MarkdownField.d.ts +11 -0
  52. package/dist/types/src/components/Form/fields/MarkdownField.d.ts.map +1 -0
  53. package/dist/types/src/components/Form/fields/MarkdownField.stories.d.ts +85 -0
  54. package/dist/types/src/components/Form/fields/MarkdownField.stories.d.ts.map +1 -0
  55. package/dist/types/src/components/Form/fields/NumberField.d.ts +4 -0
  56. package/dist/types/src/components/Form/fields/NumberField.d.ts.map +1 -0
  57. package/dist/types/src/components/Form/fields/RefField.d.ts +22 -0
  58. package/dist/types/src/components/Form/fields/RefField.d.ts.map +1 -0
  59. package/dist/types/src/components/Form/fields/SelectField.d.ts +13 -0
  60. package/dist/types/src/components/Form/fields/SelectField.d.ts.map +1 -0
  61. package/dist/types/src/components/Form/fields/SelectOptionField.d.ts +5 -0
  62. package/dist/types/src/components/Form/fields/SelectOptionField.d.ts.map +1 -0
  63. package/dist/types/src/components/Form/fields/SelectOptionField.stories.d.ts +85 -0
  64. package/dist/types/src/components/Form/fields/SelectOptionField.stories.d.ts.map +1 -0
  65. package/dist/types/src/components/Form/fields/TextAreaField.d.ts +4 -0
  66. package/dist/types/src/components/Form/fields/TextAreaField.d.ts.map +1 -0
  67. package/dist/types/src/components/Form/fields/TextField.d.ts +4 -0
  68. package/dist/types/src/components/Form/fields/TextField.d.ts.map +1 -0
  69. package/dist/types/src/components/Form/fields/TupleField.d.ts +6 -0
  70. package/dist/types/src/components/Form/fields/TupleField.d.ts.map +1 -0
  71. package/dist/types/src/components/Form/fields/index.d.ts +13 -0
  72. package/dist/types/src/components/Form/fields/index.d.ts.map +1 -0
  73. package/dist/types/src/components/Form/index.d.ts +4 -4
  74. package/dist/types/src/components/Form/index.d.ts.map +1 -1
  75. package/dist/types/src/components/ObjectForm/ObjectForm.d.ts +8 -0
  76. package/dist/types/src/components/ObjectForm/ObjectForm.d.ts.map +1 -0
  77. package/dist/types/src/components/ObjectForm/index.d.ts +2 -0
  78. package/dist/types/src/components/ObjectForm/index.d.ts.map +1 -0
  79. package/dist/types/src/components/ObjectPicker/ObjectPicker.d.ts +37 -10
  80. package/dist/types/src/components/ObjectPicker/ObjectPicker.d.ts.map +1 -1
  81. package/dist/types/src/components/ObjectPicker/ObjectPicker.stories.d.ts +65 -58
  82. package/dist/types/src/components/ObjectPicker/ObjectPicker.stories.d.ts.map +1 -1
  83. package/dist/types/src/components/ObjectProperties/ObjectProperties.d.ts +9 -0
  84. package/dist/types/src/components/ObjectProperties/ObjectProperties.d.ts.map +1 -0
  85. package/dist/types/src/components/ObjectProperties/ObjectProperties.stories.d.ts +128 -0
  86. package/dist/types/src/components/ObjectProperties/ObjectProperties.stories.d.ts.map +1 -0
  87. package/dist/types/src/components/ObjectProperties/ObjectProperties.test.d.ts +2 -0
  88. package/dist/types/src/components/ObjectProperties/ObjectProperties.test.d.ts.map +1 -0
  89. package/dist/types/src/components/ObjectProperties/index.d.ts +2 -0
  90. package/dist/types/src/components/ObjectProperties/index.d.ts.map +1 -0
  91. package/dist/types/src/components/ObjectTree/ObjectTree.d.ts +9 -0
  92. package/dist/types/src/components/ObjectTree/ObjectTree.d.ts.map +1 -0
  93. package/dist/types/src/components/ObjectTree/ObjectTree.stories.d.ts +87 -0
  94. package/dist/types/src/components/ObjectTree/ObjectTree.stories.d.ts.map +1 -0
  95. package/dist/types/src/components/ObjectTree/index.d.ts +2 -0
  96. package/dist/types/src/components/ObjectTree/index.d.ts.map +1 -0
  97. package/dist/types/src/components/Settings/Settings.d.ts +51 -0
  98. package/dist/types/src/components/Settings/Settings.d.ts.map +1 -0
  99. package/dist/types/src/components/Settings/Settings.stories.d.ts +85 -0
  100. package/dist/types/src/components/Settings/Settings.stories.d.ts.map +1 -0
  101. package/dist/types/src/components/Settings/SettingsFieldSet.d.ts +43 -0
  102. package/dist/types/src/components/Settings/SettingsFieldSet.d.ts.map +1 -0
  103. package/dist/types/src/components/Settings/SettingsItem.d.ts +16 -0
  104. package/dist/types/src/components/Settings/SettingsItem.d.ts.map +1 -0
  105. package/dist/types/src/components/Settings/SettingsSelectField.d.ts +18 -0
  106. package/dist/types/src/components/Settings/SettingsSelectField.d.ts.map +1 -0
  107. package/dist/types/src/components/Settings/index.d.ts +4 -0
  108. package/dist/types/src/components/Settings/index.d.ts.map +1 -0
  109. package/dist/types/src/components/ViewEditor/ViewEditor.d.ts +14 -18
  110. package/dist/types/src/components/ViewEditor/ViewEditor.d.ts.map +1 -1
  111. package/dist/types/src/components/ViewEditor/ViewEditor.stories.d.ts +71 -64
  112. package/dist/types/src/components/ViewEditor/ViewEditor.stories.d.ts.map +1 -1
  113. package/dist/types/src/components/index.d.ts +4 -1
  114. package/dist/types/src/components/index.d.ts.map +1 -1
  115. package/dist/types/src/components/testing.d.ts +13 -6
  116. package/dist/types/src/components/testing.d.ts.map +1 -1
  117. package/dist/types/src/hooks/index.d.ts +2 -3
  118. package/dist/types/src/hooks/index.d.ts.map +1 -1
  119. package/dist/types/src/hooks/useFormHandler.d.ts +82 -0
  120. package/dist/types/src/hooks/useFormHandler.d.ts.map +1 -0
  121. package/dist/types/src/hooks/useKeyHandler.d.ts +7 -0
  122. package/dist/types/src/hooks/useKeyHandler.d.ts.map +1 -0
  123. package/dist/types/src/index.d.ts +0 -2
  124. package/dist/types/src/index.d.ts.map +1 -1
  125. package/dist/types/src/translations.d.ts +66 -59
  126. package/dist/types/src/translations.d.ts.map +1 -1
  127. package/dist/types/src/util/field-type.d.ts +19 -0
  128. package/dist/types/src/util/field-type.d.ts.map +1 -0
  129. package/dist/types/src/util/field.d.ts +3 -3
  130. package/dist/types/src/util/field.d.ts.map +1 -1
  131. package/dist/types/src/util/formatting.d.ts +2 -2
  132. package/dist/types/src/util/formatting.d.ts.map +1 -1
  133. package/dist/types/src/util/index.d.ts +2 -1
  134. package/dist/types/src/util/index.d.ts.map +1 -1
  135. package/dist/types/src/util/properties.d.ts +17 -0
  136. package/dist/types/src/util/properties.d.ts.map +1 -0
  137. package/dist/types/src/util/properties.test.d.ts +2 -0
  138. package/dist/types/src/util/properties.test.d.ts.map +1 -0
  139. package/dist/types/src/util/refs.d.ts +2 -2
  140. package/dist/types/src/util/refs.d.ts.map +1 -1
  141. package/dist/types/tsconfig.tsbuildinfo +1 -1
  142. package/package.json +61 -52
  143. package/src/components/FieldEditor/FieldEditor.stories.tsx +44 -25
  144. package/src/components/FieldEditor/FieldEditor.test.tsx +0 -1
  145. package/src/components/FieldEditor/FieldEditor.tsx +84 -84
  146. package/src/components/Form/AUDIT.md +17 -0
  147. package/src/components/Form/Form.stories.tsx +134 -252
  148. package/src/components/Form/Form.tsx +417 -109
  149. package/src/components/Form/FormField.tsx +332 -0
  150. package/src/components/Form/FormFieldComponent.stories.tsx +65 -0
  151. package/src/components/Form/FormFieldComponent.tsx +206 -0
  152. package/src/components/Form/FormFieldSet.tsx +173 -0
  153. package/src/components/Form/FormTooltipsContext.ts +25 -0
  154. package/src/components/Form/Layout/FormLayout.stories.tsx +339 -0
  155. package/src/components/Form/Layout/FormLayout.tsx +123 -0
  156. package/src/components/Form/Layout/annotation.ts +38 -0
  157. package/src/components/Form/Layout/index.ts +7 -0
  158. package/src/components/Form/Layout/parser.test.ts +120 -0
  159. package/src/components/Form/Layout/parser.ts +167 -0
  160. package/src/components/Form/README.md +3 -0
  161. package/src/components/Form/fields/ArrayField.tsx +185 -0
  162. package/src/components/Form/fields/BooleanField.tsx +22 -0
  163. package/src/components/Form/fields/DateField.tsx +95 -0
  164. package/src/components/Form/fields/GeoPointField.tsx +124 -0
  165. package/src/components/Form/fields/MarkdownField.stories.tsx +206 -0
  166. package/src/components/Form/fields/MarkdownField.tsx +168 -0
  167. package/src/components/Form/fields/NumberField.tsx +39 -0
  168. package/src/components/Form/fields/RefField.tsx +232 -0
  169. package/src/components/Form/fields/SelectField.tsx +77 -0
  170. package/src/components/Form/{custom/SelectOptionsInput.stories.tsx → fields/SelectOptionField.stories.tsx} +23 -18
  171. package/src/components/Form/{custom/SelectOptionsInput.tsx → fields/SelectOptionField.tsx} +32 -25
  172. package/src/components/Form/fields/TextAreaField.tsx +38 -0
  173. package/src/components/Form/fields/TextField.tsx +38 -0
  174. package/src/components/Form/{custom/TupleInput.tsx → fields/TupleField.tsx} +17 -9
  175. package/src/components/Form/fields/index.ts +16 -0
  176. package/src/components/Form/index.ts +5 -4
  177. package/src/components/ObjectForm/ObjectForm.tsx +115 -0
  178. package/src/components/{ControlSection → ObjectForm}/index.ts +1 -1
  179. package/src/components/ObjectPicker/ObjectPicker.stories.tsx +26 -21
  180. package/src/components/ObjectPicker/ObjectPicker.tsx +122 -81
  181. package/src/components/ObjectProperties/ObjectProperties.stories.tsx +350 -0
  182. package/src/components/ObjectProperties/ObjectProperties.test.tsx +108 -0
  183. package/src/components/ObjectProperties/ObjectProperties.tsx +147 -0
  184. package/src/components/ObjectProperties/index.ts +5 -0
  185. package/src/components/ObjectTree/ObjectTree.stories.tsx +134 -0
  186. package/src/components/ObjectTree/ObjectTree.tsx +190 -0
  187. package/src/components/ObjectTree/index.ts +5 -0
  188. package/src/components/Settings/AUDIT.md +51 -0
  189. package/src/components/Settings/Settings.stories.tsx +104 -0
  190. package/src/components/Settings/Settings.tsx +81 -0
  191. package/src/components/Settings/SettingsFieldSet.tsx +257 -0
  192. package/src/components/Settings/SettingsItem.tsx +58 -0
  193. package/src/components/Settings/SettingsSelectField.tsx +59 -0
  194. package/src/components/Settings/index.ts +7 -0
  195. package/src/components/ViewEditor/ViewEditor.stories.tsx +62 -71
  196. package/src/components/ViewEditor/ViewEditor.test.tsx +10 -10
  197. package/src/components/ViewEditor/ViewEditor.tsx +308 -217
  198. package/src/components/index.ts +4 -1
  199. package/src/components/testing.tsx +24 -11
  200. package/src/hooks/index.ts +2 -3
  201. package/src/hooks/useFormHandler.ts +380 -0
  202. package/src/hooks/useKeyHandler.tsx +37 -0
  203. package/src/index.ts +0 -4
  204. package/src/translations.ts +71 -64
  205. package/src/util/field-type.ts +60 -0
  206. package/src/util/field.ts +25 -25
  207. package/src/util/formatting.ts +22 -22
  208. package/src/util/index.ts +2 -1
  209. package/src/util/properties.test.ts +78 -0
  210. package/src/util/properties.ts +38 -0
  211. package/src/util/refs.ts +13 -15
  212. package/src/vite-env.d.ts +10 -0
  213. package/dist/types/src/components/ControlSection/ControlSection.d.ts +0 -23
  214. package/dist/types/src/components/ControlSection/ControlSection.d.ts.map +0 -1
  215. package/dist/types/src/components/ControlSection/ControlSection.stories.d.ts +0 -83
  216. package/dist/types/src/components/ControlSection/ControlSection.stories.d.ts.map +0 -1
  217. package/dist/types/src/components/ControlSection/index.d.ts +0 -2
  218. package/dist/types/src/components/ControlSection/index.d.ts.map +0 -1
  219. package/dist/types/src/components/Form/ArrayField.d.ts +0 -16
  220. package/dist/types/src/components/Form/ArrayField.d.ts.map +0 -1
  221. package/dist/types/src/components/Form/Defaults.d.ts +0 -14
  222. package/dist/types/src/components/Form/Defaults.d.ts.map +0 -1
  223. package/dist/types/src/components/Form/FormActions.d.ts +0 -9
  224. package/dist/types/src/components/Form/FormActions.d.ts.map +0 -1
  225. package/dist/types/src/components/Form/FormContent.d.ts +0 -63
  226. package/dist/types/src/components/Form/FormContent.d.ts.map +0 -1
  227. package/dist/types/src/components/Form/FormContext.d.ts +0 -23
  228. package/dist/types/src/components/Form/FormContext.d.ts.map +0 -1
  229. package/dist/types/src/components/Form/Input.d.ts +0 -29
  230. package/dist/types/src/components/Form/Input.d.ts.map +0 -1
  231. package/dist/types/src/components/Form/RefField.d.ts +0 -22
  232. package/dist/types/src/components/Form/RefField.d.ts.map +0 -1
  233. package/dist/types/src/components/Form/custom/GeoPointInput.d.ts +0 -4
  234. package/dist/types/src/components/Form/custom/GeoPointInput.d.ts.map +0 -1
  235. package/dist/types/src/components/Form/custom/SelectOptionsInput.d.ts +0 -4
  236. package/dist/types/src/components/Form/custom/SelectOptionsInput.d.ts.map +0 -1
  237. package/dist/types/src/components/Form/custom/SelectOptionsInput.stories.d.ts +0 -78
  238. package/dist/types/src/components/Form/custom/SelectOptionsInput.stories.d.ts.map +0 -1
  239. package/dist/types/src/components/Form/custom/TupleInput.d.ts +0 -6
  240. package/dist/types/src/components/Form/custom/TupleInput.d.ts.map +0 -1
  241. package/dist/types/src/components/Form/custom/index.d.ts +0 -4
  242. package/dist/types/src/components/Form/custom/index.d.ts.map +0 -1
  243. package/dist/types/src/components/Form/factory.d.ts +0 -8
  244. package/dist/types/src/components/Form/factory.d.ts.map +0 -1
  245. package/dist/types/src/hooks/useForm.d.ts +0 -67
  246. package/dist/types/src/hooks/useForm.d.ts.map +0 -1
  247. package/dist/types/src/hooks/useQueryRefOptions.d.ts +0 -29
  248. package/dist/types/src/hooks/useQueryRefOptions.d.ts.map +0 -1
  249. package/dist/types/src/hooks/useRefQueryLookupHandler.d.ts +0 -8
  250. package/dist/types/src/hooks/useRefQueryLookupHandler.d.ts.map +0 -1
  251. package/dist/types/src/util/schema.d.ts +0 -3
  252. package/dist/types/src/util/schema.d.ts.map +0 -1
  253. package/src/components/ControlSection/ControlSection.stories.tsx +0 -38
  254. package/src/components/ControlSection/ControlSection.tsx +0 -153
  255. package/src/components/Form/ArrayField.tsx +0 -125
  256. package/src/components/Form/Defaults.tsx +0 -205
  257. package/src/components/Form/FormActions.tsx +0 -54
  258. package/src/components/Form/FormContent.tsx +0 -362
  259. package/src/components/Form/FormContext.tsx +0 -110
  260. package/src/components/Form/Input.tsx +0 -54
  261. package/src/components/Form/RefField.tsx +0 -283
  262. package/src/components/Form/custom/GeoPointInput.tsx +0 -80
  263. package/src/components/Form/custom/index.ts +0 -7
  264. package/src/components/Form/factory.tsx +0 -31
  265. package/src/hooks/useForm.ts +0 -296
  266. package/src/hooks/useQueryRefOptions.ts +0 -62
  267. package/src/hooks/useRefQueryLookupHandler.ts +0 -39
  268. package/src/util/schema.ts +0 -12
@@ -0,0 +1,124 @@
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
+ jsonPath,
19
+ readonly,
20
+ layout,
21
+ getStatus,
22
+ getValue,
23
+ onValueChange,
24
+ onBlur,
25
+ }: FormFieldComponentProps<GeoPoint>) => {
26
+ const { t } = useTranslation(translationKey);
27
+ const { status, error } = getStatus();
28
+ const geoPoint = useMemo<GeoPoint>(() => getValue() ?? [0, 0], [getValue]);
29
+ const value = useMemo(() => GeoLocation.fromGeoPoint(geoPoint), [geoPoint]);
30
+
31
+ const [longitudeText, setLongitudeText] = useState(value.longitude?.toString());
32
+ const [latitudeText, setLatitudeText] = useState(value.latitude?.toString());
33
+ useEffect(() => {
34
+ const location = GeoLocation.fromGeoPoint(geoPoint);
35
+ setLongitudeText(location.longitude?.toString());
36
+ setLatitudeText(location.latitude?.toString());
37
+ }, [geoPoint]);
38
+
39
+ const handleChange = useCallback(
40
+ (coordinateType: keyof Pick<GeoLocation, 'longitude' | 'latitude'>, setText: (text: string) => void) =>
41
+ (event: ChangeEvent<HTMLInputElement>) => {
42
+ const inputText = event.target.value;
43
+ setText(inputText);
44
+ if (inputText !== '' && inputText !== '-') {
45
+ const coord = safeParseFloat(inputText);
46
+ if (coord !== undefined && !isNaN(coord)) {
47
+ const currentLocation = GeoLocation.fromGeoPoint(getValue() ?? [0, 0]);
48
+ const newLocation = { ...currentLocation, [coordinateType]: coord };
49
+ const newValue = GeoLocation.toGeoPoint(newLocation);
50
+ onValueChange(type, newValue);
51
+ }
52
+ }
53
+ },
54
+ [type, getValue, onValueChange],
55
+ );
56
+
57
+ if ((readonly || layout === 'static') && !value?.latitude && !value?.longitude) {
58
+ return null;
59
+ }
60
+
61
+ return (
62
+ <Input.Root validationValence={status}>
63
+ {layout !== 'inline' && (
64
+ <FormFieldLabel error={error} readonly={readonly} label={label} path={jsonPath} asChild />
65
+ )}
66
+ {layout === 'static' ? (
67
+ <LatLng {...value} />
68
+ ) : (
69
+ <div className='grid grid-cols-2 gap-form-gap'>
70
+ <div>
71
+ <Input.Root>
72
+ {layout !== 'inline' && <Input.Label>{t('latitude.label')}</Input.Label>}
73
+ <Input.TextInput
74
+ type='number'
75
+ step='0.00001'
76
+ min='-90'
77
+ max='90'
78
+ disabled={!!readonly}
79
+ placeholder={t('latitude.placeholder')}
80
+ value={latitudeText ?? ''}
81
+ onChange={handleChange('latitude', setLatitudeText)}
82
+ onBlur={onBlur}
83
+ />
84
+ </Input.Root>
85
+ </div>
86
+ <div>
87
+ <Input.Root>
88
+ {layout !== 'inline' && <Input.Label>{t('longitude.label')}</Input.Label>}
89
+ <Input.TextInput
90
+ type='number'
91
+ step='0.00001'
92
+ min='-180'
93
+ max='180'
94
+ disabled={!!readonly}
95
+ placeholder={t('longitude.placeholder')}
96
+ value={longitudeText ?? ''}
97
+ onChange={handleChange('longitude', setLongitudeText)}
98
+ onBlur={onBlur}
99
+ />
100
+ </Input.Root>
101
+ </div>
102
+ </div>
103
+ )}
104
+ </Input.Root>
105
+ );
106
+ };
107
+
108
+ const LatLng = ({ latitude = 0, longitude = 0 }: GeoLocation) => {
109
+ const latHem = latitude >= 0 ? 'N' : 'S';
110
+ const lngHem = longitude >= 0 ? 'E' : 'W';
111
+
112
+ return (
113
+ <span className='inline-flex items-center gap-form-gap'>
114
+ <span>
115
+ <span>{Math.abs(latitude).toFixed(5)}</span>
116
+ <span className='text-subdued'>°{latHem}</span>
117
+ </span>
118
+ <span>
119
+ <span>{Math.abs(longitude).toFixed(5)}</span>
120
+ <span className='text-subdued'>°{lngHem}</span>
121
+ </span>
122
+ </span>
123
+ );
124
+ };
@@ -0,0 +1,206 @@
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
+ import { expect, userEvent } from 'storybook/test';
9
+
10
+ import { Filter, Format, Obj, Ref } from '@dxos/echo';
11
+ import { useQuery } from '@dxos/react-client/echo';
12
+ import { useClientStory, withClientProvider } from '@dxos/react-client/testing';
13
+ import { Panel } from '@dxos/react-ui';
14
+ import { withLayout, withTheme } from '@dxos/react-ui/testing';
15
+ import { Text } from '@dxos/schema';
16
+
17
+ import { translations } from '#translations';
18
+
19
+ import { TestLayout } from '../../testing';
20
+ import { Form } from '../Form';
21
+
22
+ //
23
+ // String-backed markdown.
24
+ //
25
+
26
+ const StringSchema = Schema.Struct({
27
+ notes: Schema.String.pipe(Format.FormatAnnotation.set(Format.TypeFormat.Markdown)).annotations({ title: 'Notes' }),
28
+ });
29
+
30
+ const StringStory = () => (
31
+ <TestLayout>
32
+ <Form.Root schema={StringSchema} defaultValues={{ notes: '# Hello\n\nMarkdown text *here*.' }}>
33
+ <Form.Viewport>
34
+ <Form.Content>
35
+ <Form.FieldSet />
36
+ </Form.Content>
37
+ </Form.Viewport>
38
+ </Form.Root>
39
+ </TestLayout>
40
+ );
41
+
42
+ //
43
+ // Ref<Text>-backed markdown.
44
+ //
45
+
46
+ const TextNoteHolder = Schema.Struct({
47
+ instructions: Ref.Ref(Text.Text)
48
+ .pipe(Format.FormatAnnotation.set(Format.TypeFormat.Markdown))
49
+ .annotations({ title: 'Instructions' }),
50
+ });
51
+
52
+ const RefStory = () => {
53
+ const { space } = useClientStory();
54
+ const [text] = useQuery(space?.db, Filter.type(Text.Text));
55
+ const values = useMemo(() => (text ? { instructions: Ref.make(text) } : undefined), [text]);
56
+
57
+ if (!space || !values) {
58
+ return null;
59
+ }
60
+
61
+ return (
62
+ <TestLayout>
63
+ <Panel.Root>
64
+ <Panel.Content asChild>
65
+ <Form.Root schema={TextNoteHolder} values={values as any} db={space.db}>
66
+ <Form.Viewport>
67
+ <Form.Content>
68
+ <Form.FieldSet />
69
+ </Form.Content>
70
+ </Form.Viewport>
71
+ </Form.Root>
72
+ </Panel.Content>
73
+ </Panel.Root>
74
+ </TestLayout>
75
+ );
76
+ };
77
+
78
+ //
79
+ // Ref<Text> empty — shows the create button.
80
+ //
81
+
82
+ const EmptyRefSchema = Schema.Struct({
83
+ instructions: Schema.optional(
84
+ Ref.Ref(Text.Text)
85
+ .pipe(Format.FormatAnnotation.set(Format.TypeFormat.Markdown))
86
+ .annotations({ title: 'Instructions' }),
87
+ ),
88
+ });
89
+
90
+ const EmptyRefStory = () => {
91
+ const { space } = useClientStory();
92
+ if (!space) {
93
+ return null;
94
+ }
95
+
96
+ return (
97
+ <TestLayout>
98
+ <Panel.Root>
99
+ <Panel.Content asChild>
100
+ <Form.Root schema={EmptyRefSchema} defaultValues={{}} db={space.db}>
101
+ <Form.Viewport>
102
+ <Form.Content>
103
+ <Form.FieldSet />
104
+ </Form.Content>
105
+ </Form.Viewport>
106
+ </Form.Root>
107
+ </Panel.Content>
108
+ </Panel.Root>
109
+ </TestLayout>
110
+ );
111
+ };
112
+
113
+ const meta = {
114
+ title: 'ui/react-ui-form/MarkdownField',
115
+ decorators: [withTheme(), withLayout({ layout: 'column' })],
116
+ parameters: {
117
+ layout: 'fullscreen',
118
+ translations,
119
+ },
120
+ } satisfies Meta;
121
+
122
+ export default meta;
123
+
124
+ type Story = StoryObj<typeof meta>;
125
+
126
+ export const StringBacked: Story = {
127
+ render: () => <StringStory />,
128
+ };
129
+
130
+ // Regression test: keystrokes into a string-backed markdown field must not
131
+ // blur the editor. Previously the EditorView was destroyed and recreated on
132
+ // every change because `onChange` was a useTextEditor dep and form callbacks
133
+ // are unstable across renders.
134
+ export const StringBackedKeepsFocus: Story = {
135
+ render: () => (
136
+ <TestLayout>
137
+ <Form.Root schema={StringSchema} defaultValues={{ notes: '' }}>
138
+ <Form.Viewport>
139
+ <Form.Content>
140
+ <Form.FieldSet />
141
+ </Form.Content>
142
+ </Form.Viewport>
143
+ </Form.Root>
144
+ </TestLayout>
145
+ ),
146
+ play: async ({ canvasElement }) => {
147
+ const waitFor = async <T,>(predicate: () => T | null | undefined, message: string): Promise<T> => {
148
+ for (let attempt = 0; attempt < 50; attempt++) {
149
+ const value = predicate();
150
+ if (value) {
151
+ return value;
152
+ }
153
+ await new Promise((resolve) => setTimeout(resolve, 100));
154
+ }
155
+ throw new Error(message);
156
+ };
157
+
158
+ const editorRoot = await waitFor(
159
+ () => canvasElement.querySelector<HTMLElement>('.cm-editor'),
160
+ 'CodeMirror editor root (.cm-editor) not found',
161
+ );
162
+ const editorContent = editorRoot.querySelector<HTMLElement>('.cm-content')!;
163
+ await expect(editorContent).toBeVisible();
164
+
165
+ // Focus editor and wait for focus to settle inside the editor root.
166
+ editorContent.focus();
167
+ await waitFor(() => (editorRoot.contains(document.activeElement) ? true : null), 'Editor did not receive focus');
168
+
169
+ // Type several characters; focus must be preserved inside the editor root
170
+ // after every keystroke. Regression: previously the EditorView was
171
+ // destroyed and recreated on every change, which detached the focused
172
+ // contentDOM and blurred the input.
173
+ const phrase = 'hello world';
174
+ for (const ch of phrase) {
175
+ await userEvent.keyboard(ch);
176
+ await expect(editorRoot.contains(document.activeElement)).toBe(true);
177
+ }
178
+
179
+ await expect(editorContent).toHaveTextContent(phrase);
180
+ },
181
+ };
182
+
183
+ export const RefPopulated: Story = {
184
+ render: () => <RefStory />,
185
+ decorators: [
186
+ withClientProvider({
187
+ createIdentity: true,
188
+ createSpace: true,
189
+ types: [Text.Text],
190
+ onCreateSpace: async ({ space }) => {
191
+ space.db.add(Obj.make(Text.Text, { content: '# Magazine brief\n\nLong-form text describing what to gather.' }));
192
+ },
193
+ }),
194
+ ],
195
+ };
196
+
197
+ export const RefEmpty: Story = {
198
+ render: () => <EmptyRefStory />,
199
+ decorators: [
200
+ withClientProvider({
201
+ createIdentity: true,
202
+ createSpace: true,
203
+ types: [Text.Text],
204
+ }),
205
+ ],
206
+ };
@@ -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
+ jsonPath,
35
+ placeholder,
36
+ layout,
37
+ db,
38
+ getStatus,
39
+ getValue,
40
+ onValueChange,
41
+ }: FormFieldComponentProps) => {
42
+ const { status, error } = getStatus();
43
+ const isRef = Ref.isRefType(type);
44
+ const value = getValue();
45
+
46
+ if ((readonly || layout === 'static') && value == null) {
47
+ return null;
48
+ }
49
+
50
+ const renderEditor = () => {
51
+ if (isRef) {
52
+ const reference = value as Ref.Ref<any> | undefined;
53
+ if (reference) {
54
+ return <RefMarkdownEditor reference={reference} placeholder={placeholder} />;
55
+ }
56
+
57
+ if (readonly) {
58
+ return null;
59
+ }
60
+
61
+ return <CreateTextButton db={db} disabled={!db} onCreate={(ref) => onValueChange(type, ref)} />;
62
+ }
63
+
64
+ return (
65
+ <StringMarkdownEditor
66
+ value={(value as string | undefined) ?? ''}
67
+ placeholder={placeholder}
68
+ readonly={!!readonly}
69
+ onChange={(next) => onValueChange(type, next)}
70
+ />
71
+ );
72
+ };
73
+
74
+ const renderStatic = () => {
75
+ if (isRef) {
76
+ const reference = value as Ref.Ref<any> | undefined;
77
+ return reference ? <RefStaticText reference={reference} /> : null;
78
+ }
79
+ return <p className='whitespace-pre-wrap'>{(value as string | undefined) ?? ''}</p>;
80
+ };
81
+
82
+ return (
83
+ <Input.Root validationValence={status}>
84
+ {layout !== 'inline' && <FormFieldLabel error={error} readonly={readonly} label={label} path={jsonPath} />}
85
+ {layout === 'static' ? renderStatic() : renderEditor()}
86
+ {layout === 'full' && <Input.Validation>{error}</Input.Validation>}
87
+ </Input.Root>
88
+ );
89
+ };
90
+
91
+ /** Read-only static rendering for a `Ref<Text>` value: resolve the ref and print its content. */
92
+ const RefStaticText = ({ reference }: { reference: Ref.Ref<any> }) => {
93
+ const text = useAtomValue(useMemo(() => AtomRef.make(reference), [reference]));
94
+ if (!text?.content) {
95
+ return null;
96
+ }
97
+ return <p className='whitespace-pre-wrap'>{text.content}</p>;
98
+ };
99
+
100
+ type RefMarkdownEditorProps = {
101
+ reference: Ref.Ref<any>;
102
+ placeholder?: string;
103
+ };
104
+
105
+ const RefMarkdownEditor = ({ reference, placeholder }: RefMarkdownEditorProps) => {
106
+ const text = useAtomValue(useMemo(() => AtomRef.make(reference), [reference]));
107
+ const dataExtensions = useMemo(
108
+ () => (text ? [createDataExtensions({ id: reference.uri, 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
+ };