@dxos/react-ui-form 0.8.4-main.d05539e30a → 0.8.4-main.d9fc60f731

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 (154) hide show
  1. package/dist/lib/browser/index.mjs +1335 -561
  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 +2 -1
  5. package/dist/lib/browser/translations.mjs.map +2 -2
  6. package/dist/lib/node-esm/index.mjs +1335 -561
  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 +2 -1
  10. package/dist/lib/node-esm/translations.mjs.map +2 -2
  11. package/dist/types/src/components/FieldEditor/FieldEditor.d.ts +2 -2
  12. package/dist/types/src/components/FieldEditor/FieldEditor.d.ts.map +1 -1
  13. package/dist/types/src/components/FieldEditor/FieldEditor.stories.d.ts +2 -1
  14. package/dist/types/src/components/FieldEditor/FieldEditor.stories.d.ts.map +1 -1
  15. package/dist/types/src/components/Form/Form.d.ts +35 -9
  16. package/dist/types/src/components/Form/Form.d.ts.map +1 -1
  17. package/dist/types/src/components/Form/Form.stories.d.ts +76 -16
  18. package/dist/types/src/components/Form/Form.stories.d.ts.map +1 -1
  19. package/dist/types/src/components/Form/FormField.d.ts +14 -4
  20. package/dist/types/src/components/Form/FormField.d.ts.map +1 -1
  21. package/dist/types/src/components/Form/FormFieldComponent.d.ts +22 -10
  22. package/dist/types/src/components/Form/FormFieldComponent.d.ts.map +1 -1
  23. package/dist/types/src/components/Form/FormFieldSet.d.ts +8 -2
  24. package/dist/types/src/components/Form/FormFieldSet.d.ts.map +1 -1
  25. package/dist/types/src/components/Form/FormTooltipsContext.d.ts +19 -0
  26. package/dist/types/src/components/Form/FormTooltipsContext.d.ts.map +1 -0
  27. package/dist/types/src/components/Form/Layout/FormLayout.d.ts +50 -0
  28. package/dist/types/src/components/Form/Layout/FormLayout.d.ts.map +1 -0
  29. package/dist/types/src/components/Form/Layout/FormLayout.stories.d.ts +119 -0
  30. package/dist/types/src/components/Form/Layout/FormLayout.stories.d.ts.map +1 -0
  31. package/dist/types/src/components/Form/Layout/FormLayout.stories.test.d.ts +2 -0
  32. package/dist/types/src/components/Form/Layout/FormLayout.stories.test.d.ts.map +1 -0
  33. package/dist/types/src/components/Form/Layout/FormLayout.test.d.ts +2 -0
  34. package/dist/types/src/components/Form/Layout/FormLayout.test.d.ts.map +1 -0
  35. package/dist/types/src/components/Form/Layout/annotation.d.ts +30 -0
  36. package/dist/types/src/components/Form/Layout/annotation.d.ts.map +1 -0
  37. package/dist/types/src/components/Form/Layout/index.d.ts +4 -0
  38. package/dist/types/src/components/Form/Layout/index.d.ts.map +1 -0
  39. package/dist/types/src/components/Form/Layout/parser.d.ts +31 -0
  40. package/dist/types/src/components/Form/Layout/parser.d.ts.map +1 -0
  41. package/dist/types/src/components/Form/Layout/parser.test.d.ts +2 -0
  42. package/dist/types/src/components/Form/Layout/parser.test.d.ts.map +1 -0
  43. package/dist/types/src/components/Form/fields/ArrayField.d.ts.map +1 -1
  44. package/dist/types/src/components/Form/fields/DateField.d.ts +4 -0
  45. package/dist/types/src/components/Form/fields/DateField.d.ts.map +1 -0
  46. package/dist/types/src/components/Form/fields/GeoPointField.d.ts +1 -1
  47. package/dist/types/src/components/Form/fields/GeoPointField.d.ts.map +1 -1
  48. package/dist/types/src/components/Form/fields/InlineRefField.d.ts +13 -0
  49. package/dist/types/src/components/Form/fields/InlineRefField.d.ts.map +1 -0
  50. package/dist/types/src/components/Form/fields/MarkdownField.d.ts +1 -1
  51. package/dist/types/src/components/Form/fields/MarkdownField.d.ts.map +1 -1
  52. package/dist/types/src/components/Form/fields/MarkdownField.stories.d.ts +2 -1
  53. package/dist/types/src/components/Form/fields/MarkdownField.stories.d.ts.map +1 -1
  54. package/dist/types/src/components/Form/fields/RefField.d.ts +1 -1
  55. package/dist/types/src/components/Form/fields/RefField.d.ts.map +1 -1
  56. package/dist/types/src/components/Form/fields/SelectField.d.ts +4 -1
  57. package/dist/types/src/components/Form/fields/SelectField.d.ts.map +1 -1
  58. package/dist/types/src/components/Form/fields/SelectOptionField.d.ts +1 -1
  59. package/dist/types/src/components/Form/fields/SelectOptionField.d.ts.map +1 -1
  60. package/dist/types/src/components/Form/fields/SelectOptionField.stories.d.ts +2 -1
  61. package/dist/types/src/components/Form/fields/SelectOptionField.stories.d.ts.map +1 -1
  62. package/dist/types/src/components/Form/fields/TupleField.d.ts +1 -1
  63. package/dist/types/src/components/Form/fields/TupleField.d.ts.map +1 -1
  64. package/dist/types/src/components/Form/fields/index.d.ts +2 -0
  65. package/dist/types/src/components/Form/fields/index.d.ts.map +1 -1
  66. package/dist/types/src/components/Form/index.d.ts +3 -0
  67. package/dist/types/src/components/Form/index.d.ts.map +1 -1
  68. package/dist/types/src/components/Form/meta-tags.d.ts +30 -0
  69. package/dist/types/src/components/Form/meta-tags.d.ts.map +1 -0
  70. package/dist/types/src/components/Form/meta-tags.test.d.ts +2 -0
  71. package/dist/types/src/components/Form/meta-tags.test.d.ts.map +1 -0
  72. package/dist/types/src/components/ObjectForm/ObjectForm.d.ts +3 -4
  73. package/dist/types/src/components/ObjectForm/ObjectForm.d.ts.map +1 -1
  74. package/dist/types/src/components/ObjectPicker/ObjectPicker.d.ts +2 -2
  75. package/dist/types/src/components/ObjectPicker/ObjectPicker.d.ts.map +1 -1
  76. package/dist/types/src/components/ObjectPicker/ObjectPicker.stories.d.ts +2 -1
  77. package/dist/types/src/components/ObjectPicker/ObjectPicker.stories.d.ts.map +1 -1
  78. package/dist/types/src/components/ObjectProperties/ObjectProperties.d.ts.map +1 -1
  79. package/dist/types/src/components/ObjectProperties/ObjectProperties.stories.d.ts +2 -1
  80. package/dist/types/src/components/ObjectProperties/ObjectProperties.stories.d.ts.map +1 -1
  81. package/dist/types/src/components/ObjectTree/ObjectTree.d.ts +9 -0
  82. package/dist/types/src/components/ObjectTree/ObjectTree.d.ts.map +1 -0
  83. package/dist/types/src/components/ObjectTree/ObjectTree.stories.d.ts +87 -0
  84. package/dist/types/src/components/ObjectTree/ObjectTree.stories.d.ts.map +1 -0
  85. package/dist/types/src/components/ObjectTree/index.d.ts +2 -0
  86. package/dist/types/src/components/ObjectTree/index.d.ts.map +1 -0
  87. package/dist/types/src/components/Settings/Settings.d.ts.map +1 -1
  88. package/dist/types/src/components/Settings/Settings.stories.d.ts +2 -1
  89. package/dist/types/src/components/Settings/Settings.stories.d.ts.map +1 -1
  90. package/dist/types/src/components/ViewEditor/ViewEditor.d.ts +7 -8
  91. package/dist/types/src/components/ViewEditor/ViewEditor.d.ts.map +1 -1
  92. package/dist/types/src/components/ViewEditor/ViewEditor.stories.d.ts +4 -4
  93. package/dist/types/src/components/ViewEditor/ViewEditor.stories.d.ts.map +1 -1
  94. package/dist/types/src/components/index.d.ts +1 -0
  95. package/dist/types/src/components/index.d.ts.map +1 -1
  96. package/dist/types/src/components/testing.d.ts +9 -0
  97. package/dist/types/src/components/testing.d.ts.map +1 -1
  98. package/dist/types/src/translations.d.ts +2 -1
  99. package/dist/types/src/translations.d.ts.map +1 -1
  100. package/dist/types/src/util/properties.d.ts +11 -0
  101. package/dist/types/src/util/properties.d.ts.map +1 -1
  102. package/dist/types/tsconfig.tsbuildinfo +1 -1
  103. package/package.json +37 -32
  104. package/src/components/FieldEditor/FieldEditor.stories.tsx +2 -2
  105. package/src/components/FieldEditor/FieldEditor.test.tsx +4 -1
  106. package/src/components/FieldEditor/FieldEditor.tsx +19 -14
  107. package/src/components/Form/Form.stories.tsx +21 -27
  108. package/src/components/Form/Form.tsx +106 -19
  109. package/src/components/Form/FormField.tsx +85 -28
  110. package/src/components/Form/FormFieldComponent.tsx +75 -20
  111. package/src/components/Form/FormFieldSet.tsx +32 -3
  112. package/src/components/Form/FormTooltipsContext.ts +25 -0
  113. package/src/components/Form/Layout/FormLayout.stories.test.tsx +51 -0
  114. package/src/components/Form/Layout/FormLayout.stories.tsx +422 -0
  115. package/src/components/Form/Layout/FormLayout.test.ts +59 -0
  116. package/src/components/Form/Layout/FormLayout.tsx +251 -0
  117. package/src/components/Form/Layout/annotation.ts +38 -0
  118. package/src/components/Form/Layout/index.ts +7 -0
  119. package/src/components/Form/Layout/parser.test.ts +120 -0
  120. package/src/components/Form/Layout/parser.ts +167 -0
  121. package/src/components/Form/fields/ArrayField.tsx +61 -38
  122. package/src/components/Form/fields/DateField.tsx +101 -0
  123. package/src/components/Form/fields/GeoPointField.tsx +4 -1
  124. package/src/components/Form/fields/InlineRefField.tsx +146 -0
  125. package/src/components/Form/fields/MarkdownField.tsx +3 -3
  126. package/src/components/Form/fields/RefField.tsx +29 -23
  127. package/src/components/Form/fields/SelectField.tsx +17 -5
  128. package/src/components/Form/fields/SelectOptionField.tsx +4 -3
  129. package/src/components/Form/fields/TextAreaField.tsx +1 -1
  130. package/src/components/Form/fields/TupleField.tsx +2 -1
  131. package/src/components/Form/fields/index.ts +2 -0
  132. package/src/components/Form/index.ts +3 -0
  133. package/src/components/Form/meta-tags.test.ts +47 -0
  134. package/src/components/Form/meta-tags.ts +33 -0
  135. package/src/components/ObjectForm/ObjectForm.tsx +21 -26
  136. package/src/components/ObjectPicker/ObjectPicker.stories.tsx +2 -2
  137. package/src/components/ObjectPicker/ObjectPicker.tsx +2 -2
  138. package/src/components/ObjectProperties/ObjectProperties.stories.tsx +14 -26
  139. package/src/components/ObjectProperties/ObjectProperties.test.tsx +20 -10
  140. package/src/components/ObjectProperties/ObjectProperties.tsx +26 -30
  141. package/src/components/ObjectTree/ObjectTree.stories.tsx +134 -0
  142. package/src/components/ObjectTree/ObjectTree.tsx +190 -0
  143. package/src/components/ObjectTree/index.ts +5 -0
  144. package/src/components/Settings/Settings.tsx +2 -1
  145. package/src/components/Settings/SettingsItem.tsx +1 -1
  146. package/src/components/ViewEditor/ViewEditor.stories.tsx +35 -40
  147. package/src/components/ViewEditor/ViewEditor.test.tsx +11 -8
  148. package/src/components/ViewEditor/ViewEditor.tsx +29 -32
  149. package/src/components/index.ts +1 -0
  150. package/src/components/testing.tsx +4 -4
  151. package/src/translations.ts +2 -1
  152. package/src/util/properties.test.ts +67 -9
  153. package/src/util/properties.ts +36 -4
  154. package/src/vite-env.d.ts +10 -0
