@dxos/react-ui-editor 0.10.0 → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (113) hide show
  1. package/dist/lib/index.mjs +1477 -0
  2. package/dist/lib/index.mjs.map +1 -0
  3. package/dist/lib/translations.mjs +32 -0
  4. package/dist/lib/translations.mjs.map +1 -0
  5. package/dist/types/src/components/Editor/Editor.d.ts +22 -17
  6. package/dist/types/src/components/Editor/Editor.d.ts.map +1 -1
  7. package/dist/types/src/components/Editor/EditorContext.d.ts +20 -0
  8. package/dist/types/src/components/Editor/EditorContext.d.ts.map +1 -0
  9. package/dist/types/src/components/Editor/EditorView.d.ts +1 -2
  10. package/dist/types/src/components/Editor/EditorView.d.ts.map +1 -1
  11. package/dist/types/src/components/Editor/index.d.ts +2 -0
  12. package/dist/types/src/components/Editor/index.d.ts.map +1 -1
  13. package/dist/types/src/components/EditorMenuProvider/EditorMenuProvider.d.ts +7 -1
  14. package/dist/types/src/components/EditorMenuProvider/EditorMenuProvider.d.ts.map +1 -1
  15. package/dist/types/src/components/EditorMenuProvider/menu.d.ts +4 -2
  16. package/dist/types/src/components/EditorMenuProvider/menu.d.ts.map +1 -1
  17. package/dist/types/src/components/EditorMenuProvider/useEditorMenu.d.ts +7 -2
  18. package/dist/types/src/components/EditorMenuProvider/useEditorMenu.d.ts.map +1 -1
  19. package/dist/types/src/components/EditorPreviewProvider/EditorPreviewContext.d.ts +18 -0
  20. package/dist/types/src/components/EditorPreviewProvider/EditorPreviewContext.d.ts.map +1 -0
  21. package/dist/types/src/components/EditorPreviewProvider/EditorPreviewProvider.d.ts +0 -6
  22. package/dist/types/src/components/EditorPreviewProvider/EditorPreviewProvider.d.ts.map +1 -1
  23. package/dist/types/src/components/EditorPreviewProvider/index.d.ts +1 -0
  24. package/dist/types/src/components/EditorPreviewProvider/index.d.ts.map +1 -1
  25. package/dist/types/src/components/EditorToolbar/EditorToolbar.d.ts +4 -1
  26. package/dist/types/src/components/EditorToolbar/EditorToolbar.d.ts.map +1 -1
  27. package/dist/types/src/components/EditorToolbar/index.d.ts +1 -0
  28. package/dist/types/src/components/EditorToolbar/index.d.ts.map +1 -1
  29. package/dist/types/src/components/EditorToolbar/lists.d.ts.map +1 -1
  30. package/dist/types/src/components/EditorToolbar/view-mode.d.ts +27 -2
  31. package/dist/types/src/components/EditorToolbar/view-mode.d.ts.map +1 -1
  32. package/dist/types/src/components/index.d.ts +1 -0
  33. package/dist/types/src/components/index.d.ts.map +1 -1
  34. package/dist/types/src/index.d.ts +0 -1
  35. package/dist/types/src/index.d.ts.map +1 -1
  36. package/dist/types/src/stories/Assistant.stories.d.ts.map +1 -0
  37. package/dist/types/src/stories/Automerge.stories.d.ts +16 -39
  38. package/dist/types/src/stories/Automerge.stories.d.ts.map +1 -1
  39. package/dist/types/src/stories/Blocks.stories.d.ts +10 -0
  40. package/dist/types/src/stories/Blocks.stories.d.ts.map +1 -0
  41. package/dist/types/src/stories/Comments.stories.d.ts +53 -14
  42. package/dist/types/src/stories/Comments.stories.d.ts.map +1 -1
  43. package/dist/types/src/stories/Diff.stories.d.ts +32 -0
  44. package/dist/types/src/stories/Diff.stories.d.ts.map +1 -0
  45. package/dist/types/src/stories/Experimental.stories.d.ts +1 -0
  46. package/dist/types/src/stories/Experimental.stories.d.ts.map +1 -1
  47. package/dist/types/src/stories/Folding.stories.d.ts +18 -0
  48. package/dist/types/src/stories/Folding.stories.d.ts.map +1 -0
  49. package/dist/types/src/stories/Outliner.stories.d.ts +4 -4
  50. package/dist/types/src/stories/Outliner.stories.d.ts.map +1 -1
  51. package/dist/types/src/stories/Popover.stories.d.ts +3 -3
  52. package/dist/types/src/stories/Popover.stories.d.ts.map +1 -1
  53. package/dist/types/src/stories/Suggest.stories.d.ts +27 -0
  54. package/dist/types/src/stories/Suggest.stories.d.ts.map +1 -0
  55. package/dist/types/src/stories/TrackChanges.stories.d.ts +44 -0
  56. package/dist/types/src/stories/TrackChanges.stories.d.ts.map +1 -0
  57. package/dist/types/src/stories/Widgets.stories.d.ts +12 -0
  58. package/dist/types/src/stories/Widgets.stories.d.ts.map +1 -1
  59. package/dist/types/src/stories/components/EditorStory.d.ts.map +1 -1
  60. package/dist/types/src/stories/components/util.d.ts +16 -0
  61. package/dist/types/src/stories/components/util.d.ts.map +1 -1
  62. package/dist/types/tsconfig.tsbuildinfo +1 -1
  63. package/package.json +40 -83
  64. package/src/components/Editor/Editor.tsx +142 -35
  65. package/src/components/Editor/EditorContext.ts +34 -0
  66. package/src/components/Editor/EditorView.tsx +9 -6
  67. package/src/components/Editor/index.ts +3 -0
  68. package/src/components/EditorMenuProvider/EditorMenuProvider.tsx +98 -8
  69. package/src/components/EditorMenuProvider/menu.ts +4 -2
  70. package/src/components/EditorMenuProvider/useEditorMenu.ts +114 -29
  71. package/src/components/EditorPreviewProvider/EditorPreviewContext.ts +21 -0
  72. package/src/components/EditorPreviewProvider/EditorPreviewProvider.tsx +1 -10
  73. package/src/components/EditorPreviewProvider/index.ts +2 -0
  74. package/src/components/EditorToolbar/EditorToolbar.tsx +6 -3
  75. package/src/components/EditorToolbar/index.ts +1 -0
  76. package/src/components/EditorToolbar/lists.ts +2 -6
  77. package/src/components/EditorToolbar/view-mode.ts +41 -12
  78. package/src/components/index.ts +1 -0
  79. package/src/index.ts +0 -1
  80. package/src/{extensions → stories}/Assistant.stories.tsx +7 -2
  81. package/src/stories/Automerge.stories.tsx +99 -78
  82. package/src/stories/Blocks.stories.tsx +83 -0
  83. package/src/stories/Comments.stories.tsx +273 -74
  84. package/src/stories/Diff.stories.tsx +48 -0
  85. package/src/stories/EditorToolbar.stories.tsx +2 -2
  86. package/src/stories/Experimental.stories.tsx +13 -2
  87. package/src/stories/Folding.stories.tsx +120 -0
  88. package/src/stories/Markdown.stories.tsx +1 -1
  89. package/src/stories/Outliner.stories.tsx +18 -7
  90. package/src/stories/Popover.stories.tsx +27 -5
  91. package/src/stories/Suggest.stories.tsx +199 -0
  92. package/src/stories/TrackChanges.stories.tsx +226 -0
  93. package/src/stories/Widgets.stories.tsx +162 -10
  94. package/src/stories/components/EditorStory.tsx +15 -6
  95. package/src/stories/components/util.tsx +40 -2
  96. package/dist/lib/browser/index.mjs +0 -1676
  97. package/dist/lib/browser/index.mjs.map +0 -7
  98. package/dist/lib/browser/meta.json +0 -1
  99. package/dist/lib/browser/translations.mjs +0 -39
  100. package/dist/lib/browser/translations.mjs.map +0 -7
  101. package/dist/lib/node-esm/index.mjs +0 -1678
  102. package/dist/lib/node-esm/index.mjs.map +0 -7
  103. package/dist/lib/node-esm/meta.json +0 -1
  104. package/dist/lib/node-esm/translations.mjs +0 -41
  105. package/dist/lib/node-esm/translations.mjs.map +0 -7
  106. package/dist/types/src/extensions/Assistant.stories.d.ts.map +0 -1
  107. package/dist/types/src/extensions/assistant-extension.d.ts +0 -24
  108. package/dist/types/src/extensions/assistant-extension.d.ts.map +0 -1
  109. package/dist/types/src/extensions/index.d.ts +0 -2
  110. package/dist/types/src/extensions/index.d.ts.map +0 -1
  111. package/src/extensions/assistant-extension.tsx +0 -223
  112. package/src/extensions/index.ts +0 -5
  113. /package/dist/types/src/{extensions → stories}/Assistant.stories.d.ts +0 -0
