@dxos/react-ui-form 0.8.4-main.1068cf700f → 0.8.4-main.1c7ec43d41
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.
- package/README.md +0 -1
- package/dist/lib/browser/index.mjs +1135 -663
- package/dist/lib/browser/index.mjs.map +4 -4
- package/dist/lib/browser/meta.json +1 -1
- package/dist/lib/browser/translations.mjs +82 -0
- package/dist/lib/browser/translations.mjs.map +7 -0
- package/dist/lib/node-esm/index.mjs +1135 -663
- package/dist/lib/node-esm/index.mjs.map +4 -4
- package/dist/lib/node-esm/meta.json +1 -1
- package/dist/lib/node-esm/translations.mjs +84 -0
- package/dist/lib/node-esm/translations.mjs.map +7 -0
- package/dist/types/src/components/FieldEditor/FieldEditor.d.ts +3 -3
- package/dist/types/src/components/FieldEditor/FieldEditor.d.ts.map +1 -1
- package/dist/types/src/components/FieldEditor/FieldEditor.stories.d.ts +63 -61
- package/dist/types/src/components/FieldEditor/FieldEditor.stories.d.ts.map +1 -1
- package/dist/types/src/components/Form/Form.d.ts +44 -16
- package/dist/types/src/components/Form/Form.d.ts.map +1 -1
- package/dist/types/src/components/Form/Form.stories.d.ts +75 -73
- package/dist/types/src/components/Form/Form.stories.d.ts.map +1 -1
- package/dist/types/src/components/Form/FormField.d.ts +11 -5
- package/dist/types/src/components/Form/FormField.d.ts.map +1 -1
- package/dist/types/src/components/Form/FormFieldComponent.d.ts +8 -7
- package/dist/types/src/components/Form/FormFieldComponent.d.ts.map +1 -1
- package/dist/types/src/components/Form/FormFieldComponent.stories.d.ts +2 -2
- package/dist/types/src/components/Form/FormFieldComponent.stories.d.ts.map +1 -1
- package/dist/types/src/components/Form/FormFieldSet.d.ts +6 -10
- package/dist/types/src/components/Form/FormFieldSet.d.ts.map +1 -1
- package/dist/types/src/components/Form/fields/ArrayField.d.ts +4 -4
- package/dist/types/src/components/Form/fields/ArrayField.d.ts.map +1 -1
- package/dist/types/src/components/Form/fields/BooleanField.d.ts.map +1 -1
- package/dist/types/src/components/Form/fields/GeoPointField.d.ts.map +1 -1
- package/dist/types/src/components/Form/fields/MarkdownField.d.ts +8 -1
- package/dist/types/src/components/Form/fields/MarkdownField.d.ts.map +1 -1
- package/dist/types/src/components/Form/fields/MarkdownField.stories.d.ts +84 -0
- package/dist/types/src/components/Form/fields/MarkdownField.stories.d.ts.map +1 -0
- package/dist/types/src/components/Form/fields/NumberField.d.ts.map +1 -1
- package/dist/types/src/components/Form/fields/RefField.d.ts +15 -7
- package/dist/types/src/components/Form/fields/RefField.d.ts.map +1 -1
- package/dist/types/src/components/Form/fields/SelectField.d.ts.map +1 -1
- package/dist/types/src/components/Form/fields/SelectOptionField.d.ts.map +1 -1
- package/dist/types/src/components/Form/fields/SelectOptionField.stories.d.ts +63 -61
- package/dist/types/src/components/Form/fields/SelectOptionField.stories.d.ts.map +1 -1
- package/dist/types/src/components/Form/fields/TextAreaField.d.ts.map +1 -1
- package/dist/types/src/components/Form/fields/TextField.d.ts +1 -1
- package/dist/types/src/components/Form/fields/TextField.d.ts.map +1 -1
- package/dist/types/src/components/Form/fields/TupleField.d.ts.map +1 -1
- package/dist/types/src/components/ObjectForm/ObjectForm.d.ts +9 -0
- package/dist/types/src/components/ObjectForm/ObjectForm.d.ts.map +1 -0
- package/dist/types/src/components/ObjectForm/index.d.ts +2 -0
- package/dist/types/src/components/ObjectForm/index.d.ts.map +1 -0
- package/dist/types/src/components/ObjectPicker/ObjectPicker.d.ts +26 -4
- package/dist/types/src/components/ObjectPicker/ObjectPicker.d.ts.map +1 -1
- package/dist/types/src/components/ObjectPicker/ObjectPicker.stories.d.ts +63 -61
- package/dist/types/src/components/ObjectPicker/ObjectPicker.stories.d.ts.map +1 -1
- package/dist/types/src/components/ObjectProperties/ObjectProperties.d.ts +9 -0
- package/dist/types/src/components/ObjectProperties/ObjectProperties.d.ts.map +1 -0
- package/dist/types/src/components/ObjectProperties/ObjectProperties.stories.d.ts +127 -0
- package/dist/types/src/components/ObjectProperties/ObjectProperties.stories.d.ts.map +1 -0
- package/dist/types/src/components/ObjectProperties/ObjectProperties.test.d.ts +2 -0
- package/dist/types/src/components/ObjectProperties/ObjectProperties.test.d.ts.map +1 -0
- package/dist/types/src/components/ObjectProperties/index.d.ts +2 -0
- package/dist/types/src/components/ObjectProperties/index.d.ts.map +1 -0
- package/dist/types/src/components/Settings/Settings.d.ts +28 -33
- package/dist/types/src/components/Settings/Settings.d.ts.map +1 -1
- package/dist/types/src/components/Settings/Settings.stories.d.ts +64 -61
- package/dist/types/src/components/Settings/Settings.stories.d.ts.map +1 -1
- package/dist/types/src/components/Settings/SettingsFieldSet.d.ts +43 -0
- package/dist/types/src/components/Settings/SettingsFieldSet.d.ts.map +1 -0
- package/dist/types/src/components/Settings/SettingsItem.d.ts +16 -0
- package/dist/types/src/components/Settings/SettingsItem.d.ts.map +1 -0
- package/dist/types/src/components/Settings/SettingsSelectField.d.ts +18 -0
- package/dist/types/src/components/Settings/SettingsSelectField.d.ts.map +1 -0
- package/dist/types/src/components/Settings/index.d.ts +2 -0
- package/dist/types/src/components/Settings/index.d.ts.map +1 -1
- package/dist/types/src/components/ViewEditor/ViewEditor.d.ts +6 -6
- package/dist/types/src/components/ViewEditor/ViewEditor.d.ts.map +1 -1
- package/dist/types/src/components/ViewEditor/ViewEditor.stories.d.ts +67 -64
- package/dist/types/src/components/ViewEditor/ViewEditor.stories.d.ts.map +1 -1
- package/dist/types/src/components/index.d.ts +2 -0
- package/dist/types/src/components/index.d.ts.map +1 -1
- package/dist/types/src/components/testing.d.ts +4 -4
- package/dist/types/src/components/testing.d.ts.map +1 -1
- package/dist/types/src/hooks/useFormHandler.d.ts.map +1 -1
- package/dist/types/src/hooks/useKeyHandler.d.ts +2 -1
- package/dist/types/src/hooks/useKeyHandler.d.ts.map +1 -1
- package/dist/types/src/index.d.ts +0 -2
- package/dist/types/src/index.d.ts.map +1 -1
- package/dist/types/src/translations.d.ts +63 -61
- package/dist/types/src/translations.d.ts.map +1 -1
- package/dist/types/src/util/field-type.d.ts +19 -0
- package/dist/types/src/util/field-type.d.ts.map +1 -0
- package/dist/types/src/util/field.d.ts.map +1 -1
- package/dist/types/src/util/formatting.d.ts.map +1 -1
- package/dist/types/src/util/index.d.ts +1 -1
- package/dist/types/src/util/index.d.ts.map +1 -1
- package/dist/types/src/util/properties.d.ts.map +1 -1
- package/dist/types/src/util/refs.d.ts +1 -0
- package/dist/types/src/util/refs.d.ts.map +1 -1
- package/dist/types/tsconfig.tsbuildinfo +1 -1
- package/package.json +42 -42
- package/src/components/FieldEditor/FieldEditor.stories.tsx +6 -8
- package/src/components/FieldEditor/FieldEditor.test.tsx +0 -1
- package/src/components/FieldEditor/FieldEditor.tsx +6 -6
- package/src/components/Form/AUDIT.md +17 -0
- package/src/components/Form/Form.stories.tsx +29 -44
- package/src/components/Form/Form.tsx +102 -54
- package/src/components/Form/FormField.tsx +39 -19
- package/src/components/Form/FormFieldComponent.stories.tsx +4 -4
- package/src/components/Form/FormFieldComponent.tsx +10 -10
- package/src/components/Form/FormFieldSet.tsx +121 -104
- package/src/components/Form/fields/ArrayField.tsx +14 -9
- package/src/components/Form/fields/GeoPointField.tsx +8 -7
- package/src/components/Form/fields/MarkdownField.stories.tsx +206 -0
- package/src/components/Form/fields/MarkdownField.tsx +135 -45
- package/src/components/Form/fields/RefField.tsx +84 -44
- package/src/components/Form/fields/SelectField.tsx +1 -1
- package/src/components/Form/fields/SelectOptionField.stories.tsx +10 -8
- package/src/components/Form/fields/SelectOptionField.tsx +12 -12
- package/src/components/Form/fields/TextField.tsx +2 -2
- package/src/components/Form/fields/TupleField.tsx +1 -1
- package/src/components/ObjectForm/ObjectForm.tsx +114 -0
- package/src/components/ObjectForm/index.ts +5 -0
- package/src/components/ObjectPicker/ObjectPicker.stories.tsx +16 -18
- package/src/components/ObjectPicker/ObjectPicker.tsx +92 -65
- package/src/components/ObjectProperties/ObjectProperties.stories.tsx +362 -0
- package/src/components/ObjectProperties/ObjectProperties.test.tsx +101 -0
- package/src/components/ObjectProperties/ObjectProperties.tsx +141 -0
- package/src/components/ObjectProperties/index.ts +5 -0
- package/src/components/Settings/AUDIT.md +52 -0
- package/src/components/Settings/Settings.stories.tsx +69 -21
- package/src/components/Settings/Settings.tsx +35 -187
- package/src/components/Settings/SettingsFieldSet.tsx +257 -0
- package/src/components/Settings/SettingsItem.tsx +62 -0
- package/src/components/Settings/SettingsSelectField.tsx +59 -0
- package/src/components/Settings/index.ts +2 -0
- package/src/components/ViewEditor/ViewEditor.stories.tsx +21 -19
- package/src/components/ViewEditor/ViewEditor.test.tsx +2 -2
- package/src/components/ViewEditor/ViewEditor.tsx +87 -49
- package/src/components/index.ts +2 -0
- package/src/components/testing.tsx +23 -12
- package/src/hooks/useFormHandler.ts +42 -24
- package/src/hooks/useKeyHandler.tsx +7 -7
- package/src/index.ts +0 -4
- package/src/translations.ts +66 -63
- package/src/util/field-type.ts +60 -0
- package/src/util/index.ts +1 -1
- package/src/util/refs.ts +7 -3
- package/dist/types/src/components/Form/Form-old.stories.d.ts +0 -131
- package/dist/types/src/components/Form/Form-old.stories.d.ts.map +0 -1
- package/dist/types/src/util/echo.d.ts +0 -6
- package/dist/types/src/util/echo.d.ts.map +0 -1
- package/src/components/Form/Form-old.stories.tsx +0 -314
- package/src/util/echo.ts +0 -25
|
@@ -3,39 +3,40 @@
|
|
|
3
3
|
//
|
|
4
4
|
|
|
5
5
|
import { type Meta, type StoryObj } from '@storybook/react-vite';
|
|
6
|
+
import * as Schema from 'effect/Schema';
|
|
6
7
|
import React from 'react';
|
|
7
8
|
|
|
8
|
-
import {
|
|
9
|
+
import { random } from '@dxos/random';
|
|
9
10
|
import { Button, Input } from '@dxos/react-ui';
|
|
10
11
|
import { withLayout, withTheme } from '@dxos/react-ui/testing';
|
|
11
12
|
|
|
12
|
-
import { translations } from '
|
|
13
|
+
import { translations } from '#translations';
|
|
13
14
|
|
|
14
15
|
import { Settings } from './Settings';
|
|
15
16
|
|
|
16
|
-
|
|
17
|
+
random.seed(132);
|
|
17
18
|
|
|
18
19
|
const DefaultStory = () => {
|
|
19
20
|
return (
|
|
20
|
-
<Settings.
|
|
21
|
-
<Settings.Section title='Settings' description={
|
|
22
|
-
<Settings.
|
|
23
|
-
<
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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>
|
|
21
|
+
<Settings.Viewport>
|
|
22
|
+
<Settings.Section title='Settings' description={random.lorem.paragraphs(1)}>
|
|
23
|
+
<Settings.Item title={random.lorem.sentence(2)} description={random.lorem.paragraphs(1)}>
|
|
24
|
+
<Input.TextInput placeholder='Input' />
|
|
25
|
+
</Settings.Item>
|
|
26
|
+
<Settings.Item title={random.lorem.sentence(2)} description={random.lorem.paragraphs(2)}>
|
|
27
|
+
<Input.Switch />
|
|
28
|
+
</Settings.Item>
|
|
29
|
+
<Settings.Item title={random.lorem.sentence(3)} description={random.lorem.paragraphs(2)}>
|
|
30
|
+
<Button>Test</Button>
|
|
31
|
+
</Settings.Item>
|
|
37
32
|
</Settings.Section>
|
|
38
|
-
|
|
33
|
+
<Settings.Section title='Panel Example'>
|
|
34
|
+
<Settings.Panel>
|
|
35
|
+
<h3 className='text-lg mb-2'>Members</h3>
|
|
36
|
+
<p className='text-description'>Content inside a panel.</p>
|
|
37
|
+
</Settings.Panel>
|
|
38
|
+
</Settings.Section>
|
|
39
|
+
</Settings.Viewport>
|
|
39
40
|
);
|
|
40
41
|
};
|
|
41
42
|
|
|
@@ -54,3 +55,50 @@ export default meta;
|
|
|
54
55
|
type Story = StoryObj<typeof meta>;
|
|
55
56
|
|
|
56
57
|
export const Default: Story = {};
|
|
58
|
+
|
|
59
|
+
const TestViewModes = ['preview', 'readonly', 'source'] as const;
|
|
60
|
+
const TestViewMode = Schema.Union(...TestViewModes.map((mode) => Schema.Literal(mode)));
|
|
61
|
+
|
|
62
|
+
const TestSettings = Schema.mutable(
|
|
63
|
+
Schema.Struct({
|
|
64
|
+
viewMode: TestViewMode.annotations({ title: 'View mode', description: 'Default document view mode.' }),
|
|
65
|
+
toolbar: Schema.optional(
|
|
66
|
+
Schema.Boolean.annotations({ title: 'Show toolbar', description: 'Display formatting toolbar.' }),
|
|
67
|
+
),
|
|
68
|
+
fontSize: Schema.optional(
|
|
69
|
+
Schema.Number.annotations({ title: 'Font size', description: 'Editor font size in pixels.' }),
|
|
70
|
+
),
|
|
71
|
+
placeholder: Schema.optional(
|
|
72
|
+
Schema.String.annotations({ title: 'Placeholder', description: 'Default placeholder text.' }),
|
|
73
|
+
),
|
|
74
|
+
debug: Schema.optional(Schema.Boolean.annotations({ title: 'Debug mode', description: 'Enable debug features.' })),
|
|
75
|
+
}),
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
type TestSettings = Schema.Schema.Type<typeof TestSettings>;
|
|
79
|
+
|
|
80
|
+
const FieldSetStory = () => {
|
|
81
|
+
const [values, setValues] = React.useState<TestSettings>({
|
|
82
|
+
viewMode: 'preview',
|
|
83
|
+
toolbar: true,
|
|
84
|
+
fontSize: 14,
|
|
85
|
+
debug: false,
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
return (
|
|
89
|
+
<Settings.Viewport>
|
|
90
|
+
<Settings.Section title='Plugin Settings (Auto-generated)'>
|
|
91
|
+
<Settings.FieldSet
|
|
92
|
+
schema={TestSettings}
|
|
93
|
+
values={values}
|
|
94
|
+
onValuesChanged={setValues}
|
|
95
|
+
visible={(path, values) => path !== 'placeholder' || !!values.debug}
|
|
96
|
+
/>
|
|
97
|
+
</Settings.Section>
|
|
98
|
+
</Settings.Viewport>
|
|
99
|
+
);
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
export const FieldSet: Story = {
|
|
103
|
+
render: FieldSetStory,
|
|
104
|
+
};
|
|
@@ -4,217 +4,69 @@
|
|
|
4
4
|
|
|
5
5
|
import React, { type PropsWithChildren } from 'react';
|
|
6
6
|
|
|
7
|
-
import {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
toLocalizedString,
|
|
15
|
-
useTranslation,
|
|
16
|
-
} from '@dxos/react-ui';
|
|
17
|
-
import { mx } from '@dxos/ui-theme';
|
|
18
|
-
|
|
19
|
-
import { translationKey } from '../../translations';
|
|
7
|
+
import { 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
|
+
import { SettingsFieldSet } from './SettingsFieldSet';
|
|
13
|
+
import { SettingsItem, SettingsPanel } from './SettingsItem';
|
|
20
14
|
|
|
21
15
|
const SETTINGS_ROOT_NAME = 'Settings.Root';
|
|
16
|
+
const SETTINGS_VIEWPORT_NAME = 'Settings.Viewport';
|
|
22
17
|
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: 'pbe-trimMd text-baseText text-lg',
|
|
34
|
-
description: 'text-base text-description',
|
|
35
|
-
grid: 'grid grid-cols-1 md:grid-cols-[3fr_2fr] gap-x-trimLg',
|
|
36
|
-
};
|
|
37
18
|
|
|
38
19
|
//
|
|
39
|
-
// Root
|
|
20
|
+
// Root (headless — no styling, no scroll).
|
|
40
21
|
//
|
|
41
22
|
|
|
42
|
-
type SettingsRootProps = PropsWithChildren
|
|
23
|
+
type SettingsRootProps = ThemedClassName<PropsWithChildren>;
|
|
43
24
|
|
|
44
|
-
const SettingsRoot = ({ children }: SettingsRootProps) => {
|
|
45
|
-
return (
|
|
46
|
-
<ScrollArea.Root orientation='vertical'>
|
|
47
|
-
<ScrollArea.Viewport classNames='p-trimMd'>{children}</ScrollArea.Viewport>
|
|
48
|
-
</ScrollArea.Root>
|
|
49
|
-
);
|
|
25
|
+
const SettingsRoot = ({ children, classNames }: SettingsRootProps) => {
|
|
26
|
+
return <div className={mx('flex flex-col gap-2', classNames)}>{children}</div>;
|
|
50
27
|
};
|
|
51
28
|
|
|
52
29
|
SettingsRoot.displayName = SETTINGS_ROOT_NAME;
|
|
53
30
|
|
|
54
31
|
//
|
|
55
|
-
//
|
|
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='is-full pbs-trimMd space-y-trimMd'>{children}</div>
|
|
68
|
-
</>
|
|
69
|
-
);
|
|
70
|
-
};
|
|
71
|
-
|
|
72
|
-
SettingsSection.displayName = SETTINGS_SECTION_NAME;
|
|
73
|
-
|
|
74
|
-
//
|
|
75
|
-
// Section Heading
|
|
32
|
+
// Viewport (scroll area + padding + document layout).
|
|
76
33
|
//
|
|
77
34
|
|
|
78
|
-
const
|
|
79
|
-
const { t } = useTranslation(translationKey);
|
|
35
|
+
const SettingsViewport = composable<HTMLDivElement>(({ children, ...props }, forwardedRef) => {
|
|
80
36
|
return (
|
|
81
|
-
|
|
82
|
-
<
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
</>
|
|
89
|
-
);
|
|
90
|
-
};
|
|
91
|
-
|
|
92
|
-
SettingsSectionHeading.displayName = SETTINGS_SECTION_HEADING_NAME;
|
|
93
|
-
|
|
94
|
-
//
|
|
95
|
-
// Group Button
|
|
96
|
-
//
|
|
97
|
-
|
|
98
|
-
const SettingsGroupButton = ({ classNames, ...props }: ButtonProps) => {
|
|
99
|
-
return <Button {...props} classNames={['md:col-span-2', classNames]} />;
|
|
100
|
-
};
|
|
101
|
-
|
|
102
|
-
SettingsGroupButton.displayName = SETTINGS_GROUP_BUTTON_NAME;
|
|
103
|
-
|
|
104
|
-
//
|
|
105
|
-
// Group
|
|
106
|
-
//
|
|
107
|
-
|
|
108
|
-
type SettingsGroupProps = ThemedClassName<PropsWithChildren>;
|
|
109
|
-
|
|
110
|
-
const SettingsGroup = ({ children, classNames }: SettingsGroupProps) => (
|
|
111
|
-
<div role='none' className={mx('group container-max-width space-y-trimMd', classNames)}>
|
|
112
|
-
{children}
|
|
113
|
-
</div>
|
|
114
|
-
);
|
|
115
|
-
|
|
116
|
-
SettingsGroup.displayName = SETTINGS_GROUP_NAME;
|
|
117
|
-
|
|
118
|
-
//
|
|
119
|
-
// Frame
|
|
120
|
-
//
|
|
121
|
-
|
|
122
|
-
const SettingsFrame = ({ children }: SettingsGroupProps) => (
|
|
123
|
-
<div role='none' className={mx('container-max-width p-trimMd', 'border border-separator rounded-md')}>
|
|
124
|
-
{children}
|
|
125
|
-
</div>
|
|
126
|
-
);
|
|
127
|
-
|
|
128
|
-
SettingsFrame.displayName = SETTINGS_FRAME_NAME;
|
|
129
|
-
|
|
130
|
-
//
|
|
131
|
-
// Frame Item
|
|
132
|
-
//
|
|
133
|
-
|
|
134
|
-
const SettingsFrameItem = ({ title, description, children }: SettingsItemProps) => {
|
|
135
|
-
const { t } = useTranslation(translationKey);
|
|
136
|
-
|
|
137
|
-
return (
|
|
138
|
-
<div role='group' className='min-is-0'>
|
|
139
|
-
<h3 className='text-lg mbe-2'>{toLocalizedString(title, t)}</h3>
|
|
140
|
-
{description && <p className='mlb-trimSm md:mbe-0 text-description'>{toLocalizedString(description, t)}</p>}
|
|
141
|
-
{children}
|
|
142
|
-
</div>
|
|
143
|
-
);
|
|
144
|
-
};
|
|
145
|
-
|
|
146
|
-
SettingsFrameItem.displayName = SETTINGS_FRAME_ITEM_NAME;
|
|
147
|
-
|
|
148
|
-
//
|
|
149
|
-
// Container
|
|
150
|
-
//
|
|
151
|
-
|
|
152
|
-
const SettingsContainer = ({ classNames, children }: ThemedClassName<PropsWithChildren>) => {
|
|
153
|
-
return (
|
|
154
|
-
<div
|
|
155
|
-
role='none'
|
|
156
|
-
className={mx([
|
|
157
|
-
'container-max-width',
|
|
158
|
-
'*:first:!mbs-0 *:last:!mbe-0 pli-trimMd plb-trimMd',
|
|
159
|
-
'border border-separator rounded-md',
|
|
160
|
-
classNames,
|
|
161
|
-
])}
|
|
162
|
-
>
|
|
163
|
-
{children}
|
|
164
|
-
</div>
|
|
37
|
+
<ScrollArea.Root {...composableProps(props)} orientation='vertical' centered thin ref={forwardedRef}>
|
|
38
|
+
<ScrollArea.Viewport classNames='px-4 pointer-coarse:px-2'>
|
|
39
|
+
<div role='none' className='dx-document flex flex-col gap-2 py-4'>
|
|
40
|
+
{children}
|
|
41
|
+
</div>
|
|
42
|
+
</ScrollArea.Viewport>
|
|
43
|
+
</ScrollArea.Root>
|
|
165
44
|
);
|
|
166
|
-
};
|
|
45
|
+
});
|
|
167
46
|
|
|
168
|
-
|
|
47
|
+
SettingsViewport.displayName = SETTINGS_VIEWPORT_NAME;
|
|
169
48
|
|
|
170
49
|
//
|
|
171
|
-
//
|
|
50
|
+
// Section (title + optional description + children with spacing).
|
|
172
51
|
//
|
|
173
52
|
|
|
174
|
-
type
|
|
53
|
+
type SettingsSectionProps = PropsWithChildren<{
|
|
175
54
|
title: Label;
|
|
176
55
|
description?: Label;
|
|
177
56
|
}>;
|
|
178
57
|
|
|
179
|
-
const
|
|
180
|
-
const { t } = useTranslation(translationKey);
|
|
181
|
-
|
|
182
|
-
return (
|
|
183
|
-
<SettingsContainer classNames={styles.grid}>
|
|
184
|
-
<h3 className={mx(styles.title, 'md:col-span-2')}>{toLocalizedString(title, t)}</h3>
|
|
185
|
-
<p className={styles.description}>{toLocalizedString(description, t)}</p>
|
|
186
|
-
<div role='none' className='text-end plb-1'>
|
|
187
|
-
{children}
|
|
188
|
-
</div>
|
|
189
|
-
</SettingsContainer>
|
|
190
|
-
);
|
|
191
|
-
};
|
|
192
|
-
|
|
193
|
-
SettingsItem.displayName = SETTINGS_ITEM_NAME;
|
|
194
|
-
|
|
195
|
-
//
|
|
196
|
-
// Item Input
|
|
197
|
-
//
|
|
198
|
-
|
|
199
|
-
const SettingsItemInput = ({ title, description = '', children }: SettingsItemProps) => {
|
|
58
|
+
const SettingsSection = ({ title, description, children }: SettingsSectionProps) => {
|
|
200
59
|
const { t } = useTranslation(translationKey);
|
|
201
|
-
|
|
202
60
|
return (
|
|
203
|
-
|
|
204
|
-
<
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
</Input.DescriptionAndValidation>
|
|
209
|
-
<div role='none' className='text-end plb-1'>
|
|
210
|
-
{children}
|
|
211
|
-
</div>
|
|
212
|
-
</SettingsContainer>
|
|
213
|
-
</Input.Root>
|
|
61
|
+
<>
|
|
62
|
+
<h2 className='px-trim-md mt-trim-md text-xl'>{toLocalizedString(title, t)}</h2>
|
|
63
|
+
{description && <p className='px-trim-md text-description'>{toLocalizedString(description, t)}</p>}
|
|
64
|
+
<div className='w-full pt-trim-md space-y-trim-md'>{children}</div>
|
|
65
|
+
</>
|
|
214
66
|
);
|
|
215
67
|
};
|
|
216
68
|
|
|
217
|
-
|
|
69
|
+
SettingsSection.displayName = SETTINGS_SECTION_NAME;
|
|
218
70
|
|
|
219
71
|
//
|
|
220
72
|
// Settings
|
|
@@ -222,13 +74,9 @@ SettingsItemInput.displayName = SETTINGS_ITEM_INPUT_NAME;
|
|
|
222
74
|
|
|
223
75
|
export const Settings = {
|
|
224
76
|
Root: SettingsRoot,
|
|
77
|
+
Viewport: SettingsViewport,
|
|
225
78
|
Section: SettingsSection,
|
|
226
|
-
|
|
227
|
-
Group: SettingsGroup,
|
|
228
|
-
GroupButton: SettingsGroupButton,
|
|
229
|
-
Container: SettingsContainer,
|
|
230
|
-
Frame: SettingsFrame,
|
|
231
|
-
FrameItem: SettingsFrameItem,
|
|
79
|
+
Panel: SettingsPanel,
|
|
232
80
|
Item: SettingsItem,
|
|
233
|
-
|
|
81
|
+
FieldSet: SettingsFieldSet,
|
|
234
82
|
};
|
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2025 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import * as SchemaAST from 'effect/SchemaAST';
|
|
6
|
+
import * as String from 'effect/String';
|
|
7
|
+
import React, { useCallback, useMemo } from 'react';
|
|
8
|
+
|
|
9
|
+
import { getAnnotation, type SchemaProperty } from '@dxos/effect';
|
|
10
|
+
import { Input, Select } from '@dxos/react-ui';
|
|
11
|
+
|
|
12
|
+
import { getFormProperties, type SelectOption, detectFieldType, getSelectOptionsFromAst } from '../../util';
|
|
13
|
+
import { SettingsItem } from './SettingsItem';
|
|
14
|
+
|
|
15
|
+
//
|
|
16
|
+
// Types
|
|
17
|
+
//
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Props for a custom field renderer.
|
|
21
|
+
* The renderer provides the control only; SettingsItem wrapper is handled by FieldSet.
|
|
22
|
+
*/
|
|
23
|
+
export type SettingsFieldProps<T = any> = {
|
|
24
|
+
value: T;
|
|
25
|
+
onChange: (value: T) => void;
|
|
26
|
+
readonly?: boolean;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Map of property names to either a custom field renderer or a nested field map for struct fields.
|
|
31
|
+
* When a value type `T` is provided, the map shape is checked against the schema's value type.
|
|
32
|
+
*/
|
|
33
|
+
export type SettingsFieldMap<T extends Record<string, any> = Record<string, any>> = {
|
|
34
|
+
[K in keyof T]?: NonNullable<T[K]> extends Record<string, any>
|
|
35
|
+
? SettingsFieldMap<NonNullable<T[K]>>
|
|
36
|
+
: React.FC<SettingsFieldProps<T[K]>>;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
export type SettingsFieldSetProps<T extends Record<string, any> = Record<string, any>> = {
|
|
40
|
+
/** Effect Schema for the settings object. */
|
|
41
|
+
schema: { ast: SchemaAST.AST };
|
|
42
|
+
|
|
43
|
+
/** Current settings values. */
|
|
44
|
+
values: T;
|
|
45
|
+
|
|
46
|
+
/** Callback when any value changes. Receives a new complete object. */
|
|
47
|
+
onValuesChanged?: (values: T) => void;
|
|
48
|
+
|
|
49
|
+
/** When true, all controls are disabled. */
|
|
50
|
+
readonly?: boolean;
|
|
51
|
+
|
|
52
|
+
/** Map of property names to custom field renderers. Nested objects map to struct sub-fields. */
|
|
53
|
+
fieldMap?: SettingsFieldMap<T>;
|
|
54
|
+
|
|
55
|
+
/** Control field visibility. Receives the dotted path (e.g. `'modelDefaults.edge'`) at every level. */
|
|
56
|
+
visible?: (path: string, values: T) => boolean;
|
|
57
|
+
|
|
58
|
+
/** Override the order of fields. Fields not listed are appended in schema order. Applied per scope. */
|
|
59
|
+
sort?: string[];
|
|
60
|
+
|
|
61
|
+
/** Internal: dot-path prefix used by recursive struct rendering. */
|
|
62
|
+
pathPrefix?: string;
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
//
|
|
66
|
+
// Component
|
|
67
|
+
//
|
|
68
|
+
|
|
69
|
+
export const SettingsFieldSet = <T extends Record<string, any>>({
|
|
70
|
+
schema,
|
|
71
|
+
values,
|
|
72
|
+
onValuesChanged,
|
|
73
|
+
readonly,
|
|
74
|
+
fieldMap,
|
|
75
|
+
visible,
|
|
76
|
+
sort,
|
|
77
|
+
pathPrefix = '',
|
|
78
|
+
}: SettingsFieldSetProps<T>) => {
|
|
79
|
+
const properties = useMemo(() => {
|
|
80
|
+
const props = getFormProperties(schema.ast);
|
|
81
|
+
if (!sort) {
|
|
82
|
+
return props;
|
|
83
|
+
}
|
|
84
|
+
const rank = (name: string) => {
|
|
85
|
+
const idx = sort.indexOf(name);
|
|
86
|
+
return idx === -1 ? Number.POSITIVE_INFINITY : idx;
|
|
87
|
+
};
|
|
88
|
+
return [...props].sort(({ name: a }, { name: b }) => rank(a.toString()) - rank(b.toString()));
|
|
89
|
+
}, [schema, sort]);
|
|
90
|
+
|
|
91
|
+
const handleChange = useCallback(
|
|
92
|
+
(name: string, value: unknown) => {
|
|
93
|
+
onValuesChanged?.({ ...values, [name]: value } as T);
|
|
94
|
+
},
|
|
95
|
+
[values, onValuesChanged],
|
|
96
|
+
);
|
|
97
|
+
|
|
98
|
+
return (
|
|
99
|
+
<>
|
|
100
|
+
{properties.map((property) => {
|
|
101
|
+
const name = property.name.toString();
|
|
102
|
+
const path = pathPrefix ? `${pathPrefix}.${name}` : name;
|
|
103
|
+
if (visible && !visible(path, values)) {
|
|
104
|
+
return null;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const entry = fieldMap?.[name as keyof typeof fieldMap] as
|
|
108
|
+
| React.FC<SettingsFieldProps>
|
|
109
|
+
| SettingsFieldMap
|
|
110
|
+
| undefined;
|
|
111
|
+
return (
|
|
112
|
+
<SettingsFieldItem
|
|
113
|
+
key={name}
|
|
114
|
+
path={path}
|
|
115
|
+
property={property}
|
|
116
|
+
value={values[name]}
|
|
117
|
+
onChange={(value) => handleChange(name, value)}
|
|
118
|
+
readonly={readonly}
|
|
119
|
+
customField={typeof entry === 'function' ? entry : undefined}
|
|
120
|
+
nestedFieldMap={typeof entry === 'object' ? entry : undefined}
|
|
121
|
+
visible={visible as (path: string, values: any) => boolean}
|
|
122
|
+
sort={sort}
|
|
123
|
+
/>
|
|
124
|
+
);
|
|
125
|
+
})}
|
|
126
|
+
</>
|
|
127
|
+
);
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
SettingsFieldSet.displayName = 'Settings.FieldSet';
|
|
131
|
+
|
|
132
|
+
//
|
|
133
|
+
// Internal: render a single settings field.
|
|
134
|
+
//
|
|
135
|
+
|
|
136
|
+
type SettingsFieldItemProps = {
|
|
137
|
+
property: SchemaProperty;
|
|
138
|
+
path: string;
|
|
139
|
+
value: any;
|
|
140
|
+
onChange: (value: any) => void;
|
|
141
|
+
readonly?: boolean;
|
|
142
|
+
customField?: React.FC<SettingsFieldProps>;
|
|
143
|
+
nestedFieldMap?: SettingsFieldMap;
|
|
144
|
+
visible?: (path: string, values: any) => boolean;
|
|
145
|
+
sort?: string[];
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
const SettingsFieldItem = ({
|
|
149
|
+
property,
|
|
150
|
+
path,
|
|
151
|
+
value,
|
|
152
|
+
onChange,
|
|
153
|
+
readonly,
|
|
154
|
+
customField,
|
|
155
|
+
nestedFieldMap,
|
|
156
|
+
visible,
|
|
157
|
+
sort,
|
|
158
|
+
}: SettingsFieldItemProps) => {
|
|
159
|
+
const { type } = property;
|
|
160
|
+
const name = property.name.toString();
|
|
161
|
+
const title = getAnnotation<string>(SchemaAST.TitleAnnotationId)(type) ?? String.capitalize(name);
|
|
162
|
+
const description = getAnnotation<string>(SchemaAST.DescriptionAnnotationId)(type);
|
|
163
|
+
|
|
164
|
+
// Custom field renderer.
|
|
165
|
+
if (customField) {
|
|
166
|
+
const CustomField = customField;
|
|
167
|
+
return (
|
|
168
|
+
<SettingsItem title={title} description={description}>
|
|
169
|
+
<CustomField value={value} onChange={onChange} readonly={readonly} />
|
|
170
|
+
</SettingsItem>
|
|
171
|
+
);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const fieldType = detectFieldType(type);
|
|
175
|
+
|
|
176
|
+
switch (fieldType) {
|
|
177
|
+
case 'struct':
|
|
178
|
+
return (
|
|
179
|
+
<SettingsFieldSet
|
|
180
|
+
schema={{ ast: type }}
|
|
181
|
+
values={value ?? {}}
|
|
182
|
+
onValuesChanged={onChange}
|
|
183
|
+
readonly={readonly}
|
|
184
|
+
fieldMap={nestedFieldMap}
|
|
185
|
+
visible={visible}
|
|
186
|
+
sort={sort}
|
|
187
|
+
pathPrefix={path}
|
|
188
|
+
/>
|
|
189
|
+
);
|
|
190
|
+
|
|
191
|
+
case 'boolean':
|
|
192
|
+
return (
|
|
193
|
+
<SettingsItem title={title} description={description}>
|
|
194
|
+
<Input.Switch disabled={readonly} checked={!!value} onCheckedChange={(checked) => onChange(!!checked)} />
|
|
195
|
+
</SettingsItem>
|
|
196
|
+
);
|
|
197
|
+
|
|
198
|
+
case 'select': {
|
|
199
|
+
const options = getSelectOptionsFromAst(type) ?? [];
|
|
200
|
+
return (
|
|
201
|
+
<SettingsItem title={title} description={description}>
|
|
202
|
+
<Select.Root
|
|
203
|
+
disabled={readonly}
|
|
204
|
+
value={value == null ? '' : globalThis.String(value)}
|
|
205
|
+
onValueChange={(selected) => {
|
|
206
|
+
const matched = options.find((option) => globalThis.String(option.value) === selected);
|
|
207
|
+
onChange(matched?.value ?? selected);
|
|
208
|
+
}}
|
|
209
|
+
>
|
|
210
|
+
<Select.TriggerButton disabled={readonly} />
|
|
211
|
+
<Select.Portal>
|
|
212
|
+
<Select.Content>
|
|
213
|
+
<Select.Viewport>
|
|
214
|
+
{options.map((option: SelectOption) => (
|
|
215
|
+
<Select.Option key={globalThis.String(option.value)} value={globalThis.String(option.value)}>
|
|
216
|
+
{option.label ?? globalThis.String(option.value)}
|
|
217
|
+
</Select.Option>
|
|
218
|
+
))}
|
|
219
|
+
</Select.Viewport>
|
|
220
|
+
<Select.Arrow />
|
|
221
|
+
</Select.Content>
|
|
222
|
+
</Select.Portal>
|
|
223
|
+
</Select.Root>
|
|
224
|
+
</SettingsItem>
|
|
225
|
+
);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
case 'string':
|
|
229
|
+
return (
|
|
230
|
+
<SettingsItem title={title} description={description}>
|
|
231
|
+
<Input.TextInput disabled={readonly} value={value ?? ''} onChange={(event) => onChange(event.target.value)} />
|
|
232
|
+
</SettingsItem>
|
|
233
|
+
);
|
|
234
|
+
|
|
235
|
+
case 'number':
|
|
236
|
+
return (
|
|
237
|
+
<SettingsItem title={title} description={description}>
|
|
238
|
+
<Input.TextInput
|
|
239
|
+
disabled={readonly}
|
|
240
|
+
type='number'
|
|
241
|
+
value={value ?? ''}
|
|
242
|
+
onChange={(event) => {
|
|
243
|
+
if (event.target.value === '') {
|
|
244
|
+
onChange(undefined);
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
const parsed = Number(event.target.value);
|
|
248
|
+
onChange(Number.isNaN(parsed) ? undefined : parsed);
|
|
249
|
+
}}
|
|
250
|
+
/>
|
|
251
|
+
</SettingsItem>
|
|
252
|
+
);
|
|
253
|
+
|
|
254
|
+
default:
|
|
255
|
+
return null;
|
|
256
|
+
}
|
|
257
|
+
};
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2025 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import React, { type PropsWithChildren } from 'react';
|
|
6
|
+
|
|
7
|
+
import { Input, type Label, type ThemedClassName, toLocalizedString, useTranslation } from '@dxos/react-ui';
|
|
8
|
+
import { mx } from '@dxos/ui-theme';
|
|
9
|
+
|
|
10
|
+
import { translationKey } from '#translations';
|
|
11
|
+
|
|
12
|
+
const SETTINGS_PANEL_NAME = 'Settings.Panel';
|
|
13
|
+
const SETTINGS_ITEM_NAME = 'Settings.Item';
|
|
14
|
+
|
|
15
|
+
//
|
|
16
|
+
// Panel (bordered card container).
|
|
17
|
+
//
|
|
18
|
+
|
|
19
|
+
type SettingsPanelProps = ThemedClassName<PropsWithChildren>;
|
|
20
|
+
|
|
21
|
+
export const SettingsPanel = ({ classNames, children }: SettingsPanelProps) => {
|
|
22
|
+
return (
|
|
23
|
+
<div role='none' className={mx('flex flex-col gap-3 p-trim-md border border-separator rounded-sm', classNames)}>
|
|
24
|
+
{children}
|
|
25
|
+
</div>
|
|
26
|
+
);
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
SettingsPanel.displayName = SETTINGS_PANEL_NAME;
|
|
30
|
+
|
|
31
|
+
//
|
|
32
|
+
// Item (label + optional description + control, proper label semantics).
|
|
33
|
+
//
|
|
34
|
+
|
|
35
|
+
type SettingsItemProps = PropsWithChildren<{
|
|
36
|
+
title: Label;
|
|
37
|
+
description?: Label;
|
|
38
|
+
}>;
|
|
39
|
+
|
|
40
|
+
export const SettingsItem = ({ title, description = '', children }: SettingsItemProps) => {
|
|
41
|
+
const { t } = useTranslation(translationKey);
|
|
42
|
+
|
|
43
|
+
return (
|
|
44
|
+
<Input.Root>
|
|
45
|
+
<SettingsPanel classNames='grid grid-cols-1 md:grid-cols-[1fr_1fr] gap-x-trim-lg gap-y-0'>
|
|
46
|
+
<Input.Label classNames='pb-trim-md text-base-surface-text text-lg md:col-span-2'>
|
|
47
|
+
{toLocalizedString(title, t)}
|
|
48
|
+
</Input.Label>
|
|
49
|
+
<Input.DescriptionAndValidation>
|
|
50
|
+
<Input.Description classNames='text-base text-description'>
|
|
51
|
+
{toLocalizedString(description, t)}
|
|
52
|
+
</Input.Description>
|
|
53
|
+
</Input.DescriptionAndValidation>
|
|
54
|
+
<div role='none' className='text-end py-1'>
|
|
55
|
+
{children}
|
|
56
|
+
</div>
|
|
57
|
+
</SettingsPanel>
|
|
58
|
+
</Input.Root>
|
|
59
|
+
);
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
SettingsItem.displayName = SETTINGS_ITEM_NAME;
|