@dxos/react-ui-form 0.8.3-main.7f5a14c → 0.8.3-staging.0fa589b

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 (63) hide show
  1. package/dist/lib/browser/index.mjs +190 -174
  2. package/dist/lib/browser/index.mjs.map +3 -3
  3. package/dist/lib/browser/meta.json +1 -1
  4. package/dist/lib/node/index.cjs +150 -134
  5. package/dist/lib/node/index.cjs.map +3 -3
  6. package/dist/lib/node/meta.json +1 -1
  7. package/dist/lib/node-esm/index.mjs +190 -174
  8. package/dist/lib/node-esm/index.mjs.map +3 -3
  9. package/dist/lib/node-esm/meta.json +1 -1
  10. package/dist/types/src/components/ControlSection/ControlSection.d.ts +1 -1
  11. package/dist/types/src/components/ControlSection/ControlSection.d.ts.map +1 -1
  12. package/dist/types/src/components/ControlSection/ControlSection.stories.d.ts.map +1 -1
  13. package/dist/types/src/components/FieldEditor/FieldEditor.d.ts +3 -2
  14. package/dist/types/src/components/FieldEditor/FieldEditor.d.ts.map +1 -1
  15. package/dist/types/src/components/FieldEditor/FieldEditor.stories.d.ts +0 -1
  16. package/dist/types/src/components/FieldEditor/FieldEditor.stories.d.ts.map +1 -1
  17. package/dist/types/src/components/Form/Defaults.d.ts.map +1 -1
  18. package/dist/types/src/components/Form/Form.d.ts +6 -15
  19. package/dist/types/src/components/Form/Form.d.ts.map +1 -1
  20. package/dist/types/src/components/Form/Form.stories.d.ts +2 -2
  21. package/dist/types/src/components/Form/Form.stories.d.ts.map +1 -1
  22. package/dist/types/src/components/Form/FormActions.d.ts +3 -1
  23. package/dist/types/src/components/Form/FormActions.d.ts.map +1 -1
  24. package/dist/types/src/components/Form/FormContent.d.ts +30 -3
  25. package/dist/types/src/components/Form/FormContent.d.ts.map +1 -1
  26. package/dist/types/src/components/Form/Input.d.ts +6 -4
  27. package/dist/types/src/components/Form/Input.d.ts.map +1 -1
  28. package/dist/types/src/components/Form/RefField.d.ts.map +1 -1
  29. package/dist/types/src/components/Form/custom/GeoPointInput.d.ts.map +1 -1
  30. package/dist/types/src/components/Form/custom/SelectOptionsInput.d.ts.map +1 -1
  31. package/dist/types/src/components/Form/custom/SelectOptionsInput.stories.d.ts.map +1 -1
  32. package/dist/types/src/components/Form/custom/TupleInput.d.ts.map +1 -1
  33. package/dist/types/src/components/ViewEditor/ViewEditor.d.ts +3 -2
  34. package/dist/types/src/components/ViewEditor/ViewEditor.d.ts.map +1 -1
  35. package/dist/types/src/components/ViewEditor/ViewEditor.stories.d.ts +0 -1
  36. package/dist/types/src/components/ViewEditor/ViewEditor.stories.d.ts.map +1 -1
  37. package/dist/types/src/components/testing.d.ts +3 -1
  38. package/dist/types/src/components/testing.d.ts.map +1 -1
  39. package/dist/types/src/translations.d.ts +3 -0
  40. package/dist/types/src/translations.d.ts.map +1 -1
  41. package/package.json +28 -27
  42. package/src/components/ControlSection/ControlSection.stories.tsx +15 -3
  43. package/src/components/ControlSection/ControlSection.tsx +11 -9
  44. package/src/components/FieldEditor/FieldEditor.stories.tsx +1 -4
  45. package/src/components/FieldEditor/FieldEditor.test.tsx +2 -1
  46. package/src/components/FieldEditor/FieldEditor.tsx +11 -2
  47. package/src/components/Form/ArrayField.tsx +1 -1
  48. package/src/components/Form/Defaults.tsx +5 -25
  49. package/src/components/Form/Form.stories.tsx +5 -5
  50. package/src/components/Form/Form.tsx +44 -52
  51. package/src/components/Form/FormActions.tsx +17 -2
  52. package/src/components/Form/FormContent.tsx +46 -46
  53. package/src/components/Form/Input.tsx +13 -8
  54. package/src/components/Form/RefField.tsx +1 -5
  55. package/src/components/Form/custom/GeoPointInput.tsx +1 -3
  56. package/src/components/Form/custom/SelectOptionsInput.stories.tsx +7 -5
  57. package/src/components/Form/custom/SelectOptionsInput.tsx +1 -3
  58. package/src/components/Form/custom/TupleInput.tsx +1 -3
  59. package/src/components/ViewEditor/ViewEditor.stories.tsx +1 -4
  60. package/src/components/ViewEditor/ViewEditor.test.tsx +2 -1
  61. package/src/components/ViewEditor/ViewEditor.tsx +66 -66
  62. package/src/components/testing.tsx +12 -7
  63. package/src/translations.ts +4 -1
