@dxos/react-ui-form 0.8.4-main.b97322e → 0.8.4-main.bc674ce

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 (197) hide show
  1. package/dist/lib/browser/index.mjs +2220 -2058
  2. package/dist/lib/browser/index.mjs.map +4 -4
  3. package/dist/lib/browser/meta.json +1 -1
  4. package/dist/lib/node-esm/index.mjs +2220 -2058
  5. package/dist/lib/node-esm/index.mjs.map +4 -4
  6. package/dist/lib/node-esm/meta.json +1 -1
  7. package/dist/types/src/components/ControlSection/ControlSection.d.ts +4 -4
  8. package/dist/types/src/components/ControlSection/ControlSection.d.ts.map +1 -1
  9. package/dist/types/src/components/ControlSection/ControlSection.stories.d.ts +84 -5
  10. package/dist/types/src/components/ControlSection/ControlSection.stories.d.ts.map +1 -1
  11. package/dist/types/src/components/FieldEditor/FieldEditor.d.ts +6 -5
  12. package/dist/types/src/components/FieldEditor/FieldEditor.d.ts.map +1 -1
  13. package/dist/types/src/components/FieldEditor/FieldEditor.stories.d.ts +82 -5
  14. package/dist/types/src/components/FieldEditor/FieldEditor.stories.d.ts.map +1 -1
  15. package/dist/types/src/components/Form/Form-old.stories.d.ts +131 -0
  16. package/dist/types/src/components/Form/Form-old.stories.d.ts.map +1 -0
  17. package/dist/types/src/components/Form/Form.d.ts +81 -25
  18. package/dist/types/src/components/Form/Form.d.ts.map +1 -1
  19. package/dist/types/src/components/Form/Form.stories.d.ts +127 -55
  20. package/dist/types/src/components/Form/Form.stories.d.ts.map +1 -1
  21. package/dist/types/src/components/Form/FormField.d.ts +37 -0
  22. package/dist/types/src/components/Form/FormField.d.ts.map +1 -0
  23. package/dist/types/src/components/Form/FormFieldComponent.d.ts +78 -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 +19 -0
  28. package/dist/types/src/components/Form/FormFieldSet.d.ts.map +1 -0
  29. package/dist/types/src/components/Form/fields/ArrayField.d.ts +18 -0
  30. package/dist/types/src/components/Form/fields/ArrayField.d.ts.map +1 -0
  31. package/dist/types/src/components/Form/fields/BooleanField.d.ts +4 -0
  32. package/dist/types/src/components/Form/fields/BooleanField.d.ts.map +1 -0
  33. package/dist/types/src/components/Form/fields/GeoPointField.d.ts +5 -0
  34. package/dist/types/src/components/Form/fields/GeoPointField.d.ts.map +1 -0
  35. package/dist/types/src/components/Form/fields/MarkdownField.d.ts +4 -0
  36. package/dist/types/src/components/Form/fields/MarkdownField.d.ts.map +1 -0
  37. package/dist/types/src/components/Form/fields/NumberField.d.ts +4 -0
  38. package/dist/types/src/components/Form/fields/NumberField.d.ts.map +1 -0
  39. package/dist/types/src/components/Form/fields/RefField.d.ts +14 -0
  40. package/dist/types/src/components/Form/fields/RefField.d.ts.map +1 -0
  41. package/dist/types/src/components/Form/fields/SelectField.d.ts +10 -0
  42. package/dist/types/src/components/Form/fields/SelectField.d.ts.map +1 -0
  43. package/dist/types/src/components/Form/fields/SelectOptionField.d.ts +5 -0
  44. package/dist/types/src/components/Form/fields/SelectOptionField.d.ts.map +1 -0
  45. package/dist/types/src/components/Form/fields/SelectOptionField.stories.d.ts +82 -0
  46. package/dist/types/src/components/Form/fields/SelectOptionField.stories.d.ts.map +1 -0
  47. package/dist/types/src/components/Form/fields/TextAreaField.d.ts +4 -0
  48. package/dist/types/src/components/Form/fields/TextAreaField.d.ts.map +1 -0
  49. package/dist/types/src/components/Form/fields/TextField.d.ts +4 -0
  50. package/dist/types/src/components/Form/fields/TextField.d.ts.map +1 -0
  51. package/dist/types/src/components/Form/fields/TupleField.d.ts +6 -0
  52. package/dist/types/src/components/Form/fields/TupleField.d.ts.map +1 -0
  53. package/dist/types/src/components/Form/fields/index.d.ts +12 -0
  54. package/dist/types/src/components/Form/fields/index.d.ts.map +1 -0
  55. package/dist/types/src/components/Form/index.d.ts +2 -4
  56. package/dist/types/src/components/Form/index.d.ts.map +1 -1
  57. package/dist/types/src/components/ObjectPicker/ObjectPicker.d.ts +46 -0
  58. package/dist/types/src/components/ObjectPicker/ObjectPicker.d.ts.map +1 -0
  59. package/dist/types/src/components/ObjectPicker/ObjectPicker.stories.d.ts +85 -0
  60. package/dist/types/src/components/ObjectPicker/ObjectPicker.stories.d.ts.map +1 -0
  61. package/dist/types/src/components/ObjectPicker/index.d.ts +2 -0
  62. package/dist/types/src/components/ObjectPicker/index.d.ts.map +1 -0
  63. package/dist/types/src/components/ViewEditor/ViewEditor.d.ts +21 -9
  64. package/dist/types/src/components/ViewEditor/ViewEditor.d.ts.map +1 -1
  65. package/dist/types/src/components/ViewEditor/ViewEditor.stories.d.ts +83 -8
  66. package/dist/types/src/components/ViewEditor/ViewEditor.stories.d.ts.map +1 -1
  67. package/dist/types/src/components/index.d.ts +1 -1
  68. package/dist/types/src/components/index.d.ts.map +1 -1
  69. package/dist/types/src/components/testing.d.ts +4 -3
  70. package/dist/types/src/components/testing.d.ts.map +1 -1
  71. package/dist/types/src/hooks/index.d.ts +2 -3
  72. package/dist/types/src/hooks/index.d.ts.map +1 -1
  73. package/dist/types/src/hooks/useFormHandler.d.ts +82 -0
  74. package/dist/types/src/hooks/useFormHandler.d.ts.map +1 -0
  75. package/dist/types/src/hooks/useKeyHandler.d.ts +6 -0
  76. package/dist/types/src/hooks/useKeyHandler.d.ts.map +1 -0
  77. package/dist/types/src/translations.d.ts +19 -10
  78. package/dist/types/src/translations.d.ts.map +1 -1
  79. package/dist/types/src/util/echo.d.ts +6 -0
  80. package/dist/types/src/util/echo.d.ts.map +1 -0
  81. package/dist/types/src/util/field.d.ts +3 -3
  82. package/dist/types/src/util/field.d.ts.map +1 -1
  83. package/dist/types/src/util/formatting.d.ts +2 -2
  84. package/dist/types/src/util/formatting.d.ts.map +1 -1
  85. package/dist/types/src/util/index.d.ts +2 -1
  86. package/dist/types/src/util/index.d.ts.map +1 -1
  87. package/dist/types/src/util/properties.d.ts +7 -0
  88. package/dist/types/src/util/properties.d.ts.map +1 -0
  89. package/dist/types/src/util/refs.d.ts +2 -3
  90. package/dist/types/src/util/refs.d.ts.map +1 -1
  91. package/dist/types/tsconfig.tsbuildinfo +1 -1
  92. package/package.json +51 -40
  93. package/src/components/ControlSection/ControlSection.stories.tsx +11 -14
  94. package/src/components/ControlSection/ControlSection.tsx +30 -16
  95. package/src/components/FieldEditor/FieldEditor.stories.tsx +52 -31
  96. package/src/components/FieldEditor/FieldEditor.test.tsx +3 -2
  97. package/src/components/FieldEditor/FieldEditor.tsx +86 -81
  98. package/src/components/Form/Form-old.stories.tsx +314 -0
  99. package/src/components/Form/Form.stories.tsx +155 -219
  100. package/src/components/Form/Form.tsx +320 -96
  101. package/src/components/Form/FormField.tsx +286 -0
  102. package/src/components/Form/FormFieldComponent.stories.tsx +65 -0
  103. package/src/components/Form/FormFieldComponent.tsx +174 -0
  104. package/src/components/Form/FormFieldSet.tsx +125 -0
  105. package/src/components/Form/README.md +3 -0
  106. package/src/components/Form/fields/ArrayField.tsx +151 -0
  107. package/src/components/Form/fields/BooleanField.tsx +22 -0
  108. package/src/components/Form/fields/GeoPointField.tsx +120 -0
  109. package/src/components/Form/fields/MarkdownField.tsx +78 -0
  110. package/src/components/Form/fields/NumberField.tsx +39 -0
  111. package/src/components/Form/fields/RefField.tsx +181 -0
  112. package/src/components/Form/fields/SelectField.tsx +65 -0
  113. package/src/components/Form/{custom/SelectOptionsInput.stories.tsx → fields/SelectOptionField.stories.tsx} +25 -22
  114. package/src/components/Form/{custom/SelectOptionsInput.tsx → fields/SelectOptionField.tsx} +47 -42
  115. package/src/components/Form/fields/TextAreaField.tsx +38 -0
  116. package/src/components/Form/fields/TextField.tsx +38 -0
  117. package/src/components/Form/{custom/TupleInput.tsx → fields/TupleField.tsx} +16 -9
  118. package/src/components/Form/fields/index.ts +15 -0
  119. package/src/components/Form/index.ts +3 -4
  120. package/src/components/ObjectPicker/ObjectPicker.stories.tsx +214 -0
  121. package/src/components/ObjectPicker/ObjectPicker.tsx +175 -0
  122. package/src/components/ObjectPicker/index.ts +5 -0
  123. package/src/components/ViewEditor/ViewEditor.stories.tsx +137 -61
  124. package/src/components/ViewEditor/ViewEditor.test.tsx +19 -17
  125. package/src/components/ViewEditor/ViewEditor.tsx +324 -203
  126. package/src/components/index.ts +1 -2
  127. package/src/components/testing.tsx +19 -11
  128. package/src/hooks/index.ts +2 -3
  129. package/src/hooks/useFormHandler.ts +362 -0
  130. package/src/hooks/useKeyHandler.tsx +37 -0
  131. package/src/translations.ts +21 -12
  132. package/src/util/echo.ts +25 -0
  133. package/src/util/field.ts +33 -43
  134. package/src/util/formatting.ts +22 -22
  135. package/src/util/index.ts +2 -1
  136. package/src/util/properties.ts +18 -0
  137. package/src/util/refs.ts +12 -18
  138. package/dist/types/src/components/Form/ArrayField.d.ts +0 -16
  139. package/dist/types/src/components/Form/ArrayField.d.ts.map +0 -1
  140. package/dist/types/src/components/Form/Defaults.d.ts +0 -14
  141. package/dist/types/src/components/Form/Defaults.d.ts.map +0 -1
  142. package/dist/types/src/components/Form/FormActions.d.ts +0 -9
  143. package/dist/types/src/components/Form/FormActions.d.ts.map +0 -1
  144. package/dist/types/src/components/Form/FormContent.d.ts +0 -57
  145. package/dist/types/src/components/Form/FormContent.d.ts.map +0 -1
  146. package/dist/types/src/components/Form/FormContext.d.ts +0 -23
  147. package/dist/types/src/components/Form/FormContext.d.ts.map +0 -1
  148. package/dist/types/src/components/Form/Input.d.ts +0 -26
  149. package/dist/types/src/components/Form/Input.d.ts.map +0 -1
  150. package/dist/types/src/components/Form/RefField.d.ts +0 -12
  151. package/dist/types/src/components/Form/RefField.d.ts.map +0 -1
  152. package/dist/types/src/components/Form/custom/GeoPointInput.d.ts +0 -4
  153. package/dist/types/src/components/Form/custom/GeoPointInput.d.ts.map +0 -1
  154. package/dist/types/src/components/Form/custom/SelectOptionsInput.d.ts +0 -4
  155. package/dist/types/src/components/Form/custom/SelectOptionsInput.d.ts.map +0 -1
  156. package/dist/types/src/components/Form/custom/SelectOptionsInput.stories.d.ts +0 -7
  157. package/dist/types/src/components/Form/custom/SelectOptionsInput.stories.d.ts.map +0 -1
  158. package/dist/types/src/components/Form/custom/TupleInput.d.ts +0 -6
  159. package/dist/types/src/components/Form/custom/TupleInput.d.ts.map +0 -1
  160. package/dist/types/src/components/Form/custom/index.d.ts +0 -4
  161. package/dist/types/src/components/Form/custom/index.d.ts.map +0 -1
  162. package/dist/types/src/components/Form/factory.d.ts +0 -8
  163. package/dist/types/src/components/Form/factory.d.ts.map +0 -1
  164. package/dist/types/src/components/deprecated/Form.d.ts +0 -15
  165. package/dist/types/src/components/deprecated/Form.d.ts.map +0 -1
  166. package/dist/types/src/components/deprecated/Form.stories.d.ts +0 -13
  167. package/dist/types/src/components/deprecated/Form.stories.d.ts.map +0 -1
  168. package/dist/types/src/components/deprecated/FormInput.d.ts +0 -16
  169. package/dist/types/src/components/deprecated/FormInput.d.ts.map +0 -1
  170. package/dist/types/src/components/deprecated/index.d.ts +0 -2
  171. package/dist/types/src/components/deprecated/index.d.ts.map +0 -1
  172. package/dist/types/src/hooks/useForm.d.ts +0 -67
  173. package/dist/types/src/hooks/useForm.d.ts.map +0 -1
  174. package/dist/types/src/hooks/useQueryRefOptions.d.ts +0 -23
  175. package/dist/types/src/hooks/useQueryRefOptions.d.ts.map +0 -1
  176. package/dist/types/src/hooks/useRefQueryLookupHandler.d.ts +0 -8
  177. package/dist/types/src/hooks/useRefQueryLookupHandler.d.ts.map +0 -1
  178. package/dist/types/src/util/schema.d.ts +0 -3
  179. package/dist/types/src/util/schema.d.ts.map +0 -1
  180. package/src/components/Form/ArrayField.tsx +0 -124
  181. package/src/components/Form/Defaults.tsx +0 -203
  182. package/src/components/Form/FormActions.tsx +0 -53
  183. package/src/components/Form/FormContent.tsx +0 -240
  184. package/src/components/Form/FormContext.tsx +0 -110
  185. package/src/components/Form/Input.tsx +0 -51
  186. package/src/components/Form/RefField.tsx +0 -213
  187. package/src/components/Form/custom/GeoPointInput.tsx +0 -72
  188. package/src/components/Form/custom/index.ts +0 -7
  189. package/src/components/Form/factory.tsx +0 -31
  190. package/src/components/deprecated/Form.stories.tsx +0 -62
  191. package/src/components/deprecated/Form.tsx +0 -119
  192. package/src/components/deprecated/FormInput.tsx +0 -71
  193. package/src/components/deprecated/index.ts +0 -5
  194. package/src/hooks/useForm.ts +0 -291
  195. package/src/hooks/useQueryRefOptions.ts +0 -53
  196. package/src/hooks/useRefQueryLookupHandler.ts +0 -39
  197. package/src/util/schema.ts +0 -14
