@dxos/react-ui-form 0.8.4-main.d05673bc65 → 0.8.4-main.dfabb4ec29

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 (141) hide show
  1. package/dist/lib/browser/index.mjs +1004 -612
  2. package/dist/lib/browser/index.mjs.map +4 -4
  3. package/dist/lib/browser/meta.json +1 -1
  4. package/dist/lib/browser/translations.mjs +82 -0
  5. package/dist/lib/browser/translations.mjs.map +7 -0
  6. package/dist/lib/node-esm/index.mjs +1004 -612
  7. package/dist/lib/node-esm/index.mjs.map +4 -4
  8. package/dist/lib/node-esm/meta.json +1 -1
  9. package/dist/lib/node-esm/translations.mjs +84 -0
  10. package/dist/lib/node-esm/translations.mjs.map +7 -0
  11. package/dist/types/src/components/FieldEditor/FieldEditor.d.ts.map +1 -1
  12. package/dist/types/src/components/FieldEditor/FieldEditor.stories.d.ts +63 -61
  13. package/dist/types/src/components/FieldEditor/FieldEditor.stories.d.ts.map +1 -1
  14. package/dist/types/src/components/Form/Form.d.ts +40 -12
  15. package/dist/types/src/components/Form/Form.d.ts.map +1 -1
  16. package/dist/types/src/components/Form/Form.stories.d.ts +76 -74
  17. package/dist/types/src/components/Form/Form.stories.d.ts.map +1 -1
  18. package/dist/types/src/components/Form/FormField.d.ts +11 -5
  19. package/dist/types/src/components/Form/FormField.d.ts.map +1 -1
  20. package/dist/types/src/components/Form/FormFieldComponent.d.ts +8 -7
  21. package/dist/types/src/components/Form/FormFieldComponent.d.ts.map +1 -1
  22. package/dist/types/src/components/Form/FormFieldComponent.stories.d.ts.map +1 -1
  23. package/dist/types/src/components/Form/FormFieldSet.d.ts +5 -9
  24. package/dist/types/src/components/Form/FormFieldSet.d.ts.map +1 -1
  25. package/dist/types/src/components/Form/fields/ArrayField.d.ts +4 -4
  26. package/dist/types/src/components/Form/fields/ArrayField.d.ts.map +1 -1
  27. package/dist/types/src/components/Form/fields/BooleanField.d.ts.map +1 -1
  28. package/dist/types/src/components/Form/fields/GeoPointField.d.ts.map +1 -1
  29. package/dist/types/src/components/Form/fields/MarkdownField.d.ts +8 -1
  30. package/dist/types/src/components/Form/fields/MarkdownField.d.ts.map +1 -1
  31. package/dist/types/src/components/Form/fields/MarkdownField.stories.d.ts +84 -0
  32. package/dist/types/src/components/Form/fields/MarkdownField.stories.d.ts.map +1 -0
  33. package/dist/types/src/components/Form/fields/NumberField.d.ts.map +1 -1
  34. package/dist/types/src/components/Form/fields/RefField.d.ts +12 -6
  35. package/dist/types/src/components/Form/fields/RefField.d.ts.map +1 -1
  36. package/dist/types/src/components/Form/fields/SelectField.d.ts.map +1 -1
  37. package/dist/types/src/components/Form/fields/SelectOptionField.d.ts.map +1 -1
  38. package/dist/types/src/components/Form/fields/SelectOptionField.stories.d.ts +63 -61
  39. package/dist/types/src/components/Form/fields/SelectOptionField.stories.d.ts.map +1 -1
  40. package/dist/types/src/components/Form/fields/TextAreaField.d.ts.map +1 -1
  41. package/dist/types/src/components/Form/fields/TextField.d.ts +1 -1
  42. package/dist/types/src/components/Form/fields/TextField.d.ts.map +1 -1
  43. package/dist/types/src/components/Form/fields/TupleField.d.ts.map +1 -1
  44. package/dist/types/src/components/ObjectForm/ObjectForm.d.ts +9 -0
  45. package/dist/types/src/components/ObjectForm/ObjectForm.d.ts.map +1 -0
  46. package/dist/types/src/components/ObjectForm/index.d.ts +2 -0
  47. package/dist/types/src/components/ObjectForm/index.d.ts.map +1 -0
  48. package/dist/types/src/components/ObjectPicker/ObjectPicker.d.ts +26 -4
  49. package/dist/types/src/components/ObjectPicker/ObjectPicker.d.ts.map +1 -1
  50. package/dist/types/src/components/ObjectPicker/ObjectPicker.stories.d.ts +63 -61
  51. package/dist/types/src/components/ObjectPicker/ObjectPicker.stories.d.ts.map +1 -1
  52. package/dist/types/src/components/ObjectProperties/ObjectProperties.d.ts +9 -0
  53. package/dist/types/src/components/ObjectProperties/ObjectProperties.d.ts.map +1 -0
  54. package/dist/types/src/components/ObjectProperties/ObjectProperties.stories.d.ts +127 -0
  55. package/dist/types/src/components/ObjectProperties/ObjectProperties.stories.d.ts.map +1 -0
  56. package/dist/types/src/components/ObjectProperties/ObjectProperties.test.d.ts +2 -0
  57. package/dist/types/src/components/ObjectProperties/ObjectProperties.test.d.ts.map +1 -0
  58. package/dist/types/src/components/ObjectProperties/index.d.ts +2 -0
  59. package/dist/types/src/components/ObjectProperties/index.d.ts.map +1 -0
  60. package/dist/types/src/components/Settings/Settings.d.ts +28 -33
  61. package/dist/types/src/components/Settings/Settings.d.ts.map +1 -1
  62. package/dist/types/src/components/Settings/Settings.stories.d.ts +64 -61
  63. package/dist/types/src/components/Settings/Settings.stories.d.ts.map +1 -1
  64. package/dist/types/src/components/Settings/SettingsFieldSet.d.ts +43 -0
  65. package/dist/types/src/components/Settings/SettingsFieldSet.d.ts.map +1 -0
  66. package/dist/types/src/components/Settings/SettingsItem.d.ts +16 -0
  67. package/dist/types/src/components/Settings/SettingsItem.d.ts.map +1 -0
  68. package/dist/types/src/components/Settings/SettingsSelectField.d.ts +18 -0
  69. package/dist/types/src/components/Settings/SettingsSelectField.d.ts.map +1 -0
  70. package/dist/types/src/components/Settings/index.d.ts +2 -0
  71. package/dist/types/src/components/Settings/index.d.ts.map +1 -1
  72. package/dist/types/src/components/ViewEditor/ViewEditor.d.ts +2 -2
  73. package/dist/types/src/components/ViewEditor/ViewEditor.d.ts.map +1 -1
  74. package/dist/types/src/components/ViewEditor/ViewEditor.stories.d.ts +65 -63
  75. package/dist/types/src/components/ViewEditor/ViewEditor.stories.d.ts.map +1 -1
  76. package/dist/types/src/components/index.d.ts +2 -0
  77. package/dist/types/src/components/index.d.ts.map +1 -1
  78. package/dist/types/src/components/testing.d.ts +4 -4
  79. package/dist/types/src/components/testing.d.ts.map +1 -1
  80. package/dist/types/src/hooks/useFormHandler.d.ts.map +1 -1
  81. package/dist/types/src/hooks/useKeyHandler.d.ts +2 -1
  82. package/dist/types/src/hooks/useKeyHandler.d.ts.map +1 -1
  83. package/dist/types/src/index.d.ts +0 -2
  84. package/dist/types/src/index.d.ts.map +1 -1
  85. package/dist/types/src/translations.d.ts +63 -61
  86. package/dist/types/src/translations.d.ts.map +1 -1
  87. package/dist/types/src/util/field-type.d.ts +19 -0
  88. package/dist/types/src/util/field-type.d.ts.map +1 -0
  89. package/dist/types/src/util/field.d.ts.map +1 -1
  90. package/dist/types/src/util/formatting.d.ts.map +1 -1
  91. package/dist/types/src/util/index.d.ts +1 -0
  92. package/dist/types/src/util/index.d.ts.map +1 -1
  93. package/dist/types/src/util/properties.d.ts.map +1 -1
  94. package/dist/types/src/util/refs.d.ts +1 -0
  95. package/dist/types/src/util/refs.d.ts.map +1 -1
  96. package/dist/types/tsconfig.tsbuildinfo +1 -1
  97. package/package.json +42 -40
  98. package/src/components/FieldEditor/FieldEditor.stories.tsx +2 -2
  99. package/src/components/FieldEditor/FieldEditor.test.tsx +0 -1
  100. package/src/components/FieldEditor/FieldEditor.tsx +4 -3
  101. package/src/components/Form/AUDIT.md +17 -0
  102. package/src/components/Form/Form.stories.tsx +23 -36
  103. package/src/components/Form/Form.tsx +77 -48
  104. package/src/components/Form/FormField.tsx +27 -15
  105. package/src/components/Form/FormFieldComponent.tsx +5 -3
  106. package/src/components/Form/FormFieldSet.tsx +98 -77
  107. package/src/components/Form/fields/ArrayField.tsx +14 -8
  108. package/src/components/Form/fields/GeoPointField.tsx +8 -7
  109. package/src/components/Form/fields/MarkdownField.stories.tsx +206 -0
  110. package/src/components/Form/fields/MarkdownField.tsx +135 -45
  111. package/src/components/Form/fields/RefField.tsx +70 -35
  112. package/src/components/Form/fields/SelectOptionField.stories.tsx +8 -6
  113. package/src/components/Form/fields/SelectOptionField.tsx +10 -10
  114. package/src/components/Form/fields/TextField.tsx +2 -2
  115. package/src/components/Form/fields/TupleField.tsx +1 -1
  116. package/src/components/ObjectForm/ObjectForm.tsx +114 -0
  117. package/src/components/ObjectForm/index.ts +5 -0
  118. package/src/components/ObjectPicker/ObjectPicker.stories.tsx +9 -7
  119. package/src/components/ObjectPicker/ObjectPicker.tsx +92 -65
  120. package/src/components/ObjectProperties/ObjectProperties.stories.tsx +362 -0
  121. package/src/components/ObjectProperties/ObjectProperties.test.tsx +101 -0
  122. package/src/components/ObjectProperties/ObjectProperties.tsx +141 -0
  123. package/src/components/ObjectProperties/index.ts +5 -0
  124. package/src/components/Settings/AUDIT.md +52 -0
  125. package/src/components/Settings/Settings.stories.tsx +69 -21
  126. package/src/components/Settings/Settings.tsx +33 -183
  127. package/src/components/Settings/SettingsFieldSet.tsx +257 -0
  128. package/src/components/Settings/SettingsItem.tsx +58 -0
  129. package/src/components/Settings/SettingsSelectField.tsx +59 -0
  130. package/src/components/Settings/index.ts +2 -0
  131. package/src/components/ViewEditor/ViewEditor.stories.tsx +10 -10
  132. package/src/components/ViewEditor/ViewEditor.test.tsx +0 -1
  133. package/src/components/ViewEditor/ViewEditor.tsx +15 -15
  134. package/src/components/index.ts +2 -0
  135. package/src/components/testing.tsx +19 -11
  136. package/src/hooks/useKeyHandler.tsx +7 -7
  137. package/src/index.ts +0 -4
  138. package/src/translations.ts +66 -63
  139. package/src/util/field-type.ts +60 -0
  140. package/src/util/index.ts +1 -0
  141. package/src/util/refs.ts +7 -3
