@dxos/react-ui-form 0.8.4-main.c4373fc → 0.8.4-main.c85a9c8dae

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 (185) hide show
  1. package/README.md +0 -1
  2. package/dist/lib/browser/index.mjs +2289 -2269
  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 +2289 -2269
  6. package/dist/lib/node-esm/index.mjs.map +4 -4
  7. package/dist/lib/node-esm/meta.json +1 -1
  8. package/dist/types/src/components/FieldEditor/FieldEditor.d.ts +8 -7
  9. package/dist/types/src/components/FieldEditor/FieldEditor.d.ts.map +1 -1
  10. package/dist/types/src/components/FieldEditor/FieldEditor.stories.d.ts +13 -7
  11. package/dist/types/src/components/FieldEditor/FieldEditor.stories.d.ts.map +1 -1
  12. package/dist/types/src/components/Form/Form.d.ts +81 -26
  13. package/dist/types/src/components/Form/Form.d.ts.map +1 -1
  14. package/dist/types/src/components/Form/Form.stories.d.ts +59 -84
  15. package/dist/types/src/components/Form/Form.stories.d.ts.map +1 -1
  16. package/dist/types/src/components/Form/FormField.d.ts +37 -0
  17. package/dist/types/src/components/Form/FormField.d.ts.map +1 -0
  18. package/dist/types/src/components/Form/FormFieldComponent.d.ts +78 -0
  19. package/dist/types/src/components/Form/FormFieldComponent.d.ts.map +1 -0
  20. package/dist/types/src/components/Form/FormFieldComponent.stories.d.ts +9 -0
  21. package/dist/types/src/components/Form/FormFieldComponent.stories.d.ts.map +1 -0
  22. package/dist/types/src/components/Form/FormFieldSet.d.ts +19 -0
  23. package/dist/types/src/components/Form/FormFieldSet.d.ts.map +1 -0
  24. package/dist/types/src/components/Form/fields/ArrayField.d.ts +18 -0
  25. package/dist/types/src/components/Form/fields/ArrayField.d.ts.map +1 -0
  26. package/dist/types/src/components/Form/fields/BooleanField.d.ts +4 -0
  27. package/dist/types/src/components/Form/fields/BooleanField.d.ts.map +1 -0
  28. package/dist/types/src/components/Form/fields/GeoPointField.d.ts +5 -0
  29. package/dist/types/src/components/Form/fields/GeoPointField.d.ts.map +1 -0
  30. package/dist/types/src/components/Form/fields/MarkdownField.d.ts +4 -0
  31. package/dist/types/src/components/Form/fields/MarkdownField.d.ts.map +1 -0
  32. package/dist/types/src/components/Form/fields/NumberField.d.ts +4 -0
  33. package/dist/types/src/components/Form/fields/NumberField.d.ts.map +1 -0
  34. package/dist/types/src/components/Form/fields/RefField.d.ts +14 -0
  35. package/dist/types/src/components/Form/fields/RefField.d.ts.map +1 -0
  36. package/dist/types/src/components/Form/fields/SelectField.d.ts +10 -0
  37. package/dist/types/src/components/Form/fields/SelectField.d.ts.map +1 -0
  38. package/dist/types/src/components/Form/fields/SelectOptionField.d.ts +5 -0
  39. package/dist/types/src/components/Form/fields/SelectOptionField.d.ts.map +1 -0
  40. package/dist/types/src/components/{ControlSection/ControlSection.stories.d.ts → Form/fields/SelectOptionField.stories.d.ts} +12 -14
  41. package/dist/types/src/components/Form/fields/SelectOptionField.stories.d.ts.map +1 -0
  42. package/dist/types/src/components/Form/fields/TextAreaField.d.ts +4 -0
  43. package/dist/types/src/components/Form/fields/TextAreaField.d.ts.map +1 -0
  44. package/dist/types/src/components/Form/fields/TextField.d.ts +4 -0
  45. package/dist/types/src/components/Form/fields/TextField.d.ts.map +1 -0
  46. package/dist/types/src/components/Form/fields/TupleField.d.ts +6 -0
  47. package/dist/types/src/components/Form/fields/TupleField.d.ts.map +1 -0
  48. package/dist/types/src/components/Form/fields/index.d.ts +12 -0
  49. package/dist/types/src/components/Form/fields/index.d.ts.map +1 -0
  50. package/dist/types/src/components/Form/index.d.ts +2 -4
  51. package/dist/types/src/components/Form/index.d.ts.map +1 -1
  52. package/dist/types/src/components/ObjectPicker/ObjectPicker.d.ts +15 -10
  53. package/dist/types/src/components/ObjectPicker/ObjectPicker.d.ts.map +1 -1
  54. package/dist/types/src/components/ObjectPicker/ObjectPicker.stories.d.ts +10 -6
  55. package/dist/types/src/components/ObjectPicker/ObjectPicker.stories.d.ts.map +1 -1
  56. package/dist/types/src/components/Settings/Settings.d.ts +56 -0
  57. package/dist/types/src/components/Settings/Settings.d.ts.map +1 -0
  58. package/dist/types/src/components/{Form/custom/SelectOptionsInput.stories.d.ts → Settings/Settings.stories.d.ts} +10 -8
  59. package/dist/types/src/components/Settings/Settings.stories.d.ts.map +1 -0
  60. package/dist/types/src/components/Settings/index.d.ts +2 -0
  61. package/dist/types/src/components/Settings/index.d.ts.map +1 -0
  62. package/dist/types/src/components/ViewEditor/ViewEditor.d.ts +16 -16
  63. package/dist/types/src/components/ViewEditor/ViewEditor.d.ts.map +1 -1
  64. package/dist/types/src/components/ViewEditor/ViewEditor.stories.d.ts +13 -9
  65. package/dist/types/src/components/ViewEditor/ViewEditor.stories.d.ts.map +1 -1
  66. package/dist/types/src/components/index.d.ts +1 -1
  67. package/dist/types/src/components/index.d.ts.map +1 -1
  68. package/dist/types/src/components/testing.d.ts +0 -2
  69. package/dist/types/src/components/testing.d.ts.map +1 -1
  70. package/dist/types/src/hooks/index.d.ts +2 -3
  71. package/dist/types/src/hooks/index.d.ts.map +1 -1
  72. package/dist/types/src/hooks/useFormHandler.d.ts +82 -0
  73. package/dist/types/src/hooks/useFormHandler.d.ts.map +1 -0
  74. package/dist/types/src/hooks/useKeyHandler.d.ts +6 -0
  75. package/dist/types/src/hooks/useKeyHandler.d.ts.map +1 -0
  76. package/dist/types/src/translations.d.ts +10 -7
  77. package/dist/types/src/translations.d.ts.map +1 -1
  78. package/dist/types/src/util/field.d.ts +3 -3
  79. package/dist/types/src/util/field.d.ts.map +1 -1
  80. package/dist/types/src/util/formatting.d.ts +2 -2
  81. package/dist/types/src/util/formatting.d.ts.map +1 -1
  82. package/dist/types/src/util/index.d.ts +1 -1
  83. package/dist/types/src/util/index.d.ts.map +1 -1
  84. package/dist/types/src/util/properties.d.ts +7 -0
  85. package/dist/types/src/util/properties.d.ts.map +1 -0
  86. package/dist/types/src/util/refs.d.ts +1 -2
  87. package/dist/types/src/util/refs.d.ts.map +1 -1
  88. package/dist/types/tsconfig.tsbuildinfo +1 -1
  89. package/package.json +50 -46
  90. package/src/components/FieldEditor/FieldEditor.stories.tsx +43 -24
  91. package/src/components/FieldEditor/FieldEditor.tsx +74 -78
  92. package/src/components/Form/Form.stories.tsx +149 -248
  93. package/src/components/Form/Form.tsx +347 -107
  94. package/src/components/Form/FormField.tsx +294 -0
  95. package/src/components/Form/FormFieldComponent.stories.tsx +65 -0
  96. package/src/components/Form/FormFieldComponent.tsx +172 -0
  97. package/src/components/Form/FormFieldSet.tsx +123 -0
  98. package/src/components/Form/README.md +3 -0
  99. package/src/components/Form/fields/ArrayField.tsx +149 -0
  100. package/src/components/Form/fields/BooleanField.tsx +22 -0
  101. package/src/components/Form/fields/GeoPointField.tsx +120 -0
  102. package/src/components/Form/fields/MarkdownField.tsx +78 -0
  103. package/src/components/Form/fields/NumberField.tsx +39 -0
  104. package/src/components/Form/fields/RefField.tsx +189 -0
  105. package/src/components/Form/fields/SelectField.tsx +65 -0
  106. package/src/components/Form/{custom/SelectOptionsInput.stories.tsx → fields/SelectOptionField.stories.tsx} +19 -16
  107. package/src/components/Form/{custom/SelectOptionsInput.tsx → fields/SelectOptionField.tsx} +23 -17
  108. package/src/components/Form/fields/TextAreaField.tsx +38 -0
  109. package/src/components/Form/fields/TextField.tsx +38 -0
  110. package/src/components/Form/{custom/TupleInput.tsx → fields/TupleField.tsx} +15 -8
  111. package/src/components/Form/fields/index.ts +15 -0
  112. package/src/components/Form/index.ts +3 -4
  113. package/src/components/ObjectPicker/ObjectPicker.stories.tsx +40 -37
  114. package/src/components/ObjectPicker/ObjectPicker.tsx +84 -70
  115. package/src/components/Settings/Settings.stories.tsx +56 -0
  116. package/src/components/Settings/Settings.tsx +230 -0
  117. package/src/components/{ControlSection → Settings}/index.ts +1 -1
  118. package/src/components/ViewEditor/ViewEditor.stories.tsx +69 -49
  119. package/src/components/ViewEditor/ViewEditor.test.tsx +8 -6
  120. package/src/components/ViewEditor/ViewEditor.tsx +302 -271
  121. package/src/components/index.ts +1 -1
  122. package/src/components/testing.tsx +12 -7
  123. package/src/hooks/index.ts +2 -3
  124. package/src/hooks/useFormHandler.ts +380 -0
  125. package/src/hooks/useKeyHandler.tsx +37 -0
  126. package/src/translations.ts +12 -10
  127. package/src/util/field.ts +25 -25
  128. package/src/util/formatting.ts +22 -22
  129. package/src/util/index.ts +1 -1
  130. package/src/util/properties.ts +18 -0
  131. package/src/util/refs.ts +11 -17
  132. package/dist/types/src/components/ControlSection/ControlSection.d.ts +0 -23
  133. package/dist/types/src/components/ControlSection/ControlSection.d.ts.map +0 -1
  134. package/dist/types/src/components/ControlSection/ControlSection.stories.d.ts.map +0 -1
  135. package/dist/types/src/components/ControlSection/index.d.ts +0 -2
  136. package/dist/types/src/components/ControlSection/index.d.ts.map +0 -1
  137. package/dist/types/src/components/Form/ArrayField.d.ts +0 -16
  138. package/dist/types/src/components/Form/ArrayField.d.ts.map +0 -1
  139. package/dist/types/src/components/Form/Defaults.d.ts +0 -14
  140. package/dist/types/src/components/Form/Defaults.d.ts.map +0 -1
  141. package/dist/types/src/components/Form/FormActions.d.ts +0 -9
  142. package/dist/types/src/components/Form/FormActions.d.ts.map +0 -1
  143. package/dist/types/src/components/Form/FormContent.d.ts +0 -63
  144. package/dist/types/src/components/Form/FormContent.d.ts.map +0 -1
  145. package/dist/types/src/components/Form/FormContext.d.ts +0 -23
  146. package/dist/types/src/components/Form/FormContext.d.ts.map +0 -1
  147. package/dist/types/src/components/Form/Input.d.ts +0 -29
  148. package/dist/types/src/components/Form/Input.d.ts.map +0 -1
  149. package/dist/types/src/components/Form/RefField.d.ts +0 -21
  150. package/dist/types/src/components/Form/RefField.d.ts.map +0 -1
  151. package/dist/types/src/components/Form/custom/GeoPointInput.d.ts +0 -4
  152. package/dist/types/src/components/Form/custom/GeoPointInput.d.ts.map +0 -1
  153. package/dist/types/src/components/Form/custom/SelectOptionsInput.d.ts +0 -4
  154. package/dist/types/src/components/Form/custom/SelectOptionsInput.d.ts.map +0 -1
  155. package/dist/types/src/components/Form/custom/SelectOptionsInput.stories.d.ts.map +0 -1
  156. package/dist/types/src/components/Form/custom/TupleInput.d.ts +0 -6
  157. package/dist/types/src/components/Form/custom/TupleInput.d.ts.map +0 -1
  158. package/dist/types/src/components/Form/custom/index.d.ts +0 -4
  159. package/dist/types/src/components/Form/custom/index.d.ts.map +0 -1
  160. package/dist/types/src/components/Form/factory.d.ts +0 -8
  161. package/dist/types/src/components/Form/factory.d.ts.map +0 -1
  162. package/dist/types/src/hooks/useForm.d.ts +0 -67
  163. package/dist/types/src/hooks/useForm.d.ts.map +0 -1
  164. package/dist/types/src/hooks/useQueryRefOptions.d.ts +0 -28
  165. package/dist/types/src/hooks/useQueryRefOptions.d.ts.map +0 -1
  166. package/dist/types/src/hooks/useRefQueryLookupHandler.d.ts +0 -8
  167. package/dist/types/src/hooks/useRefQueryLookupHandler.d.ts.map +0 -1
  168. package/dist/types/src/util/schema.d.ts +0 -3
  169. package/dist/types/src/util/schema.d.ts.map +0 -1
  170. package/src/components/ControlSection/ControlSection.stories.tsx +0 -38
  171. package/src/components/ControlSection/ControlSection.tsx +0 -153
  172. package/src/components/Form/ArrayField.tsx +0 -125
  173. package/src/components/Form/Defaults.tsx +0 -205
  174. package/src/components/Form/FormActions.tsx +0 -54
  175. package/src/components/Form/FormContent.tsx +0 -322
  176. package/src/components/Form/FormContext.tsx +0 -110
  177. package/src/components/Form/Input.tsx +0 -54
  178. package/src/components/Form/RefField.tsx +0 -269
  179. package/src/components/Form/custom/GeoPointInput.tsx +0 -80
  180. package/src/components/Form/custom/index.ts +0 -7
  181. package/src/components/Form/factory.tsx +0 -31
  182. package/src/hooks/useForm.ts +0 -295
  183. package/src/hooks/useQueryRefOptions.ts +0 -59
  184. package/src/hooks/useRefQueryLookupHandler.ts +0 -39
  185. package/src/util/schema.ts +0 -12