@@ -2,41 +2,44 @@
2
2
  // Copyright 2023 DXOS.org
3
3
  //
4
4
 
5
- import { Repo, initSubduction } from '@automerge/automerge-repo';
6
- import { BroadcastChannelNetworkAdapter } from '@automerge/automerge-repo-network-broadcastchannel';
7
5
  import { type Meta, type StoryObj } from '@storybook/react-vite';
8
- import React, { useCallback, useEffect, useState } from 'react';
6
+ import React, { useEffect, useMemo, useState } from 'react';
7
+ import { expect, waitFor } from 'storybook/test';
9
8
 
10
9
  import { Obj, Query, Ref } from '@dxos/echo';
10
+ import { createObject } from '@dxos/echo-client';
11
11
  import { Doc } from '@dxos/echo-doc';
12
+ import { useQuery, useResolveRef } from '@dxos/echo-react';
12
13
  import { TestSchema } from '@dxos/echo/testing';
13
- import { log } from '@dxos/log';
14
14
  import { type Messenger } from '@dxos/protocols';
15
- import { useQuery, useSpace } from '@dxos/react-client/echo';
16
- import { type Identity, useIdentity } from '@dxos/react-client/halo';
15
+ import { useSpace } from '@dxos/react-client/echo';
16
+ import { useIdentity } from '@dxos/react-client/halo';
17
17
  import { useClientStory, withMultiClientProvider } from '@dxos/react-client/testing';