@@ -0,0 +1,59 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ import React from 'react';
6
+
7
+ import { Select } from '@dxos/react-ui';
8
+
9
+ import { type SettingsFieldProps } from './SettingsFieldSet';
10
+
11
+ export type SelectFieldOption = {
12
+ value: string;
13
+ label?: string;
14
+ };
15
+
16
+ export type CreateSelectFieldOptions = {
17
+ /** Options to display. Strings are used as both value and label. */
18
+ options: ReadonlyArray<string | SelectFieldOption>;
19
+ /** Label for the sentinel option that maps to `undefined`. Defaults to `'Default'`. Pass `null` to omit. */
20
+ defaultLabel?: string | null;
21
+ };
22
+
23
+ /**
24
+ * Factory for a `SettingsFieldSet` `fieldMap` entry that renders a Select control
25
+ * over a fixed list of options, with an optional sentinel for `undefined`.
26
+ */
27
+ export const createSelectField = ({
28
+ options,
29
+ defaultLabel = 'Default',
30
+ }: CreateSelectFieldOptions): React.FC<SettingsFieldProps<string | undefined>> => {
31
+ const normalized = options.map((option) => (typeof option === 'string' ? { value: option, label: option } : option));
32
+ const hasDefault = defaultLabel !== null;
33
+ // Per-instance sentinel avoids collisions with real option values; empty string when the
34
+ // sentinel is omitted so `undefined` maps to a value Select.Root treats as "no selection".
35
+ const sentinel = hasDefault ? `__default__${Math.random().toString(36).slice(2)}` : '';
36
+
37
+ return ({ value, onChange, readonly }) => (
38
+ <Select.Root
39
+ disabled={readonly}
40
+ value={value ?? sentinel}
41
+ onValueChange={(next) => onChange(hasDefault && next === sentinel ? undefined : next)}
42
+ >
43
+ <Select.TriggerButton disabled={readonly} />
44
+ <Select.Portal>
45
+ <Select.Content>
46
+ <Select.Viewport>
47
+ {hasDefault && <Select.Option value={sentinel}>{defaultLabel}</Select.Option>}
48
+ {normalized.map((option) => (
49
+ <Select.Option key={option.value} value={option.value}>
50
+ {option.label ?? option.value}
51
+ </Select.Option>
52
+ ))}
53
+ </Select.Viewport>
54
+ <Select.Arrow />
55
+ </Select.Content>
56
+ </Select.Portal>
57
+ </Select.Root>
58
+ );
59
+ };
@@ -3,3 +3,5 @@
3
3
  //