@@ -3,26 +3,57 @@
3
3
  //
4
4
 
5
5
  import type * as Schema from 'effect/Schema';
6
- import React, { type KeyboardEvent, type MouseEvent, forwardRef, useCallback, useMemo, useState } from 'react';
6
+ import React, { type KeyboardEvent, forwardRef, useCallback, useState } from 'react';
7
7
 
8
- import { Icon, Popover, type ThemedClassName, useTranslation } from '@dxos/react-ui';
9
- import { PopoverCombobox } from '@dxos/react-ui-searchlist';
8
+ import { type Palette, Popover, type ThemedClassName, useTranslation } from '@dxos/react-ui';
9
+ import { Combobox, useSearchListInput, useSearchListResults } from '@dxos/react-ui-searchlist';
10
10
 
11
- import { type QueryTag } from '../../hooks';
12
11
  import { translationKey } from '../../translations';
13
12
  import { Form } from '../Form';
14
13
 
14
+ export type RefOption = {
15
+ id: string;
16
+ label: string;
17
+ hue?: Palette;
18
+ };
19
+
15
20
  export type ObjectPickerContentProps = ThemedClassName<{
16
- options: QueryTag[];
21
+ options: RefOption[];
17
22
  selectedIds?: string[];
18
- createSchema?: Schema.Schema.AnyNoContext;
19
23
  createOptionLabel?: [string, { ns: string }];
20
24
  createOptionIcon?: string;
25
+ createSchema?: Schema.Schema.AnyNoContext;
21
26
  createInitialValuePath?: string;
22
- onSelect: (id: string) => void;
23
27
  onCreate?: (values: any) => void;
28
+ onSelect: (id: string) => void;
24
29
  }>;
