@dxos/react-ui-form 0.8.4-main.bc674ce → 0.8.4-main.c351d160a8

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 (82) hide show
  1. package/README.md +0 -1
  2. package/dist/lib/browser/index.mjs +538 -458
  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 +538 -458
  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.map +1 -1
  11. package/dist/types/src/components/Form/Form.d.ts +13 -13
  12. package/dist/types/src/components/Form/Form.d.ts.map +1 -1
  13. package/dist/types/src/components/Form/Form.stories.d.ts +3 -3
  14. package/dist/types/src/components/Form/Form.stories.d.ts.map +1 -1
  15. package/dist/types/src/components/Form/FormField.d.ts.map +1 -1
  16. package/dist/types/src/components/Form/FormFieldComponent.d.ts +0 -1
  17. package/dist/types/src/components/Form/FormFieldComponent.d.ts.map +1 -1
  18. package/dist/types/src/components/Form/FormFieldComponent.stories.d.ts +2 -2
  19. package/dist/types/src/components/Form/FormFieldComponent.stories.d.ts.map +1 -1
  20. package/dist/types/src/components/Form/FormFieldSet.d.ts +9 -10
  21. package/dist/types/src/components/Form/FormFieldSet.d.ts.map +1 -1
  22. package/dist/types/src/components/Form/fields/ArrayField.d.ts.map +1 -1
  23. package/dist/types/src/components/Form/fields/RefField.d.ts +6 -4
  24. package/dist/types/src/components/Form/fields/RefField.d.ts.map +1 -1
  25. package/dist/types/src/components/ObjectPicker/ObjectPicker.d.ts.map +1 -1
  26. package/dist/types/src/components/ObjectPicker/ObjectPicker.stories.d.ts.map +1 -1
  27. package/dist/types/src/components/Settings/Settings.d.ts +56 -0
  28. package/dist/types/src/components/Settings/Settings.d.ts.map +1 -0
  29. package/dist/types/src/components/{ControlSection/ControlSection.stories.d.ts → Settings/Settings.stories.d.ts} +2 -8
  30. package/dist/types/src/components/Settings/Settings.stories.d.ts.map +1 -0
  31. package/dist/types/src/components/Settings/index.d.ts +2 -0
  32. package/dist/types/src/components/Settings/index.d.ts.map +1 -0
  33. package/dist/types/src/components/ViewEditor/ViewEditor.d.ts +5 -5
  34. package/dist/types/src/components/ViewEditor/ViewEditor.d.ts.map +1 -1
  35. package/dist/types/src/components/ViewEditor/ViewEditor.stories.d.ts +2 -1
  36. package/dist/types/src/components/ViewEditor/ViewEditor.stories.d.ts.map +1 -1
  37. package/dist/types/src/components/index.d.ts +1 -1
  38. package/dist/types/src/components/index.d.ts.map +1 -1
  39. package/dist/types/src/components/testing.d.ts.map +1 -1
  40. package/dist/types/src/hooks/useFormHandler.d.ts.map +1 -1
  41. package/dist/types/src/util/index.d.ts +0 -1
  42. package/dist/types/src/util/index.d.ts.map +1 -1
  43. package/dist/types/tsconfig.tsbuildinfo +1 -1
  44. package/package.json +35 -37
  45. package/src/components/FieldEditor/FieldEditor.stories.tsx +4 -6
  46. package/src/components/FieldEditor/FieldEditor.tsx +2 -3
  47. package/src/components/Form/Form.stories.tsx +8 -7
  48. package/src/components/Form/Form.tsx +82 -55
  49. package/src/components/Form/FormField.tsx +12 -4
  50. package/src/components/Form/FormFieldComponent.stories.tsx +5 -5
  51. package/src/components/Form/FormFieldComponent.tsx +17 -20
  52. package/src/components/Form/FormFieldSet.tsx +106 -104
  53. package/src/components/Form/fields/ArrayField.tsx +6 -8
  54. package/src/components/Form/fields/MarkdownField.tsx +1 -1
  55. package/src/components/Form/fields/RefField.tsx +28 -15
  56. package/src/components/Form/fields/SelectField.tsx +1 -1
  57. package/src/components/Form/fields/SelectOptionField.stories.tsx +2 -2
  58. package/src/components/Form/fields/SelectOptionField.tsx +4 -4
  59. package/src/components/ObjectPicker/ObjectPicker.stories.tsx +9 -13
  60. package/src/components/Settings/Settings.stories.tsx +56 -0
  61. package/src/components/Settings/Settings.tsx +230 -0
  62. package/src/components/{ControlSection → Settings}/index.ts +1 -1
  63. package/src/components/ViewEditor/ViewEditor.stories.tsx +14 -12
  64. package/src/components/ViewEditor/ViewEditor.test.tsx +3 -6
  65. package/src/components/ViewEditor/ViewEditor.tsx +92 -63
  66. package/src/components/index.ts +1 -1
  67. package/src/components/testing.tsx +9 -6
  68. package/src/hooks/useFormHandler.ts +42 -24
  69. package/src/util/index.ts +0 -1
  70. package/dist/types/src/components/ControlSection/ControlSection.d.ts +0 -23
  71. package/dist/types/src/components/ControlSection/ControlSection.d.ts.map +0 -1
  72. package/dist/types/src/components/ControlSection/ControlSection.stories.d.ts.map +0 -1
  73. package/dist/types/src/components/ControlSection/index.d.ts +0 -2
  74. package/dist/types/src/components/ControlSection/index.d.ts.map +0 -1
  75. package/dist/types/src/components/Form/Form-old.stories.d.ts +0 -131
  76. package/dist/types/src/components/Form/Form-old.stories.d.ts.map +0 -1
  77. package/dist/types/src/util/echo.d.ts +0 -6
  78. package/dist/types/src/util/echo.d.ts.map +0 -1
  79. package/src/components/ControlSection/ControlSection.stories.tsx +0 -38
  80. package/src/components/ControlSection/ControlSection.tsx +0 -153
  81. package/src/components/Form/Form-old.stories.tsx +0 -314
  82. package/src/util/echo.ts +0 -25
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dxos/react-ui-form",
3
- "version": "0.8.4-main.bc674ce",
3
+ "version": "0.8.4-main.c351d160a8",
4
4
  "description": "Data components.",
