@dxos/react-ui-form 0.8.4-main.9be5663bfe → 0.8.4-main.abd8ff62ef

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 (123) hide show
  1. package/dist/lib/browser/index.mjs +620 -440
  2. package/dist/lib/browser/index.mjs.map +4 -4
  3. package/dist/lib/browser/meta.json +1 -1
  4. package/dist/lib/browser/translations.mjs +82 -0
  5. package/dist/lib/browser/translations.mjs.map +7 -0
  6. package/dist/lib/node-esm/index.mjs +620 -440
  7. package/dist/lib/node-esm/index.mjs.map +4 -4
  8. package/dist/lib/node-esm/meta.json +1 -1
  9. package/dist/lib/node-esm/translations.mjs +84 -0
  10. package/dist/lib/node-esm/translations.mjs.map +7 -0
  11. package/dist/types/src/components/FieldEditor/FieldEditor.d.ts.map +1 -1
  12. package/dist/types/src/components/FieldEditor/FieldEditor.stories.d.ts +63 -62
  13. package/dist/types/src/components/FieldEditor/FieldEditor.stories.d.ts.map +1 -1
  14. package/dist/types/src/components/Form/Form.d.ts +27 -4
  15. package/dist/types/src/components/Form/Form.d.ts.map +1 -1
  16. package/dist/types/src/components/Form/Form.stories.d.ts +73 -72
  17. package/dist/types/src/components/Form/Form.stories.d.ts.map +1 -1
  18. package/dist/types/src/components/Form/FormField.d.ts +5 -5
  19. package/dist/types/src/components/Form/FormField.d.ts.map +1 -1
  20. package/dist/types/src/components/Form/FormFieldComponent.d.ts +8 -6
  21. package/dist/types/src/components/Form/FormFieldComponent.d.ts.map +1 -1
  22. package/dist/types/src/components/Form/FormFieldComponent.stories.d.ts.map +1 -1
  23. package/dist/types/src/components/Form/FormFieldSet.d.ts +5 -8
  24. package/dist/types/src/components/Form/FormFieldSet.d.ts.map +1 -1
  25. package/dist/types/src/components/Form/fields/ArrayField.d.ts +4 -4
  26. package/dist/types/src/components/Form/fields/ArrayField.d.ts.map +1 -1
  27. package/dist/types/src/components/Form/fields/BooleanField.d.ts.map +1 -1
  28. package/dist/types/src/components/Form/fields/GeoPointField.d.ts.map +1 -1
  29. package/dist/types/src/components/Form/fields/MarkdownField.d.ts +8 -1
  30. package/dist/types/src/components/Form/fields/MarkdownField.d.ts.map +1 -1
  31. package/dist/types/src/components/Form/fields/MarkdownField.stories.d.ts +84 -0
  32. package/dist/types/src/components/Form/fields/MarkdownField.stories.d.ts.map +1 -0
  33. package/dist/types/src/components/Form/fields/NumberField.d.ts.map +1 -1
  34. package/dist/types/src/components/Form/fields/RefField.d.ts +10 -4
  35. package/dist/types/src/components/Form/fields/RefField.d.ts.map +1 -1
  36. package/dist/types/src/components/Form/fields/SelectField.d.ts.map +1 -1
  37. package/dist/types/src/components/Form/fields/SelectOptionField.d.ts.map +1 -1
  38. package/dist/types/src/components/Form/fields/SelectOptionField.stories.d.ts +63 -62
  39. package/dist/types/src/components/Form/fields/SelectOptionField.stories.d.ts.map +1 -1
  40. package/dist/types/src/components/Form/fields/TextAreaField.d.ts.map +1 -1
  41. package/dist/types/src/components/Form/fields/TextField.d.ts +1 -1
  42. package/dist/types/src/components/Form/fields/TextField.d.ts.map +1 -1
  43. package/dist/types/src/components/Form/fields/TupleField.d.ts.map +1 -1
  44. package/dist/types/src/components/ObjectForm/ObjectForm.d.ts.map +1 -1
  45. package/dist/types/src/components/ObjectPicker/ObjectPicker.d.ts +17 -3
  46. package/dist/types/src/components/ObjectPicker/ObjectPicker.d.ts.map +1 -1
  47. package/dist/types/src/components/ObjectPicker/ObjectPicker.stories.d.ts +63 -62
  48. package/dist/types/src/components/ObjectPicker/ObjectPicker.stories.d.ts.map +1 -1
  49. package/dist/types/src/components/ObjectProperties/ObjectProperties.d.ts +1 -1
  50. package/dist/types/src/components/ObjectProperties/ObjectProperties.d.ts.map +1 -1
  51. package/dist/types/src/components/ObjectProperties/ObjectProperties.stories.d.ts +108 -64
  52. package/dist/types/src/components/ObjectProperties/ObjectProperties.stories.d.ts.map +1 -1
  53. package/dist/types/src/components/ObjectProperties/ObjectProperties.test.d.ts +2 -0
  54. package/dist/types/src/components/ObjectProperties/ObjectProperties.test.d.ts.map +1 -0
  55. package/dist/types/src/components/Settings/Settings.d.ts +24 -8
  56. package/dist/types/src/components/Settings/Settings.d.ts.map +1 -1
  57. package/dist/types/src/components/Settings/Settings.stories.d.ts +64 -62
  58. package/dist/types/src/components/Settings/Settings.stories.d.ts.map +1 -1
  59. package/dist/types/src/components/Settings/SettingsFieldSet.d.ts +43 -0
  60. package/dist/types/src/components/Settings/SettingsFieldSet.d.ts.map +1 -0
  61. package/dist/types/src/components/Settings/SettingsItem.d.ts +16 -0
  62. package/dist/types/src/components/Settings/SettingsItem.d.ts.map +1 -0
  63. package/dist/types/src/components/Settings/SettingsSelectField.d.ts +18 -0
  64. package/dist/types/src/components/Settings/SettingsSelectField.d.ts.map +1 -0
  65. package/dist/types/src/components/Settings/index.d.ts +2 -0
  66. package/dist/types/src/components/Settings/index.d.ts.map +1 -1
  67. package/dist/types/src/components/ViewEditor/ViewEditor.d.ts +2 -2
  68. package/dist/types/src/components/ViewEditor/ViewEditor.d.ts.map +1 -1
  69. package/dist/types/src/components/ViewEditor/ViewEditor.stories.d.ts +63 -62
  70. package/dist/types/src/components/ViewEditor/ViewEditor.stories.d.ts.map +1 -1
  71. package/dist/types/src/components/testing.d.ts +4 -4
  72. package/dist/types/src/components/testing.d.ts.map +1 -1
  73. package/dist/types/src/hooks/useFormHandler.d.ts.map +1 -1
  74. package/dist/types/src/hooks/useKeyHandler.d.ts.map +1 -1
  75. package/dist/types/src/index.d.ts +0 -2
  76. package/dist/types/src/index.d.ts.map +1 -1
  77. package/dist/types/src/translations.d.ts +63 -62
  78. package/dist/types/src/translations.d.ts.map +1 -1
  79. package/dist/types/src/util/field-type.d.ts +19 -0
  80. package/dist/types/src/util/field-type.d.ts.map +1 -0
  81. package/dist/types/src/util/field.d.ts.map +1 -1
  82. package/dist/types/src/util/formatting.d.ts.map +1 -1
  83. package/dist/types/src/util/index.d.ts +1 -0
  84. package/dist/types/src/util/index.d.ts.map +1 -1
  85. package/dist/types/src/util/properties.d.ts.map +1 -1
  86. package/dist/types/src/util/refs.d.ts.map +1 -1
  87. package/dist/types/tsconfig.tsbuildinfo +1 -1
  88. package/package.json +39 -38
  89. package/src/components/FieldEditor/FieldEditor.stories.tsx +3 -4
  90. package/src/components/FieldEditor/FieldEditor.tsx +3 -2
  91. package/src/components/Form/Form.stories.tsx +5 -16
  92. package/src/components/Form/Form.tsx +30 -7
  93. package/src/components/Form/FormField.tsx +7 -5
  94. package/src/components/Form/FormFieldComponent.tsx +3 -0
  95. package/src/components/Form/FormFieldSet.tsx +1 -3
  96. package/src/components/Form/fields/ArrayField.tsx +8 -3
  97. package/src/components/Form/fields/GeoPointField.tsx +2 -1
  98. package/src/components/Form/fields/MarkdownField.stories.tsx +206 -0
  99. package/src/components/Form/fields/MarkdownField.tsx +135 -45
  100. package/src/components/Form/fields/RefField.tsx +61 -30
  101. package/src/components/Form/fields/SelectOptionField.stories.tsx +8 -5
  102. package/src/components/Form/fields/SelectOptionField.tsx +3 -2
  103. package/src/components/Form/fields/TextField.tsx +2 -2
  104. package/src/components/ObjectForm/ObjectForm.tsx +6 -5
  105. package/src/components/ObjectPicker/ObjectPicker.stories.tsx +5 -2
  106. package/src/components/ObjectPicker/ObjectPicker.tsx +28 -12
  107. package/src/components/ObjectProperties/ObjectProperties.stories.tsx +321 -22
  108. package/src/components/ObjectProperties/ObjectProperties.test.tsx +101 -0
  109. package/src/components/ObjectProperties/ObjectProperties.tsx +26 -14
  110. package/src/components/Settings/AUDIT.md +52 -0
  111. package/src/components/Settings/Settings.stories.tsx +50 -1
  112. package/src/components/Settings/Settings.tsx +6 -53
  113. package/src/components/Settings/SettingsFieldSet.tsx +257 -0
  114. package/src/components/Settings/SettingsItem.tsx +62 -0
  115. package/src/components/Settings/SettingsSelectField.tsx +59 -0
  116. package/src/components/Settings/index.ts +2 -0
  117. package/src/components/ViewEditor/ViewEditor.stories.tsx +17 -18
  118. package/src/components/ViewEditor/ViewEditor.tsx +4 -3
  119. package/src/components/testing.tsx +20 -21
  120. package/src/index.ts +0 -4
  121. package/src/translations.ts +7 -4
  122. package/src/util/field-type.ts +60 -0
  123. package/src/util/index.ts +1 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dxos/react-ui-form",
