@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.
Files changed (211) hide show
  1. package/dist/lib/browser/index.mjs +1883 -1539
  2. package/dist/lib/browser/index.mjs.map +4 -4
  3. package/dist/lib/browser/meta.json +1 -1
  4. package/dist/lib/node-esm/index.mjs +1883 -1539
  5. package/dist/lib/node-esm/index.mjs.map +4 -4
  6. package/dist/lib/node-esm/meta.json +1 -1
  7. package/dist/types/src/annotations.d.ts +47 -0
  8. package/dist/types/src/annotations.d.ts.map +1 -0
  9. package/dist/types/src/components/FieldEditor/FieldEditor.d.ts.map +1 -1
  10. package/dist/types/src/components/Form/Form.d.ts +12 -4
  11. package/dist/types/src/components/Form/Form.d.ts.map +1 -1
  12. package/dist/types/src/components/Form/Form.stories.d.ts +46 -65
  13. package/dist/types/src/components/Form/Form.stories.d.ts.map +1 -1
  14. package/dist/types/src/components/Form/Form.theme.d.ts +252 -0
  15. package/dist/types/src/components/Form/Form.theme.d.ts.map +1 -0
  16. package/dist/types/src/components/Form/Form.theme.test.d.ts +2 -0
  17. package/dist/types/src/components/Form/Form.theme.test.d.ts.map +1 -0
  18. package/dist/types/src/components/Form/FormControls.d.ts +17 -8
  19. package/dist/types/src/components/Form/FormControls.d.ts.map +1 -1
  20. package/dist/types/src/components/Form/FormField/FormField.d.ts +16 -2
  21. package/dist/types/src/components/Form/FormField/FormField.d.ts.map +1 -1
  22. package/dist/types/src/components/Form/FormField/FormFieldHeader.d.ts +27 -0
  23. package/dist/types/src/components/Form/FormField/FormFieldHeader.d.ts.map +1 -0
  24. package/dist/types/src/components/Form/FormField/FormRow.d.ts +85 -0
  25. package/dist/types/src/components/Form/FormField/FormRow.d.ts.map +1 -0
  26. package/dist/types/src/components/Form/FormField/{FormFieldWrapper.stories.d.ts → FormRow.stories.d.ts} +4 -1
  27. package/dist/types/src/components/Form/FormField/FormRow.stories.d.ts.map +1 -0
  28. package/dist/types/src/components/Form/FormField/fields/ArrayField/ArrayField.d.ts.map +1 -1
  29. package/dist/types/src/components/Form/FormField/fields/ArrayField/ArrayField.stories.d.ts +1 -0
  30. package/dist/types/src/components/Form/FormField/fields/ArrayField/ArrayField.stories.d.ts.map +1 -1
  31. package/dist/types/src/components/Form/FormField/fields/AsyncSelectField/AsyncSelectField.d.ts +12 -0
  32. package/dist/types/src/components/Form/FormField/fields/AsyncSelectField/AsyncSelectField.d.ts.map +1 -0
  33. package/dist/types/src/components/Form/FormField/fields/AsyncSelectField/index.d.ts +2 -0
  34. package/dist/types/src/components/Form/FormField/fields/AsyncSelectField/index.d.ts.map +1 -0
  35. package/dist/types/src/components/Form/FormField/fields/AutofillField/AutofillField.d.ts +12 -0
  36. package/dist/types/src/components/Form/FormField/fields/AutofillField/AutofillField.d.ts.map +1 -0
  37. package/dist/types/src/components/Form/FormField/fields/AutofillField/index.d.ts +2 -0
  38. package/dist/types/src/components/Form/FormField/fields/AutofillField/index.d.ts.map +1 -0
  39. package/dist/types/src/components/Form/FormField/fields/BooleanField/BooleanField.d.ts.map +1 -1
  40. package/dist/types/src/components/Form/FormField/fields/ComboboxField/ComboboxField.d.ts +12 -0
  41. package/dist/types/src/components/Form/FormField/fields/ComboboxField/ComboboxField.d.ts.map +1 -0
  42. package/dist/types/src/components/Form/FormField/fields/ComboboxField/index.d.ts +2 -0
  43. package/dist/types/src/components/Form/FormField/fields/ComboboxField/index.d.ts.map +1 -0
  44. package/dist/types/src/components/Form/FormField/fields/GeoPointField/GeoPointField.d.ts +1 -1
  45. package/dist/types/src/components/Form/FormField/fields/GeoPointField/GeoPointField.d.ts.map +1 -1
  46. package/dist/types/src/components/Form/FormField/fields/MarkdownField/MarkdownField.d.ts +1 -1
  47. package/dist/types/src/components/Form/FormField/fields/MarkdownField/MarkdownField.d.ts.map +1 -1
  48. package/dist/types/src/components/Form/FormField/fields/NumberField/NumberField.d.ts +11 -0
  49. package/dist/types/src/components/Form/FormField/fields/NumberField/NumberField.d.ts.map +1 -1
  50. package/dist/types/src/components/Form/FormField/fields/NumberField/NumberField.test.d.ts +2 -0
  51. package/dist/types/src/components/Form/FormField/fields/NumberField/NumberField.test.d.ts.map +1 -0
  52. package/dist/types/src/components/Form/FormField/fields/PasswordField/PasswordField.d.ts +4 -0
  53. package/dist/types/src/components/Form/FormField/fields/PasswordField/PasswordField.d.ts.map +1 -0
  54. package/dist/types/src/components/{Settings/Settings.stories.d.ts → Form/FormField/fields/PasswordField/PasswordField.stories.d.ts} +1 -2
  55. package/dist/types/src/components/Form/FormField/fields/PasswordField/PasswordField.stories.d.ts.map +1 -0
  56. package/dist/types/src/components/Form/FormField/fields/PasswordField/index.d.ts +2 -0
  57. package/dist/types/src/components/Form/FormField/fields/PasswordField/index.d.ts.map +1 -0
  58. package/dist/types/src/components/Form/FormField/fields/RefField/InlineRefField.d.ts.map +1 -1
  59. package/dist/types/src/components/Form/FormField/fields/RefField/RefField.d.ts.map +1 -1
  60. package/dist/types/src/components/Form/FormField/fields/SelectField/SelectField.d.ts +18 -2
  61. package/dist/types/src/components/Form/FormField/fields/SelectField/SelectField.d.ts.map +1 -1
  62. package/dist/types/src/components/Form/FormField/fields/index.d.ts +4 -0
  63. package/dist/types/src/components/Form/FormField/fields/index.d.ts.map +1 -1
  64. package/dist/types/src/components/Form/FormField/index.d.ts +4 -3
  65. package/dist/types/src/components/Form/FormField/index.d.ts.map +1 -1
  66. package/dist/types/src/components/Form/FormField/presentation.d.ts +22 -0
  67. package/dist/types/src/components/Form/FormField/presentation.d.ts.map +1 -0
  68. package/dist/types/src/components/Form/FormFieldSet/FormFieldSet.d.ts +8 -5
  69. package/dist/types/src/components/Form/FormFieldSet/FormFieldSet.d.ts.map +1 -1
  70. package/dist/types/src/components/Form/FormFieldSet/FormFieldSetContainer.d.ts +22 -0
  71. package/dist/types/src/components/Form/FormFieldSet/FormFieldSetContainer.d.ts.map +1 -0
  72. package/dist/types/src/components/Form/FormFieldSet/index.d.ts +1 -0
  73. package/dist/types/src/components/Form/FormFieldSet/index.d.ts.map +1 -1
  74. package/dist/types/src/components/Form/FormLayout/FormLayout.d.ts +2 -0
  75. package/dist/types/src/components/Form/FormLayout/FormLayout.d.ts.map +1 -1
  76. package/dist/types/src/components/Form/FormLayout/FormLayout.stories.d.ts +5 -5
  77. package/dist/types/src/components/Form/FormLayout/FormLayout.stories.d.ts.map +1 -1
  78. package/dist/types/src/components/Form/FormLayout/index.d.ts +1 -1
  79. package/dist/types/src/components/Form/FormLayout/index.d.ts.map +1 -1
  80. package/dist/types/src/components/Form/meta-tags.d.ts +1 -12
  81. package/dist/types/src/components/Form/meta-tags.d.ts.map +1 -1
  82. package/dist/types/src/components/ObjectPicker/ObjectPicker.d.ts.map +1 -1
  83. package/dist/types/src/components/ObjectProperties/ObjectProperties.d.ts +2 -1
  84. package/dist/types/src/components/ObjectProperties/ObjectProperties.d.ts.map +1 -1
  85. package/dist/types/src/components/RefEditor/RefEditor.stories.d.ts +2 -2
  86. package/dist/types/src/components/RefEditor/RefEditor.stories.d.ts.map +1 -1
  87. package/dist/types/src/components/ViewEditor/ViewEditor.d.ts.map +1 -1
  88. package/dist/types/src/components/ViewEditor/ViewEditor.stories.d.ts +2 -2
  89. package/dist/types/src/components/ViewEditor/ViewEditor.stories.d.ts.map +1 -1
  90. package/dist/types/src/components/index.d.ts +0 -1
  91. package/dist/types/src/components/index.d.ts.map +1 -1
  92. package/dist/types/src/hooks/index.d.ts +1 -0
  93. package/dist/types/src/hooks/index.d.ts.map +1 -1
  94. package/dist/types/src/hooks/useAsyncFieldEffect.d.ts +24 -0
  95. package/dist/types/src/hooks/useAsyncFieldEffect.d.ts.map +1 -0
  96. package/dist/types/src/hooks/useFormContext.d.ts +8 -3
  97. package/dist/types/src/hooks/useFormContext.d.ts.map +1 -1
  98. package/dist/types/src/hooks/useFormHandler.d.ts +8 -2
  99. package/dist/types/src/hooks/useFormHandler.d.ts.map +1 -1
  100. package/dist/types/src/index.d.ts +1 -0
  101. package/dist/types/src/index.d.ts.map +1 -1
  102. package/dist/types/src/testing/TestLayout.d.ts +1 -1
  103. package/dist/types/src/testing/TestLayout.d.ts.map +1 -1
  104. package/dist/types/src/testing/schema.d.ts +13 -16
  105. package/dist/types/src/testing/schema.d.ts.map +1 -1
  106. package/dist/types/src/types.d.ts +50 -1
  107. package/dist/types/src/types.d.ts.map +1 -1
  108. package/dist/types/src/util/index.d.ts +0 -1
  109. package/dist/types/src/util/index.d.ts.map +1 -1
  110. package/dist/types/src/util/omit.d.ts +1 -1
  111. package/dist/types/src/util/omit.d.ts.map +1 -1
  112. package/dist/types/tsconfig.tsbuildinfo +1 -1
  113. package/package.json +41 -40
  114. package/src/annotations.ts +63 -0
  115. package/src/components/FieldEditor/FieldEditor.tsx +5 -2
  116. package/src/components/Form/DESIGN.md +24 -0
  117. package/src/components/Form/Form.stories.tsx +257 -13
  118. package/src/components/Form/Form.theme.test.ts +33 -0
  119. package/src/components/Form/Form.theme.ts +86 -0
  120. package/src/components/Form/Form.tsx +15 -9
  121. package/src/components/Form/FormControls.tsx +94 -37
  122. package/src/components/Form/FormField/FormField.tsx +83 -32
  123. package/src/components/Form/FormField/FormFieldHeader.tsx +68 -0
  124. package/src/components/Form/FormField/{FormFieldWrapper.stories.tsx → FormRow.stories.tsx} +34 -9
  125. package/src/components/Form/FormField/FormRow.tsx +298 -0
  126. package/src/components/Form/FormField/fields/ArrayField/ArrayField.stories.tsx +25 -9
  127. package/src/components/Form/FormField/fields/ArrayField/ArrayField.tsx +83 -24
  128. package/src/components/Form/FormField/fields/AsyncSelectField/AsyncSelectField.tsx +59 -0
  129. package/src/components/Form/FormField/fields/AsyncSelectField/index.ts +5 -0
  130. package/src/components/Form/FormField/fields/AutofillField/AutofillField.tsx +67 -0
  131. package/src/components/Form/FormField/fields/AutofillField/index.ts +5 -0
  132. package/src/components/Form/FormField/fields/BooleanField/BooleanField.tsx +5 -6
  133. package/src/components/Form/FormField/fields/ComboboxField/ComboboxField.tsx +104 -0
  134. package/src/components/Form/FormField/fields/ComboboxField/index.ts +5 -0
  135. package/src/components/Form/FormField/fields/DateField/DateField.tsx +9 -9
  136. package/src/components/Form/FormField/fields/GeoPointField/GeoPointField.tsx +17 -20
  137. package/src/components/Form/FormField/fields/MarkdownField/MarkdownField.stories.tsx +2 -2
  138. package/src/components/Form/FormField/fields/MarkdownField/MarkdownField.tsx +39 -40
  139. package/src/components/Form/FormField/fields/NumberField/NumberField.test.ts +29 -0
  140. package/src/components/Form/FormField/fields/NumberField/NumberField.tsx +60 -21
  141. package/src/components/Form/FormField/fields/PasswordField/PasswordField.stories.tsx +53 -0
  142. package/src/components/Form/FormField/fields/PasswordField/PasswordField.tsx +41 -0
  143. package/src/components/Form/FormField/fields/PasswordField/index.ts +5 -0
  144. package/src/components/Form/FormField/fields/RefField/InlineRefField.tsx +16 -10
  145. package/src/components/Form/FormField/fields/RefField/RefField.test.ts +8 -0
  146. package/src/components/Form/FormField/fields/RefField/RefField.tsx +50 -23
  147. package/src/components/Form/FormField/fields/SelectField/SelectField.tsx +104 -47
  148. package/src/components/Form/FormField/fields/SelectOptionField/SelectOptionField.tsx +3 -3
  149. package/src/components/Form/FormField/fields/TextAreaField/TextAreaField.tsx +3 -3
  150. package/src/components/Form/FormField/fields/TextField/TextField.tsx +3 -3
  151. package/src/components/Form/FormField/fields/TupleField/TupleField.tsx +1 -1
  152. package/src/components/Form/FormField/fields/index.ts +4 -0
  153. package/src/components/Form/FormField/index.ts +5 -3
  154. package/src/components/Form/FormField/presentation.tsx +32 -0
  155. package/src/components/Form/FormFieldSet/FormFieldSet.tsx +49 -70
  156. package/src/components/Form/FormFieldSet/FormFieldSetContainer.tsx +93 -0
  157. package/src/components/Form/FormFieldSet/index.ts +1 -0
  158. package/src/components/Form/FormLayout/FormLayout.stories.tsx +7 -5
  159. package/src/components/Form/FormLayout/FormLayout.tsx +24 -14
  160. package/src/components/Form/FormLayout/index.ts +1 -1
  161. package/src/components/Form/meta-tags.test.ts +1 -1
  162. package/src/components/ObjectForm/ObjectForm.tsx +2 -2
  163. package/src/components/ObjectPicker/ObjectPicker.tsx +1 -0
  164. package/src/components/ObjectProperties/ObjectProperties.stories.tsx +1 -1
  165. package/src/components/ObjectProperties/ObjectProperties.tsx +12 -8
  166. package/src/components/RefEditor/RefEditor.stories.tsx +2 -2
  167. package/src/components/RefEditor/RefEditor.tsx +1 -1
  168. package/src/components/ViewEditor/ViewEditor.stories.tsx +2 -2
  169. package/src/components/ViewEditor/ViewEditor.test.tsx +8 -2
  170. package/src/components/ViewEditor/ViewEditor.tsx +3 -3
  171. package/src/components/index.ts +0 -1
  172. package/src/hooks/index.ts +1 -0
  173. package/src/hooks/useAsyncFieldEffect.ts +81 -0
  174. package/src/hooks/useFormContext.ts +6 -3
  175. package/src/hooks/useFormHandler.ts +11 -6
  176. package/src/index.ts +1 -0
  177. package/src/testing/TestLayout.tsx +15 -12
  178. package/src/testing/schema.ts +43 -43
  179. package/src/types.ts +53 -1
  180. package/src/util/index.ts +0 -1
  181. package/src/util/omit.ts +25 -11
  182. package/src/util/properties.test.ts +110 -28
  183. package/src/util/refs.ts +1 -1
  184. package/dist/types/src/components/Form/FormField/FieldHeader.d.ts +0 -19
  185. package/dist/types/src/components/Form/FormField/FieldHeader.d.ts.map +0 -1
  186. package/dist/types/src/components/Form/FormField/FormFieldWrapper.d.ts +0 -47
  187. package/dist/types/src/components/Form/FormField/FormFieldWrapper.d.ts.map +0 -1
  188. package/dist/types/src/components/Form/FormField/FormFieldWrapper.stories.d.ts.map +0 -1
  189. package/dist/types/src/components/Settings/Settings.d.ts +0 -51
  190. package/dist/types/src/components/Settings/Settings.d.ts.map +0 -1
  191. package/dist/types/src/components/Settings/Settings.stories.d.ts.map +0 -1
  192. package/dist/types/src/components/Settings/SettingsFieldSet.d.ts +0 -43
  193. package/dist/types/src/components/Settings/SettingsFieldSet.d.ts.map +0 -1
  194. package/dist/types/src/components/Settings/SettingsItem.d.ts +0 -16
  195. package/dist/types/src/components/Settings/SettingsItem.d.ts.map +0 -1
  196. package/dist/types/src/components/Settings/SettingsSelectField.d.ts +0 -18
  197. package/dist/types/src/components/Settings/SettingsSelectField.d.ts.map +0 -1
  198. package/dist/types/src/components/Settings/index.d.ts +0 -4
  199. package/dist/types/src/components/Settings/index.d.ts.map +0 -1
  200. package/dist/types/src/util/field-type.d.ts +0 -19
  201. package/dist/types/src/util/field-type.d.ts.map +0 -1
  202. package/src/components/Form/FormField/FieldHeader.tsx +0 -36
  203. package/src/components/Form/FormField/FormFieldWrapper.tsx +0 -179
  204. package/src/components/Settings/AUDIT.md +0 -51
  205. package/src/components/Settings/Settings.stories.tsx +0 -104
  206. package/src/components/Settings/Settings.tsx +0 -81
  207. package/src/components/Settings/SettingsFieldSet.tsx +0 -257
  208. package/src/components/Settings/SettingsItem.tsx +0 -58
  209. package/src/components/Settings/SettingsSelectField.tsx +0 -59
  210. package/src/components/Settings/index.ts +0 -7
  211. package/src/util/field-type.ts +0 -60