@@ -0,0 +1,134 @@
1
+ //
2
+ // Copyright 2026 DXOS.org
3
+ //
4
+
5
+ import { type Meta, type StoryObj } from '@storybook/react-vite';
6
+ import React from 'react';
7
+
8
+ import { parseProto } from '@dxos/effect-proto';
9
+ import { log } from '@dxos/log';
10
+ import { type Config as ConfigProto, Runtime } from '@dxos/protocols/proto/dxos/config';
11
+ // Raw .proto source resolved through `@dxos/protocols`'s `./proto/dxos/*.proto`
12
+ // exports entry. Vite's `?raw` query suffix returns the file as a string so we
13
+ // can hand it straight to `parseProto` -- the same flow the unit test uses.
14
+ import configProto from '@dxos/protocols/proto/dxos/config.proto?raw';
15
+ import { Syntax } from '@dxos/react-ui-syntax-highlighter';
16
+ import { withLayout, withTheme } from '@dxos/react-ui/testing';
17
+
18
+ import { translations } from '#translations';
19
+
20
+ import { Form } from '../Form';
21
+ import { ObjectTree } from './ObjectTree';
22
+
23
+ const registry = parseProto(configProto);
24
+ const ConfigSchema = registry.get('dxos.config.Config');
25
+
26
+ // A representative Config value that exercises nested messages, arrays, enums
27
+ // (rendered as their string names by the proto codec), and `google.protobuf.Any`.
28
+ const value: ConfigProto = {
29
+ version: 1,
30
+ runtime: {
31
+ client: {
32
+ log: { filter: 'info', prefix: 'app' },
33
+ storage: {
34
+ persistent: true,
35
+ sqliteMode: Runtime.Client.Storage.SqliteMode.OPFS,
36
+ dataRoot: '/var/lib/dxos',
37
+ },
38
+ edgeFeatures: {
39
+ echoReplicator: true,
40
+ subductionReplicator: true,
41
+ signaling: true,
42
+ },
43
+ servicesMode: Runtime.Client.ServicesMode.SHARED_WORKER,
44
+ enableSnapshots: false,
45
+ snapshotInterval: 60_000,
46
+ },
47
+ app: {
48
+ org: 'DXOS',
49
+ theme: 'dark',
50
+ website: 'https://dxos.org',
51
+ build: {
52
+ timestamp: '2026-05-22T12:00:00Z',
53
+ commitHash: 'abc1234',
54
+ version: '0.8.3',
55
+ branch: 'main',
56
+ },
57
+ },
58
+ services: {
59
+ edge: { url: 'wss://edge.dxos.org' },
60
+ signaling: [
61
+ { server: 'wss://signal-1.dxos.org', api: 'v1' },
62
+ { server: 'wss://signal-2.dxos.org', api: 'v1' },
63
+ ],
64
+ iceProviders: [{ urls: 'turn:turn.dxos.org' }],
65
+ },
66
+ keys: [
67
+ { name: 'OPENAI_API_KEY', value: 'sk-...' },
68
+ { name: 'ANTHROPIC_API_KEY', value: 'ant-...' },
69
+ ],
70
+ },
71
+ };
72
+
73
+ const meta = {
74
+ title: 'ui/react-ui-form/ObjectTree',
75
+ component: ObjectTree,
76
+ decorators: [withTheme(), withLayout({ layout: 'column', scroll: true })],
77
+ parameters: {
78
+ layout: 'fullscreen',
79
+ translations,
80
+ },
81
+ } satisfies Meta<typeof ObjectTree>;
82
+
83
+ export default meta;
84
+
85
+ type Story = StoryObj<typeof meta>;
86
+
87
+ export const Default: Story = {
88
+ args: {
89
+ schema: ConfigSchema,
90
+ value: value,
91
+ },
92
+ };
93
+
94
+ export const JSON = () => (
95
+ <Syntax.Root data={value}>
96
+ <Syntax.Content>
97
+ <Syntax.Filter />
98
+ <Syntax.Viewport>
99
+ <Syntax.Code />
100
+ </Syntax.Viewport>
101
+ </Syntax.Content>
102
+ </Syntax.Root>
103
+ );
104
+
105
+ // Renders the same proto-derived `ConfigSchema` + `value` through the
106
+ // interactive `Form` component, for side-by-side comparison with the
107
+ // read-only ObjectTree above. `db` is intentionally omitted -- the proto
108
+ // schema is not an ECHO type, so there's no space/database in play.
109
+ export const WithForm = () => (
110
+ <Form.Root
111
+ schema={ConfigSchema as any}
112
+ defaultValues={value as any}
113
+ onSave={(next) => log.info('save', { next })}
114
+ onCancel={() => log.info('cancel')}
115
+ >
116
+ <Form.Viewport>
117
+ <Form.Content>
118
+ <Form.FieldSet />
119
+ <Form.Actions />
120
+ </Form.Content>
121
+ </Form.Viewport>
122
+ </Form.Root>
123
+ );
124
+
125
+ export const WithReadOnlyForm = () => (
126
+ <Form.Root schema={ConfigSchema as any} defaultValues={value as any} readonly={true}>
127
+ <Form.Viewport>
128
+ <Form.Content>
129
+ <Form.FieldSet />
130
+ <Form.Actions />
131
+ </Form.Content>
132
+ </Form.Viewport>
133
+ </Form.Root>
134
+ );
@@ -0,0 +1,190 @@
1
+ //
2
+ // Copyright 2026 DXOS.org
3
+ //
4
+
5
+ // A minimal, read-only schema-driven viewer. Walks an Effect Schema and a
6
+ // matching value and produces a flat list of indented key/value rows -- one
7
+ // per leaf field, with nested Struct fields contributing a header row plus
8
+ // recursively-indented children, and arrays expanded as `[index]` rows.
9
+ //
10
+ // Designed to drive a hierarchical editor over schemas produced by
11
+ // `@dxos/effect-proto` (e.g. the proto-generated `dxos.config.Config`
12
+ // schema), but works for any Effect Struct schema.
13
+
14
+ import * as Schema from 'effect/Schema';
15
+ import * as SchemaAST from 'effect/SchemaAST';
16
+ import React, { type ReactElement, type ReactNode, type RefAttributes } from 'react';
17
+
18
+ import { composable, composableProps } from '@dxos/react-ui';
19
+ import { mx } from '@dxos/ui-theme';
20
+ import { type ComposableProps } from '@dxos/ui-types';
21
+
22
+ export type ObjectTreeProps<T> = {
23
+ schema: Schema.Schema<T, any, never>;
24
+ value: T;
25
+ };
26
+
27
+ /**
28
+ * Render a value as a sequence of key/value rows whose shape and order are
29
+ * driven by `schema`. Object-typed properties become a header row whose
30
+ * children are indented under it; arrays expand element-by-element.
31
+ *
32
+ * Composable: spreads unknown props (incl. `className` / `classNames` /
33
+ * `style` / `role`) onto its root `<div>` and forwards its ref, so it can
34
+ * be slotted into themed parents like `Panel.Content`.
35
+ */
36
+ const ObjectTreeImpl = composable<HTMLDivElement, ObjectTreeProps<any>>(({ schema, value, ...props }, forwardedRef) => (
37
+ <div {...composableProps(props, { role: 'table', classNames: 'font-mono text-sm w-full' })} ref={forwardedRef}>
38
+ <Node ast={schema.ast} value={value} label={null} depth={0} />
39
+ </div>
40
+ ));
41
+
42
+ ObjectTreeImpl.displayName = 'ObjectTree';
43
+
44
+ // `composable<HTMLDivElement, ObjectTreeProps<any>>` erases the `T` type
45
+ // parameter inside the factory; the cast restores the generic signature for
46
+ // consumers without changing the underlying component (the COMPOSABLE marker
47
+ // stays attached). This is the pattern documented on `composable()` itself.
48
+ export const ObjectTree = ObjectTreeImpl as unknown as <T>(
49
+ props: ComposableProps<ObjectTreeProps<T>> & RefAttributes<HTMLDivElement>,
50
+ ) => ReactElement | null;
51
+
52
+ /**
53
+ * Peel wrappers that don't affect the runtime shape: Refinements pass their
54
+ * `from`, Suspends resolve via `f()`, and Transformations expose the encoded
55
+ * (input) side. Repeated unwrapping handles chains like `Schema.suspend ->
56
+ * Refinement -> TypeLiteral`.
57
+ */
58
+ const unwrap = (ast: SchemaAST.AST): SchemaAST.AST => {
59
+ if (SchemaAST.isRefinement(ast)) {
60
+ return unwrap(ast.from);
61
+ }
62
+ if (SchemaAST.isSuspend(ast)) {
63
+ return unwrap(ast.f());
64
+ }
65
+ if (SchemaAST.isTransformation(ast)) {
66
+ return unwrap(ast.from);
67
+ }
68
+ return ast;
69
+ };
70
+
71
+ /**
72
+ * Effect models `Schema.optional(X)` as a property whose type AST is
73
+ * `Union(X, UndefinedKeyword)`. For walking purposes we want X -- the
74
+ * undefined branch carries no schema information for the viewer.
75
+ */
76
+ const stripOptional = (ast: SchemaAST.AST): SchemaAST.AST => {
77
+ if (!SchemaAST.isUnion(ast)) {
78
+ return ast;
79
+ }
80
+ const nonUndef = ast.types.filter((t) => !SchemaAST.isUndefinedKeyword(t));
81
+ return nonUndef.length === 1 ? nonUndef[0] : ast;
82
+ };
83
+
84
+ type NodeProps = {
85
+ ast: SchemaAST.AST;
86
+ value: unknown;
87
+ /** Row label; `null` means render children directly without a header row. */
88
+ label: ReactNode | null;
89
+ depth: number;
90
+ };
91
+
92
+ const Node = ({ ast, value, label, depth }: NodeProps) => {
93
+ const inner = unwrap(stripOptional(ast));
94
+
95
+ // Struct: header row + indented children. Skipping the header at depth 0
96
+ // keeps the root flush-left.
97
+ if (SchemaAST.isTypeLiteral(inner) && isPlainObject(value)) {
98
+ const childDepth = label === null ? depth : depth + 1;
99
+ return (
100
+ <>
101
+ {label !== null && <Row label={label} value={null} depth={depth} kind='group' />}
102
+ {inner.propertySignatures.map((prop) => (
103
+ <Node
104
+ key={String(prop.name)}
105
+ ast={prop.type}
106
+ value={(value as Record<string, unknown>)[String(prop.name)]}
107
+ label={String(prop.name)}
108
+ depth={childDepth}
109
+ />
110
+ ))}
111
+ </>
112
+ );
113
+ }
114
+
115
+ // Array (`Schema.Array(X)` -> TupleType with single rest element).
116
+ if (SchemaAST.isTupleType(inner) && Array.isArray(value)) {
117
+ const elemType = inner.rest[0]?.type;
118
+ return (
119
+ <>
120
+ {label !== null && <Row label={label} value={`Array(${value.length})`} depth={depth} kind='group' />}
121
+ {value.map((item, index) => (
122
+ <Node
123
+ key={index}
124
+ ast={elemType ?? SchemaAST.unknownKeyword}
125
+ value={item}
126
+ label={`[${index}]`}
127
+ depth={depth + 1}
128
+ />
129
+ ))}
130
+ </>
131
+ );
132
+ }
133
+
134
+ // Leaf (scalars, unions of literals, unknown, etc.).
135
+ return <Row label={label ?? ''} value={formatLeaf(value)} depth={depth} kind='leaf' />;
136
+ };
137
+
138
+ type RowProps = {
139
+ label: ReactNode;
140
+ value: ReactNode;
141
+ depth: number;
142
+ kind: 'group' | 'leaf';
143
+ };
144
+
145
+ const Row = ({ label, value, depth }: RowProps) => (
146
+ <div role='row' className={mx('flex items-baseline gap-3 py-1')} style={{ paddingInlineStart: 8 + depth * 16 }}>
147
+ <div role='cell' className='text-subdued'>
148
+ {label}
149
+ </div>
150
+ <div role='cell' className={mx('break-all text-base-fg')}>
151
+ {value}
152
+ </div>
153
+ </div>
154
+ );
155
+
156
+ const isPlainObject = (value: unknown): value is Record<string, unknown> =>
157
+ typeof value === 'object' && value !== null && !Array.isArray(value);
158
+
159
+ /**
160
+ * Format a leaf value for display. Strings are quoted so empty strings are
161
+ * visible; missing values surface as an em-dash so the row stays present.
162
+ */
163
+ const formatLeaf = (value: unknown): ReactNode => {
164
+ if (value === undefined) {
165
+ return null;
166
+ }
167
+
168
+ if (value === null) {
169
+ return <span className='text-description'>null</span>;
170
+ }
171
+
172
+ switch (typeof value) {
173
+ case 'string':
174
+ // Quote strings so empty strings remain visible (`""` rather than blank).
175
+ return <span className='text-blue-text'>{JSON.stringify(value)}</span>;
176
+ case 'number':
177
+ case 'bigint':
178
+ return <span className='text-green-text'>{String(value)}</span>;
179
+ case 'boolean':
180
+ return <span className='text-purple-text'>{String(value)}</span>;
181
+ default:
182
+ // Objects we couldn't expand via schema (e.g. `google.protobuf.Any` -> Unknown)
183
+ // are rendered as compact JSON.
184
+ try {
185
+ return JSON.stringify(value);
186
+ } catch {
187
+ return String(value);
188
+ }
189
+ }
190
+ };
@@ -0,0 +1,5 @@
1
+ //
2
+ // Copyright 2026 DXOS.org
3
+ //
4
+
5
+ export * from './ObjectTree';
@@ -5,7 +5,8 @@
5
5
  import React, { type PropsWithChildren } from 'react';