3
- "version": "0.8.4-main.9be5663bfe",
3
+ "version": "0.8.4-main.abd8ff62ef",
4
4
  "description": "Data components.",
5
5
  "homepage": "https://dxos.org",
6
6
  "bugs": "https://github.com/dxos/dxos/issues",
@@ -11,54 +11,55 @@
11
11
  "license": "MIT",
12
12
  "author": "DXOS.org",
13
13
  "type": "module",
14
+ "imports": {
15
+ "#translations": "./src/translations.ts"
16
+ },
14
17
  "exports": {
15
18
  ".": {
16
19
  "source": "./src/index.ts",
17
20
  "types": "./dist/types/src/index.d.ts",
18
21
  "browser": "./dist/lib/browser/index.mjs",
19
22
  "node": "./dist/lib/node-esm/index.mjs"
23
+ },
24
+ "./translations": {
25
+ "source": "./src/translations.ts",
26
+ "types": "./dist/types/src/translations.d.ts",
27
+ "browser": "./dist/lib/browser/translations.mjs",
28
+ "node": "./dist/lib/node-esm/translations.mjs"
20
29
  }
21
30
  },
22
31
  "types": "dist/types/src/index.d.ts",
23
- "typesVersions": {
24
- "*": {}
25
- },
26
32
  "files": [
27
33
  "dist",
28
34
  "src"
29
35
  ],