@@ -1,179 +0,0 @@
1
- //
2
- // Copyright 2024 DXOS.org
3
- //
4
-
5
- import { format as formatDate } from 'date-fns';
6
- import React, { Component, type PropsWithChildren, type ReactNode } from 'react';
7
-
8
- import { Format } from '@dxos/echo';
9
- import { inputTextLabel, Icon, Input, ThemedClassName, Tooltip } from '@dxos/react-ui';
10
- import { mx } from '@dxos/ui-theme';
11
-
12
- import { type FormFieldRendererProps } from '#types';
13
-
14
- //
15
- // FormFieldLabel
16
- //
17
-
18
- export type FormFieldLabelProps = ThemedClassName<
19
- {
20
- /** Render a plain `<span>` instead of an input-associated `Input.Label`, for labels used outside an `Input.Root` (e.g. section/group headers). */
21
- standalone?: boolean;
22
- error?: string;
23
- /**
24
- * JSON path of the field this label describes (e.g. `runtime.client.storage.persistent`).
25
- * Field metadata; accepted by callers but not currently rendered.
26
- */
27
- path?: string;
28
- /**
29
- * Trailing button rendered at the end of the label row (third grid column).
30
- * Used by nested-object field sets to surface a collapse toggle.
31
- */
32
- button?: ReactNode;
33
- onClick?: () => void;
34
- } & Pick<FormFieldRendererProps, 'label' | 'readonly'>
35
- >;
36
-
37
- export const FormFieldLabel = ({
38
- classNames,
39
- label,
40
- error,
41
- readonly,
42
- standalone,
43
- button,
44
- onClick,
45
- }: FormFieldLabelProps) => {
46
- const Label = readonly || standalone ? 'span' : Input.Label;
47
- const labelNode = <Label className={mx(inputTextLabel, 'text-sm')}>{label}</Label>;
48
-
49
- return (
50
- <div
51
- className={mx('grid grid-cols-[1fr_auto_auto] items-center select-none', onClick && 'cursor-pointer', classNames)}
52
- onClick={onClick}
53
- >
54
- {labelNode}
55
- {error ? (
56
- <Tooltip.Trigger asChild content={error} side='bottom'>
57
- <Icon icon='ph--warning--regular' size={4} classNames='text-error-text' />
58
- </Tooltip.Trigger>
59
- ) : (
60
- <span />
61
- )}
62
- {button}
63
- </div>
64
- );
65
- };
66
-
67
- FormFieldLabel.displayName = 'Form.FieldLabel';
68
-
69
- //
70
- // FormFieldWrapper
71
- //
72
-
73
- /**
74
- * Formats a value for `static` (read-only, plain-DOM) presentation based on its
75
- * type `format`. Dates/times are rendered human-readable; everything else falls
76
- * back to `String(value)`.
77
- */
78
- const formatStaticValue = (value: unknown, format?: Format.TypeFormat): string => {
79
- if (value == null) {
80
- return '';
81
- }
82
-
83
- switch (format) {
84
- case Format.TypeFormat.DateTime:
85
- case Format.TypeFormat.Date:
86
- case Format.TypeFormat.Time: {
87
- const date = new Date(value as string);
88
- if (Number.isNaN(date.getTime())) {
89
- return String(value);
90
- }
91
- const pattern = format === Format.TypeFormat.DateTime ? 'PPp' : format === Format.TypeFormat.Date ? 'PP' : 'p';
92
- return formatDate(date, pattern);
93
- }
94
- default:
95
- return String(value);
96
- }
97
- };
98
-
99
- export type FormFieldWrapperProps<T = any> = Pick<
100
- FormFieldRendererProps,
101
- 'readonly' | 'label' | 'layout' | 'getStatus' | 'getValue' | 'jsonPath' | 'format'
102
- > & {
103
- children?: (props: { value: T }) => ReactNode;
104
- };
105
-
106
- export const FormFieldWrapper = <T,>(props: FormFieldWrapperProps<T>) => {
107
- const { children, readonly, layout, label, jsonPath, format, getStatus, getValue } = props;
108
- const { status, error } = getStatus();
109
-
110
- const value = getValue();
111
- if (layout === 'static' && value == null) {
112
- return null;
113
- }
114
-
115
- const str = formatStaticValue(value, format);
116
-
117
- return (
118
- <div className='contents'>
119
- <Input.Root validationValence={status}>
120
- {layout !== 'inline' && <FormFieldLabel error={error} readonly={readonly} label={label} path={jsonPath} />}
121
- {layout === 'static' ? (
122
- <p className='truncate min-w-0' title={str}>
123
- {str}
124
- </p>
125
- ) : children ? (
126
- children({ value })
127
- ) : null}
128
- {layout === 'full' && error && (
129
- <Input.DescriptionAndValidation>
130
- <Input.Validation>{error}</Input.Validation>
131
- </Input.DescriptionAndValidation>
132
- )}
133
- </Input.Root>
134
- </div>
135
- );
136
- };
137
-
138
- //
139
- // FormFieldErrorBoundary
140
- //
141
-
142
- type FormFieldErrorState = {
143
- error: Error | undefined;
144
- };
145
-
146
- type FormFieldErrorBoundaryProps = PropsWithChildren<{
147
- path?: (string | number)[];
148
- }>;
149
-
150
- export class FormFieldErrorBoundary extends Component<FormFieldErrorBoundaryProps, FormFieldErrorState> {
151
- static getDerivedStateFromError(error: Error): { error: Error } {
152
- return { error };
153
- }
154
-
155
- override state = { error: undefined };
156
-
157
- override componentDidUpdate(prevProps: FormFieldErrorBoundaryProps) {
158
- if (prevProps.path !== this.props.path) {
159
- this.resetError();
160
- }
161
- }
162
-
163
- override render() {
164
- if (this.state.error) {
165
- return (
166
- <div className='flex gap-2 border border-error-border font-mono text-sm'>
167
- <span className='bg-error-bg text-base-fg px-1 font-thin'>ERROR</span>
168
- {String(this.props.path?.join('.'))}
169
- </div>
170
- );
171
- }
172
-
173
- return this.props.children;
174
- }
175
-
176
- private resetError() {
177
- this.setState({ error: undefined });
178
- }
179
- }
@@ -1,51 +0,0 @@
1
- # Plugin Settings Audit
2
-
3
- Tracks which plugin settings panels use schema-driven `Settings.FieldSet` and which still hand-roll their controls.
4
-
5
- The target shape for a plugin settings panel is:
6
-
7
- ```tsx
8
- <SettingsForm.Viewport>
9
- <SettingsForm.Section title={t('settings.title', { ns: meta.id })}>
10
- <SettingsForm.FieldSet
11
- readonly={!onSettingsChange}
12
- schema={Settings.Settings}
13
- values={settings}
14
- onValuesChanged={(values) => onSettingsChange?.(() => values)}
15
- />
16
- </SettingsForm.Section>
17
- </SettingsForm.Viewport>
18
- ```
19
-
20
- Field labels and descriptions come from `.annotations({ title, description })` on each schema field. Use `fieldMap` for runtime-driven option lists (model pickers, etc.). Use the `visible` callback to gate fields on environment or sibling values.
21
-
22
- ## FieldSet adopters
23
-
24
- | Plugin | Notes |
25
- | ---------------------- | -------------------------------------------------------------------------------------------------------- |
26
- | `plugin-assistant` | Nested `modelDefaults` struct; `fieldMap` provides per-provider model selectors via `createSelectField`. |
27
- | `plugin-crx` | Two booleans. |
28
- | `plugin-deck` | Five booleans; `enableNativeRedirect` hidden via `visible` when `isSocket`. |
29
- | `plugin-inbox` | Single boolean. |
30
- | `plugin-markdown` | Original reference adopter. `snippets` text field gated via `visible`/`fieldMap`. |
31
- | `plugin-meeting` | Single boolean. |
32
- | `plugin-observability` | Boolean; info `Message.Root` banner kept above `FieldSet`. |
33
- | `plugin-presenter` | Single boolean. |
34
- | `plugin-sample` | Single boolean. |
35
- | `plugin-script` | Mixed: auth `Button` retained as a manual `SettingsForm.Item`; `editorInputMode` rendered by `FieldSet`. |
36
- | `plugin-sketch` | Boolean + `gridType` literal union (rendered as a Select — was previously a Switch). |
37
-
38
- ## Skipped
39
-
40
- | Plugin | Reason |
41
- | ------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
42
- | `plugin-debug` | Action buttons (download diagnostics, download logs, repair), `Toast`, and a storage-adapter selector wired to `client.config` rather than the plugin settings atom. |
43
- | `plugin-space` | Renders a list of spaces with per-space "open settings" buttons — not bound to any schema field. |
44
- | `plugin-spacetime` | Section is currently empty; nothing to render. |
45
- | `plugin-thread` | Settings schema is `Schema.Struct({})` — nothing to render. |
46
-
47
- ## When to extend `SettingsFieldSet` vs. add a custom field
48
-
49
- - **Extend `SettingsFieldSet`** when the rendering rule applies generally to a schema construct (e.g. nested structs recursing into sibling fields, literal unions becoming Selects). Keep auto-detection in `detectFieldType`.
50
- - **Use `fieldMap`** when the schema can't fully describe the rendering — runtime-driven option lists, custom inputs, or override of the auto-rendered control. Use the typed form `SettingsFieldMap<T>` (or `satisfies SettingsFieldMap<Settings>`) so the entries are checked against the values shape.
51
- - **Drop back to manual `SettingsForm.Item`** when the panel needs UI that isn't bound to a settings field at all (action buttons, info banners, lists of external entities). These coexist as siblings of `FieldSet` inside the same `Section`.
@@ -1,104 +0,0 @@
1
- //
2
- // Copyright 2024 DXOS.org
3
- //
4
-
5
- import { type Meta, type StoryObj } from '@storybook/react-vite';
6
- import * as Schema from 'effect/Schema';
7
- import React from 'react';
8
-
9
- import { random } from '@dxos/random';
10
- import { Button, Input } from '@dxos/react-ui';
11
- import { withLayout, withTheme } from '@dxos/react-ui/testing';
12
-
13
- import { translations } from '#translations';
14
-
15
- import { Settings } from './Settings';
16
-
17
- random.seed(132);
18
-
19
- const DefaultStory = () => {
20
- return (
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>
32
- </Settings.Section>
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>
40
- );
41
- };
42
-
43
- const meta = {
44
- title: 'ui/react-ui-form/Settings',
45
- render: DefaultStory,
46
- decorators: [withTheme(), withLayout({ layout: 'fullscreen' })],
47
- parameters: {
48
- layout: 'fullscreen',
49
- translations,
50
- },
51
- } satisfies Meta;
52
-
53
- export default meta;
54
-
55
- type Story = StoryObj<typeof meta>;
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
- };
@@ -1,81 +0,0 @@
1
- //
2
- // Copyright 2025 DXOS.org
3
- //
4
-
5
- import React, { type PropsWithChildren } from 'react';
6
-
7
- import { type Label, ScrollArea, type ThemedClassName, toLocalizedString, useTranslation } from '@dxos/react-ui';
8
- import { composable, composableProps } from '@dxos/react-ui';
9
- import { mx } from '@dxos/ui-theme';
10
-
11
- import { translationKey } from '#translations';
12
-
13
- import { SettingsFieldSet } from './SettingsFieldSet';
14
- import { SettingsItem, SettingsPanel } from './SettingsItem';
15
-
16
- const SETTINGS_ROOT_NAME = 'Settings.Root';
17
- const SETTINGS_VIEWPORT_NAME = 'Settings.Viewport';
18
- const SETTINGS_SECTION_NAME = 'Settings.Section';
19
-
20
- //
21
- // Root (headless — no styling, no scroll).
22
- //
23
-
24
- type SettingsRootProps = ThemedClassName<PropsWithChildren>;
25
-
26
- const SettingsRoot = ({ children, classNames }: SettingsRootProps) => {
27
- return <div className={mx('flex flex-col gap-2', classNames)}>{children}</div>;
28
- };
29
-
30
- SettingsRoot.displayName = SETTINGS_ROOT_NAME;
31
-
32
- //
33
- // Viewport (scroll area + padding + document layout).
34
- //
35
-
36
- const SettingsViewport = composable<HTMLDivElement>(({ children, ...props }, forwardedRef) => {
37
- return (
38
- <ScrollArea.Root {...composableProps(props)} orientation='vertical' centered thin ref={forwardedRef}>
39
- <ScrollArea.Viewport classNames='px-4 pointer-coarse:px-2'>
40
- <div className='dx-document flex flex-col gap-2 py-4'>{children}</div>
41
- </ScrollArea.Viewport>
42
- </ScrollArea.Root>
43
- );
44
- });
45
-
46
- SettingsViewport.displayName = SETTINGS_VIEWPORT_NAME;
47
-
48
- //
49
- // Section (title + optional description + children with spacing).
50
- //
51
-
52
- type SettingsSectionProps = PropsWithChildren<{
53
- title: Label;
54
- description?: Label;
55
- }>;
56
-
57
- const SettingsSection = ({ title, description, children }: SettingsSectionProps) => {
58
- const { t } = useTranslation(translationKey);
59
- return (
60
- <>
61
- <h2 className='px-trim-md mt-trim-md text-xl'>{toLocalizedString(title, t)}</h2>
62
- {description && <p className='px-trim-md text-description'>{toLocalizedString(description, t)}</p>}
63
- <div className='w-full pt-trim-md space-y-trim-md'>{children}</div>
64
- </>
65
- );
66
- };
67
-
68
- SettingsSection.displayName = SETTINGS_SECTION_NAME;
69
-
70
- //
71
- // Settings
72
- //
73
-
74
- export const Settings = {
75
- Root: SettingsRoot,
76
- Viewport: SettingsViewport,
77
- Section: SettingsSection,
78
- Panel: SettingsPanel,
79
- Item: SettingsItem,
80
- FieldSet: SettingsFieldSet,
81
- };
@@ -1,257 +0,0 @@
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 { SchemaEx } 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: SchemaEx.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 = SchemaEx.getAnnotation<string>(SchemaAST.TitleAnnotationId)(type) ?? String.capitalize(name);
162
- const description = SchemaEx.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
- };