18
- import { Button, useThemeContext } from '@dxos/react-ui';
18
+ import { type ThemedClassName, useThemeContext } from '@dxos/react-ui';
19
19
  import { Loading, withLayout, withTheme } from '@dxos/react-ui/testing';
20
- import { createBasicExtensions, createDataExtensions, createThemeExtensions } from '@dxos/ui-editor';
20
+ import { Text } from '@dxos/schema';
21
+ import {
22
+ type DataExtensionsIdentity,
23
+ createBasicExtensions,
24
+ createDataExtensions,
25
+ createThemeExtensions,
26
+ } from '@dxos/ui-editor';
27
+ import { mx } from '@dxos/ui-theme';
21
28
 
22
29
  import { translations } from '#translations';
23
30
 
24
31
  import { useTextEditor } from '../hooks';
25
32
 
26
- const initialContent = 'Hello world!';
33
+ const initialContent = ['# Hello world!', 'Hello Automerge', ''].join('\n\n');
27
34
 
28
- type TestObject = {
29
- text: string;
30
- };
31
-
32
- type EditorProps = {
35
+ type EditorProps = ThemedClassName<{
33
36
  source: Doc.Accessor;
34
37
  messenger?: Messenger;
35
- identity?: Identity;
38
+ identity?: DataExtensionsIdentity;
36
39
  autoFocus?: boolean;
37
- };
40
+ }>;
38
41
 
