@dxos/react-ui-form 0.8.4-main.937b3ca → 0.8.4-main.9be5663bfe

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 (113) hide show
  1. package/README.md +0 -1
  2. package/dist/lib/browser/index.mjs +1003 -668
  3. package/dist/lib/browser/index.mjs.map +4 -4
  4. package/dist/lib/browser/meta.json +1 -1
  5. package/dist/lib/node-esm/index.mjs +1003 -668
  6. package/dist/lib/node-esm/index.mjs.map +4 -4
  7. package/dist/lib/node-esm/meta.json +1 -1
  8. package/dist/types/src/components/FieldEditor/FieldEditor.d.ts +3 -3
  9. package/dist/types/src/components/FieldEditor/FieldEditor.d.ts.map +1 -1
  10. package/dist/types/src/components/FieldEditor/FieldEditor.stories.d.ts +62 -61
  11. package/dist/types/src/components/FieldEditor/FieldEditor.stories.d.ts.map +1 -1
  12. package/dist/types/src/components/Form/Form.d.ts +23 -18
  13. package/dist/types/src/components/Form/Form.d.ts.map +1 -1
  14. package/dist/types/src/components/Form/Form.stories.d.ts +68 -67
  15. package/dist/types/src/components/Form/Form.stories.d.ts.map +1 -1
  16. package/dist/types/src/components/Form/FormField.d.ts +7 -1
  17. package/dist/types/src/components/Form/FormField.d.ts.map +1 -1
  18. package/dist/types/src/components/Form/FormFieldComponent.d.ts +0 -1
  19. package/dist/types/src/components/Form/FormFieldComponent.d.ts.map +1 -1
  20. package/dist/types/src/components/Form/FormFieldComponent.stories.d.ts +2 -2
  21. package/dist/types/src/components/Form/FormFieldComponent.stories.d.ts.map +1 -1
  22. package/dist/types/src/components/Form/FormFieldSet.d.ts +9 -10
  23. package/dist/types/src/components/Form/FormFieldSet.d.ts.map +1 -1
  24. package/dist/types/src/components/Form/fields/ArrayField.d.ts.map +1 -1
  25. package/dist/types/src/components/Form/fields/RefField.d.ts +8 -6
  26. package/dist/types/src/components/Form/fields/RefField.d.ts.map +1 -1
  27. package/dist/types/src/components/Form/fields/SelectOptionField.d.ts.map +1 -1
  28. package/dist/types/src/components/Form/fields/SelectOptionField.stories.d.ts +62 -61
  29. package/dist/types/src/components/Form/fields/SelectOptionField.stories.d.ts.map +1 -1
  30. package/dist/types/src/components/ObjectForm/ObjectForm.d.ts +9 -0
  31. package/dist/types/src/components/ObjectForm/ObjectForm.d.ts.map +1 -0
  32. package/dist/types/src/components/ObjectForm/index.d.ts +2 -0
  33. package/dist/types/src/components/ObjectForm/index.d.ts.map +1 -0
  34. package/dist/types/src/components/ObjectPicker/ObjectPicker.d.ts +10 -2
  35. package/dist/types/src/components/ObjectPicker/ObjectPicker.d.ts.map +1 -1
  36. package/dist/types/src/components/ObjectPicker/ObjectPicker.stories.d.ts +62 -61
  37. package/dist/types/src/components/ObjectPicker/ObjectPicker.stories.d.ts.map +1 -1
  38. package/dist/types/src/components/ObjectProperties/ObjectProperties.d.ts +9 -0
  39. package/dist/types/src/components/ObjectProperties/ObjectProperties.d.ts.map +1 -0
  40. package/dist/types/src/components/ObjectProperties/ObjectProperties.stories.d.ts +83 -0
  41. package/dist/types/src/components/ObjectProperties/ObjectProperties.stories.d.ts.map +1 -0
  42. package/dist/types/src/components/ObjectProperties/index.d.ts +2 -0
  43. package/dist/types/src/components/ObjectProperties/index.d.ts.map +1 -0
  44. package/dist/types/src/components/Settings/Settings.d.ts +35 -0
  45. package/dist/types/src/components/Settings/Settings.d.ts.map +1 -0
  46. package/dist/types/src/components/Settings/Settings.stories.d.ts +82 -0
  47. package/dist/types/src/components/Settings/Settings.stories.d.ts.map +1 -0
  48. package/dist/types/src/components/Settings/index.d.ts +2 -0
  49. package/dist/types/src/components/Settings/index.d.ts.map +1 -0
  50. package/dist/types/src/components/ViewEditor/ViewEditor.d.ts +5 -5
  51. package/dist/types/src/components/ViewEditor/ViewEditor.d.ts.map +1 -1
  52. package/dist/types/src/components/ViewEditor/ViewEditor.stories.d.ts +66 -64
  53. package/dist/types/src/components/ViewEditor/ViewEditor.stories.d.ts.map +1 -1
  54. package/dist/types/src/components/index.d.ts +3 -1
  55. package/dist/types/src/components/index.d.ts.map +1 -1
  56. package/dist/types/src/components/testing.d.ts.map +1 -1
  57. package/dist/types/src/hooks/useFormHandler.d.ts.map +1 -1
  58. package/dist/types/src/hooks/useKeyHandler.d.ts +2 -1
  59. package/dist/types/src/hooks/useKeyHandler.d.ts.map +1 -1
  60. package/dist/types/src/translations.d.ts +62 -61
  61. package/dist/types/src/translations.d.ts.map +1 -1
  62. package/dist/types/src/util/refs.d.ts +1 -0
  63. package/dist/types/src/util/refs.d.ts.map +1 -1
  64. package/dist/types/tsconfig.tsbuildinfo +1 -1
  65. package/package.json +37 -38
  66. package/src/components/FieldEditor/FieldEditor.stories.tsx +7 -8
  67. package/src/components/FieldEditor/FieldEditor.test.tsx +0 -1
  68. package/src/components/FieldEditor/FieldEditor.tsx +3 -4
  69. package/src/components/Form/AUDIT.md +17 -0
  70. package/src/components/Form/Form.stories.tsx +25 -26
  71. package/src/components/Form/Form.tsx +101 -66
  72. package/src/components/Form/FormField.tsx +32 -14
  73. package/src/components/Form/FormFieldComponent.stories.tsx +5 -5
  74. package/src/components/Form/FormFieldComponent.tsx +17 -20
  75. package/src/components/Form/FormFieldSet.tsx +126 -105
  76. package/src/components/Form/fields/ArrayField.tsx +11 -8
  77. package/src/components/Form/fields/GeoPointField.tsx +6 -6
  78. package/src/components/Form/fields/MarkdownField.tsx +1 -1
  79. package/src/components/Form/fields/RefField.tsx +37 -20
  80. package/src/components/Form/fields/SelectField.tsx +1 -1
  81. package/src/components/Form/fields/SelectOptionField.stories.tsx +2 -3
  82. package/src/components/Form/fields/SelectOptionField.tsx +9 -10
  83. package/src/components/Form/fields/TupleField.tsx +1 -1
  84. package/src/components/ObjectForm/ObjectForm.tsx +113 -0
  85. package/src/components/{ControlSection → ObjectForm}/index.ts +1 -1
  86. package/src/components/ObjectPicker/ObjectPicker.stories.tsx +13 -18
  87. package/src/components/ObjectPicker/ObjectPicker.tsx +71 -60
  88. package/src/components/ObjectProperties/ObjectProperties.stories.tsx +63 -0
  89. package/src/components/ObjectProperties/ObjectProperties.tsx +129 -0
  90. package/src/components/ObjectProperties/index.ts +5 -0
  91. package/src/components/Settings/Settings.stories.tsx +55 -0
  92. package/src/components/Settings/Settings.tsx +129 -0
  93. package/src/components/Settings/index.ts +5 -0
  94. package/src/components/ViewEditor/ViewEditor.stories.tsx +36 -33
  95. package/src/components/ViewEditor/ViewEditor.test.tsx +3 -7
  96. package/src/components/ViewEditor/ViewEditor.tsx +117 -85
  97. package/src/components/index.ts +3 -1
  98. package/src/components/testing.tsx +20 -8
  99. package/src/hooks/useFormHandler.ts +42 -18
  100. package/src/hooks/useKeyHandler.tsx +7 -7
  101. package/src/translations.ts +63 -63
  102. package/src/util/refs.ts +7 -3
  103. package/dist/types/src/components/ControlSection/ControlSection.d.ts +0 -23
  104. package/dist/types/src/components/ControlSection/ControlSection.d.ts.map +0 -1
  105. package/dist/types/src/components/ControlSection/ControlSection.stories.d.ts +0 -87
  106. package/dist/types/src/components/ControlSection/ControlSection.stories.d.ts.map +0 -1
  107. package/dist/types/src/components/ControlSection/index.d.ts +0 -2
  108. package/dist/types/src/components/ControlSection/index.d.ts.map +0 -1
  109. package/dist/types/src/components/Form/Form-old.stories.d.ts +0 -131
  110. package/dist/types/src/components/Form/Form-old.stories.d.ts.map +0 -1
  111. package/src/components/ControlSection/ControlSection.stories.tsx +0 -38
  112. package/src/components/ControlSection/ControlSection.tsx +0 -153
  113. package/src/components/Form/Form-old.stories.tsx +0 -314