6
6
 
7
7
  import { type Label, ScrollArea, type ThemedClassName, toLocalizedString, useTranslation } from '@dxos/react-ui';
8
- import { composable, composableProps, mx } from '@dxos/ui-theme';
8
+ import { composable, composableProps } from '@dxos/react-ui';
9
+ import { mx } from '@dxos/ui-theme';
9
10
 
10
11
  import { translationKey } from '#translations';
11
12
 
@@ -41,7 +41,7 @@ export const SettingsItem = ({ title, description = '', children }: SettingsItem
41
41
  return (
42
42
  <Input.Root>
43
43
  <SettingsPanel classNames='grid grid-cols-1 md:grid-cols-[1fr_1fr] gap-x-trim-lg gap-y-0'>
44
- <Input.Label classNames='pb-trim-md text-base-foreground text-lg md:col-span-2'>
44
+ <Input.Label classNames='pb-trim-md text-base-fg text-lg md:col-span-2'>
45
45
  {toLocalizedString(title, t)}
46
46
  </Input.Label>
47
47
  <Input.DescriptionAndValidation>
@@ -6,8 +6,8 @@ import { type Meta, type StoryObj } from '@storybook/react-vite';
6
6
  import * as Schema from 'effect/Schema';
7
7
  import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
8
8
 
9
- import { DXN, Filter, JsonSchema, Obj, Query, type QueryAST, Tag, Type, type View } from '@dxos/echo';
10
- import { type EchoSchema, Format, type Mutable } from '@dxos/echo/internal';
9
+ import { DXN, EID, Filter, JsonSchema, Obj, Query, type QueryAST, Scope, Tag, Type, type View } from '@dxos/echo';
10
+ import { Format, type Mutable } from '@dxos/echo/internal';
11
11
  import { useQuery } from '@dxos/react-client/echo';
12
12
  import { useClientStory, withClientProvider } from '@dxos/react-client/testing';
13
13
  import { useAsyncEffect } from '@dxos/react-ui';
@@ -22,15 +22,15 @@ import { ViewEditor, type ViewEditorProps } from './ViewEditor';
22
22
 
23
23
  const types = [
24
24
  // TODO(burdon): Get label from annotation.
25
- { value: Organization.Organization.typename, label: 'Organization' },
26
- { value: Person.Person.typename, label: 'Person' },
27
- { value: Pipeline.Pipeline.typename, label: 'Project' },
28
- { value: Employer.Employer.typename, label: 'Employer' },
25
+ { value: Type.getTypename(Organization.Organization), label: 'Organization' },
26
+ { value: Type.getTypename(Person.Person), label: 'Person' },
27
+ { value: Type.getTypename(Pipeline.Pipeline), label: 'Project' },
28
+ { value: Type.getTypename(Employer.Employer), label: 'Employer' },
29
29
  ];
30
30
 
31
31
  // Type definition for debug objects exposed to tests.
32
32
  export type ViewEditorDebugObjects = {
33
- schema: EchoSchema;
33
+ type: Type.AnyEntity;
34
34
  view: View.View;
35
35
  projection: ProjectionModel;
36
36
  };
@@ -39,7 +39,7 @@ type DefaultStoryProps = Pick<ViewEditorProps, 'readonly' | 'mode'>;
39
39
 
40
40
  const DefaultStory = (props: DefaultStoryProps) => {
41
41
  const { space } = useClientStory();
42
- const [schema, setSchema] = useState<EchoSchema>();
42
+ const [type, setType] = useState<Type.AnyEntity>();
43
43
  const [view, setView] = useState<View.View>();
44
44
  const projectionRef = useRef<ProjectionModel>(null);
45
45
 
@@ -51,71 +51,66 @@ const DefaultStory = (props: DefaultStoryProps) => {
51
51
  name: Schema.String,
52
52
  email: Format.Email,
53
53
  salary: Format.Currency(),
54
- }).pipe(
55
- Type.object({
56
- typename: 'com.example.type.test',
57
- version: '0.1.0',
58
- }),
59
- );
54
+ }).pipe(Type.makeObject(DXN.make('com.example.type.test', '0.1.0')));
60
55
 
