@dxos/react-ui-form 0.8.4-main.c1de068 → 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 (204) hide show
  1. package/README.md +0 -1
  2. package/dist/lib/browser/index.mjs +2314 -2078
  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 +2314 -2078
  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 +82 -5
  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 -25
  13. package/dist/types/src/components/Form/Form.d.ts.map +1 -1
  14. package/dist/types/src/components/Form/Form.stories.d.ts +127 -55
  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/Form/fields/SelectOptionField.stories.d.ts +82 -0
  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 +46 -0
  53. package/dist/types/src/components/ObjectPicker/ObjectPicker.d.ts.map +1 -0
  54. package/dist/types/src/components/ObjectPicker/ObjectPicker.stories.d.ts +85 -0
  55. package/dist/types/src/components/ObjectPicker/ObjectPicker.stories.d.ts.map +1 -0
  56. package/dist/types/src/components/ObjectPicker/index.d.ts +2 -0
  57. package/dist/types/src/components/ObjectPicker/index.d.ts.map +1 -0
  58. package/dist/types/src/components/Settings/Settings.d.ts +56 -0
  59. package/dist/types/src/components/Settings/Settings.d.ts.map +1 -0
  60. package/dist/types/src/components/Settings/Settings.stories.d.ts +81 -0
  61. package/dist/types/src/components/Settings/Settings.stories.d.ts.map +1 -0
  62. package/dist/types/src/components/Settings/index.d.ts +2 -0
  63. package/dist/types/src/components/Settings/index.d.ts.map +1 -0
  64. package/dist/types/src/components/ViewEditor/ViewEditor.d.ts +22 -10
  65. package/dist/types/src/components/ViewEditor/ViewEditor.d.ts.map +1 -1
  66. package/dist/types/src/components/ViewEditor/ViewEditor.stories.d.ts +84 -8
  67. package/dist/types/src/components/ViewEditor/ViewEditor.stories.d.ts.map +1 -1
  68. package/dist/types/src/components/index.d.ts +2 -2
  69. package/dist/types/src/components/index.d.ts.map +1 -1
  70. package/dist/types/src/components/testing.d.ts +4 -3
  71. package/dist/types/src/components/testing.d.ts.map +1 -1
  72. package/dist/types/src/hooks/index.d.ts +2 -3
  73. package/dist/types/src/hooks/index.d.ts.map +1 -1
  74. package/dist/types/src/hooks/useFormHandler.d.ts +82 -0
  75. package/dist/types/src/hooks/useFormHandler.d.ts.map +1 -0
  76. package/dist/types/src/hooks/useKeyHandler.d.ts +6 -0
  77. package/dist/types/src/hooks/useKeyHandler.d.ts.map +1 -0
  78. package/dist/types/src/translations.d.ts +19 -10
  79. package/dist/types/src/translations.d.ts.map +1 -1
  80. package/dist/types/src/util/field.d.ts +3 -3
  81. package/dist/types/src/util/field.d.ts.map +1 -1
  82. package/dist/types/src/util/formatting.d.ts +2 -2
  83. package/dist/types/src/util/formatting.d.ts.map +1 -1
  84. package/dist/types/src/util/index.d.ts +1 -1
  85. package/dist/types/src/util/index.d.ts.map +1 -1
  86. package/dist/types/src/util/properties.d.ts +7 -0
  87. package/dist/types/src/util/properties.d.ts.map +1 -0
  88. package/dist/types/src/util/refs.d.ts +2 -3
  89. package/dist/types/src/util/refs.d.ts.map +1 -1
  90. package/dist/types/tsconfig.tsbuildinfo +1 -1
  91. package/package.json +51 -42
  92. package/src/components/FieldEditor/FieldEditor.stories.tsx +51 -32
  93. package/src/components/FieldEditor/FieldEditor.test.tsx +3 -2
  94. package/src/components/FieldEditor/FieldEditor.tsx +87 -83
  95. package/src/components/Form/Form.stories.tsx +156 -219
  96. package/src/components/Form/Form.tsx +347 -96
  97. package/src/components/Form/FormField.tsx +294 -0
  98. package/src/components/Form/FormFieldComponent.stories.tsx +65 -0
  99. package/src/components/Form/FormFieldComponent.tsx +172 -0
  100. package/src/components/Form/FormFieldSet.tsx +123 -0
  101. package/src/components/Form/README.md +3 -0
  102. package/src/components/Form/fields/ArrayField.tsx +149 -0
  103. package/src/components/Form/fields/BooleanField.tsx +22 -0
  104. package/src/components/Form/fields/GeoPointField.tsx +120 -0
  105. package/src/components/Form/fields/MarkdownField.tsx +78 -0
  106. package/src/components/Form/fields/NumberField.tsx +39 -0
  107. package/src/components/Form/fields/RefField.tsx +189 -0
  108. package/src/components/Form/fields/SelectField.tsx +65 -0
  109. package/src/components/Form/fields/SelectOptionField.stories.tsx +64 -0
  110. package/src/components/Form/{custom/SelectOptionsInput.tsx → fields/SelectOptionField.tsx} +46 -41
  111. package/src/components/Form/fields/TextAreaField.tsx +38 -0
  112. package/src/components/Form/fields/TextField.tsx +38 -0
  113. package/src/components/Form/{custom/TupleInput.tsx → fields/TupleField.tsx} +16 -9
  114. package/src/components/Form/fields/index.ts +15 -0
  115. package/src/components/Form/index.ts +3 -4
  116. package/src/components/ObjectPicker/ObjectPicker.stories.tsx +210 -0
  117. package/src/components/ObjectPicker/ObjectPicker.tsx +175 -0
  118. package/src/components/{ControlSection → ObjectPicker}/index.ts +1 -1
  119. package/src/components/Settings/Settings.stories.tsx +56 -0
  120. package/src/components/Settings/Settings.tsx +230 -0
  121. package/src/components/Settings/index.ts +5 -0
  122. package/src/components/ViewEditor/ViewEditor.stories.tsx +140 -62
  123. package/src/components/ViewEditor/ViewEditor.test.tsx +17 -18
  124. package/src/components/ViewEditor/ViewEditor.tsx +341 -202
  125. package/src/components/index.ts +2 -3
  126. package/src/components/testing.tsx +22 -11
  127. package/src/hooks/index.ts +2 -3
  128. package/src/hooks/useFormHandler.ts +380 -0
  129. package/src/hooks/useKeyHandler.tsx +37 -0
  130. package/src/translations.ts +21 -12
  131. package/src/util/field.ts +33 -43
  132. package/src/util/formatting.ts +22 -22
  133. package/src/util/index.ts +1 -1
  134. package/src/util/properties.ts +18 -0
  135. package/src/util/refs.ts +12 -18
  136. package/dist/types/src/components/ControlSection/ControlSection.d.ts +0 -23
  137. package/dist/types/src/components/ControlSection/ControlSection.d.ts.map +0 -1
  138. package/dist/types/src/components/ControlSection/ControlSection.stories.d.ts +0 -8
  139. package/dist/types/src/components/ControlSection/ControlSection.stories.d.ts.map +0 -1
  140. package/dist/types/src/components/ControlSection/index.d.ts +0 -2
  141. package/dist/types/src/components/ControlSection/index.d.ts.map +0 -1
  142. package/dist/types/src/components/Form/ArrayField.d.ts +0 -16
  143. package/dist/types/src/components/Form/ArrayField.d.ts.map +0 -1
  144. package/dist/types/src/components/Form/Defaults.d.ts +0 -14
  145. package/dist/types/src/components/Form/Defaults.d.ts.map +0 -1
  146. package/dist/types/src/components/Form/FormActions.d.ts +0 -9
  147. package/dist/types/src/components/Form/FormActions.d.ts.map +0 -1
  148. package/dist/types/src/components/Form/FormContent.d.ts +0 -57
  149. package/dist/types/src/components/Form/FormContent.d.ts.map +0 -1
  150. package/dist/types/src/components/Form/FormContext.d.ts +0 -23
  151. package/dist/types/src/components/Form/FormContext.d.ts.map +0 -1
  152. package/dist/types/src/components/Form/Input.d.ts +0 -26
  153. package/dist/types/src/components/Form/Input.d.ts.map +0 -1
  154. package/dist/types/src/components/Form/RefField.d.ts +0 -12
  155. package/dist/types/src/components/Form/RefField.d.ts.map +0 -1
  156. package/dist/types/src/components/Form/custom/GeoPointInput.d.ts +0 -4
  157. package/dist/types/src/components/Form/custom/GeoPointInput.d.ts.map +0 -1
  158. package/dist/types/src/components/Form/custom/SelectOptionsInput.d.ts +0 -4
  159. package/dist/types/src/components/Form/custom/SelectOptionsInput.d.ts.map +0 -1
  160. package/dist/types/src/components/Form/custom/SelectOptionsInput.stories.d.ts +0 -7
  161. package/dist/types/src/components/Form/custom/SelectOptionsInput.stories.d.ts.map +0 -1
  162. package/dist/types/src/components/Form/custom/TupleInput.d.ts +0 -6
  163. package/dist/types/src/components/Form/custom/TupleInput.d.ts.map +0 -1
  164. package/dist/types/src/components/Form/custom/index.d.ts +0 -4
  165. package/dist/types/src/components/Form/custom/index.d.ts.map +0 -1
  166. package/dist/types/src/components/Form/factory.d.ts +0 -8
  167. package/dist/types/src/components/Form/factory.d.ts.map +0 -1
  168. package/dist/types/src/components/deprecated/Form.d.ts +0 -15
  169. package/dist/types/src/components/deprecated/Form.d.ts.map +0 -1
  170. package/dist/types/src/components/deprecated/Form.stories.d.ts +0 -13
  171. package/dist/types/src/components/deprecated/Form.stories.d.ts.map +0 -1
  172. package/dist/types/src/components/deprecated/FormInput.d.ts +0 -16
  173. package/dist/types/src/components/deprecated/FormInput.d.ts.map +0 -1
  174. package/dist/types/src/components/deprecated/index.d.ts +0 -2
  175. package/dist/types/src/components/deprecated/index.d.ts.map +0 -1
  176. package/dist/types/src/hooks/useForm.d.ts +0 -67
  177. package/dist/types/src/hooks/useForm.d.ts.map +0 -1
  178. package/dist/types/src/hooks/useQueryRefOptions.d.ts +0 -23
  179. package/dist/types/src/hooks/useQueryRefOptions.d.ts.map +0 -1
  180. package/dist/types/src/hooks/useRefQueryLookupHandler.d.ts +0 -8
  181. package/dist/types/src/hooks/useRefQueryLookupHandler.d.ts.map +0 -1
  182. package/dist/types/src/util/schema.d.ts +0 -3
  183. package/dist/types/src/util/schema.d.ts.map +0 -1
  184. package/src/components/ControlSection/ControlSection.stories.tsx +0 -41
  185. package/src/components/ControlSection/ControlSection.tsx +0 -139
  186. package/src/components/Form/ArrayField.tsx +0 -124
  187. package/src/components/Form/Defaults.tsx +0 -203
  188. package/src/components/Form/FormActions.tsx +0 -53
  189. package/src/components/Form/FormContent.tsx +0 -240
  190. package/src/components/Form/FormContext.tsx +0 -110
  191. package/src/components/Form/Input.tsx +0 -51
  192. package/src/components/Form/RefField.tsx +0 -213
  193. package/src/components/Form/custom/GeoPointInput.tsx +0 -72
  194. package/src/components/Form/custom/SelectOptionsInput.stories.tsx +0 -61
  195. package/src/components/Form/custom/index.ts +0 -7
  196. package/src/components/Form/factory.tsx +0 -31
  197. package/src/components/deprecated/Form.stories.tsx +0 -62
  198. package/src/components/deprecated/Form.tsx +0 -119
  199. package/src/components/deprecated/FormInput.tsx +0 -71
  200. package/src/components/deprecated/index.ts +0 -5
  201. package/src/hooks/useForm.ts +0 -291
  202. package/src/hooks/useQueryRefOptions.ts +0 -53
  203. package/src/hooks/useRefQueryLookupHandler.ts +0 -39
  204. package/src/util/schema.ts +0 -14
