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

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 (99) hide show
  1. package/README.md +0 -1
  2. package/dist/lib/browser/index.mjs +620 -583
  3. package/dist/lib/browser/index.mjs.map +4 -4
  4. package/dist/lib/browser/meta.json +1 -1
  5. package/dist/lib/node-esm/index.mjs +620 -583
  6. package/dist/lib/node-esm/index.mjs.map +4 -4
  7. package/dist/lib/node-esm/meta.json +1 -1
  8. package/dist/types/src/components/FieldEditor/FieldEditor.d.ts +3 -3
  9. package/dist/types/src/components/FieldEditor/FieldEditor.d.ts.map +1 -1
  10. package/dist/types/src/components/FieldEditor/FieldEditor.stories.d.ts +61 -61
  11. package/dist/types/src/components/FieldEditor/FieldEditor.stories.d.ts.map +1 -1
  12. package/dist/types/src/components/Form/Form.d.ts +18 -15
  13. package/dist/types/src/components/Form/Form.d.ts.map +1 -1
  14. package/dist/types/src/components/Form/Form.stories.d.ts +67 -67
  15. package/dist/types/src/components/Form/Form.stories.d.ts.map +1 -1
  16. package/dist/types/src/components/Form/FormField.d.ts +7 -1
  17. package/dist/types/src/components/Form/FormField.d.ts.map +1 -1
  18. package/dist/types/src/components/Form/FormFieldComponent.d.ts +0 -1
  19. package/dist/types/src/components/Form/FormFieldComponent.d.ts.map +1 -1
  20. package/dist/types/src/components/Form/FormFieldComponent.stories.d.ts +2 -2
  21. package/dist/types/src/components/Form/FormFieldComponent.stories.d.ts.map +1 -1
  22. package/dist/types/src/components/Form/FormFieldSet.d.ts +9 -10
  23. package/dist/types/src/components/Form/FormFieldSet.d.ts.map +1 -1
  24. package/dist/types/src/components/Form/fields/ArrayField.d.ts.map +1 -1
  25. package/dist/types/src/components/Form/fields/RefField.d.ts +7 -5
  26. package/dist/types/src/components/Form/fields/RefField.d.ts.map +1 -1
  27. package/dist/types/src/components/Form/fields/SelectOptionField.d.ts.map +1 -1
  28. package/dist/types/src/components/Form/fields/SelectOptionField.stories.d.ts +61 -61
  29. package/dist/types/src/components/ObjectPicker/ObjectPicker.d.ts +10 -2
  30. package/dist/types/src/components/ObjectPicker/ObjectPicker.d.ts.map +1 -1
  31. package/dist/types/src/components/ObjectPicker/ObjectPicker.stories.d.ts +61 -61
  32. package/dist/types/src/components/ObjectPicker/ObjectPicker.stories.d.ts.map +1 -1
  33. package/dist/types/src/components/Settings/Settings.d.ts +39 -0
  34. package/dist/types/src/components/Settings/Settings.d.ts.map +1 -0
  35. package/dist/types/src/components/Settings/Settings.stories.d.ts +81 -0
  36. package/dist/types/src/components/Settings/Settings.stories.d.ts.map +1 -0
  37. package/dist/types/src/components/Settings/index.d.ts +2 -0
  38. package/dist/types/src/components/Settings/index.d.ts.map +1 -0
  39. package/dist/types/src/components/ViewEditor/ViewEditor.d.ts +5 -5
  40. package/dist/types/src/components/ViewEditor/ViewEditor.d.ts.map +1 -1
  41. package/dist/types/src/components/ViewEditor/ViewEditor.stories.d.ts +65 -64
  42. package/dist/types/src/components/ViewEditor/ViewEditor.stories.d.ts.map +1 -1
  43. package/dist/types/src/components/index.d.ts +1 -1
  44. package/dist/types/src/components/index.d.ts.map +1 -1
  45. package/dist/types/src/components/testing.d.ts.map +1 -1
  46. package/dist/types/src/hooks/useFormHandler.d.ts.map +1 -1
  47. package/dist/types/src/translations.d.ts +61 -61
  48. package/dist/types/src/translations.d.ts.map +1 -1
  49. package/dist/types/src/util/index.d.ts +0 -1
  50. package/dist/types/src/util/index.d.ts.map +1 -1
  51. package/dist/types/src/util/refs.d.ts +1 -0
  52. package/dist/types/src/util/refs.d.ts.map +1 -1
  53. package/dist/types/tsconfig.tsbuildinfo +1 -1
  54. package/package.json +36 -38
  55. package/src/components/FieldEditor/FieldEditor.stories.tsx +4 -6
  56. package/src/components/FieldEditor/FieldEditor.tsx +3 -4
  57. package/src/components/Form/AUDIT.md +17 -0
  58. package/src/components/Form/Form.stories.tsx +23 -24
  59. package/src/components/Form/Form.tsx +58 -65
  60. package/src/components/Form/FormField.tsx +27 -8
  61. package/src/components/Form/FormFieldComponent.stories.tsx +5 -5
  62. package/src/components/Form/FormFieldComponent.tsx +17 -20
  63. package/src/components/Form/FormFieldSet.tsx +108 -104
  64. package/src/components/Form/fields/ArrayField.tsx +11 -8
  65. package/src/components/Form/fields/GeoPointField.tsx +6 -6
  66. package/src/components/Form/fields/MarkdownField.tsx +1 -1
  67. package/src/components/Form/fields/RefField.tsx +35 -17
  68. package/src/components/Form/fields/SelectField.tsx +1 -1
  69. package/src/components/Form/fields/SelectOptionField.stories.tsx +2 -2
  70. package/src/components/Form/fields/SelectOptionField.tsx +9 -10
  71. package/src/components/Form/fields/TupleField.tsx +1 -1
  72. package/src/components/ObjectPicker/ObjectPicker.stories.tsx +10 -14
  73. package/src/components/ObjectPicker/ObjectPicker.tsx +71 -60
  74. package/src/components/Settings/Settings.stories.tsx +56 -0
  75. package/src/components/Settings/Settings.tsx +129 -0
  76. package/src/components/{ControlSection → Settings}/index.ts +1 -1
  77. package/src/components/ViewEditor/ViewEditor.stories.tsx +22 -20
  78. package/src/components/ViewEditor/ViewEditor.test.tsx +3 -6
  79. package/src/components/ViewEditor/ViewEditor.tsx +99 -70
  80. package/src/components/index.ts +1 -1
  81. package/src/components/testing.tsx +20 -8
  82. package/src/hooks/useFormHandler.ts +42 -24
  83. package/src/translations.ts +62 -63
  84. package/src/util/index.ts +0 -1
  85. package/src/util/refs.ts +7 -3
  86. package/dist/types/src/components/ControlSection/ControlSection.d.ts +0 -23
  87. package/dist/types/src/components/ControlSection/ControlSection.d.ts.map +0 -1
  88. package/dist/types/src/components/ControlSection/ControlSection.stories.d.ts +0 -87
  89. package/dist/types/src/components/ControlSection/ControlSection.stories.d.ts.map +0 -1
  90. package/dist/types/src/components/ControlSection/index.d.ts +0 -2
  91. package/dist/types/src/components/ControlSection/index.d.ts.map +0 -1
  92. package/dist/types/src/components/Form/Form-old.stories.d.ts +0 -131
  93. package/dist/types/src/components/Form/Form-old.stories.d.ts.map +0 -1
  94. package/dist/types/src/util/echo.d.ts +0 -6
  95. package/dist/types/src/util/echo.d.ts.map +0 -1
  96. package/src/components/ControlSection/ControlSection.stories.tsx +0 -38
  97. package/src/components/ControlSection/ControlSection.tsx +0 -153
  98. package/src/components/Form/Form-old.stories.tsx +0 -314
  99. package/src/util/echo.ts +0 -25