30
36
  "dependencies": {
31
- "@atlaskit/pragmatic-drag-and-drop": "1.7.7",
32
- "@atlaskit/pragmatic-drag-and-drop-hitbox": "1.1.0",
33
37
  "@effect-atom/atom-react": "^0.5.0",
34
38
  "@fluentui/react-tabster": "9.26.11",
35
39
  "@radix-ui/react-context": "1.1.1",
36
40
  "@radix-ui/react-use-controllable-state": "1.1.0",
37
41
  "date-fns": "^3.6.0",
38
42
  "tabster": "^8.5.5",
39
- "@dxos/debug": "0.8.4-main.9be5663bfe",
40
- "@dxos/echo": "0.8.4-main.9be5663bfe",
41
- "@dxos/app-toolkit": "0.8.4-main.9be5663bfe",
42
- "@dxos/async": "0.8.4-main.9be5663bfe",
43
- "@dxos/echo-query": "0.8.4-main.9be5663bfe",
44
- "@dxos/effect": "0.8.4-main.9be5663bfe",
45
- "@dxos/keys": "0.8.4-main.9be5663bfe",
46
- "@dxos/invariant": "0.8.4-main.9be5663bfe",
47
- "@dxos/lit-ui": "0.8.4-main.9be5663bfe",
48
- "@dxos/log": "0.8.4-main.9be5663bfe",
49
- "@dxos/react-ui-components": "0.8.4-main.9be5663bfe",
50
- "@dxos/react-client": "0.8.4-main.9be5663bfe",
51
- "@dxos/react-ui-editor": "0.8.4-main.9be5663bfe",
52
- "@dxos/echo-react": "0.8.4-main.9be5663bfe",
53
- "@dxos/react-ui-list": "0.8.4-main.9be5663bfe",
54
- "@dxos/react-ui-mosaic": "0.8.4-main.9be5663bfe",
55
- "@dxos/react-ui-search": "0.8.4-main.9be5663bfe",
56
- "@dxos/react-ui-stack": "0.8.4-main.9be5663bfe",
57
- "@dxos/schema": "0.8.4-main.9be5663bfe",
58
- "@dxos/react-ui-pickers": "0.8.4-main.9be5663bfe",
59
- "@dxos/types": "0.8.4-main.9be5663bfe",
60
- "@dxos/ui-types": "0.8.4-main.9be5663bfe",
61
- "@dxos/util": "0.8.4-main.9be5663bfe"
43
+ "@dxos/async": "0.8.4-main.abd8ff62ef",
44
+ "@dxos/echo": "0.8.4-main.abd8ff62ef",
45
+ "@dxos/echo-atom": "0.8.4-main.abd8ff62ef",
46
+ "@dxos/echo-react": "0.8.4-main.abd8ff62ef",
47
+ "@dxos/effect": "0.8.4-main.abd8ff62ef",
48
+ "@dxos/invariant": "0.8.4-main.abd8ff62ef",
49
+ "@dxos/keys": "0.8.4-main.abd8ff62ef",
50
+ "@dxos/echo-db": "0.8.4-main.abd8ff62ef",
51
+ "@dxos/lit-ui": "0.8.4-main.abd8ff62ef",
52
+ "@dxos/react-client": "0.8.4-main.abd8ff62ef",
53
+ "@dxos/log": "0.8.4-main.abd8ff62ef",
54
+ "@dxos/react-ui-list": "0.8.4-main.abd8ff62ef",
55
+ "@dxos/react-ui-pickers": "0.8.4-main.abd8ff62ef",
56
+ "@dxos/react-ui-components": "0.8.4-main.abd8ff62ef",
57
+ "@dxos/react-ui-search": "0.8.4-main.abd8ff62ef",
58
+ "@dxos/schema": "0.8.4-main.abd8ff62ef",
59
+ "@dxos/types": "0.8.4-main.abd8ff62ef",
60
+ "@dxos/ui-editor": "0.8.4-main.abd8ff62ef",
61
+ "@dxos/util": "0.8.4-main.abd8ff62ef",
62
+ "@dxos/react-ui-editor": "0.8.4-main.abd8ff62ef"
62
63
  },