5
5
  "homepage": "https://dxos.org",
6
6
  "bugs": "https://github.com/dxos/dxos/issues",
@@ -30,58 +30,56 @@
30
30
  "dependencies": {
31
31
  "@atlaskit/pragmatic-drag-and-drop": "1.7.7",
32
32
  "@atlaskit/pragmatic-drag-and-drop-hitbox": "1.1.0",
33
- "@effect-atom/atom-react": "^0.4.6",
33
+ "@effect-atom/atom-react": "^0.5.0",
34
34
  "@fluentui/react-tabster": "9.26.11",
35
35
  "@radix-ui/react-context": "1.1.1",
36
36
  "@radix-ui/react-use-controllable-state": "1.1.0",
37
37
  "date-fns": "^3.3.1",
38
- "jsonpath": "^1.1.1",
39
38
  "tabster": "^8.5.5",
40
- "@dxos/async": "0.8.4-main.bc674ce",
41
- "@dxos/debug": "0.8.4-main.bc674ce",
42
- "@dxos/echo": "0.8.4-main.bc674ce",
43
- "@dxos/echo-react": "0.8.4-main.bc674ce",
44
- "@dxos/effect": "0.8.4-main.bc674ce",
45
- "@dxos/invariant": "0.8.4-main.bc674ce",
46
- "@dxos/keys": "0.8.4-main.bc674ce",
47
- "@dxos/echo-query": "0.8.4-main.bc674ce",
48
- "@dxos/lit-ui": "0.8.4-main.bc674ce",
49
- "@dxos/log": "0.8.4-main.bc674ce",
50
- "@dxos/react-client": "0.8.4-main.bc674ce",
51
- "@dxos/react-ui-components": "0.8.4-main.bc674ce",
52
- "@dxos/react-ui-editor": "0.8.4-main.bc674ce",
53
- "@dxos/react-ui-mosaic": "0.8.4-main.bc674ce",
54
- "@dxos/react-ui-pickers": "0.8.4-main.bc674ce",
55
- "@dxos/react-ui-list": "0.8.4-main.bc674ce",
56
- "@dxos/schema": "0.8.4-main.bc674ce",
57
- "@dxos/react-ui-searchlist": "0.8.4-main.bc674ce",
58
- "@dxos/react-ui-stack": "0.8.4-main.bc674ce",
59
- "@dxos/types": "0.8.4-main.bc674ce",
60
- "@dxos/ui-types": "0.8.4-main.bc674ce",
61
- "@dxos/util": "0.8.4-main.bc674ce"
39
+ "@dxos/debug": "0.8.4-main.c351d160a8",
40
+ "@dxos/async": "0.8.4-main.c351d160a8",
41
+ "@dxos/echo": "0.8.4-main.c351d160a8",
42
+ "@dxos/echo-query": "0.8.4-main.c351d160a8",
43
+ "@dxos/echo-react": "0.8.4-main.c351d160a8",
44
+ "@dxos/invariant": "0.8.4-main.c351d160a8",
45
+ "@dxos/lit-ui": "0.8.4-main.c351d160a8",
46
+ "@dxos/keys": "0.8.4-main.c351d160a8",
47
+ "@dxos/log": "0.8.4-main.c351d160a8",
48
+ "@dxos/effect": "0.8.4-main.c351d160a8",
49
+ "@dxos/react-ui-components": "0.8.4-main.c351d160a8",
50
+ "@dxos/react-client": "0.8.4-main.c351d160a8",
51
+ "@dxos/react-ui-editor": "0.8.4-main.c351d160a8",
52
+ "@dxos/react-ui-mosaic": "0.8.4-main.c351d160a8",
53
+ "@dxos/react-ui-list": "0.8.4-main.c351d160a8",
54
+ "@dxos/react-ui-pickers": "0.8.4-main.c351d160a8",
55
+ "@dxos/react-ui-searchlist": "0.8.4-main.c351d160a8",
56
+ "@dxos/react-ui-stack": "0.8.4-main.c351d160a8",
57
+ "@dxos/schema": "0.8.4-main.c351d160a8",
58
+ "@dxos/ui-types": "0.8.4-main.c351d160a8",
59
+ "@dxos/util": "0.8.4-main.c351d160a8",
60
+ "@dxos/types": "0.8.4-main.c351d160a8"
62
61
  },