@@ -4,13 +4,20 @@
4
4
 
5
5
  import { createContext } from '@radix-ui/react-context';
6
6
  import * as Schema from 'effect/Schema';
7
- import type * as SchemaAST from 'effect/SchemaAST';
7
+ import * as SchemaAST from 'effect/SchemaAST';
8
8
  import React, { type PropsWithChildren, useEffect, useMemo, useRef } from 'react';
9
9
 
10
10
  import { type AnyProperties } from '@dxos/echo/internal';
11
- import { createJsonPath, getValue as getValue$, setValue as setValue$ } from '@dxos/effect';
12
- import { IconButton, type IconButtonProps, ScrollArea, type ThemedClassName, useTranslation } from '@dxos/react-ui';
13
- import { mx } from '@dxos/ui-theme';
11
+ import { createJsonPath, getValue as getValue$ } from '@dxos/effect';
12
+ import {
13
+ IconButton,
14
+ type IconButtonProps,
15
+ ScrollArea,
16
+ type ThemedClassName,
17
+ useMergeRefs,
18
+ useTranslation,
19
+ } from '@dxos/react-ui';
20
+ import { composable, composableProps, mx } from '@dxos/ui-theme';
14
21
 
15
22
  import {
16
23
  type FormHandler,
@@ -20,28 +27,13 @@ import {
20
27
  useKeyHandler,
21
28
  } from '../../hooks';
22
29
  import { translationKey } from '../../translations';
23
-
24
30
  import { FormFieldLabel, type FormFieldLabelProps, type FormFieldStateProps } from './FormFieldComponent';
25
31
  import {
26
32
  FormFieldSet as NaturalFormFieldSet,
27
33
  type FormFieldSetProps as NaturalFormFieldSetProps,
28
34
  } from './FormFieldSet';
29
35
 
30
- // New features/polish
31
- // [x] Unify readonly/inline modes
32
- // [ ] Don't call save/autoSave if value hasn't changed
33
- // [ ] Fix onCancel (restore values)
34
- // [ ] Fix useSchema Type.Obj.Any cast
35
- // [ ] Remove @dxos/echo-db deps
36
- // [ ] TableCellEditor (handleEnter/ModalController).
37
- // [ ] Use FormFieldWrapper uniformly
38
- // [ ] Inline tables for object arrays
39
- // [ ] Defer query until popover
40
- // [ ] Omit id from sub properties.
41
- // [x] Refs
42
- // [x] Single-select (fix popover)
43
- // [x] Multi-select (array)
44
- // [ ] auto save doesn't work for combobox + select due to only firing on blur (workaround is to use onValuesChanged)
36
+ // TODO(burdon): Move styles to form.ts.
45
37
 
46
38
  // TODO(burdon): Move to @dxos/schema (re-export here).
47
39
  export type ExcludeId<S extends Schema.Schema.AnyNoContext> = Omit<Schema.Schema.Type<S>, 'id'>;
@@ -69,7 +61,10 @@ type FormContextValue<T extends AnyProperties = any> = {
69
61
  * Testing.
70
62
  */
71
63
  testId?: string;
72
- } & Pick<NaturalFormFieldSetProps<T>, 'readonly' | 'layout' | 'fieldMap' | 'fieldProvider'>;
64
+ } & Pick<
65
+ NaturalFormFieldSetProps<T>,
66
+ 'readonly' | 'layout' | 'fieldMap' | 'fieldProvider' | 'projection' | 'createTypename' | 'createFieldMap'
67
+ >;
73
68
 