@@ -4,7 +4,7 @@
4
4
 
5
5
  import { Schema, SchemaAST, pipe } from 'effect';
6
6
  import { capitalize } from 'effect/String';
7
- import React, { useMemo } from 'react';
7
+ import React, { forwardRef, useMemo } from 'react';
8
8
 
9
9
  import { createJsonPath, findNode, getDiscriminatedType, isDiscriminatedUnion } from '@dxos/effect';
10
10
  import { type ThemedClassName } from '@dxos/react-ui';
@@ -42,8 +42,8 @@ export const FormField = ({
42
42
  lookupComponent,
43
43
  Custom,
44
44
  }: FormFieldProps) => {
45
- const inputProps = useInputProps(path);
46
45
  const { ast, name, type, format, title, description, options, examples, array } = property;
46
+ const inputProps = useInputProps(path);
47
47
 
48
48
  const label = useMemo(() => title ?? pipe(name, capitalize), [title, name]);
49
49
  const placeholder = useMemo(
@@ -169,7 +169,7 @@ export const FormField = ({
169
169
  if (typeLiteral) {
170
170
  return (
171
171
  <>
172
- {!inline && <h3 className={mx('text-lg mlb-card-spacing-block first:mbs-0')}>{label}</h3>}
172
+ {!inline && <h3 className={mx('text-lg mlb-inputSpacingBlock first:mbs-0')}>{label}</h3>}
173
173
  <FormFields
174
174
  schema={Schema.make(typeLiteral)}
175
175
  path={path}
@@ -187,54 +187,54 @@ export const FormField = ({
187
187
  };
188
188
 
189
189
  export type FormFieldsProps = ThemedClassName<{
190
- Custom?: Partial<Record<string, InputComponent>>;
190
+ testId?: string;
191
+ readonly?: boolean;
191
192
  schema: Schema.Schema.All;
193
+ /**
194
+ * Path to the current object from the root. Used with nested forms.
195
+ */
192
196
  path?: (string | number)[];
193
197
  filter?: (props: SchemaProperty<any>[]) => SchemaProperty<any>[];
194
198
  sort?: string[];
195
- readonly?: boolean;
196
199
  lookupComponent?: ComponentLookup;
200
+ /**
201
+ * Map of custom renderers for specific properties.
202
+ * Prefer lookupComponent for plugin specific input surfaces.
203
+ */
204
+ Custom?: Partial<Record<string, InputComponent>>;
197
205
  onQueryRefOptions?: QueryRefOptions;
198
206
  }>;
199
207
 
200
- export const FormFields = ({
201
- classNames,
202
- Custom,
203
- schema,
204
- path,
205
- filter,
206
- sort,
207
- readonly,
208
- lookupComponent,
209
- onQueryRefOptions,
210
- }: FormFieldsProps) => {
211
- const values = useFormValues(path);
212
-
213
- const properties = useMemo(() => {
214
- const props = getSchemaProperties(schema.ast, values);
215
- const filtered = filter ? filter(props) : props;
216
-
217
- return sort ? filtered.sort((a, b) => sort.indexOf(a.name) - sort.indexOf(b.name)) : filtered;
218
- }, [schema, values, filter, sort]);
219
-
220
- return (
221
- // TODO(burdon): Consistent padding (with composite forms).
222
- <div role='form' className={mx('is-full', classNames)}>
223
- {properties
224
- .map((property) => {
225
- return (
226
- <FormField
227
- key={property.name}
228
- property={property}
229
- path={[...(path ?? []), property.name]}
230
- readonly={readonly}
231
- onQueryRefOptions={onQueryRefOptions}
232
- lookupComponent={lookupComponent}
233
- Custom={Custom}
234
- />
235
- );
236
- })
237
- .filter(isNotFalsy)}
238
- </div>
239
- );
240
- };
208
+ export const FormFields = forwardRef<HTMLDivElement, FormFieldsProps>(
209
+ (
210
+ { classNames, testId, schema, path, filter, sort, readonly, lookupComponent, Custom, onQueryRefOptions },
211
+ forwardRef,
212
+ ) => {
213
+ const values = useFormValues(path);
214
+ const properties = useMemo(() => {
215
+ const props = getSchemaProperties(schema.ast, values);
216
+ const filtered = filter ? filter(props) : props;
217
+ return sort ? filtered.sort((a, b) => sort.indexOf(a.name) - sort.indexOf(b.name)) : filtered;
218
+ }, [schema, values, filter, sort]);
219
+
220
+ return (
221
+ <div role='form' className={mx('is-full', classNames)} ref={forwardRef}>
222
+ {properties
223
+ .map((property) => {
224
+ return (
225
+ <FormField
226
+ key={property.name}
227
+ property={property}
228
+ path={[...(path ?? []), property.name]}
229
+ readonly={readonly}
230
+ onQueryRefOptions={onQueryRefOptions}
231
+ lookupComponent={lookupComponent}
232
+ Custom={Custom}
233
+ />
234
+ );
235
+ })
236
+ .filter(isNotFalsy)}
237
+ </div>
238
+ );
239
+ },
240
+ );
@@ -2,12 +2,13 @@
2
2
  // Copyright 2024 DXOS.org
3
3
  //
4
4
 
5
- import React, { type FC, type PropsWithChildren } from 'react';
5
+ import React, { type FC } from 'react';
6
6
 
7
7
  import { type FormatEnum } from '@dxos/echo-schema';
8
8
  import { type SimpleType } from '@dxos/effect';
9
- import { Icon, Tooltip } from '@dxos/react-ui';
10
- import { errorText } from '@dxos/react-ui-theme';
9
+ import { Icon, Input, Tooltip } from '@dxos/react-ui';
10
+ import { labelSpacing } from '@dxos/react-ui-stack';
11
+ import { errorText, mx } from '@dxos/react-ui-theme';
11
12
 
12
13
  import { type FormInputStateProps } from './FormContext';
13
14
 
@@ -28,14 +29,18 @@ export type InputProps = {
28
29
  */
29
30
  export type InputComponent = FC<InputProps>;
30
31
 
31
- export type InputHeaderProps = PropsWithChildren<{
32
+ export type InputHeaderProps = {
32
33
  error?: string;
33
- }>;
34
+ label: string;
35
+ readonly?: boolean;
36
+ };
34
37
 
35
- export const InputHeader = ({ children, error }: InputHeaderProps) => {
38
+ export const InputHeader = ({ error, label, readonly }: InputHeaderProps) => {
39
+ const Label = readonly ? 'span' : Input.Label;
40
+ const labelProps = readonly ? {} : { classNames: '!mlb-0' };
36
41
  return (
37
- <div role='none' className='flex justify-between items-center mbs-2 mbe-1 first:mbs-0'>
38
- {children}
42
+ <div role='none' className={mx('flex justify-between items-center', labelSpacing)}>
43
+ <Label {...labelProps}>{label}</Label>
39
44
  {error && (
40
45
  <Tooltip.Trigger asChild content={error} side='bottom'>
41
46
  <Icon icon='ph--warning--regular' size={4} classNames={errorText} />
@@ -136,11 +136,7 @@ export const RefField = ({
136
136
  // NOTE(thure): I left both predicates in-place in case we decide to add variants which do render readonly but empty values.
137
137
  return disabled && items.length < 1 ? null : (
138
138
  <Input.Root validationValence={status}>
139
- {!inputOnly && (
140
- <InputHeader error={error}>
141
- <Input.Label>{label}</Input.Label>
142
- </InputHeader>
143
- )}
139
+ {!inputOnly && <InputHeader error={error} label={label} />}
144
140
  <div data-no-submit>
145
141
  {disabled ? (
146
142
  items.length < 1 ? (
@@ -46,9 +46,7 @@ export const GeoPointInput = ({ type, label, disabled, getStatus, getValue, onVa
46
46
 
47
47
  return (
48
48
  <Input.Root validationValence={status}>
49
- <InputHeader error={error}>
50
- <Input.Label>{label}</Input.Label>
51
- </InputHeader>
49
+ <InputHeader error={error} label={label} />
52
50
  <div role='none' className='grid grid-cols-2 gap-2'>
53
51
  <Input.TextInput
54
52
  type='text'
@@ -47,13 +47,15 @@ const DefaultStory = () => {
47
47
  const meta: Meta = {
48
48
  title: 'ui/react-ui-form/SelectOptionsInput',
49
49
  component: SelectOptionInput,
50
- decorators: [withTheme, withLayout()],
51
- parameters: { translations },
50
+ render: DefaultStory,
51
+ decorators: [withTheme, withLayout({ fullscreen: true })],
52
+ parameters: {
53
+ translations,
54
+ },
52
55
  };
53
56
 
54
57
  export default meta;
58
+
55
59
  type Story = StoryObj;
56
60
 
57
- export const Default: Story = {
58
- render: DefaultStory,
59
- };
61
+ export const Default: Story = {};
@@ -112,9 +112,7 @@ export const SelectOptionInput = ({ type, label, disabled, getStatus, getValue,
112
112
 
113
113
  return (
114
114
  <Input.Root validationValence={status}>
115
- <InputHeader error={error}>
116
- <Input.Label>{label}</Input.Label>
117
- </InputHeader>
115
+ <InputHeader error={error} label={label} />
118
116
  <div role='none'>
119
117
  {options && (
120
118
  <List.Root items={options} isItem={(item) => true} onMove={handleMove}>
@@ -29,9 +29,7 @@ export const TupleInput = ({
29
29
 
30
30
  return (
31
31
  <Input.Root validationValence={status}>
32
- <InputHeader error={error}>
33
- <Input.Label>{label}</Input.Label>
34
- </InputHeader>
32
+ <InputHeader error={error} label={label} />
35
33
  <div className={mx('grid gap-2', gridCols[binding.length - 1])}>
36
34
  {binding.map((prop) => (
37
35
  <Input.TextInput
@@ -17,10 +17,7 @@ import { withTheme, withLayout } from '@dxos/storybook-utils';
17
17
 
18
18
  import { ViewEditor, type ViewEditorProps } from './ViewEditor';
19
19
  import translations from '../../translations';
20
- import { TestLayout, TestPanel } from '../testing';
21
-
22
- // Symbol for accessing debug objects in tests.
23
- export const VIEW_EDITOR_DEBUG_SYMBOL = Symbol('viewEditorDebug');
20
+ import { TestLayout, TestPanel, VIEW_EDITOR_DEBUG_SYMBOL } from '../testing';
24
21
 
25
22
  // Type definition for debug objects exposed to tests.
26
23
  export type ViewEditorDebugObjects = {
@@ -10,7 +10,8 @@ import { EchoSchema } from '@dxos/echo-schema';
10
10
  import { ViewProjection, ViewType } from '@dxos/schema';
11
11
 
12
12
  import * as stories from './ViewEditor.stories';
13
- import { VIEW_EDITOR_DEBUG_SYMBOL, type ViewEditorDebugObjects } from './ViewEditor.stories';
13
+ import { type ViewEditorDebugObjects } from './ViewEditor.stories';
14
+ import { VIEW_EDITOR_DEBUG_SYMBOL } from '../testing';
14
15
 
15
16
  const { Default } = composeStories(stories);
16
17
 
@@ -8,16 +8,17 @@ import React, { useCallback, useMemo, useState } from 'react';
8
8
  import { type SchemaRegistry } from '@dxos/echo-db';
9
9
  import { EchoSchema, Format, type JsonProp, isMutable, toJsonSchema } from '@dxos/echo-schema';
10
10
  import { invariant } from '@dxos/invariant';
11
- import { Icon, IconButton, Message, type ThemedClassName, useTranslation } from '@dxos/react-ui';
11
+ import { Callout, IconButton, type ThemedClassName, useTranslation } from '@dxos/react-ui';
12
12
  import { List } from '@dxos/react-ui-list';
13
+ import { cardSpacing } from '@dxos/react-ui-stack';
13
14
  import { ghostHover, inputTextLabel, mx } from '@dxos/react-ui-theme';
14
15
  import { FieldSchema, type FieldType, type ViewType, ViewProjection, VIEW_FIELD_LIMIT } from '@dxos/schema';
15
16
 
16
17
  import { translationKey } from '../../translations';
17
18
  import { FieldEditor } from '../FieldEditor';
18
- import { Form } from '../Form';
19
+ import { Form, type FormProps } from '../Form';
19
20
 
20
- const grid = 'grid grid-cols-[32px_1fr_32px] min-bs-[2.5rem]';
21
+ const grid = 'grid grid-cols-[32px_1fr_32px_32px] min-bs-[2.5rem]';
21
22
 
22
23
  const ViewMetaSchema = Schema.Struct({
23
24
  name: Schema.String.annotations({
@@ -30,15 +31,17 @@ const ViewMetaSchema = Schema.Struct({
30
31
 
31
32
  type ViewMetaType = Schema.Schema.Type<typeof ViewMetaSchema>;
32
33
 
33
- export type ViewEditorProps = ThemedClassName<{
34
- schema: Schema.Schema.AnyNoContext;
35
- view: ViewType;
36
- registry?: SchemaRegistry;
37
- readonly?: boolean;
38
- showHeading?: boolean;
39
- onTypenameChanged?: (typename: string) => void;
40
- onDelete?: (fieldId: string) => void;
41
- }>;
34
+ export type ViewEditorProps = ThemedClassName<
35
+ {
36
+ schema: Schema.Schema.AnyNoContext;
37
+ view: ViewType;
38
+ registry?: SchemaRegistry;
39
+ readonly?: boolean;
40
+ showHeading?: boolean;
41
+ onTypenameChanged?: (typename: string) => void;
42
+ onDelete?: (fieldId: string) => void;
43
+ } & Pick<FormProps<any>, 'outerSpacing'>
44
+ >;
42
45
 
43
46
  /**
44
47
  * Schema-based object form.
@@ -48,10 +51,11 @@ export const ViewEditor = ({
48
51
  schema,
49
52
  view,
50
53
  registry,
51
- readonly,
54
+ readonly: _readonly,
52
55
  showHeading = false,
53
56
  onTypenameChanged,
54
57
  onDelete,
58
+ outerSpacing = true,
55
59
  }: ViewEditorProps) => {
56
60
  const { t } = useTranslation(translationKey);
57
61
  const projection = useMemo(() => {
@@ -60,7 +64,7 @@ export const ViewEditor = ({
60
64
  return new ViewProjection(jsonSchema, view);
61
65
  }, [schema, view]);
62
66
  const [field, setField] = useState<FieldType>();
63
- const immutable = readonly || !isMutable(schema);
67
+ const readonly = _readonly || !isMutable(schema);
64
68
 
65
69
  // TODO(burdon): Should be reactive.
66
70
  const viewValues = useMemo(() => {
@@ -74,60 +78,60 @@ export const ViewEditor = ({
74
78
 
75
79
  const handleSelect = useCallback(
76
80
  (field: FieldType) => {
77
- if (immutable) {
81
+ if (readonly) {
78
82
  return;
79
83
  }
80
84
  setField((f) => (f === field ? undefined : field));
81
85
  },
82
- [immutable],
86
+ [readonly],
83
87
  );
84
88
 
85
89
  // TODO(burdon): Check if mutable; variant of useCallback that return undefined if readonly?
86
90
 
87
91
  const handleAdd = useCallback(() => {
88
- invariant(!immutable);
92
+ invariant(!readonly);
89
93
  const field = projection.createFieldProjection();
90
94
  setField(field);
91
- }, [schema, view]);
95
+ }, [schema, view, readonly]);
92
96
 
93
97
  const handleUpdate = useCallback(
94
98
  ({ name, typename }: ViewMetaType) => {
95
- invariant(!immutable);
99
+ invariant(!readonly);
96
100
  requestAnimationFrame(() => {
97
101
  if (view.name !== name) {
98
102
  view.name = name;
99
103
  }
100
104
 
101
- if (view.query.typename !== typename && !immutable) {
105
+ if (view.query.typename !== typename && !readonly) {
102
106
  onTypenameChanged?.(typename);
103
107
  }
104
108
  });
105
109
  },
106
- [schema, view, onTypenameChanged, immutable],
110
+ [schema, view, onTypenameChanged, readonly],
107
111
  );
108
112
 
109
113
  const handleDelete = useCallback(
110
114
  (fieldId: string) => {
111
- invariant(!immutable);
115
+ invariant(!readonly);
112
116
  if (fieldId === field?.id) {
113
117
  setField(undefined);
114
118
  }
115
119
 
116
120
  onDelete?.(fieldId);
117
121
  },
118
- [schema, field, onDelete],
122
+ [schema, field, onDelete, readonly],
119
123
  );
120
124
 
121
125
  const handleMove = useCallback(
122
126
  (fromIndex: number, toIndex: number) => {
123
- invariant(!immutable);
127
+ invariant(!readonly);
124
128
  // NOTE(ZaymonFC): Using arrayMove here causes a race condition with the kanban model.
125
129
  const fields = [...view.fields];
126
130
  const [moved] = fields.splice(fromIndex, 1);
127
131
  fields.splice(toIndex, 0, moved);
128
132
  view.fields = fields;
129
133
  },
130
- [schema, view.fields],
134
+ [schema, view.fields, readonly],
131
135
  );
132
136
 
133
137
  const handleClose = useCallback(() => setField(undefined), []);
@@ -151,36 +155,35 @@ export const ViewEditor = ({
151
155
  );
152
156
 
153
157
  return (
154
- <div role='none' className={mx('overflow-y-auto', classNames)}>
155
- {immutable && (
156
- <div role='none' className='mbe-card-spacing-block'>
157
- <Message.Root valence='neutral' className='rounded'>
158
- <Message.Title>
159
- <Icon icon='ph--info--regular' size={5} classNames='inline' /> {t('system schema title')}
160
- </Message.Title>
161
- <Message.Body>{t('system schema description')}</Message.Body>
162
- </Message.Root>
158
+ <div role='none' className={mx(classNames)}>
159
+ {readonly && (
160
+ <div role='none' className={mx('is-full', outerSpacing && 'plb-cardSpacingBlock pli-cardSpacingInline')}>
161
+ <Callout.Root valence='info'>
162
+ <Callout.Title>{t('system schema description')}</Callout.Title>
163
+ </Callout.Root>
163
164
  </div>
164
165
  )}
166
+
167
+ {/* TODO(burdon): Is the form read-only or just the schema? */}
168
+ {/* TODO(burdon): Readonly fields should take up the same space as editable fields (just be ghosted). */}
165
169
  <Form<ViewMetaType>
166
170
  autoSave
167
171
  schema={ViewMetaSchema}
168
172
  values={viewValues}
173
+ readonly={readonly}
169
174
  onSave={handleUpdate}
170
- classNames='min-bs-0 overflow-y-auto'
175
+ outerSpacing={outerSpacing}
171
176
  />
172
177
 
173
- <div role='none' className='min-bs-0 overflow-y-auto'>
174
- {/* TODO(burdon): Clean up common form ux. */}
175
- <div role='none' className='pli-card-spacing-inline mlb-card-spacing-block'>
176
- <label className={mx(inputTextLabel)}>{t('fields label')}</label>
177
- </div>
178
+ <div role='none' className={outerSpacing ? cardSpacing : 'mlb-cardSpacingBlock'}>
179
+ <label className={mx(inputTextLabel)}>{t('fields label')}</label>
178
180
 
179
181
  <List.Root<FieldType>
180
182
  items={view.fields}
181
183
  isItem={Schema.is(FieldSchema)}
182
184
  getId={(field) => field.id}
183
- onMove={immutable ? undefined : handleMove}
185
+ readonly={readonly}
186
+ onMove={readonly ? undefined : handleMove}
184
187
  >
185
188
  {({ items: fields }) => (
186
189
  <>
@@ -191,30 +194,29 @@ export const ViewEditor = ({
191
194
  </div>
192
195
  )}
193
196
 
194
- <div role='list' className='flex flex-col w-full'>
197
+ <div role='list' className='flex flex-col is-full'>
195
198
  {fields?.map((field) => (
196
199
  <List.Item<FieldType>
197
200
  key={field.id}
198
201
  item={field}
199
- classNames={mx(grid, ghostHover, !immutable && 'cursor-pointer')}
202
+ classNames={mx(grid, ghostHover, 'overflow-hidden', !readonly && 'cursor-pointer')}
200
203
  >
201
204
  <List.ItemDragHandle />
202
205
  <List.ItemTitle onClick={() => handleSelect(field)}>{field.path}</List.ItemTitle>
203
- <div className='flex items-center gap-2 -ml-4'>
204
- <List.ItemButton
205
- icon='ph--eye-slash--regular'
206
+ <List.ItemButton
207
+ data-testid='hide-field-button'
208
+ icon='ph--eye-slash--regular'
209
+ // TDOO(burdon): Is this the correct test?
210
+ disabled={view.fields.length <= 1}
211
+ onClick={() => handleHide(field.id)}
212
+ />
213
+ {/* TODO(burdon): Remove unless implement undo. */}
214
+ {!readonly && (
215
+ <List.ItemDeleteButton
206
216
  disabled={view.fields.length <= 1}
207
- onClick={() => handleHide(field.id)}
208
- data-testid='hide-field-button'
217
+ onClick={() => handleDelete(field.id)}
209
218
  />
210
- {!immutable && (
211
- <List.ItemDeleteButton
212
- icon='ph--trash--regular'
213
- disabled={view.fields.length <= 1}
214
- onClick={() => handleDelete(field.id)}
215
- />
216
- )}
217
- </div>
219
+ )}
218
220
  </List.Item>
219
221
  ))}
220
222
  </div>
@@ -223,10 +225,8 @@ export const ViewEditor = ({
223
225
  </List.Root>
224
226
 
225
227
  {hiddenProperties.length > 0 && (
226
- <div>
227
- <div role='none' className='pli-card-spacing-inline mlb-card-spacing-block'>
228
- <label className={mx(inputTextLabel)}>{t('hidden fields label')}</label>
229
- </div>
228
+ <>
229
+ <label className={mx(inputTextLabel)}>{t('hidden fields label')}</label>
230
230
 
231
231
  <List.Root<string>
232
232
  items={hiddenProperties}
@@ -234,14 +234,14 @@ export const ViewEditor = ({
234
234
  getId={(property) => property}
235
235
  >
236
236
  {({ items: properties }) => (
237
- <div role='list' className='flex flex-col w-full'>
237
+ <div role='list' className='flex flex-col is-full'>
238
238
  {properties?.map((property) => (
239
239
  <List.Item<string>
240
240
  key={property}
241
241
  item={property}
242
- classNames={mx(grid, ghostHover, !immutable && 'cursor-pointer')}
242
+ classNames={mx(grid, ghostHover, !readonly && 'cursor-pointer')}
243
243
  >
244
- <div />
244
+ <List.ItemDragHandle disabled />
245
245
  <List.ItemTitle>{property}</List.ItemTitle>
246
246
  <List.ItemButton
247
247
  icon='ph--eye--regular'
@@ -253,7 +253,7 @@ export const ViewEditor = ({
253
253
  </div>
254
254
  )}
255
255
  </List.Root>
256
- </div>
256
+ </>
257
257
  )}
258
258
  </div>
259
259
 
@@ -269,11 +269,11 @@ export const ViewEditor = ({
269
269
  )}
270
270
 
271
271
  {!readonly && !field && (
272
- <div role='none' className='pli-card-spacing-chrome mlb-card-spacing-chrome'>
272
+ <div role='none' className={outerSpacing ? cardSpacing : 'mlb-cardSpacingBlock'}>
273
273
  <IconButton
274
274
  icon='ph--plus--regular'
275
275
  label={t('button add property')}
276
- onClick={immutable ? undefined : handleAdd}
276
+ onClick={readonly ? undefined : handleAdd}
277
277
  // TODO(burdon): Show field limit in ux (not tooltip).
278
278
  disabled={view.fields.length >= VIEW_FIELD_LIMIT}
279
279
  classNames='flex is-full'
@@ -5,16 +5,21 @@
5
5
  import React, { type PropsWithChildren } from 'react';
6
6
 
7
7
  import { type ThemedClassName } from '@dxos/react-ui';
8
+ import { Card } from '@dxos/react-ui-stack';
8
9
  import { JsonFilter } from '@dxos/react-ui-syntax-highlighter';
9
- import { mx } from '@dxos/react-ui-theme';
10
-
11
- export const TestPanel = ({ classNames, children }: ThemedClassName<PropsWithChildren>) => (
12
- <div className={mx('w-full max-w-[30rem] border border-separator p-2 rounded', classNames)}>{children}</div>
13
- );
10
+ import { mx, textBlockWidth } from '@dxos/react-ui-theme';
14
11
 
15
12
  export const TestLayout = ({ classNames, json, children }: ThemedClassName<PropsWithChildren<{ json?: any }>>) => (
16
- <div className='w-full h-full grid grid-cols-[1fr_1fr]'>
17
- <div className={mx('m-2 justify-center overflow-y-auto', classNames)}>{children}</div>
13
+ <div className='w-full h-full grid grid-cols-[1fr_1fr] gap-2 overflow-hidden'>
14
+ <div className={mx('p-4 justify-center overflow-y-auto', classNames)}>{children}</div>
18
15
  <JsonFilter data={json} classNames='w-full text-xs' testId='debug' />
19
16
  </div>
20
17
  );
18
+
19
+ export const TestPanel = ({ classNames, children }: ThemedClassName<PropsWithChildren>) => (
20
+ <Card.Content classNames={[textBlockWidth, classNames]}>{children}</Card.Content>
21
+ );
22
+
23
+ // Symbol for accessing debug objects in tests.
24
+ export const VIEW_EDITOR_DEBUG_SYMBOL = Symbol('viewEditorDebug');
25
+ export const FIELD_EDITOR_DEBUG_SYMBOL = Symbol('fieldEditorDebug');
@@ -35,6 +35,7 @@ export default [
35
35
  'format date-time': 'DateTime',
36
36
  'format did': 'DID',
37
37
  'format duration': 'Duration',
38
+ 'format dxn': 'DXN',
38
39
  'format email': 'Email',
39
40
  'format formula': 'Formula',
40
41
  'format hostname': 'Hostname',
@@ -47,12 +48,14 @@ export default [
47
48
  'format regex': 'RegExp',
48
49
  'format string': 'String',
49
50
  'format single-select': 'Select',
51
+ 'format multi-select': 'Multi-select',
50
52
  'format text': 'Long text',
51
53
  'format time': 'Time',
52
54
  'format timestamp': 'Timestamp',
53
55
  'format user': 'User',
54
56
  'format number': 'Number',
55
57
  'format uri': 'URI',
58
+ 'format url': 'URL',
56
59
  'format uuid': 'UUID',
57
60
 
58
61
  // Placeholders.
@@ -68,7 +71,7 @@ export default [
68
71
 
69
72
  // System schema message.
70
73
  'system schema title': 'System Schema',
71
- 'system schema description': 'This schema is managed by the system and cannot be modified.',
74
+ 'system schema description': 'This is a system schema and cannot be modified.',
72
75
  },
73
76
  },
74
77
  },