@@ -2,22 +2,21 @@
2
2
  // Copyright 2025 DXOS.org
3
3
  //
4
4
 
5
- import '@dxos-theme';
6
-
7
5
  import { type Meta, type StoryObj } from '@storybook/react-vite';
8
- import { Schema } from 'effect';
6
+ import * as Schema from 'effect/Schema';
9
7
  import React, { useState } from 'react';
10
8
 
11
- import { SelectOptionSchema, type SelectOption } from '@dxos/echo-schema';
12
- import { withLayout, withTheme } from '@dxos/storybook-utils';
9
+ import { SelectOption } from '@dxos/echo/internal';
10
+ import { withTheme } from '@dxos/react-ui/testing';
13
11
 
14
- import { SelectOptionInput } from './SelectOptionsInput';
15
12
  import { translations } from '../../../translations';
16
- import { TestLayout, TestPanel } from '../../testing';
13
+ import { TestLayout } from '../../testing';
17
14
  import { Form } from '../Form';
18
15
 
16
+ import { SelectOptionField } from './SelectOptionField';
17
+
19
18
  const schema = Schema.Struct({
20
- options: Schema.Array(SelectOptionSchema).pipe(Schema.mutable),
19
+ options: Schema.Array(SelectOption).pipe(Schema.mutable),
21
20
  }).pipe(Schema.mutable);