63
64
  "devDependencies": {
64
65
  "@types/react": "~19.2.7",
@@ -67,20 +68,20 @@
67
68
  "fast-check": "^3.19.0",
68
69
  "react": "~19.2.3",
69
70
  "react-dom": "~19.2.3",
70
- "vite": "^7.1.11",
71
- "@dxos/random": "0.8.4-main.9be5663bfe",
72
- "@dxos/react-client": "0.8.4-main.9be5663bfe",
73
- "@dxos/react-ui": "0.8.4-main.9be5663bfe",
74
- "@dxos/react-ui-syntax-highlighter": "0.8.4-main.9be5663bfe",
75
- "@dxos/storybook-utils": "0.8.4-main.9be5663bfe",
76
- "@dxos/ui-theme": "0.8.4-main.9be5663bfe"
71
+ "vite": "^8.0.10",
72
+ "@dxos/random": "0.8.4-main.abd8ff62ef",
73
+ "@dxos/react-client": "0.8.4-main.abd8ff62ef",
74
+ "@dxos/storybook-utils": "0.8.4-main.abd8ff62ef",
75
+ "@dxos/react-ui": "0.8.4-main.abd8ff62ef",
76
+ "@dxos/ui-theme": "0.8.4-main.abd8ff62ef",
77
+ "@dxos/react-ui-syntax-highlighter": "0.8.4-main.abd8ff62ef"
77
78
  },