74
69
  const [FormContextProvider, useFormContext] = createContext<FormContextValue>('Form');
75
70
 
@@ -83,19 +78,25 @@ const useFormValues: {
83
78
  path: (string | number)[] | undefined,
84
79
  defaultValue: () => T,
85
80
  ): T | undefined;
86
- } = (componentName: string, path: (string | number)[] = [], defaultValue?: () => any) => {
87
- const jsonPath = createJsonPath(path);
81
+ } = (componentName: string, path?: (string | number)[], defaultValue?: () => any) => {
82
+ const stablePath = useMemo(() => path ?? [], [path ? path.join('.') : undefined]);
83
+ const jsonPath = createJsonPath(stablePath);
88
84
  const {
89
- form: { values },
85
+ form: { values, onValueChange },
90
86
  } = useFormContext(componentName);
91
87
 
92
88
  const value = getValue$(values, jsonPath);
93
89
 
90
+ // Apply default value once when the field has no value. lastAppliedPathRef prevents
91
+ // re-applying on every render (e.g. when defaultValue() returns null) and ensures
92
+ // we apply per path when the hook is used for different fields.
93
+ const lastAppliedPathRef = useRef<string | null>(null);
94
94
  useEffect(() => {
95
- if (!value && defaultValue) {
96
- setValue$(values, jsonPath, defaultValue());
95
+ if (value == null && defaultValue && lastAppliedPathRef.current !== jsonPath) {
96
+ lastAppliedPathRef.current = jsonPath;
97
+ onValueChange(stablePath, SchemaAST.stringKeyword, defaultValue());
97
98
  }
98
- }, [value, defaultValue]);
99
+ }, [value, defaultValue, onValueChange, stablePath, jsonPath]);
99
100
 
