@dxos/react-ui-form 0.9.1-main.c7dcc2e112 → 0.10.0
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/dist/lib/browser/index.mjs +1883 -1539
- package/dist/lib/browser/index.mjs.map +4 -4
- package/dist/lib/browser/meta.json +1 -1
- package/dist/lib/node-esm/index.mjs +1883 -1539
- package/dist/lib/node-esm/index.mjs.map +4 -4
- package/dist/lib/node-esm/meta.json +1 -1
- package/dist/types/src/annotations.d.ts +47 -0
- package/dist/types/src/annotations.d.ts.map +1 -0
- package/dist/types/src/components/FieldEditor/FieldEditor.d.ts.map +1 -1
- package/dist/types/src/components/Form/Form.d.ts +12 -4
- package/dist/types/src/components/Form/Form.d.ts.map +1 -1
- package/dist/types/src/components/Form/Form.stories.d.ts +46 -65
- package/dist/types/src/components/Form/Form.stories.d.ts.map +1 -1
- package/dist/types/src/components/Form/Form.theme.d.ts +252 -0
- package/dist/types/src/components/Form/Form.theme.d.ts.map +1 -0
- package/dist/types/src/components/Form/Form.theme.test.d.ts +2 -0
- package/dist/types/src/components/Form/Form.theme.test.d.ts.map +1 -0
- package/dist/types/src/components/Form/FormControls.d.ts +17 -8
- package/dist/types/src/components/Form/FormControls.d.ts.map +1 -1
- package/dist/types/src/components/Form/FormField/FormField.d.ts +16 -2
- package/dist/types/src/components/Form/FormField/FormField.d.ts.map +1 -1
- package/dist/types/src/components/Form/FormField/FormFieldHeader.d.ts +27 -0
- package/dist/types/src/components/Form/FormField/FormFieldHeader.d.ts.map +1 -0
- package/dist/types/src/components/Form/FormField/FormRow.d.ts +85 -0
- package/dist/types/src/components/Form/FormField/FormRow.d.ts.map +1 -0
- package/dist/types/src/components/Form/FormField/{FormFieldWrapper.stories.d.ts → FormRow.stories.d.ts} +4 -1
- package/dist/types/src/components/Form/FormField/FormRow.stories.d.ts.map +1 -0
- package/dist/types/src/components/Form/FormField/fields/ArrayField/ArrayField.d.ts.map +1 -1
- package/dist/types/src/components/Form/FormField/fields/ArrayField/ArrayField.stories.d.ts +1 -0
- package/dist/types/src/components/Form/FormField/fields/ArrayField/ArrayField.stories.d.ts.map +1 -1
- package/dist/types/src/components/Form/FormField/fields/AsyncSelectField/AsyncSelectField.d.ts +12 -0
- package/dist/types/src/components/Form/FormField/fields/AsyncSelectField/AsyncSelectField.d.ts.map +1 -0
- package/dist/types/src/components/Form/FormField/fields/AsyncSelectField/index.d.ts +2 -0
- package/dist/types/src/components/Form/FormField/fields/AsyncSelectField/index.d.ts.map +1 -0
- package/dist/types/src/components/Form/FormField/fields/AutofillField/AutofillField.d.ts +12 -0
- package/dist/types/src/components/Form/FormField/fields/AutofillField/AutofillField.d.ts.map +1 -0
- package/dist/types/src/components/Form/FormField/fields/AutofillField/index.d.ts +2 -0
- package/dist/types/src/components/Form/FormField/fields/AutofillField/index.d.ts.map +1 -0
- package/dist/types/src/components/Form/FormField/fields/BooleanField/BooleanField.d.ts.map +1 -1
- package/dist/types/src/components/Form/FormField/fields/ComboboxField/ComboboxField.d.ts +12 -0
- package/dist/types/src/components/Form/FormField/fields/ComboboxField/ComboboxField.d.ts.map +1 -0
- package/dist/types/src/components/Form/FormField/fields/ComboboxField/index.d.ts +2 -0
- package/dist/types/src/components/Form/FormField/fields/ComboboxField/index.d.ts.map +1 -0
- package/dist/types/src/components/Form/FormField/fields/GeoPointField/GeoPointField.d.ts +1 -1
- package/dist/types/src/components/Form/FormField/fields/GeoPointField/GeoPointField.d.ts.map +1 -1
- package/dist/types/src/components/Form/FormField/fields/MarkdownField/MarkdownField.d.ts +1 -1
- package/dist/types/src/components/Form/FormField/fields/MarkdownField/MarkdownField.d.ts.map +1 -1
- package/dist/types/src/components/Form/FormField/fields/NumberField/NumberField.d.ts +11 -0
- package/dist/types/src/components/Form/FormField/fields/NumberField/NumberField.d.ts.map +1 -1
- package/dist/types/src/components/Form/FormField/fields/NumberField/NumberField.test.d.ts +2 -0
- package/dist/types/src/components/Form/FormField/fields/NumberField/NumberField.test.d.ts.map +1 -0
- package/dist/types/src/components/Form/FormField/fields/PasswordField/PasswordField.d.ts +4 -0
- package/dist/types/src/components/Form/FormField/fields/PasswordField/PasswordField.d.ts.map +1 -0
- package/dist/types/src/components/{Settings/Settings.stories.d.ts → Form/FormField/fields/PasswordField/PasswordField.stories.d.ts} +1 -2
- package/dist/types/src/components/Form/FormField/fields/PasswordField/PasswordField.stories.d.ts.map +1 -0
- package/dist/types/src/components/Form/FormField/fields/PasswordField/index.d.ts +2 -0
- package/dist/types/src/components/Form/FormField/fields/PasswordField/index.d.ts.map +1 -0
- package/dist/types/src/components/Form/FormField/fields/RefField/InlineRefField.d.ts.map +1 -1
- package/dist/types/src/components/Form/FormField/fields/RefField/RefField.d.ts.map +1 -1
- package/dist/types/src/components/Form/FormField/fields/SelectField/SelectField.d.ts +18 -2
- package/dist/types/src/components/Form/FormField/fields/SelectField/SelectField.d.ts.map +1 -1
- package/dist/types/src/components/Form/FormField/fields/index.d.ts +4 -0
- package/dist/types/src/components/Form/FormField/fields/index.d.ts.map +1 -1
- package/dist/types/src/components/Form/FormField/index.d.ts +4 -3
- package/dist/types/src/components/Form/FormField/index.d.ts.map +1 -1
- package/dist/types/src/components/Form/FormField/presentation.d.ts +22 -0
- package/dist/types/src/components/Form/FormField/presentation.d.ts.map +1 -0
- package/dist/types/src/components/Form/FormFieldSet/FormFieldSet.d.ts +8 -5
- package/dist/types/src/components/Form/FormFieldSet/FormFieldSet.d.ts.map +1 -1
- package/dist/types/src/components/Form/FormFieldSet/FormFieldSetContainer.d.ts +22 -0
- package/dist/types/src/components/Form/FormFieldSet/FormFieldSetContainer.d.ts.map +1 -0
- package/dist/types/src/components/Form/FormFieldSet/index.d.ts +1 -0
- package/dist/types/src/components/Form/FormFieldSet/index.d.ts.map +1 -1
- package/dist/types/src/components/Form/FormLayout/FormLayout.d.ts +2 -0
- package/dist/types/src/components/Form/FormLayout/FormLayout.d.ts.map +1 -1
- package/dist/types/src/components/Form/FormLayout/FormLayout.stories.d.ts +5 -5
- package/dist/types/src/components/Form/FormLayout/FormLayout.stories.d.ts.map +1 -1
- package/dist/types/src/components/Form/FormLayout/index.d.ts +1 -1
- package/dist/types/src/components/Form/FormLayout/index.d.ts.map +1 -1
- package/dist/types/src/components/Form/meta-tags.d.ts +1 -12
- package/dist/types/src/components/Form/meta-tags.d.ts.map +1 -1
- package/dist/types/src/components/ObjectPicker/ObjectPicker.d.ts.map +1 -1
- package/dist/types/src/components/ObjectProperties/ObjectProperties.d.ts +2 -1
- package/dist/types/src/components/ObjectProperties/ObjectProperties.d.ts.map +1 -1
- package/dist/types/src/components/RefEditor/RefEditor.stories.d.ts +2 -2
- package/dist/types/src/components/RefEditor/RefEditor.stories.d.ts.map +1 -1
- package/dist/types/src/components/ViewEditor/ViewEditor.d.ts.map +1 -1
- package/dist/types/src/components/ViewEditor/ViewEditor.stories.d.ts +2 -2
- package/dist/types/src/components/ViewEditor/ViewEditor.stories.d.ts.map +1 -1
- package/dist/types/src/components/index.d.ts +0 -1
- package/dist/types/src/components/index.d.ts.map +1 -1
- package/dist/types/src/hooks/index.d.ts +1 -0
- package/dist/types/src/hooks/index.d.ts.map +1 -1
- package/dist/types/src/hooks/useAsyncFieldEffect.d.ts +24 -0
- package/dist/types/src/hooks/useAsyncFieldEffect.d.ts.map +1 -0
- package/dist/types/src/hooks/useFormContext.d.ts +8 -3
- package/dist/types/src/hooks/useFormContext.d.ts.map +1 -1
- package/dist/types/src/hooks/useFormHandler.d.ts +8 -2
- package/dist/types/src/hooks/useFormHandler.d.ts.map +1 -1
- package/dist/types/src/index.d.ts +1 -0
- package/dist/types/src/index.d.ts.map +1 -1
- package/dist/types/src/testing/TestLayout.d.ts +1 -1
- package/dist/types/src/testing/TestLayout.d.ts.map +1 -1
- package/dist/types/src/testing/schema.d.ts +13 -16
- package/dist/types/src/testing/schema.d.ts.map +1 -1
- package/dist/types/src/types.d.ts +50 -1
- package/dist/types/src/types.d.ts.map +1 -1
- package/dist/types/src/util/index.d.ts +0 -1
- package/dist/types/src/util/index.d.ts.map +1 -1
- package/dist/types/src/util/omit.d.ts +1 -1
- package/dist/types/src/util/omit.d.ts.map +1 -1
- package/dist/types/tsconfig.tsbuildinfo +1 -1
- package/package.json +41 -40
- package/src/annotations.ts +63 -0
- package/src/components/FieldEditor/FieldEditor.tsx +5 -2
- package/src/components/Form/DESIGN.md +24 -0
- package/src/components/Form/Form.stories.tsx +257 -13
- package/src/components/Form/Form.theme.test.ts +33 -0
- package/src/components/Form/Form.theme.ts +86 -0
- package/src/components/Form/Form.tsx +15 -9
- package/src/components/Form/FormControls.tsx +94 -37
- package/src/components/Form/FormField/FormField.tsx +83 -32
- package/src/components/Form/FormField/FormFieldHeader.tsx +68 -0
- package/src/components/Form/FormField/{FormFieldWrapper.stories.tsx → FormRow.stories.tsx} +34 -9
- package/src/components/Form/FormField/FormRow.tsx +298 -0
- package/src/components/Form/FormField/fields/ArrayField/ArrayField.stories.tsx +25 -9
- package/src/components/Form/FormField/fields/ArrayField/ArrayField.tsx +83 -24
- package/src/components/Form/FormField/fields/AsyncSelectField/AsyncSelectField.tsx +59 -0
- package/src/components/Form/FormField/fields/AsyncSelectField/index.ts +5 -0
- package/src/components/Form/FormField/fields/AutofillField/AutofillField.tsx +67 -0
- package/src/components/Form/FormField/fields/AutofillField/index.ts +5 -0
- package/src/components/Form/FormField/fields/BooleanField/BooleanField.tsx +5 -6
- package/src/components/Form/FormField/fields/ComboboxField/ComboboxField.tsx +104 -0
- package/src/components/Form/FormField/fields/ComboboxField/index.ts +5 -0
- package/src/components/Form/FormField/fields/DateField/DateField.tsx +9 -9
- package/src/components/Form/FormField/fields/GeoPointField/GeoPointField.tsx +17 -20
- package/src/components/Form/FormField/fields/MarkdownField/MarkdownField.stories.tsx +2 -2
- package/src/components/Form/FormField/fields/MarkdownField/MarkdownField.tsx +39 -40
- package/src/components/Form/FormField/fields/NumberField/NumberField.test.ts +29 -0
- package/src/components/Form/FormField/fields/NumberField/NumberField.tsx +60 -21
- package/src/components/Form/FormField/fields/PasswordField/PasswordField.stories.tsx +53 -0
- package/src/components/Form/FormField/fields/PasswordField/PasswordField.tsx +41 -0
- package/src/components/Form/FormField/fields/PasswordField/index.ts +5 -0
- package/src/components/Form/FormField/fields/RefField/InlineRefField.tsx +16 -10
- package/src/components/Form/FormField/fields/RefField/RefField.test.ts +8 -0
- package/src/components/Form/FormField/fields/RefField/RefField.tsx +50 -23
- package/src/components/Form/FormField/fields/SelectField/SelectField.tsx +104 -47
- package/src/components/Form/FormField/fields/SelectOptionField/SelectOptionField.tsx +3 -3
- package/src/components/Form/FormField/fields/TextAreaField/TextAreaField.tsx +3 -3
- package/src/components/Form/FormField/fields/TextField/TextField.tsx +3 -3
- package/src/components/Form/FormField/fields/TupleField/TupleField.tsx +1 -1
- package/src/components/Form/FormField/fields/index.ts +4 -0
- package/src/components/Form/FormField/index.ts +5 -3
- package/src/components/Form/FormField/presentation.tsx +32 -0
- package/src/components/Form/FormFieldSet/FormFieldSet.tsx +49 -70
- package/src/components/Form/FormFieldSet/FormFieldSetContainer.tsx +93 -0
- package/src/components/Form/FormFieldSet/index.ts +1 -0
- package/src/components/Form/FormLayout/FormLayout.stories.tsx +7 -5
- package/src/components/Form/FormLayout/FormLayout.tsx +24 -14
- package/src/components/Form/FormLayout/index.ts +1 -1
- package/src/components/Form/meta-tags.test.ts +1 -1
- package/src/components/ObjectForm/ObjectForm.tsx +2 -2
- package/src/components/ObjectPicker/ObjectPicker.tsx +1 -0
- package/src/components/ObjectProperties/ObjectProperties.stories.tsx +1 -1
- package/src/components/ObjectProperties/ObjectProperties.tsx +12 -8
- package/src/components/RefEditor/RefEditor.stories.tsx +2 -2
- package/src/components/RefEditor/RefEditor.tsx +1 -1
- package/src/components/ViewEditor/ViewEditor.stories.tsx +2 -2
- package/src/components/ViewEditor/ViewEditor.test.tsx +8 -2
- package/src/components/ViewEditor/ViewEditor.tsx +3 -3
- package/src/components/index.ts +0 -1
- package/src/hooks/index.ts +1 -0
- package/src/hooks/useAsyncFieldEffect.ts +81 -0
- package/src/hooks/useFormContext.ts +6 -3
- package/src/hooks/useFormHandler.ts +11 -6
- package/src/index.ts +1 -0
- package/src/testing/TestLayout.tsx +15 -12
- package/src/testing/schema.ts +43 -43
- package/src/types.ts +53 -1
- package/src/util/index.ts +0 -1
- package/src/util/omit.ts +25 -11
- package/src/util/properties.test.ts +110 -28
- package/src/util/refs.ts +1 -1
- package/dist/types/src/components/Form/FormField/FieldHeader.d.ts +0 -19
- package/dist/types/src/components/Form/FormField/FieldHeader.d.ts.map +0 -1
- package/dist/types/src/components/Form/FormField/FormFieldWrapper.d.ts +0 -47
- package/dist/types/src/components/Form/FormField/FormFieldWrapper.d.ts.map +0 -1
- package/dist/types/src/components/Form/FormField/FormFieldWrapper.stories.d.ts.map +0 -1
- package/dist/types/src/components/Settings/Settings.d.ts +0 -51
- package/dist/types/src/components/Settings/Settings.d.ts.map +0 -1
- package/dist/types/src/components/Settings/Settings.stories.d.ts.map +0 -1
- package/dist/types/src/components/Settings/SettingsFieldSet.d.ts +0 -43
- package/dist/types/src/components/Settings/SettingsFieldSet.d.ts.map +0 -1
- package/dist/types/src/components/Settings/SettingsItem.d.ts +0 -16
- package/dist/types/src/components/Settings/SettingsItem.d.ts.map +0 -1
- package/dist/types/src/components/Settings/SettingsSelectField.d.ts +0 -18
- package/dist/types/src/components/Settings/SettingsSelectField.d.ts.map +0 -1
- package/dist/types/src/components/Settings/index.d.ts +0 -4
- package/dist/types/src/components/Settings/index.d.ts.map +0 -1
- package/dist/types/src/util/field-type.d.ts +0 -19
- package/dist/types/src/util/field-type.d.ts.map +0 -1
- package/src/components/Form/FormField/FieldHeader.tsx +0 -36
- package/src/components/Form/FormField/FormFieldWrapper.tsx +0 -179
- package/src/components/Settings/AUDIT.md +0 -51
- package/src/components/Settings/Settings.stories.tsx +0 -104
- package/src/components/Settings/Settings.tsx +0 -81
- package/src/components/Settings/SettingsFieldSet.tsx +0 -257
- package/src/components/Settings/SettingsItem.tsx +0 -58
- package/src/components/Settings/SettingsSelectField.tsx +0 -59
- package/src/components/Settings/index.ts +0 -7
- package/src/util/field-type.ts +0 -60
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dxos/react-ui-form",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.0",
|
|
4
4
|
"description": "Data components.",
|
|
5
5
|
"homepage": "https://dxos.org",
|
|
6
6
|
"bugs": "https://github.com/dxos/dxos/issues",
|
|
@@ -44,52 +44,53 @@
|
|
|
44
44
|
"@radix-ui/react-use-controllable-state": "1.1.0",
|
|
45
45
|
"date-fns": "^3.6.0",
|
|
46
46
|
"tabster": "^8.5.5",
|
|
47
|
-
"@dxos/
|
|
48
|
-
"@dxos/echo-
|
|
49
|
-
"@dxos/echo
|
|
50
|
-
"@dxos/
|
|
51
|
-
"@dxos/
|
|
52
|
-
"@dxos/
|
|
53
|
-
"@dxos/
|
|
54
|
-
"@dxos/lit-ui": "0.
|
|
55
|
-
"@dxos/
|
|
56
|
-
"@dxos/
|
|
57
|
-
"@dxos/react-
|
|
58
|
-
"@dxos/react-ui-
|
|
59
|
-
"@dxos/
|
|
60
|
-
"@dxos/react-ui-
|
|
61
|
-
"@dxos/react-ui-search": "0.
|
|
62
|
-
"@dxos/
|
|
63
|
-
"@dxos/
|
|
64
|
-
"@dxos/
|
|
65
|
-
"@dxos/
|
|
66
|
-
"@dxos/ui
|
|
67
|
-
"@dxos/
|
|
47
|
+
"@dxos/async": "0.10.0",
|
|
48
|
+
"@dxos/echo-doc": "0.10.0",
|
|
49
|
+
"@dxos/echo": "0.10.0",
|
|
50
|
+
"@dxos/echo-protocol": "0.10.0",
|
|
51
|
+
"@dxos/echo-react": "0.10.0",
|
|
52
|
+
"@dxos/keys": "0.10.0",
|
|
53
|
+
"@dxos/log": "0.10.0",
|
|
54
|
+
"@dxos/lit-ui": "0.10.0",
|
|
55
|
+
"@dxos/effect": "0.10.0",
|
|
56
|
+
"@dxos/react-client": "0.10.0",
|
|
57
|
+
"@dxos/react-ui-components": "0.10.0",
|
|
58
|
+
"@dxos/react-ui-list": "0.10.0",
|
|
59
|
+
"@dxos/invariant": "0.10.0",
|
|
60
|
+
"@dxos/react-ui-editor": "0.10.0",
|
|
61
|
+
"@dxos/react-ui-search": "0.10.0",
|
|
62
|
+
"@dxos/react-ui-markdown": "0.10.0",
|
|
63
|
+
"@dxos/schema": "0.10.0",
|
|
64
|
+
"@dxos/types": "0.10.0",
|
|
65
|
+
"@dxos/react-ui-pickers": "0.10.0",
|
|
66
|
+
"@dxos/ui": "0.10.0",
|
|
67
|
+
"@dxos/ui-editor": "0.10.0",
|
|
68
|
+
"@dxos/util": "0.10.0"
|
|
68
69
|
},
|
|
69
70
|
"devDependencies": {
|
|
70
|
-
"@types/react": "~19.2.
|
|
71
|
+
"@types/react": "~19.2.17",
|
|
71
72
|
"@types/react-dom": "~19.2.3",
|
|
72
|
-
"effect": "3.21.
|
|
73
|
+
"effect": "3.21.4",
|
|
73
74
|
"fast-check": "^3.19.0",
|
|
74
|
-
"react": "~19.2.
|
|
75
|
-
"react-dom": "~19.2.
|
|
75
|
+
"react": "~19.2.7",
|
|
76
|
+
"react-dom": "~19.2.7",
|
|
76
77
|
"vite": "^8.0.16",
|
|
77
|
-
"@dxos/
|
|
78
|
-
"@dxos/
|
|
79
|
-
"@dxos/
|
|
80
|
-
"@dxos/react-ui": "0.
|
|
81
|
-
"@dxos/react-
|
|
82
|
-
"@dxos/
|
|
83
|
-
"@dxos/
|
|
84
|
-
"@dxos/ui-
|
|
85
|
-
"@dxos/
|
|
78
|
+
"@dxos/effect-proto": "0.10.0",
|
|
79
|
+
"@dxos/protocols": "0.10.0",
|
|
80
|
+
"@dxos/random": "0.10.0",
|
|
81
|
+
"@dxos/react-ui": "0.10.0",
|
|
82
|
+
"@dxos/react-client": "0.10.0",
|
|
83
|
+
"@dxos/react-ui-syntax-highlighter": "0.10.0",
|
|
84
|
+
"@dxos/storybook-utils": "0.10.0",
|
|
85
|
+
"@dxos/ui-theme": "0.10.0",
|
|
86
|
+
"@dxos/ui-types": "0.10.0"
|
|
86
87
|
},
|
|
87
88
|
"peerDependencies": {
|
|
88
|
-
"effect": "3.21.
|
|
89
|
-
"react": "~19.2.
|
|
90
|
-
"react-dom": "~19.2.
|
|
91
|
-
"@dxos/
|
|
92
|
-
"@dxos/ui
|
|
89
|
+
"effect": "3.21.4",
|
|
90
|
+
"react": "~19.2.7",
|
|
91
|
+
"react-dom": "~19.2.7",
|
|
92
|
+
"@dxos/ui-theme": "0.10.0",
|
|
93
|
+
"@dxos/react-ui": "0.10.0"
|
|
93
94
|
},
|
|
94
95
|
"publishConfig": {
|
|
95
96
|
"access": "public"
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2025 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import type * as Effect from 'effect/Effect';
|
|
6
|
+
|
|
7
|
+
import { createAnnotationHelper } from '@dxos/echo/internal';
|
|
8
|
+
|
|
9
|
+
/** One selectable option produced by an {@link OptionsLookup}. */
|
|
10
|
+
export type OptionsLookupEntry = { value: string; label?: string; secondaryLabel?: string; icon?: string };
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Loads a select field's options dynamically from a declared subset of the form values, so a select can
|
|
14
|
+
* depend on a sibling field (e.g. publications for a typed handle). `deps` are the field names the loader
|
|
15
|
+
* reads; the form re-runs it only when one of those values changes. The returned Effect must be
|
|
16
|
+
* self-contained (`R = never`): it provides its own dependencies (network layer, etc.) in the closure.
|
|
17
|
+
* Construct via {@link optionsLookup} for typed `deps`/`values`. Pass `{ combobox: true }` to render
|
|
18
|
+
* an editable combobox (type-to-search) instead of a constrained select.
|
|
19
|
+
*/
|
|
20
|
+
export type OptionsLookup = {
|
|
21
|
+
readonly deps: readonly string[];
|
|
22
|
+
readonly load: (values: any) => Effect.Effect<readonly OptionsLookupEntry[], unknown>;
|
|
23
|
+
readonly combobox?: boolean;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export const OptionsLookupAnnotationId = Symbol.for('@dxos/schema/annotation/OptionsLookup');
|
|
27
|
+
export const OptionsLookupAnnotation = createAnnotationHelper<OptionsLookup>(OptionsLookupAnnotationId);
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Builds an {@link OptionsLookup} typed against a schema's value type `Values`: `deps` is checked
|
|
31
|
+
* against its field names, and `load` receives only those fields, narrowed. Pass `{ combobox: true }` to
|
|
32
|
+
* render an editable combobox.
|
|
33
|
+
*/
|
|
34
|
+
export const optionsLookup =
|
|
35
|
+
<Values>() =>
|
|
36
|
+
<const Deps extends readonly (keyof Values & string)[]>(
|
|
37
|
+
deps: Deps,
|
|
38
|
+
load: (values: Pick<Values, Deps[number]>) => Effect.Effect<readonly OptionsLookupEntry[], unknown>,
|
|
39
|
+
options?: { combobox?: boolean },
|
|
40
|
+
): OptionsLookup => ({ deps, load, combobox: options?.combobox });
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Derives a (text) field's value from a declared subset of the form values, so a field can be pre-filled
|
|
44
|
+
* from a sibling (e.g. a feed name fetched from a typed URL). `deps` are the field names the derivation
|
|
45
|
+
* reads; the form re-runs it only when one of those changes. The returned Effect must be self-contained
|
|
46
|
+
* (`R = never`); `undefined` means "no value to fill". The form pre-fills only while the user has not
|
|
47
|
+
* typed their own value, so a manual edit is never clobbered. Construct via {@link autofill}.
|
|
48
|
+
*/
|
|
49
|
+
export type Autofill = {
|
|
50
|
+
readonly deps: readonly string[];
|
|
51
|
+
readonly derive: (values: any) => Effect.Effect<string | undefined, unknown>;
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
export const AutofillAnnotationId = Symbol.for('@dxos/schema/annotation/Autofill');
|
|
55
|
+
export const AutofillAnnotation = createAnnotationHelper<Autofill>(AutofillAnnotationId);
|
|
56
|
+
|
|
57
|
+
/** Builds an {@link Autofill} typed against a schema's value type `Values` (see {@link optionsLookup}). */
|
|
58
|
+
export const autofill =
|
|
59
|
+
<Values>() =>
|
|
60
|
+
<const Deps extends readonly (keyof Values & string)[]>(
|
|
61
|
+
deps: Deps,
|
|
62
|
+
derive: (values: Pick<Values, Deps[number]>) => Effect.Effect<string | undefined, unknown>,
|
|
63
|
+
): Autofill => ({ deps, derive });
|
|
@@ -58,7 +58,10 @@ export const FieldEditor = ({ readonly, projection, field, registry, view, onSav
|
|
|
58
58
|
|
|
59
59
|
const [referenceSchema, setReferenceSchema] = useState<Type.Type>();
|
|
60
60
|
useEffect(() => {
|
|
61
|
-
|
|
61
|
+
// A React state setter invokes a function argument as an updater. A class-based `Type.Type` is
|
|
62
|
+
// itself a function, so it must be stored via an updater lambda — passing it directly makes React
|
|
63
|
+
// call it as `Organization(prev)`, throwing "Class constructor … cannot be invoked without 'new'".
|
|
64
|
+
setReferenceSchema(() => schemas.find((schema) => Type.getTypename(schema) === props?.referenceSchema));
|
|
62
65
|
}, [schemas, props?.referenceSchema]);
|
|
63
66
|
|
|
64
67
|
// TODO(burdon): Need to wrap otherwise throws error:
|
|
@@ -191,7 +194,7 @@ export const FieldEditor = ({ readonly, projection, field, registry, view, onSav
|
|
|
191
194
|
readonly={readonly}
|
|
192
195
|
schema={fieldSchema}
|
|
193
196
|
values={props}
|
|
194
|
-
|
|
197
|
+
filter={propIsNotType}
|
|
195
198
|
sort={['property', 'format']}
|
|
196
199
|
onValuesChanged={handleValuesChanged}
|
|
197
200
|
onValidate={handleValidate}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# `@dxos/react-ui-form` — Design
|
|
2
|
+
|
|
3
|
+
Schema-driven forms. Give `Form.Root` an Effect `schema` + `values`; the field set renders typed
|
|
4
|
+
controls automatically. Bespoke controls drop in via `Form.Row` or a per-path `fieldMap`.
|
|
5
|
+
|
|
6
|
+
## Component hierarchy
|
|
7
|
+
|
|
8
|
+
```text
|
|
9
|
+
Form.Root context: schema, values, validation, onValuesChanged
|
|
10
|
+
└─ Form.Viewport scroll chrome
|
|
11
|
+
└─ Form.Content
|
|
12
|
+
├─ Form.Section titled group of fields
|
|
13
|
+
│ ├─ Form.FieldSet schema-driven field set
|
|
14
|
+
│ │ └─ FormFieldSetContainer group wrapper: FormFieldHeader (label + collapse) over the body
|
|
15
|
+
│ │ └─ FormField one per schema property; dispatches by type:
|
|
16
|
+
│ │ ├─ ArrayField array → FormFieldHeader + item rows
|
|
17
|
+
│ │ ├─ Form.FieldSet nested object → recurses (its own FormFieldSetContainer)
|
|
18
|
+
│ │ └─ Form.Row scalar → label + description + the control (<field>)
|
|
19
|
+
│ └─ Form.Row OR a hand-written row (label + description + any control)
|
|
20
|
+
├─ Form.Layout alternative to FieldSet: lays fields out per a parsed layout spec
|
|
21
|
+
└─ Form.Actions
|
|
22
|
+
└─ Form.Submit
|
|
23
|
+
Form.Error
|
|
24
|
+
```
|
|
@@ -3,35 +3,38 @@
|
|
|
3
3
|
//
|
|
4
4
|
|
|
5
5
|
import { type Meta, type StoryObj } from '@storybook/react-vite';
|
|
6
|
+
import * as Effect from 'effect/Effect';
|
|
6
7
|
import * as Schema from 'effect/Schema';
|
|
7
|
-
import React, { useCallback, useState } from 'react';
|
|
8
|
+
import React, { useCallback, useMemo, useState } from 'react';
|
|
8
9
|
|
|
9
|
-
import { Obj, Tag, Type } from '@dxos/echo';
|
|
10
|
+
import { Annotation, Filter, Format, Obj, Ref, Tag, Type } from '@dxos/echo';
|
|
10
11
|
import { type AnyProperties } from '@dxos/echo/internal';
|
|
11
12
|
import { log } from '@dxos/log';
|
|
12
|
-
import { useSpaces } from '@dxos/react-client/echo';
|
|
13
|
+
import { useQuery, useSpaces } from '@dxos/react-client/echo';
|
|
13
14
|
import { withClientProvider } from '@dxos/react-client/testing';
|
|
14
15
|
import { Tooltip } from '@dxos/react-ui';
|
|
15
16
|
import { Loading, withLayout, withTheme } from '@dxos/react-ui/testing';
|
|
17
|
+
import { Text } from '@dxos/schema';
|
|
16
18
|
|
|
17
19
|
import { translations } from '#translations';
|
|
18
20
|
|
|
21
|
+
import { AutofillAnnotation, OptionsLookupAnnotation, autofill, optionsLookup } from '../../annotations';
|
|
19
22
|
import { Organization, Person, TestLayout } from '../../testing';
|
|
20
23
|
import { type ExcludeId, omitId } from '../../util';
|
|
21
24
|
import { Form, type FormRootProps } from './Form';
|
|
22
25
|
|
|
23
|
-
type
|
|
24
|
-
schema?: Schema.Schema<T>;
|
|
25
|
-
} & FormRootProps<T>;
|
|
26
|
+
type StoryArgs<T extends AnyProperties> = FormRootProps<T> & { json?: boolean };
|
|
26
27
|
|
|
27
28
|
const DefaultStory = <T extends AnyProperties = AnyProperties>({
|
|
28
29
|
schema,
|
|
29
30
|
values: valuesProp,
|
|
31
|
+
json = true,
|
|
30
32
|
...props
|
|
31
|
-
}:
|
|
33
|
+
}: StoryArgs<T>) => {
|
|
32
34
|
const [values, setValues] = useState<Partial<T>>(valuesProp ?? {});
|
|
33
35
|
const spaces = useSpaces();
|
|
34
36
|
const space = spaces[0];
|
|
37
|
+
|
|
35
38
|
const handleSave = useCallback<NonNullable<FormRootProps<T>['onSave']>>((values) => {
|
|
36
39
|
log.info('save', { values, meta });
|
|
37
40
|
setValues(values);
|
|
@@ -48,7 +51,7 @@ const DefaultStory = <T extends AnyProperties = AnyProperties>({
|
|
|
48
51
|
|
|
49
52
|
return (
|
|
50
53
|
<Tooltip.Provider>
|
|
51
|
-
<TestLayout json={{ values, schema: schema?.ast }}>
|
|
54
|
+
<TestLayout json={json ? { values, schema: schema?.ast } : undefined}>
|
|
52
55
|
<Form.Root
|
|
53
56
|
schema={schema}
|
|
54
57
|
defaultValues={values}
|
|
@@ -59,7 +62,7 @@ const DefaultStory = <T extends AnyProperties = AnyProperties>({
|
|
|
59
62
|
>
|
|
60
63
|
<Form.Viewport scroll>
|
|
61
64
|
<Form.Content>
|
|
62
|
-
<Form.Section
|
|
65
|
+
<Form.Section title='Section' description='This is a [section description](https://dxos.org).' />
|
|
63
66
|
<Form.FieldSet />
|
|
64
67
|
<Form.Actions />
|
|
65
68
|
</Form.Content>
|
|
@@ -80,11 +83,12 @@ const meta = {
|
|
|
80
83
|
withClientProvider({
|
|
81
84
|
createIdentity: true,
|
|
82
85
|
createSpace: true,
|
|
83
|
-
types: [Tag.Tag, Organization, Person],
|
|
86
|
+
types: [Tag.Tag, Organization, Person, Text.Text],
|
|
84
87
|
onCreateSpace: ({ space }) => {
|
|
85
88
|
[
|
|
86
89
|
...Array.from({ length: 3 }).map((_, i) => Obj.make(Tag.Tag, { label: `Tag ${i}` })),
|
|
87
90
|
...Array.from({ length: 50 }).map((_, i) => Obj.make(Organization, { name: `Organization ${i}` })),
|
|
91
|
+
Obj.make(Text.Text, { content: '# Brief\n\nEdit this **inline** markdown text.' }),
|
|
88
92
|
].map((obj) => space.db.add(obj));
|
|
89
93
|
},
|
|
90
94
|
}),
|
|
@@ -93,12 +97,10 @@ const meta = {
|
|
|
93
97
|
layout: 'fullscreen',
|
|
94
98
|
translations,
|
|
95
99
|
},
|
|
96
|
-
} satisfies Meta<
|
|
100
|
+
} satisfies Meta<StoryArgs<any>>;
|
|
97
101
|
|
|
98
102
|
export default meta;
|
|
99
103
|
|
|
100
|
-
type Story<T extends AnyProperties> = StoryObj<DefaultStoryProps<T>>;
|
|
101
|
-
|
|
102
104
|
const PersonSchema = Type.getSchema(Person);
|
|
103
105
|
|
|
104
106
|
const values: Partial<Person> = {
|
|
@@ -110,6 +112,8 @@ const values: Partial<Person> = {
|
|
|
110
112
|
reminderAt: '09:00:00',
|
|
111
113
|
};
|
|
112
114
|
|
|
115
|
+
type Story<T extends AnyProperties> = StoryObj<StoryArgs<T>>;
|
|
116
|
+
|
|
113
117
|
/**
|
|
114
118
|
* Build a data-entry surface by handing an Effect schema to `Form.Root` — the form derives its fields,
|
|
115
119
|
* types, validation, and selects from the schema, so you don't hand-wire one control per property.
|
|
@@ -147,6 +151,246 @@ export const Static: Story<ExcludeId<typeof PersonSchema>> = {
|
|
|
147
151
|
},
|
|
148
152
|
};
|
|
149
153
|
|
|
154
|
+
const SettingsSchema = Schema.mutable(
|
|
155
|
+
Schema.Struct({
|
|
156
|
+
viewMode: Schema.Literal('preview', 'readonly', 'source').annotations({
|
|
157
|
+
title: 'Default view mode',
|
|
158
|
+
description: 'Set whether documents open in editing or read-only mode.',
|
|
159
|
+
}),
|
|
160
|
+
toolbar: Schema.optional(
|
|
161
|
+
Schema.Boolean.annotations({
|
|
162
|
+
title: 'Show toolbar',
|
|
163
|
+
description: 'Display a formatting toolbar above the editor.',
|
|
164
|
+
}),
|
|
165
|
+
),
|
|
166
|
+
fontSize: Schema.optional(
|
|
167
|
+
Schema.Number.annotations({ title: 'Font size', description: 'Editor font size, in pixels.' }),
|
|
168
|
+
),
|
|
169
|
+
}),
|
|
170
|
+
);
|
|
171
|
+
|
|
172
|
+
export const Variants: Story<Schema.Schema.Type<typeof SettingsSchema>> = {
|
|
173
|
+
render: (args) => (
|
|
174
|
+
<div className='grid grid-cols-2 w-full'>
|
|
175
|
+
<DefaultStory {...args} />
|
|
176
|
+
<DefaultStory {...args} variant='settings' />
|
|
177
|
+
</div>
|
|
178
|
+
),
|
|
179
|
+
args: {
|
|
180
|
+
json: false,
|
|
181
|
+
schema: SettingsSchema,
|
|
182
|
+
values: {
|
|
183
|
+
viewMode: 'preview',
|
|
184
|
+
toolbar: true,
|
|
185
|
+
fontSize: 14,
|
|
186
|
+
},
|
|
187
|
+
},
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
const InlineMarkdownTextSchema = Schema.mutable(
|
|
191
|
+
Schema.Struct({
|
|
192
|
+
text: Schema.String,
|
|
193
|
+
instructions: Ref.Ref(Text.Text).pipe(
|
|
194
|
+
Format.FormatAnnotation.set(Format.TypeFormat.Markdown),
|
|
195
|
+
Annotation.FormInlineAnnotation.set(true),
|
|
196
|
+
Schema.annotations({
|
|
197
|
+
title: 'Instructions',
|
|
198
|
+
description: 'Ref to a Text object with both markdown and inline-ref annotations.',
|
|
199
|
+
}),
|
|
200
|
+
),
|
|
201
|
+
}),
|
|
202
|
+
);
|
|
203
|
+
|
|
204
|
+
const InlineMarkdownTextStory = (args: StoryArgs<any>) => {
|
|
205
|
+
const spaces = useSpaces();
|
|
206
|
+
const space = spaces[0];
|
|
207
|
+
const [text] = useQuery(space?.db, Filter.type(Text.Text));
|
|
208
|
+
const values = useMemo(() => (text ? { instructions: Ref.make(text) } : undefined), [text]);
|
|
209
|
+
|
|
210
|
+
if (!space || !values) {
|
|
211
|
+
return <Loading />;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
return (
|
|
215
|
+
<DefaultStory
|
|
216
|
+
{...args}
|
|
217
|
+
schema={InlineMarkdownTextSchema}
|
|
218
|
+
values={values}
|
|
219
|
+
onCreate={(_type, props) => space.db.add(Obj.make(Text.Text, { content: '', ...props }))}
|
|
220
|
+
/>
|
|
221
|
+
);
|
|
222
|
+
};
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Exercises a `Ref<Text>` field carrying both `Format.TypeFormat.Markdown` and
|
|
226
|
+
* `FormInlineAnnotation` — the markdown editor should render inline rather than
|
|
227
|
+
* opening a ref picker or nested struct form.
|
|
228
|
+
*/
|
|
229
|
+
export const InlineMarkdownText: Story<any> = {
|
|
230
|
+
render: (args) => <InlineMarkdownTextStory {...args} />,
|
|
231
|
+
args: {
|
|
232
|
+
autoSave: true,
|
|
233
|
+
},
|
|
234
|
+
};
|
|
235
|
+
|
|
150
236
|
export const Empty: Story<ExcludeId<typeof PersonSchema>> = {
|
|
151
237
|
args: {},
|
|
152
238
|
};
|
|
239
|
+
|
|
240
|
+
// Demonstrates the value-driven dynamic-field annotations (here backed by in-memory Effects with an
|
|
241
|
+
// artificial delay rather than network calls, so the story is deterministic): a select whose options
|
|
242
|
+
// derive from the `query` field, and a text field auto-filled from the `url` field. Each annotation
|
|
243
|
+
// declares the fields it depends on, so it only re-runs when one of those changes. Structural validation
|
|
244
|
+
// is the schema's own job (`Format.URL`).
|
|
245
|
+
const isValidUrl = Schema.is(Format.URL);
|
|
246
|
+
|
|
247
|
+
// Base struct (no dynamic annotations) — its value type drives the typed `deps`/`values` below.
|
|
248
|
+
const DynamicFieldsBase = Schema.Struct({
|
|
249
|
+
query: Schema.String.annotations({ title: 'Query', description: 'Type to load the choices below.' }),
|
|
250
|
+
choice: Schema.optional(Schema.String),
|
|
251
|
+
tag: Schema.optional(Schema.String),
|
|
252
|
+
url: Format.URL.annotations({ title: 'URL', description: 'A valid URL auto-fills the name below.' }),
|
|
253
|
+
name: Schema.optional(Schema.String),
|
|
254
|
+
});
|
|
255
|
+
type DynamicFieldsValues = Schema.Schema.Type<typeof DynamicFieldsBase>;
|
|
256
|
+
|
|
257
|
+
const TAG_VOCABULARY = ['react', 'effect', 'schema', 'echo', 'composer'];
|
|
258
|
+
|
|
259
|
+
const DynamicFieldsSchema = Schema.mutable(
|
|
260
|
+
Schema.Struct({
|
|
261
|
+
...DynamicFieldsBase.fields,
|
|
262
|
+
choice: Schema.optional(
|
|
263
|
+
Schema.String.pipe(
|
|
264
|
+
OptionsLookupAnnotation.set(
|
|
265
|
+
optionsLookup<DynamicFieldsValues>()(['query'], ({ query }) =>
|
|
266
|
+
(query && query.length > 0
|
|
267
|
+
? Effect.succeed(
|
|
268
|
+
[1, 2, 3].map((index) => ({ value: `${query}-${index}`, label: `${query} choice ${index}` })),
|
|
269
|
+
)
|
|
270
|
+
: Effect.succeed([])
|
|
271
|
+
).pipe(Effect.delay('600 millis')),
|
|
272
|
+
),
|
|
273
|
+
),
|
|
274
|
+
Schema.annotations({ title: 'Choice', description: 'Options load from the query (after a delay).' }),
|
|
275
|
+
),
|
|
276
|
+
),
|
|
277
|
+
// Combobox: the field's own value is the query; the typed text is the auto-selected first option.
|
|
278
|
+
tag: Schema.optional(
|
|
279
|
+
Schema.String.pipe(
|
|
280
|
+
OptionsLookupAnnotation.set(
|
|
281
|
+
optionsLookup<DynamicFieldsValues>()(
|
|
282
|
+
// No deps: the pool is fetched once; the combobox filters it by the typed text client-side.
|
|
283
|
+
[],
|
|
284
|
+
() =>
|
|
285
|
+
Effect.succeed(TAG_VOCABULARY.map((value) => ({ value, label: value }))).pipe(Effect.delay('400 millis')),
|
|
286
|
+
{ combobox: true },
|
|
287
|
+
),
|
|
288
|
+
),
|
|
289
|
+
Schema.annotations({ title: 'Tag', description: 'Combobox: type to filter; your text stays selectable.' }),
|
|
290
|
+
),
|
|
291
|
+
),
|
|
292
|
+
name: Schema.optional(
|
|
293
|
+
Schema.String.pipe(
|
|
294
|
+
// Only derive a value once the URL is structurally valid, and only after the artificial wait —
|
|
295
|
+
// an incomplete/invalid URL produces nothing.
|
|
296
|
+
AutofillAnnotation.set(
|
|
297
|
+
autofill<DynamicFieldsValues>()(['url'], ({ url }) =>
|
|
298
|
+
isValidUrl(url)
|
|
299
|
+
? Effect.succeed(`Feed for ${url}`).pipe(Effect.delay('800 millis'))
|
|
300
|
+
: Effect.succeed(undefined),
|
|
301
|
+
),
|
|
302
|
+
),
|
|
303
|
+
Schema.annotations({ title: 'Name', description: 'Auto-filled from a valid URL (editable).' }),
|
|
304
|
+
),
|
|
305
|
+
),
|
|
306
|
+
}),
|
|
307
|
+
);
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* Exercises the dynamic-field annotations: `OptionsLookupAnnotation` (select options loaded from a
|
|
311
|
+
* sibling) and `AutofillAnnotation` (value derived from a sibling, gated on validity and delayed). Each
|
|
312
|
+
* runs a self-contained Effect from the current form values.
|
|
313
|
+
*/
|
|
314
|
+
export const DynamicFields: Story<Schema.Schema.Type<typeof DynamicFieldsSchema>> = {
|
|
315
|
+
args: {
|
|
316
|
+
json: true,
|
|
317
|
+
schema: DynamicFieldsSchema,
|
|
318
|
+
},
|
|
319
|
+
};
|
|
320
|
+
|
|
321
|
+
// Discriminated-union create form mirroring the magazine create-feed schema: a `type` selector picks the
|
|
322
|
+
// member, whose fields then render — `standard-site` (handle combobox → publication select) or `rss`
|
|
323
|
+
// (URL → auto-filled name). Backed by in-memory Effects so the story is deterministic. Regression harness
|
|
324
|
+
// for union create forms (e.g. the `omitId` union-flattening bug).
|
|
325
|
+
const HANDLE_SUGGESTIONS = ['dxos.org', 'alice.bsky.social', 'bob.example.com'];
|
|
326
|
+
|
|
327
|
+
const StandardSiteCreateBase = Schema.Struct({
|
|
328
|
+
type: Schema.Literal('standard-site'),
|
|
329
|
+
handle: Schema.String.annotations({ title: 'Handle', description: 'atproto handle, e.g. dxos.org.' }),
|
|
330
|
+
// No `name`: the feed name is taken from the selected publication.
|
|
331
|
+
publication: Schema.String.annotations({ title: 'Publication', description: 'Choose a publication.' }),
|
|
332
|
+
});
|
|
333
|
+
type StandardSiteValues = Schema.Schema.Type<typeof StandardSiteCreateBase>;
|
|
334
|
+
|
|
335
|
+
const StandardSiteCreate = Schema.Struct({
|
|
336
|
+
...StandardSiteCreateBase.fields,
|
|
337
|
+
handle: StandardSiteCreateBase.fields.handle.pipe(
|
|
338
|
+
OptionsLookupAnnotation.set(
|
|
339
|
+
optionsLookup<StandardSiteValues>()(
|
|
340
|
+
['handle'],
|
|
341
|
+
() =>
|
|
342
|
+
Effect.succeed(HANDLE_SUGGESTIONS.map((value) => ({ value, label: value }))).pipe(Effect.delay('300 millis')),
|
|
343
|
+
{ combobox: true },
|
|
344
|
+
),
|
|
345
|
+
),
|
|
346
|
+
),
|
|
347
|
+
publication: StandardSiteCreateBase.fields.publication.pipe(
|
|
348
|
+
OptionsLookupAnnotation.set(
|
|
349
|
+
optionsLookup<StandardSiteValues>()(['handle'], ({ handle }) =>
|
|
350
|
+
Effect.succeed(
|
|
351
|
+
handle
|
|
352
|
+
? [
|
|
353
|
+
{ value: `at://${handle}/pub/blog`, label: `${handle} — Blog` },
|
|
354
|
+
{ value: `at://${handle}/pub/notes`, label: `${handle} — Notes` },
|
|
355
|
+
]
|
|
356
|
+
: [],
|
|
357
|
+
).pipe(Effect.delay('300 millis')),
|
|
358
|
+
),
|
|
359
|
+
),
|
|
360
|
+
),
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
const RssCreateBase = Schema.Struct({
|
|
364
|
+
type: Schema.Literal('rss'),
|
|
365
|
+
url: Format.URL.annotations({ title: 'URL', description: 'RSS feed URL.' }),
|
|
366
|
+
name: Schema.optional(Schema.String.annotations({ title: 'Name' })),
|
|
367
|
+
});
|
|
368
|
+
type RssValues = Schema.Schema.Type<typeof RssCreateBase>;
|
|
369
|
+
|
|
370
|
+
const RssCreate = Schema.Struct({
|
|
371
|
+
...RssCreateBase.fields,
|
|
372
|
+
name: Schema.optional(
|
|
373
|
+
Schema.String.pipe(
|
|
374
|
+
AutofillAnnotation.set(
|
|
375
|
+
autofill<RssValues>()(['url'], ({ url }) =>
|
|
376
|
+
isValidUrl(url)
|
|
377
|
+
? Effect.succeed(`Feed for ${url}`).pipe(Effect.delay('500 millis'))
|
|
378
|
+
: Effect.succeed(undefined),
|
|
379
|
+
),
|
|
380
|
+
),
|
|
381
|
+
).annotations({ title: 'Name' }),
|
|
382
|
+
),
|
|
383
|
+
});
|
|
384
|
+
|
|
385
|
+
const CreateFeedSchema = Schema.Union(StandardSiteCreate, RssCreate);
|
|
386
|
+
|
|
387
|
+
/**
|
|
388
|
+
* A discriminated-union create form: selecting `type` reveals that member's fields (combobox + select for
|
|
389
|
+
* `standard-site`, URL + autofill for `rss`). Mirrors the magazine create-feed schema.
|
|
390
|
+
*/
|
|
391
|
+
export const DiscriminatedUnion: Story<Schema.Schema.Type<typeof CreateFeedSchema>> = {
|
|
392
|
+
args: {
|
|
393
|
+
json: true,
|
|
394
|
+
schema: CreateFeedSchema,
|
|
395
|
+
},
|
|
396
|
+
};
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2026 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { describe, test } from 'vitest';
|
|
6
|
+
|
|
7
|
+
import { formTheme } from './Form.theme';
|
|
8
|
+
|
|
9
|
+
describe('formTheme', () => {
|
|
10
|
+
test('default variant adds no chrome to the field', ({ expect }) => {
|
|
11
|
+
const styles = formTheme.styles({ variant: 'default' });
|
|
12
|
+
expect(styles.field()).toBeFalsy();
|
|
13
|
+
expect(formTheme.behavior.default.showDescription).toBe(false);
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
test('settings variant frames the field and shows the description', ({ expect }) => {
|
|
17
|
+
const styles = formTheme.styles({ variant: 'settings' });
|
|
18
|
+
// Assert stable invariants (bordered, grid-framed, prominent label, right-aligned control),
|
|
19
|
+
// not the specific grid mechanism, so the test survives layout tweaks.
|
|
20
|
+
expect(styles.field()).toContain('border');
|
|
21
|
+
expect(styles.field()).toContain('grid');
|
|
22
|
+
expect(styles.fieldLabel()).toContain('[grid-area:header]');
|
|
23
|
+
expect(styles.fieldLabelText()).toContain('text-lg');
|
|
24
|
+
expect(styles.fieldControl()).toContain('justify-end');
|
|
25
|
+
expect(formTheme.behavior.settings.showDescription).toBe(true);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
test('per-slot override wins', ({ expect }) => {
|
|
29
|
+
const styles = formTheme.styles({ variant: 'settings' });
|
|
30
|
+
expect(styles.fieldLabelText({ class: 'text-2xl' })).toContain('text-2xl');
|
|
31
|
+
expect(styles.fieldLabelText({ class: 'text-2xl' })).not.toContain('text-lg');
|
|
32
|
+
});
|
|
33
|
+
});
|