@@ -0,0 +1,175 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ import type * as Schema from 'effect/Schema';
6
+ import React, { type KeyboardEvent, forwardRef, useCallback, useState } from 'react';
7
+
8
+ import { type Palette, Popover, type ThemedClassName, useTranslation } from '@dxos/react-ui';
9
+ import { Combobox, useSearchListInput, useSearchListResults } from '@dxos/react-ui-searchlist';
10
+
11
+ import { translationKey } from '../../translations';
12
+ import { Form } from '../Form';
13
+
14
+ export type RefOption = {
15
+ id: string;
16
+ label: string;
17
+ hue?: Palette;
18
+ };
19
+
20
+ export type ObjectPickerContentProps = ThemedClassName<{
21
+ options: RefOption[];
22
+ selectedIds?: string[];
23
+ createOptionLabel?: [string, { ns: string }];
24
+ createOptionIcon?: string;
25
+ createSchema?: Schema.Schema.AnyNoContext;
26
+ createInitialValuePath?: string;
27
+ onCreate?: (values: any) => void;
28
+ onSelect: (id: string) => void;
29
+ }>;
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
+
57
+ const ObjectPickerContent = forwardRef<HTMLDivElement, ObjectPickerContentProps>(
58
+ (
59
+ {
60
+ options,
61
+ selectedIds,
62
+ createSchema,
63
+ createOptionLabel,
64
+ createOptionIcon,
65
+ createInitialValuePath,
66
+ onSelect,
67
+ onCreate,
68
+ ...props
69
+ },
70
+ ref,
71
+ ) => {
72
+ const { t } = useTranslation(translationKey);
73
+ const [showForm, setShowForm] = useState(false);
74
+ const [formInitialValue, setFormInitialValue] = useState<string>('');
75
+
76
+ const { results, handleSearch } = useSearchListResults({
77
+ items: options,
78
+ });
79
+
80
+ const handleFormSave = useCallback(
81
+ (values: any) => {
82
+ onCreate?.(values);
83
+ setShowForm(false);
84
+ setFormInitialValue('');
85
+ },
86
+ [onCreate],
87
+ );
88
+
89
+ const handleFormCancel = useCallback(() => {
90
+ setShowForm(false);
91
+ setFormInitialValue('');
92
+ }, []);
93
+
94
+ const handleKeyDown = useCallback(
95
+ (event: KeyboardEvent) => {
96
+ if (
97
+ event.key === 'Enter' &&
98
+ createSchema &&
99
+ (event.currentTarget as HTMLElement).querySelector(
100
+ '[role="option"][aria-selected="true"][data-value="__create__"]',
101
+ )
102
+ ) {
103
+ event.stopPropagation();
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);
109
+ setShowForm(true);
110
+ }
111
+ },
112
+ [createSchema],
113
+ );
114
+
115
+ return (
116
+ <Combobox.Content {...props} ref={ref} onSearch={handleSearch} onKeyDownCapture={handleKeyDown}>
117
+ {showForm && createSchema ? (
118
+ <Popover.Viewport>
119
+ <Form.Root
120
+ testId='create-referenced-object-form'
121
+ schema={createSchema}
122
+ values={createInitialValuePath ? { [createInitialValuePath]: formInitialValue } : {}}
123
+ onSave={handleFormSave}
124
+ onCancel={handleFormCancel}
125
+ >
126
+ <Form.Viewport>
127
+ <Form.Content>
128
+ <Form.FieldSet />
129
+ <Form.Actions />
130
+ </Form.Content>
131
+ </Form.Viewport>
132
+ </Form.Root>
133
+ </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
+ )}
161
+ <Combobox.Arrow />
162
+ </Combobox.Content>
163
+ );
164
+ },
165
+ );
166
+
167
+ ObjectPickerContent.displayName = 'ObjectPickerContent';
168
+
169
+ export const ObjectPicker = {
170
+ Root: Combobox.Root,
171
+ Portal: Combobox.Portal,
172
+ Trigger: Combobox.Trigger,
173
+ VirtualTrigger: Combobox.VirtualTrigger,
174
+ Content: ObjectPickerContent,
175
+ };
@@ -2,4 +2,4 @@
2
2
  // Copyright 2025 DXOS.org
3
3
  //
4
4
 
5
- export * from './ControlSection';
5
+ export * from './ObjectPicker';
@@ -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
+ };
@@ -0,0 +1,5 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ export * from './Settings';