100
101
  return value;
101
102
  };
@@ -163,65 +164,69 @@ FormRoot.displayName = 'Form.Root';
163
164
  // Viewport
164
165
  //
165
166
 
166
- type FormViewportProps = ThemedClassName<PropsWithChildren<{}>>;
167
+ const FORM_VIEWPORT_NAME = 'Form.Viewport';
167
168
 
168
- const FormViewport = ({ classNames, children }: FormViewportProps) => {
169
+ type FormViewportProps = {};
170
+
171
+ const FormViewport = composable<HTMLDivElement>(({ children, ...props }, forwardedRef) => {
169
172
  return (
170
- <ScrollArea.Root>
171
- <ScrollArea.Viewport classNames={['plb-cardSpacingBlock', classNames]}>{children}</ScrollArea.Viewport>
172
- <ScrollArea.Scrollbar orientation='vertical'>
173
- <ScrollArea.Thumb />
174
- </ScrollArea.Scrollbar>
173
+ <ScrollArea.Root {...composableProps(props)} orientation='vertical' centered padding thin ref={forwardedRef}>
174
+ <ScrollArea.Viewport>{children}</ScrollArea.Viewport>
175
175
  </ScrollArea.Root>
176
176
  );
177
- };
177
+ });
178
178
 
179
- FormViewport.displayName = 'Form.Viewport';
179
+ FormViewport.displayName = FORM_VIEWPORT_NAME;
180
180
 
181
181
  //
182
182
  // Content
183
183
  //
184
184
 
185
+ const FORM_CONTENT_NAME = 'Form.Content';
186
+
185
187
  type FormContentProps = ThemedClassName<PropsWithChildren<{}>>;
186
188
 
187
- // TOOD(burdon): Figure out nesting (indent and testId).
188
- const FormContent = ({ classNames, children }: FormContentProps) => {
189
- const { form, testId } = useFormContext(FormContent.displayName);
190
- const ref = useRef<HTMLDivElement>(null);
191
- useKeyHandler(ref.current, form);
189
+ const FormContent = composable<HTMLDivElement, FormContentProps>(({ children, ...props }, forwardedRef) => {
190
+ const { form, testId } = useFormContext(FORM_CONTENT_NAME);
191
+ const localRef = useRef<HTMLDivElement>(null);
192
+ const mergedRef = useMergeRefs([forwardedRef, localRef]);
193
+ useKeyHandler(localRef, form);
192
194
 
193
195
  return (
194
196
  <div
195
- ref={ref}
196
- role='form'
197
- className={mx('flex flex-col is-full pli-cardSpacingInline density-fine', classNames)}
197
+ {...composableProps(props, { role: 'form', classNames: 'flex flex-col w-full pb-form-gap' })}
198
198
  data-testid={testId}
199
+ ref={mergedRef}
199
200
  >
200
201
  {children}
201
202
  </div>
202
203
  );
203
- };
204
+ });
204
205
 
205
- FormContent.displayName = 'Form.Content';
206
+ FormContent.displayName = FORM_CONTENT_NAME;
206
207
 
207
208
  //
208
209
  // FieldSet
209
210
  //
210
211
 