39
- const Editor = ({ source, messenger, identity, autoFocus }: EditorProps) => {
42
+ const Editor = ({ classNames, source, messenger, identity, autoFocus }: EditorProps) => {
40
43
  const { themeMode } = useThemeContext();
41
44
  const { parentRef } = useTextEditor(
42
45
  () => ({
@@ -51,42 +54,29 @@ const Editor = ({ source, messenger, identity, autoFocus }: EditorProps) => {
51
54
  [source, themeMode],
52
55
  );
53
56
 
54
- return <div ref={parentRef} className='flex w-full' />;
57
+ return <div ref={parentRef} className={mx('w-full', classNames)} />;
55
58
  };
56
59
 
60
+ /**
61
+ * Two editors bound to the same in-process Automerge document (no client or networking). Editing
62
+ * either column mutates the shared handle; the automerge data extension on the other column observes
63
+ * the handle's `change` event and reconciles, so the columns stay in sync. For true cross-peer
64
+ * replication over a transport, see the `WithEcho` story.
65
+ */
57
66
  const DefaultStory = () => {
58
- const [object1, setObject1] = useState<Doc.Accessor<TestObject>>();
59
- const [object2, setObject2] = useState<Doc.Accessor<TestObject>>();
60
-
61
- useEffect(() => {
62
- queueMicrotask(async () => {
63
- // Subduction-fork `Repo` constructs a `MemorySigner` internally; WASM must be
64
- // initialized first or the constructor throws `'set_subduction_logger' of undefined`.
65
- await initSubduction();
66
- const repo1 = new Repo({ network: [new BroadcastChannelNetworkAdapter()] });
67
- const repo2 = new Repo({ network: [new BroadcastChannelNetworkAdapter()] });
68
-
69
- const object1 = repo1.create<TestObject>();
70
- object1.change((doc: TestObject) => {
71
- doc.text = initialContent;
72
- });
73
-
74
- const object2 = await repo2.find<TestObject>(object1.url);
75
- await object2.whenReady();
76
-
77
- setObject1({ handle: object1, path: ['text'] });
78
- setObject2({ handle: object2, path: ['text'] });
79
- });
80
- }, []);
81
-
82
- if (!object1 || !object2) {
83
- return <Loading data={{ object1: !!object1, object2: !!object2 }} />;
84
- }
67
+ const source = useMemo<Doc.Accessor>(
68
+ () => Doc.createAccessor(createObject(Text.make({ content: initialContent })), ['content']),
69
+ [],
70
+ );
85
71
 
86
72
  return (
87
- <div className='h-full w-full grid grid-cols-2 gap-4'>
88
- <Editor source={object1} autoFocus />
89
- <Editor source={object2} />
73
+ <div className='dx-container grid grid-cols-2 gap-3 p-3'>
74
+ <div className='dx-container p-2 dx-base-surface rounded-md border border-subdued-separator'>
75
+ <Editor source={source} autoFocus />
76
+ </div>
77
+ <div className='dx-container p-2 dx-base-surface rounded-md border border-subdued-separator'>
78
+ <Editor source={source} />
79
+ </div>
90
80
  </div>
91
81
  );
92
82
  };
@@ -96,28 +86,16 @@ const EchoStory = () => {
96
86
  const identity = useIdentity();
97
87
  const space = useSpace(spaceId);
98
88
  const objects = useQuery(space?.db, Query.type(TestSchema.Expando, { type: 'test' }));
89
+ // NOTE: `objects[0]?.content.target` is not reactive to the ref loading; `useResolveRef`
90
+ // subscribes to the ref's load event so the editor mounts once the target arrives.
91
+ const content = useResolveRef(objects[0]?.content);
99
92
 
100
93
  const [source, setSource] = useState<Doc.Accessor>();
101
- const init = useCallback(() => {
102
- const content = objects[0]?.content.target;
103
- if (!content) {
104
- if (objects.length) {
105
- // TODO(burdon): Initially ref isn't ready.
106
- log.warn('no content', { index, objects: JSON.stringify(objects) });
107
- }
108
- return;
109
- }
110
-
111
- setSource(Doc.createAccessor(content, ['content']));
112
- }, [objects]);
113
-
114
94
  useEffect(() => {
115
- if (source) {
116
- return;
95
+ if (!source && content) {
96
+ setSource(Doc.createAccessor(content, ['content']));
117
97
  }
118
-
119
- init();
120
- }, [objects, source]);
98
+ }, [content, source]);
121
99
 
122
100
  return (
123
101
  <div className='h-full w-full flex flex-col overflow-hidden'>
@@ -126,43 +104,52 @@ const EchoStory = () => {
126
104
  </pre>
127
105
  {identity && source ? (
128
106
  <div className='p-2 flex grow overflow-hidden'>
129
- <Editor identity={identity} messenger={space} source={source} />
107
+ <Editor
108
+ identity={{
109
+ identityKey: identity.identityKey.toHex(),
110
+ displayName: identity.profile?.displayName,
111
+ data: identity.profile?.data,
112
+ }}
113
+ messenger={space}
114
+ source={source}
115
+ />
130
116
  </div>
131
117
  ) : (
132
- <div className='p-2'>
133
- <Button onClick={init}>Init</Button>
134
- </div>
118
+ <Loading data={{ identity: !!identity, content: !!content }} />
135
119
  )}
136
120
  </div>
137
121
  );
138
122
  };
139
123
 
140
- const meta = {
124
+ const meta: Meta<typeof DefaultStory> = {
141
125
  title: 'ui/react-ui-editor/Automerge',
142
- component: Editor as any,
126
+ render: DefaultStory,
143
127
  decorators: [withTheme(), withLayout({ layout: 'fullscreen' })],
144
128
  parameters: {
145
129
  layout: 'fullscreen',
146
130
  translations,
147
131
  },
148
- } satisfies Meta<typeof DefaultStory>;
132
+ };
149
133
 
150
134
  export default meta;
151
135
 
152
136
  type Story = StoryObj<typeof meta>;
153
137
 
154
- // TODO(burdon): ERROR: factories.ts:126 Error: Non-base58 character
155
- export const Default: Story = {
156
- render: DefaultStory,
157
- };
138
+ export const Default: Story = {};
158
139
 
159
- // TODO(burdon): Failing (doesn't sync)
140
+ /**
141
+ * Two in-memory ECHO peers (no real networking, see `withMultiClientProvider`) sharing a
142
+ * document, exercising the `Space.postMessage`/`listen` gossip channel and the CodeMirror
143
+ * `awareness` extension end to end. This covers the same wiring as the (currently skipped,
144
+ * flaky) composer-app e2e collaboration cursor test, deterministically and without a browser.
145
+ */
160
146
  export const WithEcho: Story = {
161
147
  decorators: [
162
148
  withMultiClientProvider({
163
149
  numClients: 2,
164
150
  createIdentity: true,
165
151
  createSpace: true,
152
+ types: [TestSchema.Expando],
166
153
  onCreateSpace: async ({ space }) => {
167
154
  space.db.add(
168
155
  Obj.make(TestSchema.Expando, {
@@ -174,4 +161,38 @@ export const WithEcho: Story = {
174
161
  }),
175
162
  ],
176
163
  render: EchoStory,
164
+ play: async ({ canvasElement }) => {
165
+ // ECHO identity/space creation and invitation are async; wait for both peers to mount an editor.
166
+ const editors = await waitFor(
167
+ () => {
168
+ const found = Array.from(canvasElement.querySelectorAll<HTMLElement>('.cm-editor'));
169
+ void expect(found).toHaveLength(2);
170
+ return found;
171
+ },
172
+ { timeout: 15_000 },
173
+ );
174
+
175
+ const [contentA, contentB] = editors.map((editor) => editor.querySelector<HTMLElement>('.cm-content')!);
176
+
177
+ // Initial content replicated from the host's space to the guest. CodeMirror concatenates its
178
+ // per-line elements without newline separators, so the multiline `initialContent` (with literal
179
+ // `\n`s) never matches as substring text content; assert on each non-empty line instead.
180
+ for (const line of initialContent.split('\n').filter(Boolean)) {
181
+ await expect(contentA).toHaveTextContent(line);
182
+ await expect(contentB).toHaveTextContent(line);
183
+ }
184
+
185
+ // Focusing peer A broadcasts its cursor position over the gossip channel; peer B renders it
186
+ // as a `.cm-collab-selectionInfo` decoration.
187
+ contentA.focus();
188
+ await waitFor(() => expect(editors[1].querySelector('.cm-collab-selectionInfo')).toBeInTheDocument(), {
189
+ timeout: 10_000,
190
+ });
191
+
192
+ // And symmetrically in the other direction.
193
+ contentB.focus();
194
+ await waitFor(() => expect(editors[0].querySelector('.cm-collab-selectionInfo')).toBeInTheDocument(), {
195
+ timeout: 10_000,
196
+ });
197
+ },
177
198
  };
@@ -0,0 +1,83 @@
1
+ //
2
+ // Copyright 2026 DXOS.org
3
+ //
4
+
5
+ import type { Meta, StoryObj } from '@storybook/react-vite';
6
+ import React, { useMemo } from 'react';
7
+
8
+ import { random } from '@dxos/random';
9
+ import { useThemeContext } from '@dxos/react-ui';
10
+ import { withLayout, withTheme } from '@dxos/react-ui/testing';
11
+ import {
12
+ blocks,
13
+ createBasicExtensions,
14
+ createMarkdownExtensions,
15
+ createThemeExtensions,
16
+ decorateMarkdown,
17
+ } from '@dxos/ui-editor';
18
+
19
+ import { translations } from '#translations';
20
+
21
+ import { Editor, type EditorViewProps } from '../components';
22
+
23
+ random.seed(123);
24
+
25
+ // `blocks` composes the gutter drag grip (`blockDrag`), the whole-block selection + clipboard
26
+ // (`blockSelection`), and the border/background behind selected blocks (`blockSelectionHighlight`).
27
+ type StoryArgs = Pick<EditorViewProps, 'value'>;
28
+
29
+ const DefaultStory = (props: StoryArgs) => {
30
+ const { themeMode } = useThemeContext();
31
+ const extensions = useMemo(
32
+ () => [
33
+ createBasicExtensions({ placeholder: 'Type here...' }),
34
+ createThemeExtensions({ themeMode }),
35
+ createMarkdownExtensions(),
36
+ decorateMarkdown(),
37
+ blocks(),
38
+ ],
39
+ [themeMode],
40
+ );
41
+
42
+ return (
43
+ <Editor.Root>
44
+ <Editor.View {...props} classNames='dx-container' extensions={extensions} />
45
+ </Editor.Root>
46
+ );
47
+ };
48
+
49
+ const meta: Meta<typeof DefaultStory> = {
50
+ title: 'ui/react-ui-editor/Blocks',
51
+ render: DefaultStory,
52
+ decorators: [withTheme(), withLayout({ layout: 'column' })],
53
+ parameters: {
54
+ layout: 'fullscreen',
55
+ translations,
56
+ },
57
+ };
58
+
59
+ export default meta;
60
+
61
+ type Story = StoryObj<typeof meta>;
62
+
63
+ const content = Array.from({ length: 30 }, (_, index) => {
64
+ const roll = random.number.int(10);
65
+ if (index === 0 || roll < 2) {
66
+ return `# Header ${index + 1}`;
67
+ }
68
+
69
+ if (roll > 8) {
70
+ return Array.from(
71
+ {
72
+ length: random.number.int({ min: 2, max: 8 }),
73
+ },
74
+ (_, itemIndex) => `- Item ${itemIndex + 1}`,
75
+ ).join('\n');
76
+ }
77
+
78
+ return random.lorem.paragraph();
79
+ }).join('\n\n');
80
+
81
+ export const Default: Story = {
82
+ args: { value: content },
83
+ };