@@ -6,14 +6,15 @@ import '@dxos/lit-ui/dx-tag-picker.pcss';
6
6
 
7
7
  import React, { useCallback, useMemo } from 'react';
8
8
 
9
- import { type Database, Entity, Filter, Ref, type Type } from '@dxos/echo';
9
+ import { type Database, Entity, Filter, Obj, Ref, Type } from '@dxos/echo';
10
10
  import { ReferenceAnnotationId, type ReferenceAnnotationValue } from '@dxos/echo/internal';
11
11
  import { useQuery, useSchema as useSchema$ } from '@dxos/echo-react';
12
12
  import { findAnnotation } from '@dxos/effect';
13
13
  import { DXN } from '@dxos/keys';
14
14
  import { DxAnchor } from '@dxos/lit-ui/react';
15
15
  import { Button, Icon, Input, useTranslation } from '@dxos/react-ui';
16
- import { descriptionText, mx } from '@dxos/ui-theme';
16
+ import { ParentLabelAnnotationId } from '@dxos/schema';
17
+ import { mx } from '@dxos/ui-theme';
17
18
 
18
19
  import { translationKey } from '../../../translations';
19
20
  import { ObjectPicker, type ObjectPickerContentProps, type RefOption } from '../../ObjectPicker';
@@ -25,23 +26,35 @@ const isRefSnapshot = (val: any): val is { '/': string } => {
25
26
  return typeof val === 'object' && typeof (val as any)?.['/'] === 'string';
26
27
  };