25
30
 
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
+
26
57
  const ObjectPickerContent = forwardRef<HTMLDivElement, ObjectPickerContentProps>(
27
58
  (
28
59
  {
@@ -39,39 +70,27 @@ const ObjectPickerContent = forwardRef<HTMLDivElement, ObjectPickerContentProps>
39
70
  ref,
40
71
  ) => {
41
72
  const { t } = useTranslation(translationKey);
42
-
43
- const [searchString, setSearchString] = useState('');
44
73
  const [showForm, setShowForm] = useState(false);
74
+ const [formInitialValue, setFormInitialValue] = useState<string>('');
75
+
76
+ const { results, handleSearch } = useSearchListResults({
77
+ items: options,
78
+ });
45
79
 
46
80
  const handleFormSave = useCallback(
47
81
  (values: any) => {
48
82
  onCreate?.(values);
49
83
  setShowForm(false);
50
- setSearchString('');
84
+ setFormInitialValue('');
51
85
  },
52
86
  [onCreate],
53
87
  );
54
88
 
55
89
  const handleFormCancel = useCallback(() => {
56
90
  setShowForm(false);
57
- setSearchString('');
91
+ setFormInitialValue('');
58
92
  }, []);
59
93
 
60
- // TODO(thure): The following click and keydown handlers are necessary because `onSelect` is called after the
61
- // Popover is already closed. Augment/refactor CmdK, if possible, to facilitate stopping event default
62
- // and propagation.
63
-
64
- const handleClick = useCallback(
65
- (event: MouseEvent) => {
66
- if (createSchema && (event.target as HTMLElement).closest('[data-value="__create__"]')) {
67
- event.stopPropagation();
68
- event.preventDefault();
69
- setShowForm(true);
70
- }
71
- },
72
- [createSchema],
73
- );
74
-
75
94
  const handleKeyDown = useCallback(
76
95
  (event: KeyboardEvent) => {
77
96
  if (
@@ -83,70 +102,64 @@ const ObjectPickerContent = forwardRef<HTMLDivElement, ObjectPickerContentProps>
83
102
  ) {
84
103
  event.stopPropagation();
85
104
  event.preventDefault();
105
+ // Get the current query from the input element
106
+ const input = (event.currentTarget as HTMLElement).querySelector('input[type="text"]') as HTMLInputElement;
107
+ const currentQuery = input?.value || '';
108
+ setFormInitialValue(currentQuery);
86
109
  setShowForm(true);
87
110
  }
88
111
  },
89
112
  [createSchema],
90
113
  );
91
114
 
92
- const labelById = useMemo(
93
- () =>
94
- options.reduce((acc: Record<string, string>, option) => {
95
- acc[option.id.toLowerCase()] = option.label.toLowerCase();
96
- return acc;
97
- }, {}),
98
- [options],
99
- );
100
-
101
115
  return (
102
- <PopoverCombobox.Content
103
- {...props}
104
- ref={ref}
105
- filter={(value, search) => (value === '__create__' || labelById[value]?.includes(search.toLowerCase()) ? 1 : 0)}
106
- onClickCapture={handleClick}
107
- onKeyDownCapture={handleKeyDown}
108
- >
116
+ <Combobox.Content {...props} ref={ref} onSearch={handleSearch} onKeyDownCapture={handleKeyDown}>
109
117
  {showForm && createSchema ? (
110
118
  <Popover.Viewport>
111
- <Form
119
+ <Form.Root
120
+ testId='create-referenced-object-form'
112
121
  schema={createSchema}
113
- values={createInitialValuePath ? { [createInitialValuePath]: searchString } : {}}
122
+ values={createInitialValuePath ? { [createInitialValuePath]: formInitialValue } : {}}
114
123
  onSave={handleFormSave}
115
124
  onCancel={handleFormCancel}
116
- testId='create-referenced-object-form'
117
- />
125
+ >
126
+ <Form.Viewport>
127
+ <Form.Content>
128
+ <Form.FieldSet />
129
+ <Form.Actions />
130
+ </Form.Content>
131
+ </Form.Viewport>
132
+ </Form.Root>
118
133
  </Popover.Viewport>
119
134
  ) : (
120
135
  <>
121
- <PopoverCombobox.Input
122
- placeholder={t('ref field combobox input placeholder')}
123
- value={searchString}
124
- onValueChange={setSearchString}
125
- autoFocus
126
- />
127
- <PopoverCombobox.List>
128
- {options.map((option) => (
129
- <PopoverCombobox.Item
136
+ <Combobox.Input placeholder={t('ref field combobox input placeholder')} autoFocus />
137
+ <Combobox.List>
138
+ {results.map((option) => (
139
+ <Combobox.Item
130
140
  key={option.id}
131
141
  value={option.id}
142
+ label={option.label}
143
+ checked={selectedIds?.includes(option.id)}
132
144
  onSelect={() => onSelect(option.id)}
133
145
  classNames='flex items-center gap-2'
134
- >
135
- <span className='grow'>{option.label}</span>
136
- {selectedIds?.includes(option.id) && <Icon icon='ph--check--regular' />}
137
- </PopoverCombobox.Item>
146
+ />
138
147
  ))}
139
- {searchString.length > 0 && createOptionLabel && createOptionIcon && createSchema && onCreate && (
140
- <PopoverCombobox.Item value='__create__' classNames='flex items-center gap-2'>
141
- <Icon icon={createOptionIcon} />
142
- {t(createOptionLabel[0], { ns: createOptionLabel[1].ns, text: searchString })}
143
- </PopoverCombobox.Item>
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
+ />
144
157
  )}
145
- </PopoverCombobox.List>
158
+ </Combobox.List>
146
159
  </>
147
160
  )}
148
- <PopoverCombobox.Arrow />
149
- </PopoverCombobox.Content>
161
+ <Combobox.Arrow />
162
+ </Combobox.Content>
150
163
  );
151
164
  },
152
165
  );
@@ -154,8 +167,9 @@ const ObjectPickerContent = forwardRef<HTMLDivElement, ObjectPickerContentProps>
154
167
  ObjectPickerContent.displayName = 'ObjectPickerContent';
155
168
 
156
169
  export const ObjectPicker = {
157
- Root: PopoverCombobox.Root,
158
- Trigger: PopoverCombobox.Trigger,
159
- VirtualTrigger: PopoverCombobox.VirtualTrigger,
170
+ Root: Combobox.Root,
171
+ Portal: Combobox.Portal,
172
+ Trigger: Combobox.Trigger,
173
+ VirtualTrigger: Combobox.VirtualTrigger,
160
174
  Content: ObjectPickerContent,
161
175
  };
@@ -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.Root>
21
+ <Settings.Section title='Settings' description={faker.lorem.paragraphs(1)}>
22
+ <Settings.Group>
23
+ <Settings.ItemInput title={faker.lorem.sentence(2)} description={faker.lorem.paragraphs(1)}>
24
+ <Input.Root>
25
+ <Input.TextInput placeholder='Input' />
26
+ </Input.Root>
27
+ </Settings.ItemInput>
28
+ <Settings.ItemInput title={faker.lorem.sentence(2)} description={faker.lorem.paragraphs(2)}>
29
+ <Input.Root>
30
+ <Input.Switch />
31
+ </Input.Root>
32
+ </Settings.ItemInput>
33
+ <Settings.ItemInput title={faker.lorem.sentence(3)} description={faker.lorem.paragraphs(2)}>
34
+ <Button>Test</Button>
35
+ </Settings.ItemInput>
36
+ </Settings.Group>
37
+ </Settings.Section>
38
+ </Settings.Root>
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,230 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ import React, { type PropsWithChildren } from 'react';
6
+
7
+ import {
8
+ Button,
9
+ type ButtonProps,
10
+ Input,
11
+ type Label,
12
+ ScrollArea,
13
+ type ThemedClassName,
14
+ toLocalizedString,
15
+ useTranslation,
16
+ } from '@dxos/react-ui';
17
+ import { mx } from '@dxos/ui-theme';
18
+
19
+ import { translationKey } from '../../translations';
20
+
21
+ const SETTINGS_ROOT_NAME = 'Settings.Root';
22
+ const SETTINGS_SECTION_NAME = 'Settings.Section';
23
+ const SETTINGS_SECTION_HEADING_NAME = 'Settings.SectionHeading';
24
+ const SETTINGS_GROUP_NAME = 'Settings.Group';
25
+ const SETTINGS_GROUP_BUTTON_NAME = 'Settings.GroupButton';
26
+ const SETTINGS_CONTAINER_NAME = 'Settings.Container';
27
+ const SETTINGS_FRAME_NAME = 'Settings.Frame';
28
+ const SETTINGS_FRAME_ITEM_NAME = 'Settings.FrameItem';
29
+ const SETTINGS_ITEM_NAME = 'Settings.Item';
30
+ const SETTINGS_ITEM_INPUT_NAME = 'Settings.ItemInput';
31
+
32
+ const styles = {
33
+ title: 'pb-trim-md text-base-surface-text text-lg',
34
+ description: 'text-base text-description',
35
+ grid: 'grid grid-cols-1 md:grid-cols-[1fr_1fr] gap-x-trim-lg',
36
+ };
37
+
38
+ //
39
+ // Root
40
+ //
41
+
42
+ type SettingsRootProps = PropsWithChildren;
43
+
44
+ const SettingsRoot = ({ children }: SettingsRootProps) => {
45
+ return (
46
+ <ScrollArea.Root orientation='vertical' className='dx-article'>
47
+ <ScrollArea.Viewport classNames='p-trim-md'>{children}</ScrollArea.Viewport>
48
+ </ScrollArea.Root>
49
+ );
50
+ };
51
+
52
+ SettingsRoot.displayName = SETTINGS_ROOT_NAME;
53
+
54
+ //
55
+ // Section
56
+ //
57
+
58
+ type SettingsSectionProps = PropsWithChildren<{
59
+ title: Label;
60
+ description?: Label;
61
+ }>;
62
+
63
+ const SettingsSection = ({ title, description, children }: SettingsSectionProps) => {
64
+ return (
65
+ <>
66
+ <SettingsSectionHeading title={title} description={description} />
67
+ <div className='w-full pt-trim-md space-y-trim-md'>{children}</div>
68
+ </>
69
+ );
70
+ };
71
+
72
+ SettingsSection.displayName = SETTINGS_SECTION_NAME;
73
+
74
+ //
75
+ // Section Heading
76
+ //
77
+
78
+ const SettingsSectionHeading = ({ title, description }: Omit<SettingsSectionProps, 'children'>) => {
79
+ const { t } = useTranslation(translationKey);
80
+ return (
81
+ <>
82
+ <h2 className='px-trim-md mt-trim-md mb-trim-md text-xl'>{toLocalizedString(title, t)}</h2>
83
+ {description && <p className='px-trim-md my-trim-md text-description'>{toLocalizedString(description, t)}</p>}
84
+ </>
85
+ );
86
+ };
87
+
88
+ SettingsSectionHeading.displayName = SETTINGS_SECTION_HEADING_NAME;
89
+
90
+ //
91
+ // Group Button
92
+ //
93
+
94
+ const SettingsGroupButton = ({ classNames, ...props }: ButtonProps) => {
95
+ return <Button {...props} classNames={['md:col-span-2', classNames]} />;
96
+ };
97
+
98
+ SettingsGroupButton.displayName = SETTINGS_GROUP_BUTTON_NAME;
99
+
100
+ //
101
+ // Group
102
+ //
103
+
104
+ type SettingsGroupProps = ThemedClassName<PropsWithChildren>;
105
+
106
+ const SettingsGroup = ({ children, classNames }: SettingsGroupProps) => (
107
+ <div role='none' className={mx('group space-y-trim-md', classNames)}>
108
+ {children}
109
+ </div>
110
+ );
111
+
112
+ SettingsGroup.displayName = SETTINGS_GROUP_NAME;
113
+
114
+ //
115
+ // Frame
116
+ //
117
+
118
+ const SettingsFrame = ({ children }: SettingsGroupProps) => (
119
+ <div role='none' className='p-trim-md border border-separator rounded-md'>
120
+ {children}
121
+ </div>
122
+ );
123
+
124
+ SettingsFrame.displayName = SETTINGS_FRAME_NAME;
125
+
126
+ //
127
+ // Frame Item
128
+ //
129
+
130
+ const SettingsFrameItem = ({ title, description, children }: SettingsItemProps) => {
131
+ const { t } = useTranslation(translationKey);
132
+
133
+ return (
134
+ <div role='group' className='min-w-0'>
135
+ <h3 className='text-lg mb-2'>{toLocalizedString(title, t)}</h3>
136
+ {description && <p className='my-trim-sm md:mb-0 text-description'>{toLocalizedString(description, t)}</p>}
137
+ {children}
138
+ </div>
139
+ );
140
+ };
141
+
142
+ SettingsFrameItem.displayName = SETTINGS_FRAME_ITEM_NAME;
143
+
144
+ //
145
+ // Container
146
+ //
147
+
148
+ const SettingsContainer = ({ classNames, children }: ThemedClassName<PropsWithChildren>) => {
149
+ return (
150
+ <div
151
+ role='none'
152
+ className={mx([
153
+ 'dx-article',
154
+ '*:first:mt-0! *:last:mb-0! px-trim-md py-trim-md',
155
+ 'border border-separator rounded-md',
156
+ classNames,
157
+ ])}
158
+ >
159
+ {children}
160
+ </div>
161
+ );
162
+ };
163
+
164
+ SettingsContainer.displayName = SETTINGS_CONTAINER_NAME;
165
+
166
+ //
167
+ // Item
168
+ //
169
+
170
+ type SettingsItemProps = PropsWithChildren<{
171
+ title: Label;
172
+ description?: Label;
173
+ }>;
174
+
175
+ const SettingsItem = ({ title, description = '', children }: SettingsItemProps) => {
176
+ const { t } = useTranslation(translationKey);
177
+
178
+ return (
179
+ <SettingsContainer classNames={styles.grid}>
180
+ <h3 className={mx(styles.title, 'md:col-span-2')}>{toLocalizedString(title, t)}</h3>
181
+ <p className={styles.description}>{toLocalizedString(description, t)}</p>
182
+ <div role='none' className='overflow-hidden text-end py-1'>
183
+ {children}
184
+ </div>
185
+ </SettingsContainer>
186
+ );
187
+ };
188
+
189
+ SettingsItem.displayName = SETTINGS_ITEM_NAME;
190
+
191
+ //
192
+ // Item Input
193
+ //
194
+
195
+ const SettingsItemInput = ({ title, description = '', children }: SettingsItemProps) => {
196
+ const { t } = useTranslation(translationKey);
197
+
198
+ return (
199
+ <Input.Root>
200
+ <SettingsContainer classNames={styles.grid}>
201
+ <Input.Label classNames={mx(styles.title, 'md:col-span-2')}>{toLocalizedString(title, t)}</Input.Label>
202
+ <Input.DescriptionAndValidation>
203
+ <Input.Description classNames={styles.description}>{toLocalizedString(description, t)}</Input.Description>
204
+ </Input.DescriptionAndValidation>
205
+ <div role='none' className='text-end py-1'>
206
+ {children}
207
+ </div>
208
+ </SettingsContainer>
209
+ </Input.Root>
210
+ );
211
+ };
212
+
213
+ SettingsItemInput.displayName = SETTINGS_ITEM_INPUT_NAME;
214
+
215
+ //
216
+ // Settings
217
+ //
218
+
219
+ export const Settings = {
220
+ Root: SettingsRoot,
221
+ Section: SettingsSection,
222
+ SectionHeading: SettingsSectionHeading,
223
+ Group: SettingsGroup,
224
+ GroupButton: SettingsGroupButton,
225
+ Container: SettingsContainer,
226
+ Frame: SettingsFrame,
227
+ FrameItem: SettingsFrameItem,
228
+ Item: SettingsItem,
229
+ ItemInput: SettingsItemInput,
230
+ };
@@ -2,4 +2,4 @@
2
2
  // Copyright 2025 DXOS.org
3
3
  //
4
4
 
5
- export * from './ControlSection';
5
+ export * from './Settings';