4
4
 
5
5
  export * from './Settings';
6
+ export { type SettingsFieldMap, type SettingsFieldProps, type SettingsFieldSetProps } from './SettingsFieldSet';
7
+ export { createSelectField, type CreateSelectFieldOptions, type SelectFieldOption } from './SettingsSelectField';
@@ -16,9 +16,9 @@ import { withLayout, withTheme } from '@dxos/react-ui/testing';
16
16
  import { type ProjectionModel, ViewModel, getTypenameFromQuery } from '@dxos/schema';
17
17
  import { Employer, Organization, Person, Pipeline } from '@dxos/types';
18
18
 
19
- import { translations } from '../../translations';
20
- import { TestLayout, VIEW_EDITOR_DEBUG_SYMBOL } from '../testing';
19
+ import { translations } from '#translations';
21
20
 
21
+ import { TestLayout, VIEW_EDITOR_DEBUG_SYMBOL } from '../testing';
22
22
  import { ViewEditor, type ViewEditorProps } from './ViewEditor';
23
23
 
24
24
  const types = [
@@ -36,9 +36,9 @@ export type ViewEditorDebugObjects = {
36
36
  projection: ProjectionModel;
37
37
  };
38
38
 
39
- type StoryProps = Pick<ViewEditorProps, 'readonly' | 'mode'>;
39
+ type DefaultStoryProps = Pick<ViewEditorProps, 'readonly' | 'mode'>;
40
40
 
41
- const DefaultStory = (props: StoryProps) => {
41
+ const DefaultStory = (props: DefaultStoryProps) => {
42
42
  const { space } = useClientStory();
43
43
  const [schema, setSchema] = useState<EchoSchema>();
44
44
  const [view, setView] = useState<View.View>();
@@ -91,8 +91,8 @@ const DefaultStory = (props: StoryProps) => {
91
91
  if (props.mode === 'tag') {
92
92
  const queue = target && DXN.tryParse(target) ? target : undefined;
93
93
  const query = queue ? Query.fromAst(newQuery).from({ queues: [queue] }) : Query.fromAst(newQuery);
94
- Obj.change(view, (v) => {
95
- v.query.ast = query.ast as Mutable<typeof query.ast>;
94
+ Obj.update(view, (view) => {
95
+ view.query.ast = query.ast as Mutable<typeof query.ast>;
96
96
  });
97
97
 
98
98
  const typename = getTypenameFromQuery(query.ast);
@@ -105,13 +105,13 @@ const DefaultStory = (props: StoryProps) => {
105
105
  query,
106
106
  jsonSchema: newSchema.jsonSchema,
107
107
  });
108
- Obj.change(view, (v) => {
109
- v.projection = Obj.getSnapshot(newView).projection as Mutable<typeof v.projection>;
108
+ Obj.update(view, (view) => {
109
+ view.projection = Obj.getSnapshot(newView).projection as Mutable<typeof view.projection>;
110
110
  });
111
111
  setSchema(() => newSchema);
112
112
  } else {
113
- Obj.change(view, (v) => {
114
- v.query.ast = newQuery as Mutable<typeof newQuery>;
113
+ Obj.update(view, (view) => {
114
+ view.query.ast = newQuery as Mutable<typeof newQuery>;
115
115
  });
116
116
  schema.updateTypename(getTypenameFromQuery(newQuery));
117
117
  }
@@ -11,7 +11,6 @@ import { EchoSchema, isInstanceOf } from '@dxos/echo/internal';
11
11
  import { ProjectionModel } from '@dxos/schema';
12
12
 
13
13
  import { VIEW_EDITOR_DEBUG_SYMBOL } from '../testing';
14
-
15
14
  import * as stories from './ViewEditor.stories';
16
15
  import { type ViewEditorDebugObjects } from './ViewEditor.stories';
17
16
 
@@ -26,7 +26,8 @@ import {
26
26
  } from '@dxos/schema';
27
27
  import { mx, osTranslations, subtleHover } from '@dxos/ui-theme';
28
28
 
29
- import { translationKey } from '../../translations';
29
+ import { translationKey } from '#translations';
30
+
30
31
  import { FieldEditor } from '../FieldEditor';
31
32
  import {
32
33
  Form,
@@ -189,11 +190,11 @@ export const ViewEditor = forwardRef<ProjectionModel, ViewEditorProps>(
189
190
  );
190
191
 
191
192
  return (
192
- <div role='none' className={mx(classNames)}>
193
+ <div className={mx(classNames)}>
193
194
  {/* If readonly is set, then the callout is not needed. */}
194
195
  {schemaReadonly && !readonly && (
195
196
  <Message.Root valence='info' classNames='my-form-padding'>
196
- <Message.Title>{t('system schema description')}</Message.Title>
197
+ <Message.Title>{t('system-schema.description')}</Message.Title>
197
198
  </Message.Root>
198
199
  )}
199
200
 
@@ -201,7 +202,7 @@ export const ViewEditor = forwardRef<ProjectionModel, ViewEditorProps>(
201
202
  <Form.Root schema={viewSchema} values={viewValues} fieldMap={fieldMap} db={db} onValuesChanged={handleUpdate}>
202
203
  <Form.FieldSet />
203
204
 
204
- <FormFieldLabel label={t('fields label')} asChild />
205
+ <FormFieldLabel label={t('fields.label')} asChild />
205
206
  <FieldList
206
207
  schema={schema}
207
208
  view={view}
@@ -274,12 +275,12 @@ const FieldList = ({ schema, view, registry, readonly, showHeading = false, onDe
274
275
  const handleMove = useCallback(
275
276
  (fromIndex: number, toIndex: number) => {
276
277
  invariant(!readonly);
277
- Obj.change(view, (v) => {
278
+ Obj.update(view, (view) => {
278
279
  // NOTE(ZaymonFC): Using arrayMove here causes a race condition with the kanban model.
279
- const fields = [...v.projection.fields];
280
+ const fields = [...view.projection.fields];
280
281
  const [moved] = fields.splice(fromIndex, 1);
281
282
  fields.splice(toIndex, 0, moved);
282
- v.projection.fields = fields;
283
+ view.projection.fields = fields;
283
284
  });
284
285
  },
285
286
  [view, readonly],
@@ -317,7 +318,7 @@ const FieldList = ({ schema, view, registry, readonly, showHeading = false, onDe
317
318
  >
318
319
  {({ items: fields }) => (
319
320
  <>
320
- {showHeading && <h3 className='text-sm'>{t('field path label')}</h3>}
321
+ {showHeading && <h3 className='text-sm'>{t('field-path.label')}</h3>}
321
322
  <div role='list' className='grid grid-cols-[min-content_1fr_min-content_min-content_min-content]'>
322
323
  {fields?.map((field) => {
323
324
  const hidden = field.visible === false;
@@ -329,7 +330,6 @@ const FieldList = ({ schema, view, registry, readonly, showHeading = false, onDe
329
330
  aria-expanded={expandedField === field.id}
330
331
  >
331
332
  <div
332
- role='none'
333
333
  className={mx(
334
334
  subtleHover,
335
335
  'grid grid-cols-subgrid col-span-5',
@@ -341,7 +341,7 @@ const FieldList = ({ schema, view, registry, readonly, showHeading = false, onDe
341
341
  {field.path}
342
342
  </List.ItemTitle>
343
343
  <List.ItemIconButton
344
- label={t(hidden ? 'show field label' : 'hide field label')}
344
+ label={t(hidden ? 'show-field.label' : 'hide-field.label')}
345
345
  data-testid={hidden ? 'show-field-button' : 'hide-field-button'}
346
346
  icon={hidden ? 'ph--eye-closed--regular' : 'ph--eye--regular'}
347
347
  autoHide={false}
@@ -351,7 +351,7 @@ const FieldList = ({ schema, view, registry, readonly, showHeading = false, onDe
351
351
  {!readonly && (
352
352
  <>
353
353
  <List.ItemDeleteButton
354
- label={t('delete field label')}
354
+ label={t('delete-field.label')}
355
355
  autoHide={false}
356
356
  disabled={readonly || schemaReadonly || viewSnapshot.projection.fields.length <= 1}
357
357
  onClick={() => handleDelete(field.id)}
@@ -360,7 +360,7 @@ const FieldList = ({ schema, view, registry, readonly, showHeading = false, onDe
360
360
  <List.ItemIconButton
361
361
  iconOnly
362
362
  variant='ghost'
363
- label={t('toggle expand label', { ns: osTranslations })}
363
+ label={t('toggle-expand.label', { ns: osTranslations })}
364
364
  icon={expandedField === field.id ? 'ph--caret-down--regular' : 'ph--caret-right--regular'}
365
365
  onClick={() => handleToggleField(field)}
366
366
  data-testid='field.toggle'
@@ -369,7 +369,7 @@ const FieldList = ({ schema, view, registry, readonly, showHeading = false, onDe
369
369
  )}
370
370
  </div>
371
371
  {expandedField === field.id && !readonly && (
372
- <div role='none' className='col-span-5 mt-1 mb-1 border border-separator rounded-md'>
372
+ <div className='col-span-full mt-1 mb-1 border border-separator rounded-md'>
373
373
  <FieldEditor
374
374
  readonly={readonly || schemaReadonly}
375
375
  registry={registry}
@@ -384,10 +384,10 @@ const FieldList = ({ schema, view, registry, readonly, showHeading = false, onDe
384
384
  })}
385
385
  </div>
386
386
  {!readonly && !expandedField && (
387
- <div role='none' className='my-form-padding'>
387
+ <div className='my-form-padding'>
388
388
  <IconButton
389
389
  icon='ph--plus--regular'
390
- label={t('add property button label')}
390
+ label={t('add-property-button.label')}
391
391
  onClick={handleAdd}
392
392
  disabled={viewSnapshot.projection.fields.length >= VIEW_FIELD_LIMIT}
393
393
  classNames='w-full'
@@ -4,6 +4,8 @@
4
4
 
5
5
  export * from './FieldEditor';
6
6
  export * from './Form';
7
+ export * from './ObjectForm';
7
8
  export * from './ObjectPicker';
9
+ export * from './ObjectProperties';
8
10
  export * from './Settings';
9
11
  export * from './ViewEditor';
@@ -5,17 +5,24 @@
5
5
  import React, { type PropsWithChildren } from 'react';
6
6
 
7
7
  import { type ThemedClassName } from '@dxos/react-ui';
8
- import { JsonFilter } from '@dxos/react-ui-syntax-highlighter';
9
- import { mx, textBlockWidth } from '@dxos/ui-theme';
8
+ import { Syntax } from '@dxos/react-ui-syntax-highlighter';
9
+ import { composableProps, slottable } from '@dxos/ui-theme';
10
10
 
11
- type TestLayoutProps = ThemedClassName<PropsWithChildren<{ json?: any }>>;
11
+ type TestLayoutProps = PropsWithChildren<{ json?: any }>;
12
12
 
13
- export const TestLayout = ({ classNames, children, json }: TestLayoutProps) => {
13
+ export const TestLayout = ({ children, json }: TestLayoutProps) => {
14
14
  return (
15
- <div className='w-full h-full grid grid-cols-[1fr_1fr] p-4 gap-4 overflow-hidden'>
16
- <TestPanel classNames={['flex flex-col h-full overflow-y-auto', classNames]}>{children}</TestPanel>
17
- <TestPanel classNames={'overflow-hidden'}>
18
- <JsonFilter testId='debug' data={json} classNames='h-full text-xs' />
15
+ <div className='dx-container grid grid-cols-[1fr_1fr] p-4 gap-4'>
16
+ <TestPanel>{children}</TestPanel>
17
+ <TestPanel>
18
+ <Syntax.Root data={json}>
19
+ <Syntax.Content>
20
+ <Syntax.Filter />
21
+ <Syntax.Viewport>
22
+ <Syntax.Code testId='debug' classNames='text-xs' />
23
+ </Syntax.Viewport>
24
+ </Syntax.Content>
25
+ </Syntax.Root>
19
26
  </TestPanel>
20
27
  </div>
21
28
  );
@@ -23,14 +30,15 @@ export const TestLayout = ({ classNames, children, json }: TestLayoutProps) => {
23
30
 
24
31
  type TestPanelProps = ThemedClassName<PropsWithChildren>;
25
32
 
26
- const TestPanel = ({ classNames, children }: TestPanelProps) => {
33
+ const TestPanel = slottable<HTMLDivElement, TestPanelProps>(({ children }, forwardedRef) => {
27
34
  return (
28
- <div role='none' className={mx(['h-full bg-modal-surface rounded-md', textBlockWidth, classNames])}>
35
+ <div {...composableProps({ classNames: 'dx-container bg-modal-surface rounded-md' })} ref={forwardedRef}>
29
36
  {children}
30
37
  </div>
31
38
  );
32
- };
39
+ });
33
40
 
34
41
  // Symbol for accessing debug objects in tests.
35
42
  export const VIEW_EDITOR_DEBUG_SYMBOL = Symbol('viewEditorDebug');
36
43
  export const FIELD_EDITOR_DEBUG_SYMBOL = Symbol('fieldEditorDebug');
44
+ export const OBJECT_PROPERTIES_DEBUG_SYMBOL = Symbol('objectPropertiesDebug');
@@ -2,7 +2,7 @@
2
2
  // Copyright 2025 DXOS.org
3
3
  //
4
4
 
5
- import { useCallback, useEffect } from 'react';
5
+ import { type RefObject, useCallback, useLayoutEffect } from 'react';
6
6
 
7
7
  import { addEventListener } from '@dxos/async';
8
8
 
@@ -11,7 +11,7 @@ import { type FormHandler } from './useFormHandler';
11
11
  /**
12
12
  * Key handler.
13
13
  */
14
- export const useKeyHandler = (formElement: HTMLDivElement | null, form: FormHandler<any>) => {
14
+ export const useKeyHandler = (elRef: RefObject<HTMLDivElement | null>, form: FormHandler<any>) => {
15
15
  const handleKeyDown = useCallback(
16
16
  (event: KeyboardEvent) => {
17
17
  switch (event.key) {
@@ -26,12 +26,12 @@ export const useKeyHandler = (formElement: HTMLDivElement | null, form: FormHand
26
26
  [form.isValid, form.canSave, form.onSave, form.autoSave],
27
27
  );
28
28
 
29
- useEffect(() => {
30
- if (!formElement) {
29
+ useLayoutEffect(() => {
30
+ const el = elRef.current;
31
+ if (!el) {
31
32
  return;
32
33
  }
33
34
 
34
- // TODO(burdon): Move to @dxos/dom-util.
35
- return addEventListener(formElement, 'keydown', handleKeyDown);
36
- }, [formElement, handleKeyDown]);
35
+ return addEventListener(el, 'keydown', handleKeyDown);
36
+ }, [elRef, handleKeyDown]);
37
37
  };
package/src/index.ts CHANGED
@@ -2,10 +2,6 @@
2
2
  // Copyright 2024 DXOS.org
3
3
  //
4
4
 
5
- import { translations } from './translations';
6
-
7
5
  export * from './components';
8
6
  export * from './hooks';
9
7
  export * from './util';
10
-
11
- export { translations };
@@ -10,79 +10,82 @@ export const translations = [
10
10
  {
11
11
  'en-US': {
12
12
  [translationKey]: {
13
- 'fields label': 'Fields',
14
- 'hidden fields label': 'Hidden Fields',
15
- 'empty readonly ref field label': '(none)',
13
+ 'fields.label': 'Fields',
14
+ 'hidden-fields.label': 'Hidden Fields',
15
+ 'empty-readonly-ref-field.label': '(none)',
16
16
 
17
- // TODO(burdon): Standardize field/property.
18
- 'add property button label': 'Add property',
19
- 'field property label': 'Property',
20
- 'field property placeholder': 'Property name',
21
- 'field format label': 'Type',
22
- 'field path label': 'Field path',
23
- 'add field': 'Add field',
24
- 'delete field': 'Delete field',
25
- 'field limit reached': 'Maximum number of fields reached',
26
- 'boolean input true value': 'Yes',
27
- 'boolean input false value': 'No',
28
- 'show field label': 'Show field',
29
- 'hide field label': 'Hide field',
30
- 'delete field label': 'Delete field',
31
- 'ref field combobox input placeholder': 'Search…',
32
- 'ref field placeholder_one': 'Select…',
33
- 'ref field placeholder_other': 'Select items…',
17
+ 'field-property.label': 'Property',
18
+ 'field-property.placeholder': 'Property name',
19
+ 'field-format.label': 'Type',
20
+ 'field-path.label': 'Field path',
34
21
 
35
- 'example placeholder': 'Example',
36
- 'latitude placeholder': 'Latitude (e.g., 40.7128)',
37
- 'longitude placeholder': 'Longitude (e.g., -74.0060)',
22
+ 'add-item.button': 'Add item',
23
+ 'add-property-button.label': 'Add property',
24
+ 'boolean-input-true.value': 'Yes',
25
+ 'boolean-input-false.value': 'No',
26
+ 'show-field.label': 'Show field',
27
+ 'hide-field.label': 'Hide field',
28
+ 'delete-field.label': 'Delete field',
29
+ 'create-option.label': 'Create',
30
+ 'add-tag.label': 'Add tag',
38
31
 
39
- 'cancel button label': 'Cancel',
40
- 'save button label': 'Save',
32
+ 'ref-field-combobox-input.placeholder': 'Search…',
33
+ 'ref-field.placeholder': 'Select…',
34
+ 'ref-field.placeholder_one': 'Select…',
35
+ 'ref-field.placeholder_other': 'Select items…',
41
36
 
42
- // FormatEnum
43
- 'format boolean': 'Boolean',
44
- 'format currency': 'Currency',
45
- 'format date': 'Date',
46
- 'format date-time': 'DateTime',
47
- 'format did': 'DID',
48
- 'format duration': 'Duration',
49
- 'format dxn': 'DXN',
50
- 'format email': 'Email',
51
- 'format formula': 'Formula',
52
- 'format hostname': 'Hostname',
53
- 'format integer': 'Integer',
54
- 'format json': 'JSON',
55
- 'format lnglat': 'Geopoint',
56
- 'format markdown': 'Markdown',
57
- 'format percent': 'Percent',
58
- 'format ref': 'Reference',
59
- 'format regex': 'RegExp',
60
- 'format string': 'String',
61
- 'format single-select': 'Select',
62
- 'format multi-select': 'Multi-select',
63
- 'format text': 'Long text',
64
- 'format time': 'Time',
65
- 'format timestamp': 'Timestamp',
66
- 'format user': 'User',
67
- 'format number': 'Number',
68
- 'format uri': 'URI',
69
- 'format url': 'URL',
70
- 'format uuid': 'UUID',
37
+ 'example.placeholder': 'Example',
38
+ 'latitude.placeholder': 'Latitude (e.g., 40.7128)',
39
+ 'longitude.placeholder': 'Longitude (e.g., -74.0060)',
40
+
41
+ 'cancel-button.label': 'Cancel',
42
+ 'save-button.label': 'Save',
43
+ 'create-text.label': 'Create text',
44
+
45
+ // TypeFormat
46
+ 'format.boolean.label': 'Boolean',
47
+ 'format.currency.label': 'Currency',
48
+ 'format.date.label': 'Date',
49
+ 'format.date-time.label': 'DateTime',
50
+ 'format.did.label': 'DID',
51
+ 'format.duration.label': 'Duration',
52
+ 'format.dxn.label': 'DXN',
53
+ 'format.email.label': 'Email',
54
+ 'format.formula.label': 'Formula',
55
+ 'format.hostname.label': 'Hostname',
56
+ 'format.integer.label': 'Integer',
57
+ 'format.json.label': 'JSON',
58
+ 'format.lnglat.label': 'Geopoint',
59
+ 'format.markdown.label': 'Markdown',
60
+ 'format.percent.label': 'Percent',
61
+ 'format.ref.label': 'Reference',
62
+ 'format.regex.label': 'RegExp',
63
+ 'format.string.label': 'String',
64
+ 'format.single-select.label': 'Select',
65
+ 'format.multi-select.label': 'Multi-select',
66
+ 'format.text.label': 'Long text',
67
+ 'format.time.label': 'Time',
68
+ 'format.timestamp.label': 'Timestamp',
69
+ 'format.user.label': 'User',
70
+ 'format.number.label': 'Number',
71
+ 'format.uri.label': 'URI',
72
+ 'format.url.label': 'URL',
73
+ 'format.uuid.label': 'UUID',
71
74
 
72
75
  // GeoPointField.
73
- 'latitude label': 'Latitude',
74
- 'longitude label': 'Longitude',
76
+ 'latitude.label': 'Latitude',
77
+ 'longitude.label': 'Longitude',
75
78
 
76
79
  // SelectOptionsField.
77
- 'select option label': 'Label',
78
- 'select option label placeholder': 'Option label',
79
- 'select option color': 'Color',
80
- 'select option delete': 'Delete',
81
- 'select option add': 'Add option',
80
+ 'select-option.label': 'Label',
81
+ 'select-option-label.placeholder': 'Option label',
82
+ 'select-option-delete.button': 'Delete',
83
+ 'select-option-add.button': 'Add option',
82
84
 
83
85
  // System schema message.
84
- 'system schema title': 'System type',
85
- 'system schema description': 'System type: cannot add or remove fields.',
86
+ 'system-schema.title': 'System type',
87
+ 'system-schema.description': 'System type: cannot add or remove fields.',
88
+ 'remove.button': 'Remove',
86
89
  },
87
90
  },
88
91
  },
@@ -0,0 +1,60 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ import * as SchemaAST from 'effect/SchemaAST';
6
+
7
+ import { getAnnotation, isLiteralUnion } from '@dxos/effect';
8
+
9
+ /**
10
+ * Detected field type for a schema property.
11
+ */
12
+ export type SettingsFieldType = 'boolean' | 'string' | 'number' | 'select' | 'struct';
13
+
14
+ export type SelectOption = {
15
+ value: string | number;
16
+ label?: string;
17
+ };
18
+
19
+ /**
20
+ * Detect the field type from an Effect Schema AST node.
21
+ */
22
+ export const detectFieldType = (ast: SchemaAST.AST): SettingsFieldType | undefined => {
23
+ if (isLiteralUnion(ast)) {
24
+ return 'select';
25
+ }
26
+ if (SchemaAST.isTypeLiteral(ast)) {
27
+ return 'struct';
28
+ }
29
+
30
+ switch (ast._tag) {
31
+ case 'BooleanKeyword':
32
+ return 'boolean';
33
+ case 'StringKeyword':
34
+ return 'string';
35
+ case 'NumberKeyword':
36
+ return 'number';
37
+ }
38
+
39
+ return undefined;
40
+ };
41
+
42
+ /**
43
+ * Extract select options from a literal union AST node.
44
+ * Reads title annotations from individual literals for labels.
45
+ */
46
+ export const getSelectOptionsFromAst = (ast: SchemaAST.AST): SelectOption[] | undefined => {
47
+ if (!isLiteralUnion(ast)) {
48
+ return undefined;
49
+ }
50
+
51
+ return (ast as SchemaAST.Union<SchemaAST.Literal>).types.flatMap((literalNode) => {
52
+ const value = literalNode.literal;
53
+ if (typeof value !== 'string' && typeof value !== 'number') {
54
+ return [];
55
+ }
56
+ const title = getAnnotation<string>(SchemaAST.TitleAnnotationId, false)(literalNode);
57
+ const label = title ?? (typeof value === 'string' ? value.charAt(0).toUpperCase() + value.slice(1) : String(value));
58
+ return [{ value, label }];
59
+ });
60
+ };
package/src/util/index.ts CHANGED
@@ -3,6 +3,7 @@
3
3
  //
4
4
 
5
5
  export * from './field';
6
+ export * from './field-type';
6
7
  export * from './formatting';
7
8
  export * from './properties';
8
9
  export * from './refs';
package/src/util/refs.ts CHANGED
@@ -5,11 +5,13 @@
5
5
  import type * as SchemaAST from 'effect/SchemaAST';
6
6
 
7
7
  import { Ref } from '@dxos/echo';
8
- import { getArrayElementType, isArrayType } from '@dxos/effect';
8
+ import { type ReferenceAnnotationValue, ReferenceAnnotationId } from '@dxos/echo/internal';
9
+ import { findAnnotation, getArrayElementType, isArrayType } from '@dxos/effect';
9
10
 
10
11
  type RefProps = {
11
12
  ast: SchemaAST.AST;
12
13
  isArray: boolean;
14
+ typename?: string;
13
15
  };
14
16
 
15
17
  export const getRefProps = (ast: SchemaAST.AST): RefProps | undefined => {
@@ -18,14 +20,16 @@ export const getRefProps = (ast: SchemaAST.AST): RefProps | undefined => {
18
20
  const elementType = getArrayElementType(ast);
19
21
  if (elementType) {
20
22
  if (Ref.isRefType(elementType)) {
21
- return { ast: elementType, isArray: true };
23
+ const typename = findAnnotation<ReferenceAnnotationValue>(elementType, ReferenceAnnotationId)?.typename;
24
+ return { ast: elementType, isArray: true, typename };
22
25
  }
23
26
  }
24
27
  }
25
28
 
26
29
  // Direct reference.
27
30
  if (Ref.isRefType(ast)) {
28
- return { ast, isArray: false };
31
+ const typename = findAnnotation<ReferenceAnnotationValue>(ast, ReferenceAnnotationId)?.typename;
32
+ return { ast, isArray: false, typename };
29
33
  }
30
34
 
31
35
  return undefined;