211
- type FormFieldSetProps = ThemedClassName<{}>;
212
+ const FORM_FIELDSET_NAME = 'Form.FieldSet';
213
+
214
+ type FormFieldSetProps = ThemedClassName<NaturalFormFieldSetProps<any>>;
212
215
 
213
- const FormFieldSet = ({ classNames }: FormFieldSetProps) => {
214
- const { form, ...props } = useFormContext(FormFieldSet.displayName);
216
+ const FormFieldSet = (props: FormFieldSetProps) => {
217
+ const { form, ...contextProps } = useFormContext(FORM_FIELDSET_NAME);
215
218
 
216
- return <NaturalFormFieldSet classNames={classNames} schema={form.schema} {...props} />;
219
+ return <NaturalFormFieldSet schema={form.schema} {...contextProps} {...props} />;
217
220
  };
218
221
 
219
- FormFieldSet.displayName = 'Form.FieldSet';
222
+ FormFieldSet.displayName = FORM_FIELDSET_NAME;
220
223
 
221
224
  //
222
225
  // Actions
223
226
  //
224
227
 
228
+ const FORM_ACTIONS_NAME = 'Form.Actions';
229
+
225
230
  type FormActionsProps = ThemedClassName<{}>;
226
231
 
227
232
  const FormActions = ({ classNames }: FormActionsProps) => {
@@ -230,7 +235,7 @@ const FormActions = ({ classNames }: FormActionsProps) => {
230
235
  form: { canSave, onSave, onCancel },
231
236
  readonly,
232
237
  layout,
233
- } = useFormContext(FormActions.displayName);
238
+ } = useFormContext(FORM_ACTIONS_NAME);
234
239
 
235
240
  if (readonly || layout === 'static') {
236
241
  return null;
@@ -240,12 +245,12 @@ const FormActions = ({ classNames }: FormActionsProps) => {
240
245
  // Deprecate FormSubmit ans use FormActions without Cancel button if no callback is supplied.
241
246
 
242
247
  return (
243
- <div role='none' className={mx('grid grid-flow-col auto-cols-fr gap-2 pbs-cardSpacingBlock', classNames)}>
248
+ <div role='none' className={mx('grid grid-flow-col gap-form-gap auto-cols-fr py-form-padding', classNames)}>
244
249
  {onCancel && (
245
250
  <IconButton
246
251
  icon='ph--x--regular'
247
252
  iconEnd
248
- label={t('cancel button label')}
253
+ label={t('cancel-button.label')}
249
254
  onClick={onCancel}
250
255
  data-testid='cancel-button'
251
256
  />
@@ -257,7 +262,7 @@ const FormActions = ({ classNames }: FormActionsProps) => {
257
262
  disabled={!canSave}
258
263
  icon='ph--check--regular'
259
264
  iconEnd
260
- label={t('save button label')}
265
+ label={t('save-button.label')}
261
266
  onClick={onSave}
262
267
  data-testid='save-button'
263
268
  />
@@ -266,35 +271,62 @@ const FormActions = ({ classNames }: FormActionsProps) => {
266
271
  );
267
272
  };
268
273
 
269
- FormActions.displayName = 'Form.Actions';
274
+ FormActions.displayName = FORM_ACTIONS_NAME;
275
+
276
+ //
277
+ // Section
278
+ //
279
+
280
+ const FORM_SECTION_NAME = 'Form.Section';
281
+
282
+ type FormSectionProps = ThemedClassName<{ label: string; description?: string }>;
283
+
284
+ const FormSection = composable<HTMLDivElement, FormSectionProps>(
285
+ ({ children, label, description, ...props }, forwardedRef) => {
286
+ return (
287
+ <div
288
+ {...composableProps(props, { classNames: 'flex flex-col pt-form-section-gap first:pt-0' })}
289
+ ref={forwardedRef}
290
+ >
291
+ <h2 className='text-lg'>{label}</h2>
292
+ {description && <p className='text-description'>{description}</p>}
293
+ {children}
294
+ </div>
295
+ );
296
+ },
297
+ );
298
+
299
+ FormSection.displayName = FORM_SECTION_NAME;
270
300
 
271
301
  //
272
302
  // Submit
273
303
  //
274
304
 
275
- type FormSubmitProps = ThemedClassName<Partial<Pick<IconButtonProps, 'icon' | 'label'>>>;
305
+ const FORM_SUBMIT_NAME = 'Form.Submit';
306
+
307
+ type FormSubmitProps = ThemedClassName<Partial<Pick<IconButtonProps, 'icon' | 'label' | 'disabled'>>>;
276
308
 
277
- const FormSubmit = ({ classNames, label, icon }: FormSubmitProps) => {
309
+ const FormSubmit = ({ classNames, label, icon, disabled }: FormSubmitProps) => {
278
310
  const { t } = useTranslation(translationKey);
279
311
  const {
280
312
  form: { canSave, onSave },
281
313
  readonly,
282
314
  layout,
283
- } = useFormContext(FormSubmit.displayName);
315
+ } = useFormContext(FORM_SUBMIT_NAME);
284
316
 
285
317
  if (readonly || layout === 'static') {
286
318
  return null;
287
319
  }
288
320
 
289
321
  return (
290
- <div role='none' className={mx('flex is-full pbs-cardSpacingBlock', classNames)}>
322
+ <div role='none' className={mx('flex w-full pt-form-padding', classNames)}>
291
323
  <IconButton
292
- classNames='is-full'
324
+ classNames='w-full'
293
325
  type='submit'
294
326
  variant='primary'
295
- disabled={!canSave}
327
+ disabled={disabled ?? !canSave}
296
328
  icon={icon ?? 'ph--check--regular'}
297
- label={label ?? t('save button label')}
329
+ label={label ?? t('save-button.label')}
298
330
  onClick={onSave}
299
331
  data-testid='save-button'
300
332
  />
@@ -302,7 +334,7 @@ const FormSubmit = ({ classNames, label, icon }: FormSubmitProps) => {
302
334
  );
303
335
  };
304
336
 
305
- FormSubmit.displayName = 'Form.Submit';
337
+ FormSubmit.displayName = FORM_SUBMIT_NAME;
306
338
 
307
339
  //
308
340
  // Form
@@ -313,10 +345,11 @@ export const Form = {
313
345
  Root: FormRoot,
314
346
  Viewport: FormViewport,
315
347
  Content: FormContent,
348
+ Section: FormSection,
316
349
  FieldSet: FormFieldSet,
350
+ Label: FormFieldLabel,
317
351
  Actions: FormActions,
318
352
  Submit: FormSubmit,
319
- Label: FormFieldLabel,
320
353
  };
321
354
 
322
355
  export { useFormContext, useFormValues, useFormFieldState };
@@ -325,7 +358,9 @@ export type {
325
358
  FormRootProps,
326
359
  FormViewportProps,
327
360
  FormContentProps,
361
+ FormSectionProps,
328
362
  FormFieldSetProps,
363
+ FormFieldLabelProps,
329
364
  FormActionsProps,
330
- FormFieldLabelProps as LabelProps,
365
+ FormSubmitProps,
331
366
  };
@@ -25,7 +25,6 @@ import { type ProjectionModel } from '@dxos/schema';
25
25
 
26
26
  import { translationKey } from '../../translations';
27
27
  import { getRefProps } from '../../util';
28
-
29
28
  import {
30
29
  ArrayField,
31
30
  BooleanField,
@@ -78,12 +77,19 @@ export type FormFieldProps = {
78
77
  * Function to lookup custom renderers for specific properties.
79
78
  */
80
79
  fieldProvider?: FormFieldProvider;
80
+ /**
81
+ * Typename of the ref type that the create props apply to.
82
+ * When set, createOptionLabel/createOptionIcon/createInitialValuePath/createFieldMap/onCreate
83
+ * are only passed to ref fields whose typename matches.
84
+ */
85
+ createTypename?: string;
81
86
  } & Pick<FormFieldComponentProps, 'autoFocus' | 'readonly' | 'layout'> &
82
87
  Pick<
83
88
  RefFieldProps,
84
89
  | 'createOptionLabel'
85
90
  | 'createOptionIcon'
86
91
  | 'createInitialValuePath'
92
+ | 'createFieldMap'
87
93
  | 'db'
88
94
  | 'schemaHook'
89
95
  | 'getOptions'
@@ -102,12 +108,14 @@ export const FormField = (props: FormFieldProps) => {
102
108
  layout,
103
109
 
104
110
  // RefFieldProps
111
+ createTypename,
105
112
  createOptionLabel,
106
113
  createOptionIcon,
107
114
  createInitialValuePath,
115
+ createFieldMap,
108
116
  db,
109
117
  schemaHook,
110
- getOptions: getRefOptions,
118
+ getOptions,
111
119
  onCreate,
112
120
  } = props;
113
121
  const { t } = useTranslation(translationKey);
@@ -117,7 +125,7 @@ export const FormField = (props: FormFieldProps) => {
117
125
 
118
126
  const label = useMemo(() => title ?? String.capitalize(name), [title, name]);
119
127
  const placeholder = useMemo(
120
- () => (examples?.length ? `${t('example placeholder')}: ${examples[0]}` : (description ?? label)),
128
+ () => (examples?.length ? `${t('example.placeholder')}: ${examples[0]}` : (description ?? label)),
121
129
  [examples, description, label],
122
130
  );
123
131
 
@@ -169,15 +177,23 @@ export const FormField = (props: FormFieldProps) => {
169
177
  // Select field.
170
178
  //
171
179
 
172
- const options = getOptions(type);
180
+ const options = getSelectOptions(type);
173
181
  if (options) {
182
+ // Resolve labels from projection metadata when available.
183
+ const fieldProjections = projection?.getFieldProjections();
184
+ const fieldProjection = fieldProjections?.find((fp) => fp.field.path === name);
185
+ const selectOptions = fieldProjection?.props.options;
186
+
174
187
  return (
175
188
  <SelectField
176
189
  {...fieldProps}
177
- options={options.map((option) => ({
178
- value: option,
179
- label: option.toString(),
180
- }))}
190
+ options={options.map((option) => {
191
+ const selectOption = selectOptions?.find((so) => so.id === globalThis.String(option));
192
+ return {
193
+ value: option,
194
+ label: selectOption?.title ?? option.toString(),
195
+ };
196
+ })}
181
197
  />
182
198
  );
183
199
  }
@@ -188,16 +204,18 @@ export const FormField = (props: FormFieldProps) => {
188
204
 
189
205
  const refProps = getRefProps(type);
190
206
  if (refProps) {
207
+ const isCreateTarget = !createTypename || refProps.typename === createTypename;
191
208
  return (
192
209
  <RefField
193
210
  {...fieldProps}
194
211
  {...refProps}
195
- createOptionLabel={createOptionLabel}
196
- createOptionIcon={createOptionIcon}
197
- createInitialValuePath={createInitialValuePath}
212
+ createOptionLabel={isCreateTarget ? createOptionLabel : undefined}
213
+ createOptionIcon={isCreateTarget ? createOptionIcon : undefined}
214
+ createInitialValuePath={isCreateTarget ? createInitialValuePath : undefined}
215
+ createFieldMap={isCreateTarget ? createFieldMap : undefined}
198
216
  db={db}
199
217
  schemaHook={schemaHook}
200
- getOptions={getRefOptions}
218
+ getOptions={getOptions}
201
219
  onCreate={onCreate}
202
220
  />
203
221
  );
@@ -230,7 +248,7 @@ export const FormField = (props: FormFieldProps) => {
230
248
  createInitialValuePath={createInitialValuePath}
231
249
  db={db}
232
250
  schemaHook={schemaHook}
233
- getOptions={getRefOptions}
251
+ getOptions={getOptions}
234
252
  onCreate={onCreate}
235
253
  />
236
254
  );
@@ -277,7 +295,7 @@ const getFormField = ({ type, format }: FormFieldComponentProps): FormFieldCompo
277
295
  }
278
296
  };
279
297
 
280
- const getOptions = (ast: SchemaAST.AST): Format.Options[] | undefined => {
298
+ const getSelectOptions = (ast: SchemaAST.AST): Format.Options[] | undefined => {
281
299
  if (isLiteralUnion(ast)) {
282
300
  return ast.types.map((type) => type.literal).filter((v): v is string | number => v !== null);
283
301
  }
@@ -10,7 +10,7 @@ import { withLayout, withTheme } from '@dxos/react-ui/testing';
10
10
 
11
11
  import { type FormFieldComponentProps, FormFieldWrapper } from './FormFieldComponent';
12
12
 
13
- const Component = ({
13
+ const DefaultStory = ({
14
14
  type,
15
15
  placeholder,
16
16
  readonly,
@@ -24,7 +24,7 @@ const Component = ({
24
24
  );
25
25
 
26
26
  return (
27
- <div className='plb-cardSpacingBlock pli-cardSpacingInline'>
27
+ <div className='p-form-padding'>
28
28
  <FormFieldWrapper<string> {...props}>
29
29
  {({ value = '' }) => (
30
30
  <Input.TextInput
@@ -41,10 +41,10 @@ const Component = ({
41
41
  );
42
42
  };
43
43
 
44
- const meta: Meta<typeof Component> = {
44
+ const meta: Meta<typeof DefaultStory> = {
45
45
  title: 'ui/react-ui-form/FormFieldComponent',
46
- component: Component,
47
- decorators: [withTheme, withLayout({ layout: 'column' })],
46
+ component: DefaultStory,
47
+ decorators: [withTheme(), withLayout({ layout: 'column' })],
48
48
  parameters: {
49
49
  layout: 'fullscreen',
50
50
  },
@@ -15,12 +15,10 @@ import React, {
15
15
 
16
16
  import { type Format } from '@dxos/echo/internal';
17
17
  import { Icon, Input, Tooltip } from '@dxos/react-ui';
18
- import { errorText, mx } from '@dxos/ui-theme';
18
+ import { inputTextLabel } from '@dxos/ui-theme';
19
19
 
20
20
  import { type FormFieldStatus } from '../../hooks';
21
21
 
22
- const labelSpacing = 'mbs-inputSpacingBlock mbe-labelSpacingBlock';
23
-
24
22
  /**
25
23
  * Dynamic props passed to input components.
26
24
  */
@@ -33,7 +31,6 @@ export type FormFieldStateProps<T = any> = {
33
31
 
34
32
  /**
35
33
  * Presentation mode.
36
- *
37
34
  * - full: Show label, control, and status.
38
35
  * - compact: Show label and control.
39
36
  * - inline: Control only.
@@ -79,14 +76,12 @@ export type FormFieldLabelProps = {
79
76
 
80
77
  export const FormFieldLabel = ({ label, error, readonly, asChild }: FormFieldLabelProps) => {
81
78
  const Label = readonly || asChild ? 'span' : Input.Label;
82
- const labelProps = readonly || asChild ? { className: 'text-description text-sm' } : { classNames: '!mlb-0 text-sm' };
83
-
84
79
  return (
85
- <div role='none' className={mx('flex justify-between items-center', labelSpacing)}>
86
- <Label {...labelProps}>{label}</Label>
80
+ <div role='none' className='flex items-center justify-between'>
81
+ <Label className={inputTextLabel}>{label}</Label>
87
82
  {error && (
88
83
  <Tooltip.Trigger asChild content={error} side='bottom'>
89
- <Icon icon='ph--warning--regular' size={4} classNames={errorText} />
84
+ <Icon icon='ph--warning--regular' size={4} classNames='text-error-text' />
90
85
  </Tooltip.Trigger>
91
86
  )}
92
87
  </div>
@@ -118,15 +113,17 @@ export const FormFieldWrapper = <T,>(props: FormFieldWrapperProps<T>) => {
118
113
  const str = String(value ?? '');
119
114
 
120
115
  return (
121
- <Input.Root validationValence={status}>
122
- {layout !== 'inline' && <FormFieldLabel error={error} readonly={readonly} label={label} />}
123
- {layout === 'static' ? <p>{str}</p> : children ? children({ value }) : null}
124
- {layout === 'full' && (
125
- <Input.DescriptionAndValidation>
126
- <Input.Validation>{error}</Input.Validation>
127
- </Input.DescriptionAndValidation>
128
- )}
129
- </Input.Root>
116
+ <div role='none' className='contents'>
117
+ <Input.Root validationValence={status}>
118
+ {layout !== 'inline' && <FormFieldLabel error={error} readonly={readonly} label={label} />}
119
+ {layout === 'static' ? <p>{str}</p> : children ? children({ value }) : null}
120
+ {layout === 'full' && (
121
+ <Input.DescriptionAndValidation>
122
+ <Input.Validation>{error}</Input.Validation>
123
+ </Input.DescriptionAndValidation>
124
+ )}
125
+ </Input.Root>
126
+ </div>
130
127
  );
131
128
  };
132
129
 
@@ -158,8 +155,8 @@ export class FormFieldErrorBoundary extends Component<FormFieldErrorBoundaryProp
158
155
  override render() {
159
156
  if (this.state.error) {
160
157
  return (
161
- <div className='flex gap-2 border border-roseFill font-mono text-sm'>
162
- <span className='bg-roseFill text-surfaceText pli-1 font-thin'>ERROR</span>
158
+ <div className='flex gap-2 border border-rose-fill font-mono text-sm'>
159
+ <span className='bg-rose-fill text-surface-text px-1 font-thin'>ERROR</span>
163
160
  {String(this.props.path?.join('.'))}
164
161
  </div>
165
162
  );