61
56
  const AlternateSchema = Schema.Struct({
62
57
  title: Schema.String,
63
58
  description: Schema.String,
64
59
  completed: Schema.Boolean,
65
- }).pipe(
66
- Type.object({
67
- typename: 'com.example.type.alternate',
68
- version: '0.1.0',
69
- }),
70
- );
71
-
72
- const [testSchema] = await space.db.schemaRegistry.register([TestSchema, AlternateSchema]);
60
+ }).pipe(Type.makeObject(DXN.make('com.example.type.alternate', '0.1.0')));
61
+
62
+ const testType = await space.db.addType(TestSchema);
63
+ await space.db.addType(AlternateSchema);
73
64
  const view = ViewModel.make({
74
65
  name: 'Test',
75
66
  query: Query.select(Filter.type(TestSchema)),
76
67
  jsonSchema: JsonSchema.toJsonSchema(TestSchema),
77
68
  });
78
69
 
79
- setSchema(testSchema);
70
+ setType(testType);
80
71
  setView(view);
81
72
  }
82
73
  }, [space]);
83
74
 
84
75
  const updateViewQuery = useCallback(
85
- async (newQuery: QueryAST.Query, target?: string) => {
86
- if (!schema || !view || !space) {
76
+ async (newQuery: QueryAST.Query, target?: EID.EID) => {
77
+ if (!type || !view || !space) {
87
78
  return;
88
79
  }
89
80
 
90
81
  if (props.mode === 'tag') {
91
- const queue = target && DXN.tryParse(target) ? target : undefined;
92
- const query = queue ? Query.fromAst(newQuery).from({ feeds: [queue] }) : Query.fromAst(newQuery);
82
+ const queue = target;
83
+ const query = queue
84
+ ? Query.fromAst(newQuery).from([
85
+ Scope.feed(`dxn:queue:data:${EID.getSpaceId(queue)}:${EID.getEntityId(queue)}`),
86
+ ])
87
+ : Query.fromAst(newQuery);
93
88
  Obj.update(view, (view) => {
94
89
  view.query.ast = query.ast as Mutable<typeof query.ast>;
95
90
  });
96
91
 
97
92
  const typename = getTypenameFromQuery(query.ast);
98
- const [newSchema] = await space.db.schemaRegistry.query({ typename }).run();
93
+ const allTypes = space.db.graph.registry.list().filter(Type.isType);
94
+ const newSchema = allTypes.find((t) => Type.getTypename(t) === typename);
99
95
  if (!newSchema) {
100
96
  return;
101
97
  }
102
98
 
103
99
  const newView = ViewModel.make({
104
100
  query,
105
- jsonSchema: newSchema.jsonSchema,
101
+ jsonSchema: JsonSchema.toJsonSchema(newSchema),
106
102
  });
107
103
  Obj.update(view, (view) => {
108
104
  view.projection = Obj.getSnapshot(newView).projection as Mutable<typeof view.projection>;
109
105
  });
110
- setSchema(() => newSchema);
106
+ setType(() => newSchema);
111
107
  } else {
112
108
  Obj.update(view, (view) => {
113
109
  view.query.ast = newQuery as Mutable<typeof newQuery>;
114
110
  });
115
- schema.updateTypename(getTypenameFromQuery(newQuery));
116
111
  }
117
112
  },
118
- [view, schema],
113
+ [view, type],
119
114
  );
120
115
 
121
116
  const handleDelete = useCallback(
@@ -125,22 +120,22 @@ const DefaultStory = (props: DefaultStoryProps) => {
125
120
 
126
121
  // Expose objects on window for test access.
127
122
  useEffect(() => {
128
- if (typeof window !== 'undefined' && schema && view && projectionRef.current) {
123
+ if (typeof window !== 'undefined' && type && view && projectionRef.current) {
129
124
  (window as any)[VIEW_EDITOR_DEBUG_SYMBOL] = {
130
- schema,
125
+ type,
131
126
  view,
132
127
  projection: projectionRef.current,
133
128
  } satisfies ViewEditorDebugObjects;
134
129
  }
135
- }, [schema, view]);
130
+ }, [type, view]);
136
131
 
137
132
  // NOTE(ZaymonFC): This looks awkward but it resolves an infinite parsing issue with sb.
138
133
  const json = useMemo(
139
- () => JSON.parse(JSON.stringify({ schema, view, projection: projectionRef.current })),
140
- [JSON.stringify(schema), JSON.stringify(view)],
134
+ () => JSON.parse(JSON.stringify({ schema: type, view, projection: projectionRef.current })),
135
+ [JSON.stringify(type), JSON.stringify(view)],
141
136
  );
142
137
 
143
- if (!schema || !view) {
138
+ if (!type || !view) {
144
139
  return <div />;
145
140
  }
146
141
 
@@ -148,9 +143,9 @@ const DefaultStory = (props: DefaultStoryProps) => {
148
143
  <TestLayout json={json}>
149
144
  <ViewEditor
150
145
  ref={projectionRef}
151
- schema={schema}
146
+ type={type}
152
147
  view={view}
153
- registry={space?.db.schemaRegistry}
148
+ registry={space?.db.graph.registry}
154
149
  mode={props.mode}
155
150
  readonly={props.readonly}
156
151
  types={types}
@@ -3,11 +3,11 @@
3
3
  //
4
4
 
5
5
  import { composeStories } from '@storybook/react';
6
- import { cleanup, fireEvent, screen, waitFor } from '@testing-library/react';
6
+ import { act, cleanup, fireEvent, screen, waitFor } from '@testing-library/react';
7
7
  import { afterEach, describe, expect, test } from 'vitest';
8
8
 
9
- import { View } from '@dxos/echo';
10
- import { EchoSchema, isInstanceOf } from '@dxos/echo/internal';
9
+ import { Type, View } from '@dxos/echo';
10
+ import { isInstanceOf } from '@dxos/echo/internal';
11
11
  import { ProjectionModel } from '@dxos/schema';
12
12
 
13
13
  import { VIEW_EDITOR_DEBUG_SYMBOL } from '../testing';
@@ -19,7 +19,7 @@ const { Default } = composeStories(stories);
19
19
  const getViewEditorDebugObjects = (): ViewEditorDebugObjects => {
20
20
  const debugObjects = (window as any)[VIEW_EDITOR_DEBUG_SYMBOL] as ViewEditorDebugObjects;
21
21
  expect(debugObjects).toBeDefined();
22
- expect(debugObjects.schema).toBeInstanceOf(EchoSchema);
22
+ expect(Type.isType(debugObjects.type)).toBe(true);
23
23
  expect(isInstanceOf(View.View, debugObjects.view)).toBeTruthy();
24
24
  expect(debugObjects.projection).toBeInstanceOf(ProjectionModel);
25
25
  return debugObjects;
@@ -35,7 +35,10 @@ const waitForViewEditor = async () => {
35
35
  };
36
36
 
37
37
  describe('ViewEditor', () => {
38
- afterEach(() => {
38
+ afterEach(async () => {
39
+ // Flush pending React scheduler work before teardown to prevent
40
+ // "window is not defined" errors from setImmediate callbacks firing after happy-dom cleanup.
41
+ await act(async () => {});
39
42
  cleanup();
40
43
  });
41
44
 
@@ -68,7 +71,7 @@ describe('ViewEditor', () => {
68
71
  const debugObjects = getViewEditorDebugObjects();
69
72
 
70
73
  // Check schema contains new_property.
71
- const schemaProperties = debugObjects.schema.jsonSchema.properties;
74
+ const schemaProperties = debugObjects.type.jsonSchema.properties;
72
75
  expect(schemaProperties).toBeDefined();
73
76
  expect(schemaProperties!.new_property).toBeDefined();
74
77
  expect(schemaProperties!.name).toBeUndefined();
@@ -116,7 +119,7 @@ describe('ViewEditor', () => {
116
119
  const debugObjects = getViewEditorDebugObjects();
117
120
 
118
121
  // Check schema contains added_property.
119
- const schemaProperties = debugObjects.schema.jsonSchema.properties;
122
+ const schemaProperties = debugObjects.type.jsonSchema.properties;
120
123
  expect(schemaProperties).toBeDefined();
121
124
  expect(schemaProperties!.added_property).toBeDefined();
122
125
 
@@ -160,7 +163,7 @@ describe('ViewEditor', () => {
160
163
  const debugObjects = getViewEditorDebugObjects();
161
164
 
162
165
  // Check schema no longer contains the name property.
163
- const schemaProperties = debugObjects.schema.jsonSchema.properties;
166
+ const schemaProperties = debugObjects.type.jsonSchema.properties;
164
167
  expect(schemaProperties).toBeDefined();
165
168
  expect(schemaProperties!.name).toBeUndefined();
166
169