63
62
  "devDependencies": {
64
- "@types/jsonpath": "^0.2.4",
65
63
  "@types/react": "~19.2.7",
66
64
  "@types/react-dom": "~19.2.3",
67
- "effect": "3.19.11",
65
+ "effect": "3.19.16",
68
66
  "fast-check": "^3.19.0",
69
67
  "react": "~19.2.3",
70
68
  "react-dom": "~19.2.3",
71
- "vite": "7.1.9",
72
- "@dxos/react-client": "0.8.4-main.bc674ce",
73
- "@dxos/react-ui": "0.8.4-main.bc674ce",
74
- "@dxos/react-ui-syntax-highlighter": "0.8.4-main.bc674ce",
75
- "@dxos/random": "0.8.4-main.bc674ce",
76
- "@dxos/ui-theme": "0.8.4-main.bc674ce",
77
- "@dxos/storybook-utils": "0.8.4-main.bc674ce"
69
+ "vite": "^7.1.11",
70
+ "@dxos/random": "0.8.4-main.c351d160a8",
71
+ "@dxos/react-client": "0.8.4-main.c351d160a8",
72
+ "@dxos/react-ui": "0.8.4-main.c351d160a8",
73
+ "@dxos/storybook-utils": "0.8.4-main.c351d160a8",
74
+ "@dxos/react-ui-syntax-highlighter": "0.8.4-main.c351d160a8",
75
+ "@dxos/ui-theme": "0.8.4-main.c351d160a8"
78
76
  },
79
77
  "peerDependencies": {
80
- "effect": "3.19.11",
78
+ "effect": "3.19.16",
81
79
  "react": "~19.2.3",
82
80
  "react-dom": "~19.2.3",
83
- "@dxos/ui-theme": "0.8.4-main.bc674ce",
84
- "@dxos/react-ui": "0.8.4-main.bc674ce"
81
+ "@dxos/react-ui": "0.8.4-main.c351d160a8",
82
+ "@dxos/ui-theme": "0.8.4-main.c351d160a8"
85
83
  },