22
21
 
23
22
  const DefaultStory = () => {
@@ -32,30 +31,34 @@ const DefaultStory = () => {
32
31
 
33
32
  return (
34
33
  <TestLayout>
35
- <TestPanel>
36
- <Form
37
- schema={schema}
38
- values={values}
39
- onSave={(vals) => setValues(vals)}
40
- Custom={{ options: SelectOptionInput }}
41
- />
42
- </TestPanel>
34
+ <Form.Root
35
+ schema={schema}
36
+ values={values}
37
+ fieldMap={{ options: SelectOptionField }}
38
+ onSave={(values) => setValues(values)}
39
+ >
40
+ <Form.Content>
41
+ <Form.FieldSet />
42
+ <Form.Actions />
43
+ </Form.Content>
44
+ </Form.Root>
43
45
  </TestLayout>
44
46
  );
45
47
  };
46
48
 
47
- const meta: Meta = {
48
- title: 'ui/react-ui-form/SelectOptionsInput',
49
- component: SelectOptionInput,
49
+ const meta = {
50
+ title: 'ui/react-ui-form/SelectOptionField',
51
+ component: SelectOptionField as any,
50
52
  render: DefaultStory,
51
- decorators: [withTheme, withLayout({ fullscreen: true })],
53
+ decorators: [withTheme],
52
54
  parameters: {
55
+ layout: 'fullscreen',
53
56
  translations,
54
57
  },
55
- };
58
+ } satisfies Meta<typeof DefaultStory>;
56
59
 
57
60
  export default meta;
58
61
 
59
- type Story = StoryObj;
62
+ type Story = StoryObj<typeof meta>;
60
63
 
61
64
  export const Default: Story = {};
@@ -2,33 +2,39 @@
2
2
  // Copyright 2025 DXOS.org
3
3
  //
4
4
 
5
- import React, { type ChangeEvent, type KeyboardEvent, useCallback, useEffect, useState, useRef } from 'react';
5
+ import React, { type ChangeEvent, type KeyboardEvent, useCallback, useEffect, useRef, useState } from 'react';
6
6
 
7
- import { type SelectOption } from '@dxos/echo-schema';
7
+ import { type SelectOption } from '@dxos/echo/internal';
8
8
  import { PublicKey } from '@dxos/keys';
9
- import { type ChromaticPalette, Icon, IconButton, Input, Tag, Toolbar, useTranslation } from '@dxos/react-ui';
9
+ import { type ChromaticPalette, IconButton, Input, Tag, useTranslation } from '@dxos/react-ui';
10
10
  import { List } from '@dxos/react-ui-list';
11
11
  import { HuePicker } from '@dxos/react-ui-pickers';
12
- import { hues, mx } from '@dxos/react-ui-theme';
12
+ import { hues, osTranslations, subtleHover } from '@dxos/ui-theme';
13
13
 
14
14
  import { translationKey } from '../../../translations';
15
- import { InputHeader, type InputProps } from '../Input';
16
-
17
- // TODO(ZaymonFC): Handle disabled.
18
- export const SelectOptionInput = ({ type, label, disabled, getStatus, getValue, onValueChange }: InputProps) => {
15
+ import { type FormFieldComponentProps, FormFieldLabel } from '../FormFieldComponent';
16
+
17
+ export const SelectOptionField = ({
18
+ type,
19
+ readonly,
20
+ label,
21
+ getStatus,
22
+ getValue,
23
+ onValueChange,
24
+ }: FormFieldComponentProps<SelectOption[] | undefined>) => {
25
+ const { t } = useTranslation(translationKey);
26
+ const { status, error } = getStatus();
19
27
  const [selected, setSelectedId] = useState<string | null>(null);
20
28
  const [isNewOption, setIsNewOption] = useState(false);
21
29
  const inputRef = useRef<HTMLInputElement>(null);
22
- const { t } = useTranslation(translationKey);
23
- const { status, error } = getStatus();
24
- const options = getValue<SelectOption[] | undefined>();
30
+ const options = getValue();
25
31
 
26
32
  // Initialization.
27
33
  useEffect(() => {
28
34
  if (options === undefined) {
29
35
  onValueChange(type, []);
30
36
  }
31
- }, [options, onValueChange, type]);
37
+ }, [options, type, onValueChange]);
32
38
 
33
39
  const randomHue = () => {
34
40
  const usedHues = new Set(options?.map((option) => option.color) ?? []);
@@ -89,8 +95,8 @@ export const SelectOptionInput = ({ type, label, disabled, getStatus, getValue,
89
95
  [options, type, onValueChange],
90
96
  );
91
97
 
92
- const handleKeyDown = useCallback((e: KeyboardEvent<HTMLInputElement>) => {
93
- if (e.key === 'Enter') {
98
+ const handleKeyDown = useCallback((ev: KeyboardEvent<HTMLInputElement>) => {
99
+ if (ev.key === 'Enter') {
94
100
  setSelectedId(null);
95
101
  }
96
102
  }, []);
@@ -112,53 +118,56 @@ export const SelectOptionInput = ({ type, label, disabled, getStatus, getValue,
112
118
 
113
119
  return (
114
120
  <Input.Root validationValence={status}>
115
- <InputHeader error={error} label={label} />
121
+ <FormFieldLabel error={error} readonly={readonly} label={label} />
116
122
  <div role='none'>
117
123
  {options && (
118
- <List.Root items={options} isItem={(item) => true} onMove={handleMove}>
124
+ <List.Root
125
+ items={options}
126
+ isItem={(_item) => true}
127
+ onMove={readonly ? undefined : handleMove}
128
+ readonly={!!readonly}
129
+ >
119
130
  {({ items }) => (
120
- <div role='list' className='w-full overflow-auto'>
131
+ <div role='list' className='is-full overflow-auto'>
121
132
  {items?.map((item) => (
122
133
  <div key={item.id}>
123
134
  <List.Item
124
135
  role='button'
125
136
  item={item}
126
- classNames={['p-1 plb-2', 'flex flex-col', 'cursor-pointer', 'hover:bg-hoverOverlay']}
137
+ classNames={[subtleHover, 'flex flex-col cursor-pointer rounded-sm']}
127
138
  aria-expanded={selected === item.id}
128
139
  >
129
140
  <div className='flex items-center'>
130
- <List.ItemDragHandle />
141
+ <List.ItemDragHandle disabled={!!readonly} />
131
142
  <List.ItemTitle onClick={() => handleClick(item.id)} classNames='flex-1'>
132
143
  {/* TODO(ZaymonFC): Move spacer into Tag component. */}
133
144
  <Tag palette={item.color as ChromaticPalette}>{item.title || '\u200b'}</Tag>
134
145
  </List.ItemTitle>
135
- <Icon
146
+ <IconButton
147
+ iconOnly
148
+ variant='ghost'
149
+ label={t(selected === item.id ? 'collapse label' : 'expand label', { ns: osTranslations })}
136
150
  icon={selected === item.id ? 'ph--caret-down--regular' : 'ph--caret-right--regular'}
137
151
  onClick={() => handleClick(item.id)}
138
152
  />
139
153
  </div>
140
154
  {selected === item.id && (
141
- <div className='mis-4 mbs-2 flex flex-col gap-1'>
155
+ <div className='pli-cardSpacingInline plb-cardSpacingBlock flex flex-col gap-1 density-fine'>
142
156
  {/* 16px to match drag handle width. */}
143
157
  <Input.Label classNames='text-xs'>{t('select option label')}</Input.Label>
144
- <div className='flex flex-row items-center gap-1'>
158
+ <div className='grid grid-cols-[1fr_min-content_min-content] gap-1'>
145
159
  <Input.TextInput
160
+ disabled={!!readonly}
146
161
  placeholder={t('select option label placeholder')}
147
162
  ref={selected === item.id ? inputRef : undefined}
148
163
  value={item.title}
149
164
  onChange={handleTitleChange(item.id)}
150
165
  onKeyDown={handleKeyDown}
151
166
  classNames='flex-1'
152
- data-no-submit
153
167
  />
154
- <Toolbar.Root classNames='p-0 m-0 !is-auto'>
155
- <HuePicker
156
- value={item.color}
157
- onChange={handleColorChange(item.id)}
158
- rootVariant='toolbar-button'
159
- />
160
- </Toolbar.Root>
168
+ <HuePicker disabled={!!readonly} value={item.color} onChange={handleColorChange(item.id)} />
161
169
  <IconButton
170
+ disabled={!!readonly}
162
171
  icon='ph--trash--fill'
163
172
  iconOnly
164
173
  label={t('select option delete')}
@@ -170,18 +179,14 @@ export const SelectOptionInput = ({ type, label, disabled, getStatus, getValue,
170
179
  </List.Item>
171
180
  </div>
172
181
  ))}
173
- <div role='listitem'>
174
- <div
175
- role='button'
176
- className={mx('p-1 plb-2', 'flex flex-col', 'cursor-pointer', 'hover:bg-hoverOverlay')}
177
- onClick={handleAdd}
178
- >
179
- <div className='flex items-center gap-1'>
180
- <Icon icon='ph--plus--regular' />
181
- <span className='text-sm'>{t('select option add')}</span>
182
- </div>
183
- </div>
184
- </div>
182
+ <IconButton
183
+ variant='ghost'
184
+ icon='ph--plus--regular'
185
+ label={t('select option add')}
186
+ onClick={handleAdd}
187
+ disabled={!!readonly}
188
+ classNames='is-full'
189
+ />
185
190
  </div>
186
191
  )}
187
192
  </List.Root>
@@ -0,0 +1,38 @@
1
+ //
2
+ // Copyright 2024 DXOS.org
3
+ //
4
+
5
+ import React, { useCallback } from 'react';
6
+
7
+ import { Input, type TextAreaProps } from '@dxos/react-ui';
8
+
9
+ import { type FormFieldComponentProps, FormFieldWrapper } from '../FormFieldComponent';
10
+
11
+ export const TextAreaField = ({
12
+ type,
13
+ readonly,
14
+ placeholder,
15
+ onValueChange,
16
+ onBlur,
17
+ ...props
18
+ }: FormFieldComponentProps<string>) => {
19
+ const handleChange = useCallback<NonNullable<TextAreaProps['onChange']>>(
20
+ (event) => onValueChange(type, event.target.value),
21
+ [type, onValueChange],
22
+ );
23
+
24
+ return (
25
+ <FormFieldWrapper<string> readonly={readonly} {...props}>
26
+ {({ value = '' }) => (
27
+ <Input.TextArea
28
+ rows={3}
29
+ disabled={!!readonly}
30
+ placeholder={placeholder}
31
+ value={value}
32
+ onChange={handleChange}
33
+ onBlur={onBlur}
34
+ />
35
+ )}
36
+ </FormFieldWrapper>
37
+ );
38
+ };
@@ -0,0 +1,38 @@
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
+
9
+ import { type FormFieldComponentProps, FormFieldWrapper } from '../FormFieldComponent';
10
+
11
+ export const TextField = ({
12
+ type,
13
+ readonly,
14
+ placeholder,
15
+ onValueChange,
16
+ onBlur,
17
+ ...props
18
+ }: FormFieldComponentProps<string>) => {
19
+ const handleChange = useCallback<NonNullable<TextInputProps['onChange']>>(
20
+ (event) => onValueChange(type, event.target.value),
21
+ [type, onValueChange],
22
+ );
23
+
24
+ return (
25
+ <FormFieldWrapper<string> readonly={readonly} {...props}>
26
+ {({ value = '' }) => (
27
+ <Input.TextInput
28
+ noAutoFill
29
+ disabled={!!readonly}
30
+ placeholder={placeholder}
31
+ value={value}
32
+ onChange={handleChange}
33
+ onBlur={onBlur}
34
+ />
35
+ )}
36
+ </FormFieldWrapper>
37
+ );
38
+ };
@@ -5,39 +5,46 @@
5
5
  import React from 'react';
6
6
 
7
7
  import { Input } from '@dxos/react-ui';
8
- import { mx } from '@dxos/react-ui-theme';
8
+ import { mx } from '@dxos/ui-theme';
9
9
  import { safeParseFloat } from '@dxos/util';
10
10
 
11
- import { InputHeader, type InputProps } from '../Input';
11
+ import { type FormFieldComponentProps, FormFieldLabel } from '../FormFieldComponent';
12
12
 
13
13
  const gridCols = ['grid-cols-1', 'grid-cols-2', 'grid-cols-3', 'grid-cols-4'];
14
14
 
15
- export const TupleInput = ({
15
+ export const TupleField = ({
16
16
  binding,
17
17
  type,
18
+ readonly,
18
19
  label,
19
- disabled,
20
20
  getStatus,
21
21
  getValue,
22
22
  onValueChange,
23
23
  onBlur,
24
- }: InputProps & { binding: string[] }) => {
24
+ }: FormFieldComponentProps<Record<string, number>> & {
25
+ binding: string[];
26
+ }) => {
25
27
  const { status, error } = getStatus();
26
28
 
27
29
  // TODO(burdon): Generalize number/float/string, etc.
28
- const values: Record<string, number> = getValue<Record<string, number>>() ?? {};
30
+ const values: Record<string, number> = getValue() ?? {};
29
31
 
30
32
  return (
31
33
  <Input.Root validationValence={status}>
32
- <InputHeader error={error} label={label} />
34
+ <FormFieldLabel error={error} readonly={readonly} label={label} />
33
35
  <div className={mx('grid gap-2', gridCols[binding.length - 1])}>
34
36
  {binding.map((prop) => (
35
37
  <Input.TextInput
36
38
  key={prop}
37
39
  type='number'
38
- disabled={disabled}
40
+ disabled={!!readonly}
39
41
  value={values[prop]}
40
- onChange={(event) => onValueChange(type, { ...values, [prop]: safeParseFloat(event.target.value, 0) })}
42
+ onChange={(event) => {
43
+ onValueChange(type, {
44
+ ...values,
45
+ [prop]: safeParseFloat(event.target.value, 0),
46
+ });
47
+ }}
41
48
  onBlur={onBlur}
42
49
  />
43
50
  ))}
@@ -0,0 +1,15 @@
1
+ //
2
+ // Copyright 2024 DXOS.org
3
+ //
4
+
5
+ export * from './ArrayField';
6
+ export * from './BooleanField';
7
+ export * from './GeoPointField';
8
+ export * from './MarkdownField';
9
+ export * from './NumberField';
10
+ export * from './RefField';
11
+ export * from './SelectField';
12
+ export * from './SelectOptionField';
13
+ export * from './TextField';
14
+ export * from './TextAreaField';
15
+ export * from './TupleField';
@@ -2,8 +2,7 @@
2
2
  // Copyright 2024 DXOS.org
3
3
  //
4
4
 
5
- export * from './custom';
6
- export * from './Defaults';
5
+ export * from './fields';
6
+
7
7
  export * from './Form';
8
- export * from './FormContext';
9
- export * from './Input';
8
+ export * from './FormFieldComponent';
@@ -0,0 +1,214 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ import { type Meta, type StoryObj } from '@storybook/react-vite';
6
+ import * as Schema from 'effect/Schema';
7
+ import React, { useCallback, useEffect, useMemo, useState } from 'react';
8
+ import { expect, fn, userEvent, within } from 'storybook/test';
9
+
10
+ import { Obj } from '@dxos/echo';
11
+ import { faker } from '@dxos/random';
12
+ import { Filter, useQuery } from '@dxos/react-client/echo';
13
+ import { useClientStory, withClientProvider } from '@dxos/react-client/testing';
14
+ import { Button } from '@dxos/react-ui';
15
+ import { withTheme } from '@dxos/react-ui/testing';
16
+ import { Person } from '@dxos/types';
17
+ import { osTranslations } from '@dxos/ui-theme';
18
+
19
+ import { translations } from '../../translations';
20
+
21
+ import { ObjectPicker } from './ObjectPicker';
22
+
23
+ faker.seed(1);
24
+
25
+ const createPerson = () =>
26
+ Obj.make(Person.Person, {
27
+ fullName: faker.person.fullName(),
28
+ });
29
+
30
+ const omitId = Schema.omit<any, any, ['id']>('id');
31
+ const personSchema = omitId(Person.Person);
32
+
33
+ // Mock functions for testing
34
+ const mockHandleSelect = fn();
35
+ const mockHandleCreate = fn();
36
+
37
+ const DefaultStory = () => {
38
+ const { space } = useClientStory();
39
+ const [isOpen, setIsOpen] = useState(false);
40
+
41
+ // Create sample Person objects
42
+ useEffect(() => {
43
+ if (space) {
44
+ // Create some sample Person objects for testing
45
+ Array.from({ length: 10 }).forEach(() => {
46
+ space.db.add(createPerson());
47
+ });
48
+ }
49
+ }, [space]);
50
+
51
+ // Get all objects in the space (similar to BoardContainer)
52
+ const allObjects = useQuery(space?.db, Filter.everything());
53
+
54
+ // Map objects to options format expected by ObjectPicker
55
+ const options = useMemo(
56
+ () =>
57
+ allObjects.map((obj) => ({
58
+ id: obj.id,
59
+ label: obj.fullName || obj.name || obj.title || obj.id,
60
+ hue: 'neutral' as const,
61
+ })),
62
+ [allObjects],
63
+ );
64
+
65
+ const handleCreateCallback = useCallback(
66
+ (values: any) => {
67
+ console.log('[on create]', values);
68
+ if (!space) return;
69
+ const newPerson = space.db.add(Obj.make(Person.Person, values));
70
+ mockHandleCreate(values);
71
+ return newPerson;
72
+ },
73
+ [space],
74
+ );
75
+
76
+ return (
77
+ <div role='none' className='flex is-96'>
78
+ <ObjectPicker.Root open={isOpen} onOpenChange={setIsOpen}>
79
+ <ObjectPicker.Trigger asChild>
80
+ <Button variant='primary' data-testid='trigger' classNames='is-full'>
81
+ Select Person
82
+ </Button>
83
+ </ObjectPicker.Trigger>
84
+ <ObjectPicker.Content
85
+ classNames='popover-card-width'
86
+ options={options}
87
+ createSchema={personSchema}
88
+ createOptionLabel={['create new person label', { ns: osTranslations }]}
89
+ createOptionIcon='ph--user-plus--regular'
90
+ createInitialValuePath='fullName'
91
+ onCreate={handleCreateCallback}
92
+ onSelect={mockHandleSelect}
93
+ />
94
+ </ObjectPicker.Root>
95
+ </div>
96
+ );
97
+ };
98
+
99
+ const meta = {
100
+ title: 'ui/react-ui-form/ObjectPicker',
101
+ render: DefaultStory,
102
+ decorators: [
103
+ withTheme,
104
+ withClientProvider({
105
+ types: [Person.Person],
106
+ createIdentity: true,
107
+ createSpace: true,
108
+ }),
109
+ ],
110
+ parameters: {
111
+ layout: 'centered',
112
+ controls: { disable: true },
113
+ translations,
114
+ },
115
+ } satisfies Meta<typeof DefaultStory>;
116
+
117
+ export default meta;
118
+
119
+ type Story = StoryObj<typeof meta>;
120
+
121
+ export const Default: Story = {};
122
+
123
+ export const WithTest: Story = {
124
+ play: async ({ canvasElement }) => {
125
+ const canvas = within(canvasElement);
126
+ const body = within(document.body);
127
+
128
+ // Find and click the trigger button.
129
+ const triggerButton = await canvas.findByTestId('trigger', undefined, { timeout: 5000 });
130
+ await expect(triggerButton).toBeVisible();
131
+ await userEvent.click(triggerButton);
132
+
133
+ // Check that the listbox renders when the trigger is clicked.
134
+ const listbox = await body.findByRole('listbox', undefined, { timeout: 5000 });
135
+ await expect(listbox).toBeVisible();
136
+
137
+ // Find the search input.
138
+ const searchInput = await body.findByPlaceholderText('Search…');
139
+ await expect(searchInput).toBeVisible();
140
+
141
+ // Test searching filters the results - get initial options count.
142
+ const initialOptions = await body.findAllByRole('option');
143
+ const initialCount = initialOptions.length;
144
+ await expect(initialCount).toBeGreaterThan(0);
145
+
146
+ // Type a search term that should filter results.
147
+ await userEvent.type(searchInput, 'John');
148
+
149
+ // Wait a moment for filtering.
150
+ await new Promise((resolve) => setTimeout(resolve, 100));
151
+
152
+ // Check that results are filtered (should be fewer options)
153
+ const _filteredOptions = await body.findAllByRole('option');
154
+ // Note: We expect either fewer results or the same if no John exists
155
+
156
+ // Clear search and test selecting a result
157
+ await userEvent.clear(searchInput);
158
+
159
+ // Get the first option and click it
160
+ const updatedOptions = await body.findAllByRole('option');
161
+ const firstOption = updatedOptions[0];
162
+ await expect(firstOption).toBeVisible();
163
+
164
+ const _firstOptionText = firstOption.textContent;
165
+ await userEvent.click(firstOption);
166
+
167
+ // Check that clicking a result fires onSelect with that id
168
+ // The onSelect should have been called with the selected option's id
169
+ await expect(mockHandleSelect).toHaveBeenCalled();
170
+
171
+ // Re-open the picker to test create functionality.
172
+ await userEvent.click(triggerButton);
173
+
174
+ // Wait for listbox to appear again.
175
+ const newListbox = await body.findByRole('listbox', undefined, { timeout: 5000 });
176
+ await expect(newListbox).toBeVisible();
177
+
178
+ const newSearchInput = await body.findByPlaceholderText('Search…');
179
+
180
+ // Test searching an unrelated term provides the __create__ option.
181
+ const unrelatedTerm = 'ZxyUnrelatedName123';
182
+ await userEvent.type(newSearchInput, unrelatedTerm);
183
+
184
+ // Wait for the create option to appear.
185
+ await new Promise((resolve) => setTimeout(resolve, 200));
186
+
187
+ // Look for the create option.
188
+ const createOption = await body.findByRole('option', undefined, { timeout: 3000 });
189
+ await expect(createOption).toBeVisible();
190
+
191
+ // Click the create option - this should change state to the form field.
192
+ await userEvent.click(createOption);
193
+
194
+ // Check that clicking that option changes state to the form field.
195
+ const form = await body.findByTestId('create-referenced-object-form', undefined, { timeout: 5000 });
196
+ await expect(form).toBeVisible();
197
+
198
+ // Find and click the save button.
199
+ const saveButton = await within(form).findByTestId('save-button');
200
+ await expect(saveButton).toBeVisible();
201
+ await userEvent.click(saveButton);
202
+
203
+ // Check that clicking the save button calls onCreate with expected values.
204
+ await expect(mockHandleCreate).toHaveBeenCalledWith({
205
+ fullName: unrelatedTerm,
206
+ addresses: [],
207
+ phoneNumbers: [],
208
+ emails: [],
209
+ identities: [],
210
+ urls: [],
211
+ fields: [],
212
+ });
213
+ },
214
+ };