78
79
  "peerDependencies": {
79
80
  "effect": "3.20.0",
80
81
  "react": "~19.2.3",
81
82
  "react-dom": "~19.2.3",
82
- "@dxos/react-ui": "0.8.4-main.9be5663bfe",
83
- "@dxos/ui-theme": "0.8.4-main.9be5663bfe"
83
+ "@dxos/ui-theme": "0.8.4-main.abd8ff62ef",
84
+ "@dxos/react-ui": "0.8.4-main.abd8ff62ef"
84
85
  },
85
86
  "publishConfig": {
86
87
  "access": "public"
@@ -12,7 +12,8 @@ import { withLayout, withTheme } from '@dxos/react-ui/testing';
12
12
  import { ProjectionModel, ViewModel, createEchoChangeCallback } from '@dxos/schema';
13
13
  import { Example } from '@dxos/schema/testing';
14
14
 
15
- import { translations } from '../../translations';
15
+ import { translations } from '#translations';
16
+
16
17
  import { FIELD_EDITOR_DEBUG_SYMBOL, TestLayout } from '../testing';
17
18
  import { FieldEditor, type FieldEditorProps } from './FieldEditor';
18
19
 
@@ -70,9 +71,7 @@ const DefaultStory = () => {
70
71
 
71
72
  return (
72
73
  <TestLayout json={json}>
73
- <div role='none' className='col-span-full'>
74
- <FieldEditor {...props} />
75
- </div>
74
+ <FieldEditor {...props} />
76
75
  </TestLayout>
77
76
  );
78
77
  };
@@ -16,7 +16,8 @@ import {
16
16
  getFormatSchema,
17
17
  } from '@dxos/schema';
18
18
 
19
- import { translationKey } from '../../translations';
19
+ import { translationKey } from '#translations';
20
+
20
21
  import { getFormProperties } from '../../util';
21
22
  import { Form, type FormFieldMap, type FormRootProps, SelectField, SelectOptionField } from '../Form';
22
23
 
@@ -151,7 +152,7 @@ export const FieldEditor = ({ readonly, projection, field, registry, view, onSav
151
152
  const handleSave = useCallback<NonNullable<FormRootProps<PropertyType>['onSave']>>(
152
153
  (props) => {
153
154
  if (view) {
154
- Obj.change(view, () => {
155
+ Obj.update(view, () => {
155
156
  projection.setFieldProjection({ field, props });
156
157
  });
157
158
  } else {
@@ -14,7 +14,8 @@ import { withClientProvider } from '@dxos/react-client/testing';
14
14
  import { Tooltip } from '@dxos/react-ui';
15
15
  import { Loading, withLayout, withTheme } from '@dxos/react-ui/testing';
16
16
 
17
- import { translations } from '../../translations';
17
+ import { translations } from '#translations';
18
+
18
19
  import { TestLayout } from '../testing';
19
20
  import { type ExcludeId, Form, type FormRootProps, omitId } from './Form';
20
21
 
@@ -72,8 +73,8 @@ const Person = Schema.Struct({
72
73
  export interface Person extends Schema.Schema.Type<typeof Person> {}
73
74
 
74
75
  type DefaultStoryProps<T extends AnyProperties> = {
76
+ schema?: Schema.Schema<T>;
75
77
  debug?: boolean;
76
- schema: Schema.Schema<T>;
77
78
  } & FormRootProps<T>;
78
79
 
79
80
  const DefaultStory = <T extends AnyProperties = AnyProperties>({
@@ -102,7 +103,7 @@ const DefaultStory = <T extends AnyProperties = AnyProperties>({
102
103
 
103
104
  return (
104
105
  <Tooltip.Provider>
105
- <TestLayout json={{ values, schema: schema.ast }}>
106
+ <TestLayout json={{ values, schema: schema?.ast }}>
106
107
  <Form.Root
107
108
  debug={debug}
108
109
  schema={schema}
@@ -129,7 +130,6 @@ const meta = {
129
130
  title: 'ui/react-ui-form/Form',
130
131
  component: Form.Root,
131
132
  render: DefaultStory,
132
-
133
133
  decorators: [
134
134
  withTheme(),
135
135
  withLayout({ layout: 'fullscreen' }),
@@ -186,16 +186,5 @@ export const Static: Story<ExcludeId<typeof Person>> = {
186
186
  };
187
187
 
188
188
  export const Empty: Story<ExcludeId<typeof Person>> = {
189
- render: () => (
190
- <TestLayout>
191
- <Form.Root>
192
- <Form.Viewport>
193
- <Form.Content>
194
- <Form.FieldSet />
195
- <Form.Actions />
196
- </Form.Content>
197
- </Form.Viewport>
198
- </Form.Root>
199
- </TestLayout>
200
- ),
189
+ args: {},
201
190
  };
@@ -3,10 +3,12 @@
3
3
  //
4
4
 
5
5
  import { createContext } from '@radix-ui/react-context';
6
+ import * as Option from 'effect/Option';
6
7
  import * as Schema from 'effect/Schema';
7
8
  import * as SchemaAST from 'effect/SchemaAST';
8
9
  import React, { type PropsWithChildren, useEffect, useMemo, useRef } from 'react';
9
10
 
11
+ import { Annotation as EchoAnnotation } from '@dxos/echo';
10
12
  import { type AnyProperties } from '@dxos/echo/internal';
11
13
  import { createJsonPath, getValue as getValue$ } from '@dxos/effect';
12
14
  import {
@@ -17,7 +19,9 @@ import {
17
19
  useMergeRefs,
18
20
  useTranslation,
19
21
  } from '@dxos/react-ui';
20
- import { composable, composableProps, mx } from '@dxos/ui-theme';
22
+ import { composable, composableProps, mx, withColumn } from '@dxos/ui-theme';
23
+
24
+ import { translationKey } from '#translations';
21
25
 
22
26
  import {
23
27
  type FormHandler,
@@ -26,22 +30,35 @@ import {
26
30
  useFormHandler,
27
31
  useKeyHandler,
28
32
  } from '../../hooks';
29
- import { translationKey } from '../../translations';
30
33
  import { FormFieldLabel, type FormFieldLabelProps, type FormFieldStateProps } from './FormFieldComponent';
31
34
  import {
32
35
  FormFieldSet as NaturalFormFieldSet,
33
36
  type FormFieldSetProps as NaturalFormFieldSetProps,
34
37
  } from './FormFieldSet';
35
38
 
36
- // TODO(burdon): Move styles to form.ts.
39
+ // TODO(burdon): Move styles to form.ts (as with ui-theme).
37
40
 
38
- // TODO(burdon): Move to @dxos/schema (re-export here).
41
+ // TODO(burdon): Reconcile with @dxos/echo.
39
42
  export type ExcludeId<S extends Schema.Schema.AnyNoContext> = Omit<Schema.Schema.Type<S>, 'id'>;
40
43
 
41
44
  // TODO(burdon): Move to @dxos/schema (re-export here).
42
45
  export const omitId = <S extends Schema.Schema.AnyNoContext>(schema: S): Schema.Schema<ExcludeId<S>, ExcludeId<S>> =>
43
46
  schema.pipe(Schema.omit('id')) as any;
44
47
 
48
+ /**
49
+ * Drop fields annotated with `FormInputAnnotation.set(false)` from a schema so
50
+ * the form's validator doesn't trip on required-but-hidden fields. Used by
51
+ * the picker's inline create form, where a `FactoryAnnotation` typically
52
+ * supplies the hidden values (e.g. a backing-object Ref) outside the form.
53
+ */
54
+ export const omitHiddenFormFields = <S extends Schema.Schema.AnyNoContext>(schema: S): S => {
55
+ const properties = SchemaAST.getPropertySignatures(schema.ast);
56
+ const hidden = properties
57
+ .filter((prop) => Option.getOrElse(EchoAnnotation.FormInputAnnotation.getFromAst(prop.type), () => true) === false)
58
+ .map((prop) => prop.name as string);
59
+ return hidden.length === 0 ? schema : (schema.pipe(Schema.omit(...(hidden as [string, ...string[]]))) as any);
60
+ };
61
+
45
62
  //
46
63
  // Context
47
64
  //
@@ -63,7 +80,7 @@ type FormContextValue<T extends AnyProperties = any> = {
63
80
  testId?: string;
64
81
  } & Pick<
65
82
  NaturalFormFieldSetProps<T>,
66
- 'readonly' | 'layout' | 'fieldMap' | 'fieldProvider' | 'projection' | 'createTypename' | 'createFieldMap'
83
+ 'readonly' | 'layout' | 'fieldMap' | 'fieldProvider' | 'projection' | 'createTypename' | 'createFieldMap' | 'db'
67
84
  >;
68
85
 
69
86
  const [FormContextProvider, useFormContext] = createContext<FormContextValue>('Form');
@@ -194,7 +211,10 @@ const FormContent = composable<HTMLDivElement, FormContentProps>(({ children, ..
194
211
 
195
212
  return (
196
213
  <div
197
- {...composableProps(props, { role: 'form', classNames: 'flex flex-col w-full pb-form-gap' })}
214
+ {...composableProps(props, {
215
+ role: 'form',
216
+ classNames: mx(withColumn.center(), 'flex flex-col w-full pb-form-gap'),
217
+ })}
198
218
  data-testid={testId}
199
219
  ref={mergedRef}
200
220
  >
@@ -245,7 +265,10 @@ const FormActions = ({ classNames }: FormActionsProps) => {
245
265
  // Deprecate FormSubmit ans use FormActions without Cancel button if no callback is supplied.
246
266
 
247
267
  return (
248
- <div role='none' className={mx('grid grid-flow-col gap-form-gap auto-cols-fr py-form-padding', classNames)}>
268
+ <div
269
+ role='none'
270
+ className={mx(withColumn.center(), 'grid grid-flow-col gap-form-gap auto-cols-fr py-form-padding', classNames)}
271
+ >
249
272
  {onCancel && (
250
273
  <IconButton
251
274
  icon='ph--x--regular'
@@ -23,7 +23,8 @@ import {
23
23
  import { useTranslation } from '@dxos/react-ui';
24
24
  import { type ProjectionModel } from '@dxos/schema';
25
25
 
26
- import { translationKey } from '../../translations';
26
+ import { translationKey } from '#translations';
27
+
27
28
  import { getRefProps } from '../../util';
28
29
  import {
29
30
  ArrayField,
@@ -91,7 +92,7 @@ export type FormFieldProps = {
91
92
  | 'createInitialValuePath'
92
93
  | 'createFieldMap'
93
94
  | 'db'
94
- | 'schemaHook'
95
+ | 'useSchema'
95
96
  | 'getOptions'
96
97
  | 'onCreate'
97
98
  >;
@@ -114,7 +115,7 @@ export const FormField = (props: FormFieldProps) => {
114
115
  createInitialValuePath,
115
116
  createFieldMap,
116
117
  db,
117
- schemaHook,
118
+ useSchema: schemaHook,
118
119
  getOptions,
119
120
  onCreate,
120
121
  } = props;
@@ -137,6 +138,7 @@ export const FormField = (props: FormFieldProps) => {
137
138
  label,
138
139
  placeholder,
139
140
  layout,
141
+ db,
140
142
  ...fieldState,
141
143
  };
142
144
 
@@ -214,7 +216,7 @@ export const FormField = (props: FormFieldProps) => {
214
216
  createInitialValuePath={isCreateTarget ? createInitialValuePath : undefined}
215
217
  createFieldMap={isCreateTarget ? createFieldMap : undefined}
216
218
  db={db}
217
- schemaHook={schemaHook}
219
+ useSchema={schemaHook}
218
220
  getOptions={getOptions}
219
221
  onCreate={onCreate}
220
222
  />
@@ -247,7 +249,7 @@ export const FormField = (props: FormFieldProps) => {
247
249
  createOptionIcon={createOptionIcon}
248
250
  createInitialValuePath={createInitialValuePath}
249
251
  db={db}
250
- schemaHook={schemaHook}
252
+ useSchema={schemaHook}
251
253
  getOptions={getOptions}
252
254
  onCreate={onCreate}
253
255
  />
@@ -13,6 +13,7 @@ import React, {
13
13
  type ReactNode,
14
14
  } from 'react';
15
15
 
16
+ import { type Database } from '@dxos/echo';
16
17
  import { type Format } from '@dxos/echo/internal';
17
18
  import { Icon, Input, Tooltip } from '@dxos/react-ui';
18
19
  import { inputTextLabel } from '@dxos/ui-theme';
@@ -50,6 +51,8 @@ export type FormFieldComponentProps<T = any> = {
50
51
  placeholder?: string;
51
52
  autoFocus?: boolean;
52
53
  layout?: Presentation;
54
+ /** Database the form is editing against; populated for fields whose value is an ECHO object/Ref. */
55
+ db?: Database.Database;
53
56
  } & FormFieldStateProps<T>;
54
57
 
55
58
  /**
@@ -35,7 +35,7 @@ export type FormFieldSetProps<T extends AnyProperties> = {
35
35
  | 'createInitialValuePath'
36
36
  | 'createFieldMap'
37
37
  | 'db'
38
- | 'schemaHook'
38
+ | 'useSchema'
39
39
  | 'getOptions'
40
40
  | 'onCreate'
41
41
  >;
@@ -55,9 +55,7 @@ export const FormFieldSet = ({
55
55
  ...props
56
56
  }: FormFieldSetProps<any>) => {
57
57
  const values = useFormValues(FORM_FIELDSET_NAME, path);
58
-
59
58
  const properties = useFormFieldSetProperties({ schema, values, exclude, sort, projection });
60
-
61
59
  if ((readonly || layout === 'static') && values == null) {
62
60
  return null;
63
61
  }
@@ -9,7 +9,8 @@ import React, { useCallback } from 'react';
9
9
  import { findNode, getArrayElementType, getDiscriminatedType, isDiscriminatedUnion, isNestedType } from '@dxos/effect';
10
10
  import { IconButton, useTranslation } from '@dxos/react-ui';
11
11
 
12
- import { translationKey } from '../../../translations';
12
+ import { translationKey } from '#translations';
13
+
13
14
  import { getFormProperties } from '../../../util';
14
15
  import { useFormValues } from '../Form';
15
16
  import { FormField, type FormFieldProps } from '../FormField';
@@ -80,7 +81,11 @@ export const ArrayField = ({
80
81
  <div role='none' className='flex flex-col gap-form-gap'>
81
82
  {values?.map((_, index) => {
82
83
  return (
83
- <div role='none' key={index} className='flex gap-form-gap last:mb-form-gap items-center'>
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
+ >
84
89
  <FormField
85
90
  autoFocus={index === values.length - 1}
86
91
  type={elementType}
@@ -114,7 +119,7 @@ export const ArrayField = ({
114
119
  <IconButton
115
120
  classNames='flex w-full'
116
121
  icon='ph--plus--regular'
117
- label={t('add-field.button')}
122
+ label={t('add-item.button')}
118
123
  onClick={handleAdd}
119
124
  />
120
125
  )}
@@ -8,7 +8,8 @@ import { GeoLocation, type GeoPoint } from '@dxos/echo/internal';
8
8
  import { Input, useTranslation } from '@dxos/react-ui';
9
9
  import { safeParseFloat } from '@dxos/util';
10
10
 
11
- import { translationKey } from '../../../translations';
11
+ import { translationKey } from '#translations';
12
+
12
13
  import { type FormFieldComponentProps, FormFieldLabel } from '../FormFieldComponent';
13
14
 
14
15
  export const GeoPointField = ({
@@ -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
+ };