86
84
  "publishConfig": {
87
85
  "access": "public"
@@ -8,8 +8,8 @@ import React, { useEffect, useMemo } from 'react';
8
8
  import { Filter, Query } from '@dxos/echo';
9
9
  import { createEchoSchema } from '@dxos/echo/testing';
10
10
  import { log } from '@dxos/log';
11
- import { withTheme } from '@dxos/react-ui/testing';
12
- import { ProjectionModel, View, createEchoChangeCallback } from '@dxos/schema';
11
+ import { withLayout, withTheme } from '@dxos/react-ui/testing';
12
+ import { ProjectionModel, ViewModel, createEchoChangeCallback } from '@dxos/schema';
13
13
  import { Example } from '@dxos/schema/testing';
14
14
 
15
15
  import { translations } from '../../translations';
@@ -17,17 +17,15 @@ import { FIELD_EDITOR_DEBUG_SYMBOL, TestLayout } from '../testing';
17
17
 
18
18
  import { FieldEditor, type FieldEditorProps } from './FieldEditor';
19
19
 
20
- // Type definition for debug objects exposed to tests.
21
20
  export type FieldEditorDebugObjects = {
22
21
  props: FieldEditorProps;
23
22
  projection: ProjectionModel;
24
23
  };
25
24
 
26
- // TODO(wittjosiah): ECHO objects don't work when passed via Storybook args.
27
25
  const useTestProjection = () => {
28
26
  return useMemo(() => {
29
27
  const schema = createEchoSchema(Example);
30
- const view = View.make({
28
+ const view = ViewModel.make({
31
29
  name: 'Test',
32
30
  query: Query.select(Filter.type(Example)),
33
31
  jsonSchema: schema.jsonSchema,
@@ -82,7 +80,7 @@ const meta = {
82
80
  title: 'ui/react-ui-form/FieldEditor',
83
81
  component: FieldEditor as any,
84
82
  render: DefaultStory,
85
- decorators: [withTheme],
83
+ decorators: [withTheme(), withLayout({ layout: 'fullscreen' })],
86
84
  parameters: {
87
85
  layout: 'fullscreen',
88
86
  translations,
@@ -4,13 +4,12 @@
4
4
 
5
5
  import React, { useCallback, useEffect, useMemo, useState } from 'react';
6
6
 
7
- import { Obj, type SchemaRegistry } from '@dxos/echo';
7
+ import { Obj, type SchemaRegistry, type View } from '@dxos/echo';
8
8
  import { type EchoSchema, Format, FormatEnums, formatToType } from '@dxos/echo/internal';
9
9
  import { type SchemaProperty } from '@dxos/effect';
10
10
  import { log } from '@dxos/log';
11
11
  import { useAsyncEffect, useTranslation } from '@dxos/react-ui';
12
12
  import {
13
- type FieldType,
14
13
  type ProjectionModel,
15
14
  type PropertyType,
16
15
  formatToAdditionalPropertyAttributes,
@@ -23,7 +22,7 @@ import { Form, type FormFieldMap, type FormRootProps, SelectField, SelectOptionF
23
22
 
24
23
  export type FieldEditorProps = {
25
24
  projection: ProjectionModel;
26
- field: FieldType;
25
+ field: View.FieldType;
27
26
  registry?: SchemaRegistry.SchemaRegistry;
28
27
  view?: Obj.Unknown;
29
28
  onSave: () => void;
@@ -6,13 +6,13 @@ import { type Meta, type StoryObj } from '@storybook/react-vite';
6
6
  import * as Schema from 'effect/Schema';
7
7
  import React, { useCallback, useState } from 'react';
8
8
 
9
- import { Annotation, Format, Obj, Tag, Type } from '@dxos/echo';
9
+ import { Annotation, Format, Obj, Ref, Tag, Type } from '@dxos/echo';
10
10
  import { type AnyProperties } from '@dxos/echo/internal';
11
11
  import { log } from '@dxos/log';
12
12
  import { useClient } from '@dxos/react-client';
13
13
  import { withClientProvider } from '@dxos/react-client/testing';
14
14
  import { Tooltip } from '@dxos/react-ui';
15
- import { withTheme } from '@dxos/react-ui/testing';
15
+ import { withLayout, withTheme } from '@dxos/react-ui/testing';
16
16
 
17
17
  import { translations } from '../../translations';
18
18
  import { TestLayout } from '../testing';
@@ -23,7 +23,7 @@ const Organization = Schema.Struct({
23
23
  name: Schema.String.pipe(Schema.minLength(1)).annotations({ title: 'Full name' }),
24
24
  }).pipe(
25
25
  Type.object({
26
- typename: 'example.com/type/Organization', // TODO(burdon): Change all types to /schema
26
+ typename: 'com.example.type.organization',
27
27
  version: '0.1.0',
28
28
  }),
29
29
  );
@@ -46,8 +46,8 @@ const Person = Schema.Struct({
46
46
  zip: Schema.Number,
47
47
  }).annotations({ title: 'Address' }),
48
48
  ),
49
- employer: Schema.optional(Type.Ref(Organization).annotations({ title: 'Employer' })),
50
- tags: Schema.optional(Schema.Array(Type.Ref(Tag.Tag)).annotations({ title: 'Tags' })),
49
+ employer: Schema.optional(Ref.Ref(Organization).annotations({ title: 'Employer' })),
50
+ tags: Schema.optional(Schema.Array(Ref.Ref(Tag.Tag)).annotations({ title: 'Tags' })),
51
51
  status: Schema.optional(Schema.Literal('active', 'inactive').annotations({ title: 'Status' })),
52
52
  notes: Schema.optional(Format.Text.annotations({ title: 'Notes' })),
53
53
  location: Schema.optional(Format.GeoPoint.annotations({ title: 'Location' })),
@@ -65,7 +65,7 @@ const Person = Schema.Struct({
65
65
  ),
66
66
  }).pipe(
67
67
  Type.object({
68
- typename: 'dxos.org/type/Person', // TODO(burdon): Change all types to /schema
68
+ typename: 'org.dxos.type.person', // TODO(burdon): Change all types to /schema
69
69
  version: '0.1.0',
70
70
  }),
71
71
  );
@@ -127,7 +127,8 @@ const meta = {
127
127
  render: DefaultStory,
128
128
 
129
129
  decorators: [
130
- withTheme,
130
+ withTheme(),
131
+ withLayout({ layout: 'fullscreen' }),
131
132
  withClientProvider({
132
133
  createIdentity: true,
133
134
  createSpace: true,
@@ -4,12 +4,19 @@
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
11
  import { createJsonPath, getValue as getValue$ } from '@dxos/effect';
12
- import { IconButton, type IconButtonProps, ScrollArea, type ThemedClassName, useTranslation } from '@dxos/react-ui';
12
+ import {
13
+ IconButton,
14
+ type IconButtonProps,
15
+ ScrollArea,
16
+ type ScrollAreaRootProps,
17
+ type ThemedClassName,
18
+ useTranslation,
19
+ } from '@dxos/react-ui';
13
20
  import { mx } from '@dxos/ui-theme';
14
21
 
15
22
  import {
@@ -20,7 +27,6 @@ import {
20
27
  useKeyHandler,
21
28
  } from '../../hooks';
22
29
  import { translationKey } from '../../translations';
23
- import { setValueEchoAware } from '../../util';
24
30
 
25
31
  import { FormFieldLabel, type FormFieldLabelProps, type FormFieldStateProps } from './FormFieldComponent';
26
32
  import {
@@ -29,20 +35,19 @@ import {
29
35
  } from './FormFieldSet';
30
36
 
31
37
  // New features/polish
32
- // [x] Unify readonly/inline modes
33
- // [ ] Don't call save/autoSave if value hasn't changed
34
- // [ ] Fix onCancel (restore values)
35
- // [ ] Fix useSchema Type.Obj.Any cast
36
- // [ ] Remove @dxos/echo-db deps
37
- // [ ] TableCellEditor (handleEnter/ModalController).
38
- // [ ] Use FormFieldWrapper uniformly
39
- // [ ] Inline tables for object arrays
40
- // [ ] Defer query until popover
41
- // [ ] Omit id from sub properties.
38
+ // [x] Unify readonly/inline modes.
42
39
  // [x] Refs
43
40
  // [x] Single-select (fix popover)
44
41
  // [x] Multi-select (array)
45
- // [ ] auto save doesn't work for combobox + select due to only firing on blur (workaround is to use onValuesChanged)
42
+ // [ ] auto save doesn't work for combobox + select due to only firing on blur (workaround is to use onValuesChanged).
43
+ // [ ] Don't call save/autoSave if value hasn't changed.
44
+ // [ ] Fix onCancel (restore values).
45
+ // [ ] Fix useSchema Type.AnyObj cast.
46
+ // [ ] TableCellEditor (handleEnter/ModalController).
47
+ // [ ] Use FormFieldWrapper uniformly
48
+ // [ ] Inline tables.
49
+ // [ ] Defer query until popover.
50
+ // [ ] Omit id from sub properties.
46
51
 
47
52
  // TODO(burdon): Move to @dxos/schema (re-export here).
48
53
  export type ExcludeId<S extends Schema.Schema.AnyNoContext> = Omit<Schema.Schema.Type<S>, 'id'>;
@@ -70,7 +75,7 @@ type FormContextValue<T extends AnyProperties = any> = {
70
75
  * Testing.
71
76
  */
72
77
  testId?: string;
73
- } & Pick<NaturalFormFieldSetProps<T>, 'readonly' | 'layout' | 'fieldMap' | 'fieldProvider'>;
78
+ } & Pick<NaturalFormFieldSetProps<T>, 'readonly' | 'layout' | 'fieldMap' | 'fieldProvider' | 'projection'>;
74
79
 
75
80
  const [FormContextProvider, useFormContext] = createContext<FormContextValue>('Form');
76
81
 
@@ -84,19 +89,25 @@ const useFormValues: {
84
89
  path: (string | number)[] | undefined,
85
90
  defaultValue: () => T,
86
91
  ): T | undefined;
87
- } = (componentName: string, path: (string | number)[] = [], defaultValue?: () => any) => {
88
- const jsonPath = createJsonPath(path);
92
+ } = (componentName: string, path?: (string | number)[], defaultValue?: () => any) => {
93
+ const stablePath = useMemo(() => path ?? [], [path ? path.join('.') : undefined]);
94
+ const jsonPath = createJsonPath(stablePath);
89
95
  const {
90
- form: { values },
96
+ form: { values, onValueChange },
91
97
  } = useFormContext(componentName);
92
98
 
93
99
  const value = getValue$(values, jsonPath);
94
100
 
101
+ // Apply default value once when the field has no value. lastAppliedPathRef prevents
102
+ // re-applying on every render (e.g. when defaultValue() returns null) and ensures
103
+ // we apply per path when the hook is used for different fields.
104
+ const lastAppliedPathRef = useRef<string | null>(null);
95
105
  useEffect(() => {
96
- if (!value && defaultValue) {
97
- setValueEchoAware(values, jsonPath, defaultValue());
106
+ if (value == null && defaultValue && lastAppliedPathRef.current !== jsonPath) {
107
+ lastAppliedPathRef.current = jsonPath;
108
+ onValueChange(stablePath, SchemaAST.stringKeyword, defaultValue());
98
109
  }
99
- }, [value, defaultValue]);
110
+ }, [value, defaultValue, onValueChange, stablePath, jsonPath]);
100
111
 
101
112
  return value;
102
113
  };
@@ -164,65 +175,78 @@ FormRoot.displayName = 'Form.Root';
164
175
  // Viewport
165
176
  //
166
177
 
167
- type FormViewportProps = ThemedClassName<PropsWithChildren<{}>>;
178
+ const FORM_VIEWPORT_NAME = 'Form.Viewport';
168
179
 
169
- const FormViewport = ({ classNames, children }: FormViewportProps) => {
180
+ type FormViewportProps = PropsWithChildren<ScrollAreaRootProps>;
181
+
182
+ const FormViewport = ({
183
+ children,
184
+ classNames,
185
+ margin = true,
186
+ padding = true,
187
+ thin = true,
188
+ ...props
189
+ }: FormViewportProps) => {
170
190
  return (
171
- <ScrollArea.Root>
172
- <ScrollArea.Viewport classNames={['plb-cardSpacingBlock', classNames]}>{children}</ScrollArea.Viewport>
173
- <ScrollArea.Scrollbar orientation='vertical'>
174
- <ScrollArea.Thumb />
175
- </ScrollArea.Scrollbar>
191
+ <ScrollArea.Root
192
+ orientation='vertical'
193
+ classNames={classNames}
194
+ margin={margin}
195
+ padding={padding}
196
+ thin={thin}
197
+ {...props}
198
+ >
199
+ <ScrollArea.Viewport>{children}</ScrollArea.Viewport>
176
200
  </ScrollArea.Root>
177
201
  );
178
202
  };
179
203
 
180
- FormViewport.displayName = 'Form.Viewport';
204
+ FormViewport.displayName = FORM_VIEWPORT_NAME;
181
205
 
182
206
  //
183
207
  // Content
184
208
  //
185
209
 
210
+ const FORM_CONTENT_NAME = 'Form.Content';
211
+
186
212
  type FormContentProps = ThemedClassName<PropsWithChildren<{}>>;
187
213
 
188
- // TOOD(burdon): Figure out nesting (indent and testId).
189
214
  const FormContent = ({ classNames, children }: FormContentProps) => {
190
- const { form, testId } = useFormContext(FormContent.displayName);
215
+ const { form, testId } = useFormContext(FORM_CONTENT_NAME);
191
216
  const ref = useRef<HTMLDivElement>(null);
192
217
  useKeyHandler(ref.current, form);
193
218
 
194
219
  return (
195
- <div
196
- ref={ref}
197
- role='form'
198
- className={mx('flex flex-col is-full pli-cardSpacingInline density-fine', classNames)}
199
- data-testid={testId}
200
- >
220
+ <div ref={ref} role='form' className={mx('w-full flex flex-col gap-form-padding', classNames)} data-testid={testId}>
201
221
  {children}
202
222
  </div>
203
223
  );
204
224
  };
205
225
 
206
- FormContent.displayName = 'Form.Content';
226
+ FormContent.displayName = FORM_CONTENT_NAME;
207
227
 
208
228
  //
209
229
  // FieldSet
210
230
  //
211
231
 
212
- type FormFieldSetProps = ThemedClassName<{}>;
232
+ const FORM_FIELDSET_NAME = 'Form.FieldSet';
233
+
234
+ type FormFieldSetProps = ThemedClassName<NaturalFormFieldSetProps<any>>;
213
235
 
214
- const FormFieldSet = ({ classNames }: FormFieldSetProps) => {
215
- const { form, ...props } = useFormContext(FormFieldSet.displayName);
236
+ const FormFieldSet = (props: FormFieldSetProps) => {
237
+ const { form, ...contextProps } = useFormContext(FORM_FIELDSET_NAME);
216
238
 
217
- return <NaturalFormFieldSet classNames={classNames} schema={form.schema} {...props} />;
239
+ return <NaturalFormFieldSet schema={form.schema} {...contextProps} {...props} />;
218
240
  };
219
241
 
220
- FormFieldSet.displayName = 'Form.FieldSet';
242
+ FormFieldSet.displayName = FORM_FIELDSET_NAME;
221
243
 
222
244
  //
223
245
  // Actions
224
246
  //
225
247
 
248
+ const FORM_ACTIONS_NAME = 'Form.Actions';
249
+
226
250
  type FormActionsProps = ThemedClassName<{}>;
227
251
 
228
252
  const FormActions = ({ classNames }: FormActionsProps) => {
@@ -231,7 +255,7 @@ const FormActions = ({ classNames }: FormActionsProps) => {
231
255
  form: { canSave, onSave, onCancel },
232
256
  readonly,
233
257
  layout,
234
- } = useFormContext(FormActions.displayName);
258
+ } = useFormContext(FORM_ACTIONS_NAME);
235
259
 
236
260
  if (readonly || layout === 'static') {
237
261
  return null;
@@ -241,7 +265,7 @@ const FormActions = ({ classNames }: FormActionsProps) => {
241
265
  // Deprecate FormSubmit ans use FormActions without Cancel button if no callback is supplied.
242
266
 
243
267
  return (
244
- <div role='none' className={mx('grid grid-flow-col auto-cols-fr gap-2 pbs-cardSpacingBlock', classNames)}>
268
+ <div role='none' className={mx('grid grid-flow-col gap-2 auto-cols-fr py-form-padding', classNames)}>
245
269
  {onCancel && (
246
270
  <IconButton
247
271
  icon='ph--x--regular'
@@ -267,33 +291,35 @@ const FormActions = ({ classNames }: FormActionsProps) => {
267
291
  );
268
292
  };
269
293
 
270
- FormActions.displayName = 'Form.Actions';
294
+ FormActions.displayName = FORM_ACTIONS_NAME;
271
295
 
272
296
  //
273
297
  // Submit
274
298
  //
275
299
 
276
- type FormSubmitProps = ThemedClassName<Partial<Pick<IconButtonProps, 'icon' | 'label'>>>;
300
+ const FORM_SUBMIT_NAME = 'Form.Submit';
301
+
302
+ type FormSubmitProps = ThemedClassName<Partial<Pick<IconButtonProps, 'icon' | 'label' | 'disabled'>>>;
277
303
 
278
- const FormSubmit = ({ classNames, label, icon }: FormSubmitProps) => {
304
+ const FormSubmit = ({ classNames, label, icon, disabled }: FormSubmitProps) => {
279
305
  const { t } = useTranslation(translationKey);
280
306
  const {
281
307
  form: { canSave, onSave },
282
308
  readonly,
283
309
  layout,
284
- } = useFormContext(FormSubmit.displayName);
310
+ } = useFormContext(FORM_SUBMIT_NAME);
285
311
 
286
312
  if (readonly || layout === 'static') {
287
313
  return null;
288
314
  }
289
315
 
290
316
  return (
291
- <div role='none' className={mx('flex is-full pbs-cardSpacingBlock', classNames)}>
317
+ <div role='none' className={mx('flex w-full pt-form-padding', classNames)}>
292
318
  <IconButton
293
- classNames='is-full'
319
+ classNames='w-full'
294
320
  type='submit'
295
321
  variant='primary'
296
- disabled={!canSave}
322
+ disabled={disabled ?? !canSave}
297
323
  icon={icon ?? 'ph--check--regular'}
298
324
  label={label ?? t('save button label')}
299
325
  onClick={onSave}
@@ -303,7 +329,7 @@ const FormSubmit = ({ classNames, label, icon }: FormSubmitProps) => {
303
329
  );
304
330
  };
305
331
 
306
- FormSubmit.displayName = 'Form.Submit';
332
+ FormSubmit.displayName = FORM_SUBMIT_NAME;
307
333
 
308
334
  //
309
335
  // Form
@@ -315,9 +341,9 @@ export const Form = {
315
341
  Viewport: FormViewport,
316
342
  Content: FormContent,
317
343
  FieldSet: FormFieldSet,
344
+ Label: FormFieldLabel,
318
345
  Actions: FormActions,
319
346
  Submit: FormSubmit,
320
- Label: FormFieldLabel,
321
347
  };
322
348
 
323
349
  export { useFormContext, useFormValues, useFormFieldState };
@@ -327,6 +353,7 @@ export type {
327
353
  FormViewportProps,
328
354
  FormContentProps,
329
355
  FormFieldSetProps,
356
+ FormFieldLabelProps,
330
357
  FormActionsProps,
331
- FormFieldLabelProps as LabelProps,
358
+ FormSubmitProps,
332
359
  };
@@ -171,13 +171,21 @@ export const FormField = (props: FormFieldProps) => {
171
171
 
172
172
  const options = getOptions(type);
173
173
  if (options) {
174
+ // Resolve labels from projection metadata when available.
175
+ const fieldProjections = projection?.getFieldProjections();
176
+ const fieldProjection = fieldProjections?.find((fp) => fp.field.path === name);
177
+ const selectOptions = fieldProjection?.props.options;
178
+
174
179
  return (
175
180
  <SelectField
176
181
  {...fieldProps}
177
- options={options.map((option) => ({
178
- value: option,
179
- label: option.toString(),
180
- }))}
182
+ options={options.map((option) => {
183
+ const selectOption = selectOptions?.find((so) => so.id === globalThis.String(option));
184
+ return {
185
+ value: option,
186
+ label: selectOption?.title ?? option.toString(),
187
+ };
188
+ })}
181
189
  />
182
190
  );
183
191
  }
@@ -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
  );