27
28
 
28
- const defaultGetOptions: NonNullable<RefFieldProps['getOptions']> = (results) =>
29
+ const defaultGetOptions: NonNullable<RefFieldProps['getOptions']> = (results, { parentLabel } = {}) =>
29
30
  results.map((result) => {
30
31
  const id = Entity.getDXN(result).toString();
31
- const label = Entity.getLabel(result);
32
+ const parent = parentLabel ? Obj.getParent(result as Obj.Unknown) : undefined;
33
+ const label = parent ? Entity.getLabel(parent) : Entity.getLabel(result);
32
34
  return { id, label: label ?? id };
33
35
  });
34
36
 
35
37
  const defaultResultsHook: NonNullable<RefFieldProps['resultsHook']> = (db, typename) =>
36
- useQuery(db, typename ? Filter.typename(typename) : Filter.nothing());
38
+ useQuery(
39
+ db,
40
+ typename
41
+ ? // For Ref.Ref(Obj.Unknown) we want to show all objects.
42
+ typename === Type.getTypename(Obj.Unknown)
43
+ ? Filter.everything()
44
+ : Filter.typename(typename)
45
+ : Filter.nothing(),
46
+ );
37
47
 
38
48
  export type RefFieldProps = FormFieldComponentProps &
39
- Pick<ObjectPickerContentProps, 'createOptionLabel' | 'createOptionIcon' | 'createInitialValuePath'> & {
49
+ Pick<
50
+ ObjectPickerContentProps,
51
+ 'createOptionLabel' | 'createOptionIcon' | 'createInitialValuePath' | 'createFieldMap'
52
+ > & {
40
53
  db?: Database.Database;
41
54
  resultsHook?: (db?: Database.Database, typename?: string) => Entity.Any[];
42
- schemaHook?: (db?: Database.Database, typename?: string) => Type.Entity.Any;
43
- getOptions?: (objects: Entity.Any[]) => RefOption[];
44
- onCreate?: (schema: Type.Entity.Any, values: any) => void;
55
+ schemaHook?: (db?: Database.Database, typename?: string) => Type.AnyEntity;
56
+ getOptions?: (objects: Entity.Any[], options?: { parentLabel?: boolean }) => RefOption[];
57
+ onCreate?: (schema: Type.AnyEntity, values: any) => void;
45
58
  };
46
59
 
47
60
  export const RefField = (props: RefFieldProps) => {
@@ -56,6 +69,7 @@ export const RefField = (props: RefFieldProps) => {
56
69
  createOptionLabel,
57
70
  createOptionIcon,
58
71
  createInitialValuePath,
72
+ createFieldMap,
59
73
  db,
60
74
  resultsHook: useResults = defaultResultsHook,
61
75
  schemaHook: useSchema = useSchema$,
@@ -72,7 +86,10 @@ export const RefField = (props: RefFieldProps) => {
72
86
  );
73
87
 
74
88
  const results = useResults(db, typename);
75
- const options = useMemo(() => getOptions(results), [results, getOptions]);
89
+ const options = useMemo(() => {
90
+ const parentLabel = type ? findAnnotation<boolean>(type, ParentLabelAnnotationId) === true : false;
91
+ return getOptions(results, { parentLabel });
92
+ }, [results, getOptions, type]);
76
93
 
77
94
  const handleGetValue = useCallback(() => {
78
95
  const formValue = getValue();
@@ -135,9 +152,9 @@ export const RefField = (props: RefFieldProps) => {
135
152
  <div>
136
153
  {readonly ? (
137
154
  !item ? (
138
- <p className={mx(descriptionText, 'mbe-2')}>{t('empty readonly ref field label')}</p>
155
+ <p className={mx('text-description', 'mb-2')}>{t('empty-readonly-ref-field.label')}</p>
139
156
  ) : (
140
- <DxAnchor key={item.id} refid={item.id} rootclassname='mie-1'>
157
+ <DxAnchor key={item.id} dxn={item.id} rootclassname='me-1'>
141
158
  {item.label}
142
159
  </DxAnchor>
143
160
  )
@@ -145,15 +162,15 @@ export const RefField = (props: RefFieldProps) => {
145
162
  <ObjectPicker.Root>
146
163
  <ObjectPicker.Trigger asChild classNames='p-0'>
147
164
  {item ? (
148
- <div className='flex gap-2 is-full'>
165
+ <div className='flex gap-form-gap w-full'>
149
166
  <Input.Root key={item.id}>
150
- <Input.TextInput value={item.label} readOnly classNames='is-full' />
167
+ <Input.TextInput value={item.label} readOnly classNames='w-full' />
151
168
  </Input.Root>
152
169
  </div>
153
170
  ) : (
154
- <Button classNames='is-full text-start gap-2'>
171
+ <Button classNames='w-full text-start gap-form-gap'>
155
172
  <div role='none' className='grow overflow-hidden'>
156
- <span className='flex truncate text-description'>{placeholder ?? t('ref field placeholder')}</span>
173
+ <span className='flex truncate text-description'>{placeholder ?? t('ref-field.placeholder')}</span>
157
174
  </div>
158
175
  <Icon size={3} icon='ph--caret-down--bold' />
159
176
  </Button>
@@ -161,13 +178,14 @@ export const RefField = (props: RefFieldProps) => {
161
178
  </ObjectPicker.Trigger>
162
179
  <ObjectPicker.Portal>
163
180
  <ObjectPicker.Content
164
- classNames='popover-card-width'
181
+ classNames='dx-card-popover-width'
165
182
  options={options}
166
183
  selectedIds={selectedIds}
167
184
  createSchema={createSchema && omitId(createSchema)}
168
185
  createOptionLabel={createOptionLabel}
169
186
  createOptionIcon={createOptionIcon}
170
187
  createInitialValuePath={createInitialValuePath}
188
+ createFieldMap={createFieldMap}
171
189
  onCreate={handleCreate}
172
190
  onSelect={handleSelect}
173
191
  />
@@ -43,7 +43,7 @@ export const SelectField = ({
43
43
  ) : (
44
44
  <Select.Root value={value} onValueChange={handleValueChange}>
45
45
  {/* TODO(burdon): Placeholder not working? */}
46
- <Select.TriggerButton classNames='is-full' disabled={!!readonly} placeholder={placeholder} />
46
+ <Select.TriggerButton classNames='w-full' disabled={!!readonly} placeholder={placeholder} />
47
47
  <Select.Portal>
48
48
  <Select.Content>
49
49
  <Select.Viewport>
@@ -7,7 +7,7 @@ import * as Schema from 'effect/Schema';
7
7
  import React, { useState } from 'react';
8
8
 
9
9
  import { SelectOption } from '@dxos/echo/internal';
10
- import { withTheme } from '@dxos/react-ui/testing';
10
+ import { withLayout, withTheme } from '@dxos/react-ui/testing';
11
11
 
12
12
  import { translations } from '../../../translations';
13
13
  import { TestLayout } from '../../testing';
@@ -50,7 +50,7 @@ const meta = {
50
50
  title: 'ui/react-ui-form/SelectOptionField',
51
51
  component: SelectOptionField as any,
52
52
  render: DefaultStory,
53
- decorators: [withTheme],
53
+ decorators: [withTheme(), withLayout({ layout: 'fullscreen' })],
54
54
  parameters: {
55
55
  layout: 'fullscreen',
56
56
  translations,
@@ -128,13 +128,13 @@ export const SelectOptionField = ({
128
128
  readonly={!!readonly}
129
129
  >
130
130
  {({ items }) => (
131
- <div role='list' className='is-full overflow-auto'>
131
+ <div role='list' className='w-full overflow-auto'>
132
132
  {items?.map((item) => (
133
133
  <div key={item.id}>
134
134
  <List.Item
135
135
  role='button'
136
136
  item={item}
137
- classNames={[subtleHover, 'flex flex-col cursor-pointer rounded-sm']}
137
+ classNames={[subtleHover, 'flex flex-col cursor-pointer rounded-xs']}
138
138
  aria-expanded={selected === item.id}
139
139
  >
140
140
  <div className='flex items-center'>
@@ -152,13 +152,12 @@ export const SelectOptionField = ({
152
152
  />
153
153
  </div>
154
154
  {selected === item.id && (
155
- <div className='pli-cardSpacingInline plb-cardSpacingBlock flex flex-col gap-1 density-fine'>
156
- {/* 16px to match drag handle width. */}
157
- <Input.Label classNames='text-xs'>{t('select option label')}</Input.Label>
158
- <div className='grid grid-cols-[1fr_min-content_min-content] gap-1'>
155
+ <div className='flex flex-col p-form-padding gap-form-gap dx-density-fine'>
156
+ <Input.Label classNames='text-xs'>{t('select-option.label')}</Input.Label>
157
+ <div className='grid grid-cols-[1fr_min-content_min-content] gap-form-gap'>
159
158
  <Input.TextInput
160
159
  disabled={!!readonly}
161
- placeholder={t('select option label placeholder')}
160
+ placeholder={t('select-option-label.placeholder')}
162
161
  ref={selected === item.id ? inputRef : undefined}
163
162
  value={item.title}
164
163
  onChange={handleTitleChange(item.id)}
@@ -170,7 +169,7 @@ export const SelectOptionField = ({
170
169
  disabled={!!readonly}
171
170
  icon='ph--trash--fill'
172
171
  iconOnly
173
- label={t('select option delete')}
172
+ label={t('select-option-delete.button')}
174
173
  onClick={() => handleDelete(item.id)}
175
174
  />
176
175
  </div>
@@ -182,10 +181,10 @@ export const SelectOptionField = ({
182
181
  <IconButton
183
182
  variant='ghost'
184
183
  icon='ph--plus--regular'
185
- label={t('select option add')}
184
+ label={t('select-option-add.button')}
186
185
  onClick={handleAdd}
187
186
  disabled={!!readonly}
188
- classNames='is-full'
187
+ classNames='w-full'
189
188
  />
190
189
  </div>
191
190
  )}
@@ -32,7 +32,7 @@ export const TupleField = ({
32
32
  return (
33
33
  <Input.Root validationValence={status}>
34
34
  <FormFieldLabel error={error} readonly={readonly} label={label} />
35
- <div className={mx('grid gap-2', gridCols[binding.length - 1])}>
35
+ <div className={mx('grid gap-form-gap', gridCols[binding.length - 1])}>
36
36
  {binding.map((prop) => (
37
37
  <Input.TextInput
38
38
  key={prop}
@@ -74,18 +74,18 @@ const DefaultStory = () => {
74
74
  );
75
75
 
76
76
  return (
77
- <div role='none' className='flex is-96'>
77
+ <div role='none' className='w-96'>
78
78
  <ObjectPicker.Root open={isOpen} onOpenChange={setIsOpen}>
79
79
  <ObjectPicker.Trigger asChild>
80
- <Button variant='primary' data-testid='trigger' classNames='is-full'>
80
+ <Button variant='primary' data-testid='trigger' classNames='w-full'>
81
81
  Select Person
82
82
  </Button>
83
83
  </ObjectPicker.Trigger>
84
84
  <ObjectPicker.Content
85
- classNames='popover-card-width'
85
+ classNames='dx-card-popover-width'
86
86
  options={options}
87
87
  createSchema={personSchema}
88
- createOptionLabel={['create new person label', { ns: osTranslations }]}
88
+ createOptionLabel={['create-new-person.label', { ns: osTranslations }]}
89
89
  createOptionIcon='ph--user-plus--regular'
90
90
  createInitialValuePath='fullName'
91
91
  onCreate={handleCreateCallback}
@@ -100,7 +100,7 @@ const meta = {
100
100
  title: 'ui/react-ui-form/ObjectPicker',
101
101
  render: DefaultStory,
102
102
  decorators: [
103
- withTheme,
103
+ withTheme(),
104
104
  withClientProvider({
105
105
  types: [Person.Person],
106
106
  createIdentity: true,
@@ -201,14 +201,10 @@ export const WithTest: Story = {
201
201
  await userEvent.click(saveButton);
202
202
 
203
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
- });
204
+ await expect(mockHandleCreate).toHaveBeenCalledWith(
205
+ expect.objectContaining({
206
+ fullName: unrelatedTerm,
207
+ }),
208
+ );
213
209
  },
214
210
  };
@@ -6,10 +6,11 @@ import type * as Schema from 'effect/Schema';
6
6
  import React, { type KeyboardEvent, forwardRef, useCallback, useState } from 'react';
7
7
 
8
8
  import { type Palette, Popover, type ThemedClassName, useTranslation } from '@dxos/react-ui';
9
- import { Combobox, useSearchListInput, useSearchListResults } from '@dxos/react-ui-searchlist';
9
+ import { Combobox, useSearchListInput, useSearchListResults } from '@dxos/react-ui-search';
10
10
 
11
11
  import { translationKey } from '../../translations';
12
12
  import { Form } from '../Form';
13
+ import { type FormFieldMap } from '../Form/FormFieldComponent';
13
14
 
14
15
  export type RefOption = {
15
16
  id: string;
@@ -24,36 +25,11 @@ export type ObjectPickerContentProps = ThemedClassName<{
24
25
  createOptionIcon?: string;
25
26
  createSchema?: Schema.Schema.AnyNoContext;
26
27
  createInitialValuePath?: string;
28
+ createFieldMap?: FormFieldMap;
27
29
  onCreate?: (values: any) => void;
28
30
  onSelect: (id: string) => void;
29
31
  }>;
30
32
 
31
- const CreateItem = ({
32
- createOptionLabel,
33
- createOptionIcon,
34
- onCreateItemSelect,
35
- }: {
36
- createOptionLabel: [string, { ns: string }];
37
- createOptionIcon: string;
38
- onCreateItemSelect: (query: string) => void;
39
- }) => {
40
- const { t } = useTranslation(translationKey);
41
- const { query } = useSearchListInput();
42
-
43
- return (
44
- <Combobox.Item
45
- value='__create__'
46
- label={t(createOptionLabel[0], { ns: createOptionLabel[1].ns, text: query })}
47
- icon={createOptionIcon}
48
- classNames='flex items-center gap-2'
49
- closeOnSelect={false}
50
- onSelect={() => {
51
- onCreateItemSelect(query);
52
- }}
53
- />
54
- );
55
- };
56
-
57
33
  const ObjectPickerContent = forwardRef<HTMLDivElement, ObjectPickerContentProps>(
58
34
  (
59
35
  {
@@ -63,11 +39,12 @@ const ObjectPickerContent = forwardRef<HTMLDivElement, ObjectPickerContentProps>
63
39
  createOptionLabel,
64
40
  createOptionIcon,
65
41
  createInitialValuePath,
42
+ createFieldMap,
66
43
  onSelect,
67
44
  onCreate,
68
45
  ...props
69
46
  },
70
- ref,
47
+ forwardedRef,
71
48
  ) => {
72
49
  const { t } = useTranslation(translationKey);
73
50
  const [showForm, setShowForm] = useState(false);
@@ -112,14 +89,15 @@ const ObjectPickerContent = forwardRef<HTMLDivElement, ObjectPickerContentProps>
112
89
  [createSchema],
113
90
  );
114
91
 
115
- return (
116
- <Combobox.Content {...props} ref={ref} onSearch={handleSearch} onKeyDownCapture={handleKeyDown}>
117
- {showForm && createSchema ? (
92
+ if (showForm && createSchema) {
93
+ return (
94
+ <Combobox.Content {...props} onSearch={handleSearch} onKeyDownCapture={handleKeyDown} ref={forwardedRef}>
118
95
  <Popover.Viewport>
119
96
  <Form.Root
120
97
  testId='create-referenced-object-form'
121
98
  schema={createSchema}
122
- values={createInitialValuePath ? { [createInitialValuePath]: formInitialValue } : {}}
99
+ defaultValues={createInitialValuePath ? { [createInitialValuePath]: formInitialValue } : {}}
100
+ fieldMap={createFieldMap}
123
101
  onSave={handleFormSave}
124
102
  onCancel={handleFormCancel}
125
103
  >
@@ -131,40 +109,73 @@ const ObjectPickerContent = forwardRef<HTMLDivElement, ObjectPickerContentProps>
131
109
  </Form.Viewport>
132
110
  </Form.Root>
133
111
  </Popover.Viewport>
134
- ) : (
135
- <>
136
- <Combobox.Input placeholder={t('ref field combobox input placeholder')} autoFocus />
137
- <Combobox.List>
138
- {results.map((option) => (
139
- <Combobox.Item
140
- key={option.id}
141
- value={option.id}
142
- label={option.label}
143
- checked={selectedIds?.includes(option.id)}
144
- onSelect={() => onSelect(option.id)}
145
- classNames='flex items-center gap-2'
146
- />
147
- ))}
148
- {createOptionLabel && createOptionIcon && createSchema && onCreate && (
149
- <CreateItem
150
- createOptionLabel={createOptionLabel}
151
- createOptionIcon={createOptionIcon}
152
- onCreateItemSelect={(query: string) => {
153
- setFormInitialValue(query);
154
- setShowForm(true);
155
- }}
156
- />
157
- )}
158
- </Combobox.List>
159
- </>
160
- )}
112
+ <Combobox.Arrow />
113
+ </Combobox.Content>
114
+ );
115
+ }
116
+
117
+ return (
118
+ <Combobox.Content {...props} onSearch={handleSearch} onKeyDownCapture={handleKeyDown} ref={forwardedRef}>
119
+ <Combobox.Input placeholder={t('ref-field-combobox-input.placeholder')} autoFocus />
120
+ <Combobox.List>
121
+ {results.map((option) => (
122
+ <Combobox.Item
123
+ key={option.id}
124
+ value={option.id}
125
+ label={option.label}
126
+ checked={selectedIds?.includes(option.id)}
127
+ onSelect={() => onSelect(option.id)}
128
+ classNames='flex items-center gap-2'
129
+ />
130
+ ))}
131
+ {createSchema && onCreate && (
132
+ <CreateItem
133
+ createOptionLabel={createOptionLabel}
134
+ createOptionIcon={createOptionIcon ?? 'ph--plus--regular'}
135
+ onCreateItemSelect={(query: string) => {
136
+ setFormInitialValue(query);
137
+ setShowForm(true);
138
+ }}
139
+ />
140
+ )}
141
+ </Combobox.List>
161
142
  <Combobox.Arrow />
162
143
  </Combobox.Content>
163
144
  );
164
145
  },
165
146
  );
166
147
 
167
- ObjectPickerContent.displayName = 'ObjectPickerContent';
148
+ const CreateItem = ({
149
+ createOptionLabel,
150
+ createOptionIcon,
151
+ onCreateItemSelect,
152
+ }: {
153
+ createOptionLabel?: [string, { ns: string }];
154
+ createOptionIcon: string;
155
+ onCreateItemSelect: (query: string) => void;
156
+ }) => {
157
+ const { t } = useTranslation(translationKey);
158
+ const { query } = useSearchListInput();
159
+
160
+ const label = createOptionLabel
161
+ ? t(createOptionLabel[0], { ns: createOptionLabel[1].ns, text: query })
162
+ : t('create-option.label');
163
+
164
+ return (
165
+ <Combobox.Item
166
+ value='__create__'
167
+ label={label}
168
+ icon={createOptionIcon}
169
+ classNames='flex items-center gap-2'
170
+ closeOnSelect={false}
171
+ onSelect={() => {
172
+ onCreateItemSelect(query);
173
+ }}
174
+ />
175
+ );
176
+ };
177
+
178
+ ObjectPickerContent.displayName = 'ObjectPicker.Content';
168
179
 
169
180
  export const ObjectPicker = {
170
181
  Root: Combobox.Root,
@@ -0,0 +1,56 @@
1
+ //
2
+ // Copyright 2024 DXOS.org
3
+ //
4
+
5
+ import { type Meta, type StoryObj } from '@storybook/react-vite';
6
+ import React from 'react';
7
+
8
+ import { faker } from '@dxos/random';
9
+ import { Button, Input } from '@dxos/react-ui';
10
+ import { withLayout, withTheme } from '@dxos/react-ui/testing';
11
+
12
+ import { translations } from '../../translations';
13
+
14
+ import { Settings } from './Settings';
15
+
16
+ faker.seed(132);
17
+
18
+ const DefaultStory = () => {
19
+ return (
20
+ <Settings.Viewport>
21
+ <Settings.Section title='Settings' description={faker.lorem.paragraphs(1)}>
22
+ <Settings.Item title={faker.lorem.sentence(2)} description={faker.lorem.paragraphs(1)}>
23
+ <Input.TextInput placeholder='Input' />
24
+ </Settings.Item>
25
+ <Settings.Item title={faker.lorem.sentence(2)} description={faker.lorem.paragraphs(2)}>
26
+ <Input.Switch />
27
+ </Settings.Item>
28
+ <Settings.Item title={faker.lorem.sentence(3)} description={faker.lorem.paragraphs(2)}>
29
+ <Button>Test</Button>
30
+ </Settings.Item>
31
+ </Settings.Section>
32
+ <Settings.Section title='Panel Example'>
33
+ <Settings.Panel>
34
+ <h3 className='text-lg mb-2'>Members</h3>
35
+ <p className='text-description'>Content inside a panel.</p>
36
+ </Settings.Panel>
37
+ </Settings.Section>
38
+ </Settings.Viewport>
39
+ );
40
+ };
41
+
42
+ const meta = {
43
+ title: 'ui/react-ui-form/Settings',
44
+ render: DefaultStory,
45
+ decorators: [withTheme(), withLayout({ layout: 'fullscreen' })],
46
+ parameters: {
47
+ layout: 'fullscreen',
48
+ translations,
49
+ },
50
+ } satisfies Meta;
51
+
52
+ export default meta;
53
+
54
+ type Story = StoryObj<typeof meta>;
55
+
56
+ export const Default: Story = {};
@@ -0,0 +1,129 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ import React, { type PropsWithChildren } from 'react';
6
+
7
+ import { Input, type Label, ScrollArea, type ThemedClassName, toLocalizedString, useTranslation } from '@dxos/react-ui';
8
+ import { composable, composableProps, mx } from '@dxos/ui-theme';
9
+
10
+ import { translationKey } from '../../translations';
11
+
12
+ const SETTINGS_ROOT_NAME = 'Settings.Root';
13
+ const SETTINGS_VIEWPORT_NAME = 'Settings.Viewport';
14
+ const SETTINGS_SECTION_NAME = 'Settings.Section';
15
+ const SETTINGS_PANEL_NAME = 'Settings.Panel';
16
+ const SETTINGS_ITEM_NAME = 'Settings.Item';
17
+
18
+ //
19
+ // Root (headless — no styling, no scroll).
20
+ //
21
+
22
+ type SettingsRootProps = ThemedClassName<PropsWithChildren>;
23
+
24
+ const SettingsRoot = ({ children, classNames }: SettingsRootProps) => {
25
+ return <div className={mx('flex flex-col gap-2', classNames)}>{children}</div>;
26
+ };
27
+
28
+ SettingsRoot.displayName = SETTINGS_ROOT_NAME;
29
+
30
+ //
31
+ // Viewport (scroll area + padding + document layout).
32
+ //
33
+
34
+ const SettingsViewport = composable<HTMLDivElement>(({ children, ...props }, forwardedRef) => {
35
+ return (
36
+ <ScrollArea.Root {...composableProps(props)} orientation='vertical' centered thin ref={forwardedRef}>
37
+ <ScrollArea.Viewport classNames='px-4 pointer-coarse:px-2'>
38
+ <div role='none' className='dx-document flex flex-col gap-2 py-4'>
39
+ {children}
40
+ </div>
41
+ </ScrollArea.Viewport>
42
+ </ScrollArea.Root>
43
+ );
44
+ });
45
+
46
+ SettingsViewport.displayName = SETTINGS_VIEWPORT_NAME;
47
+
48
+ //
49
+ // Section (title + optional description + children with spacing).
50
+ //
51
+
52
+ type SettingsSectionProps = PropsWithChildren<{
53
+ title: Label;
54
+ description?: Label;
55
+ }>;
56
+
57
+ const SettingsSection = ({ title, description, children }: SettingsSectionProps) => {
58
+ const { t } = useTranslation(translationKey);
59
+ return (
60
+ <>
61
+ <h2 className='px-trim-md mt-trim-md text-xl'>{toLocalizedString(title, t)}</h2>
62
+ {description && <p className='px-trim-md text-description'>{toLocalizedString(description, t)}</p>}
63
+ <div className='w-full pt-trim-md space-y-trim-md'>{children}</div>
64
+ </>
65
+ );
66
+ };
67
+
68
+ SettingsSection.displayName = SETTINGS_SECTION_NAME;
69
+
70
+ //
71
+ // Panel (bordered card container).
72
+ //
73
+
74
+ type SettingsPanelProps = ThemedClassName<PropsWithChildren>;
75
+
76
+ const SettingsPanel = ({ classNames, children }: SettingsPanelProps) => {
77
+ return (
78
+ <div role='none' className={mx('flex flex-col gap-3 p-trim-md border border-separator rounded-sm', classNames)}>
79
+ {children}
80
+ </div>
81
+ );
82
+ };
83
+
84
+ SettingsPanel.displayName = SETTINGS_PANEL_NAME;
85
+
86
+ //
87
+ // Item (label + optional description + control, proper label semantics).
88
+ //
89
+
90
+ type SettingsItemProps = PropsWithChildren<{
91
+ title: Label;
92
+ description?: Label;
93
+ }>;
94
+
95
+ const SettingsItem = ({ title, description = '', children }: SettingsItemProps) => {
96
+ const { t } = useTranslation(translationKey);
97
+
98
+ return (
99
+ <Input.Root>
100
+ <SettingsPanel classNames='grid grid-cols-1 md:grid-cols-[1fr_1fr] gap-x-trim-lg gap-y-0'>
101
+ <Input.Label classNames='pb-trim-md text-base-surface-text text-lg md:col-span-2'>
102
+ {toLocalizedString(title, t)}
103
+ </Input.Label>
104
+ <Input.DescriptionAndValidation>
105
+ <Input.Description classNames='text-base text-description'>
106
+ {toLocalizedString(description, t)}
107
+ </Input.Description>
108
+ </Input.DescriptionAndValidation>
109
+ <div role='none' className='text-end py-1'>
110
+ {children}
111
+ </div>
112
+ </SettingsPanel>
113
+ </Input.Root>
114
+ );
115
+ };
116
+
117
+ SettingsItem.displayName = SETTINGS_ITEM_NAME;
118
+
119
+ //
120
+ // Settings
121
+ //
122
+
123
+ export const Settings = {
124
+ Root: SettingsRoot,
125
+ Viewport: SettingsViewport,
126
+ Section: SettingsSection,
127
+ Panel: SettingsPanel,
128
+ Item: SettingsItem,
129
+ };
@@ -2,4 +2,4 @@
2
2
  // Copyright 2025 DXOS.org
3
3
  //
4
4
 
5
- export * from './ControlSection';
5
+ export * from './Settings';