@dxos/react-ui-editor 0.4.5 → 0.4.6-main.20d1ec4

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 (48) hide show
  1. package/dist/lib/browser/index.mjs +156 -137
  2. package/dist/lib/browser/index.mjs.map +3 -3
  3. package/dist/lib/browser/meta.json +1 -1
  4. package/dist/types/src/components/TextEditor/TextEditor.d.ts +2 -0
  5. package/dist/types/src/components/TextEditor/TextEditor.d.ts.map +1 -1
  6. package/dist/types/src/components/TextEditor/automerge.stories.d.ts +3 -5
  7. package/dist/types/src/components/TextEditor/automerge.stories.d.ts.map +1 -1
  8. package/dist/types/src/components/TextEditor/hooks.stories.d.ts.map +1 -1
  9. package/dist/types/src/components/Toolbar/Toolbar.d.ts.map +1 -1
  10. package/dist/types/src/components/Toolbar/Toolbar.stories.d.ts.map +1 -1
  11. package/dist/types/src/extensions/automerge/automerge.d.ts +2 -7
  12. package/dist/types/src/extensions/automerge/automerge.d.ts.map +1 -1
  13. package/dist/types/src/extensions/automerge/cursor.d.ts +2 -3
  14. package/dist/types/src/extensions/automerge/cursor.d.ts.map +1 -1
  15. package/dist/types/src/extensions/automerge/defs.d.ts +9 -2
  16. package/dist/types/src/extensions/automerge/defs.d.ts.map +1 -1
  17. package/dist/types/src/extensions/markdown/decorate.d.ts.map +1 -1
  18. package/dist/types/src/hooks/defs.d.ts +1 -0
  19. package/dist/types/src/hooks/defs.d.ts.map +1 -1
  20. package/dist/types/src/hooks/useEditorView.d.ts +1 -0
  21. package/dist/types/src/hooks/useEditorView.d.ts.map +1 -1
  22. package/dist/types/src/hooks/useTextEditor.d.ts +3 -3
  23. package/dist/types/src/hooks/useTextEditor.d.ts.map +1 -1
  24. package/dist/types/src/hooks/useTextModel.d.ts +12 -4
  25. package/dist/types/src/hooks/useTextModel.d.ts.map +1 -1
  26. package/dist/types/src/index.d.ts +1 -1
  27. package/dist/types/src/index.d.ts.map +1 -1
  28. package/dist/types/src/styles/layout.d.ts +2 -3
  29. package/dist/types/src/styles/layout.d.ts.map +1 -1
  30. package/dist/types/src/themes/default.d.ts.map +1 -1
  31. package/package.json +26 -25
  32. package/src/components/TextEditor/MarkdownEditor.stories.tsx +1 -1
  33. package/src/components/TextEditor/TextEditor.tsx +36 -18
  34. package/src/components/TextEditor/automerge.stories.tsx +15 -16
  35. package/src/components/TextEditor/hooks.stories.tsx +11 -8
  36. package/src/components/Toolbar/Toolbar.stories.tsx +9 -6
  37. package/src/components/Toolbar/Toolbar.tsx +22 -2
  38. package/src/extensions/automerge/automerge.ts +10 -15
  39. package/src/extensions/automerge/cursor.ts +2 -3
  40. package/src/extensions/automerge/defs.ts +15 -2
  41. package/src/extensions/markdown/decorate.ts +13 -49
  42. package/src/hooks/defs.ts +2 -0
  43. package/src/hooks/useEditorView.ts +2 -0
  44. package/src/hooks/useTextEditor.ts +8 -12
  45. package/src/hooks/useTextModel.ts +34 -5
  46. package/src/index.ts +1 -7
  47. package/src/styles/layout.ts +2 -5
  48. package/src/themes/default.ts +14 -3
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dxos/react-ui-editor",
3
- "version": "0.4.5",
3
+ "version": "0.4.6-main.20d1ec4",
4
4
  "description": "Document editing experience within a DXOS shell.",
5
5
  "homepage": "https://dxos.org",
6
6
  "bugs": "https://github.com/dxos/dxos/issues",
@@ -15,6 +15,7 @@
15
15
  "dependencies": {
16
16
  "@codemirror/autocomplete": "^6.12.0",
17
17
  "@codemirror/commands": "^6.3.3",
18
+ "@codemirror/lang-javascript": "^6.2.2",
18
19
  "@codemirror/lang-markdown": "^6.2.4",
19
20
  "@codemirror/language": "^6.10.1",
20
21
  "@codemirror/language-data": "^6.3.1",
@@ -29,27 +30,27 @@
29
30
  "@lezer/markdown": "^1.2.0",
30
31
  "@radix-ui/react-context": "^1.0.0",
31
32
  "@replit/codemirror-vim": "^6.0.14",
32
- "codemirror": "6.0.1",
33
+ "codemirror": "^6.0.1",
33
34
  "lib0": "^0.2.65",
34
35
  "lodash.defaultsdeep": "^4.6.1",
35
36
  "lodash.get": "^4.4.2",
36
37
  "lodash.pick": "^4.4.0",
37
38
  "lodash.sortby": "^4.7.0",
38
39
  "style-mod": "^4.1.0",
39
- "@dxos/async": "0.4.5",
40
- "@dxos/debug": "0.4.5",
41
- "@dxos/automerge": "0.4.5",
42
- "@dxos/context": "0.4.5",
43
- "@dxos/echo-schema": "0.4.5",
44
- "@dxos/display-name": "0.4.5",
45
- "@dxos/protocols": "0.4.5",
46
- "@dxos/invariant": "0.4.5",
47
- "@dxos/log": "0.4.5",
48
- "@dxos/react-ui": "0.4.5",
49
- "@dxos/react-async": "0.4.5",
50
- "@dxos/react-ui-theme": "0.4.5",
51
- "@dxos/text-model": "0.4.5",
52
- "@dxos/util": "0.4.5"
40
+ "@dxos/async": "0.4.6-main.20d1ec4",
41
+ "@dxos/automerge": "0.4.6-main.20d1ec4",
42
+ "@dxos/context": "0.4.6-main.20d1ec4",
43
+ "@dxos/debug": "0.4.6-main.20d1ec4",
44
+ "@dxos/display-name": "0.4.6-main.20d1ec4",
45
+ "@dxos/invariant": "0.4.6-main.20d1ec4",
46
+ "@dxos/echo-schema": "0.4.6-main.20d1ec4",
47
+ "@dxos/log": "0.4.6-main.20d1ec4",
48
+ "@dxos/protocols": "0.4.6-main.20d1ec4",
49
+ "@dxos/react-async": "0.4.6-main.20d1ec4",
50
+ "@dxos/react-ui-theme": "0.4.6-main.20d1ec4",
51
+ "@dxos/react-ui": "0.4.6-main.20d1ec4",
52
+ "@dxos/util": "0.4.6-main.20d1ec4",
53
+ "@dxos/text-model": "0.4.6-main.20d1ec4"
53
54
  },
54
55
  "devDependencies": {
55
56
  "@automerge/automerge-repo-network-broadcastchannel": "^1.1.1",
@@ -77,14 +78,14 @@
77
78
  "vite-plugin-top-level-await": "^1.4.1",
78
79
  "vite-plugin-wasm": "^3.3.0",
79
80
  "vitest": "^1.2.2",
80
- "@dxos/config": "0.4.5",
81
- "@dxos/echo-signals": "0.4.5",
82
- "@dxos/echo-typegen": "0.4.5",
83
- "@dxos/keyboard": "0.4.5",
84
- "@dxos/react-client": "0.4.5",
85
- "@dxos/random": "0.4.5",
86
- "@dxos/storybook-utils": "0.4.5",
87
- "@dxos/react-ui": "0.4.5"
81
+ "@dxos/echo-typegen": "0.4.6-main.20d1ec4",
82
+ "@dxos/config": "0.4.6-main.20d1ec4",
83
+ "@dxos/echo-signals": "0.4.6-main.20d1ec4",
84
+ "@dxos/keyboard": "0.4.6-main.20d1ec4",
85
+ "@dxos/random": "0.4.6-main.20d1ec4",
86
+ "@dxos/react-ui": "0.4.6-main.20d1ec4",
87
+ "@dxos/storybook-utils": "0.4.6-main.20d1ec4",
88
+ "@dxos/react-client": "0.4.6-main.20d1ec4"
88
89
  },
89
90
  "peerDependencies": {
90
91
  "@phosphor-icons/react": "^2.0.5",
@@ -92,7 +93,7 @@
92
93
  "@vitest/ui": "^1.2.2",
93
94
  "react": "^18.2.0",
94
95
  "react-dom": "^18.2.0",
95
- "@dxos/react-client": "0.4.5"
96
+ "@dxos/react-client": "0.4.6-main.20d1ec4"
96
97
  },
97
98
  "publishConfig": {
98
99
  "access": "public"
@@ -288,7 +288,7 @@ export const Empty = {
288
288
  };
289
289
 
290
290
  export const Scrolling = {
291
- render: () => <Story text={str('# Large Document', '', large)} extensions={[]} />,
291
+ render: () => <Story text={str('# Large Document', '', large)} />,
292
292
  };
293
293
 
294
294
  export const ScrollingWithImages = {
@@ -3,13 +3,13 @@
3
3
  //
4
4
 
5
5
  import { EditorState, type Extension, type StateEffect } from '@codemirror/state';
6
- import { EditorView } from '@codemirror/view';
6
+ import { EditorView, scrollPastEnd } from '@codemirror/view';
7
7
  import { useFocusableGroup } from '@fluentui/react-tabster';
8
8
  import defaultsDeep from 'lodash.defaultsdeep';
9
9
  import React, {
10
10
  type ComponentProps,
11
- forwardRef,
12
11
  type KeyboardEventHandler,
12
+ forwardRef,
13
13
  useCallback,
14
14
  useEffect,
15
15
  useImperativeHandle,
@@ -52,6 +52,8 @@ export type TextEditorProps = {
52
52
  model: EditorModel; // TODO(burdon): Optional (e.g., just provide content if readonly).
53
53
  readonly?: boolean; // TODO(burdon): Move into model.
54
54
  autoFocus?: boolean;
55
+ scrollPastEnd?: boolean;
56
+ moveToEndOfLine?: boolean;
55
57
  lineWrapping?: boolean;
56
58
  scrollTo?: StateEffect<any>; // TODO(burdon): Restore scroll position: scrollTo EditorView.scrollSnapshot().
57
59
  selection?: { anchor: number; head?: number };
@@ -68,7 +70,19 @@ export type TextEditorProps = {
68
70
  // TODO(burdon): Replace with useTextEditor.
69
71
  export const BaseTextEditor = forwardRef<EditorView | null, TextEditorProps>(
70
72
  (
71
- { model, readonly, autoFocus, scrollTo, selection, theme, slots = defaultSlots, extensions = [], debug },
73
+ {
74
+ model,
75
+ readonly,
76
+ autoFocus,
77
+ scrollTo = EditorView.scrollIntoView(0),
78
+ moveToEndOfLine,
79
+ selection,
80
+ theme,
81
+ slots = defaultSlots,
82
+ extensions = [],
83
+ scrollPastEnd: _scrollPastEnd,
84
+ debug,
85
+ },
72
86
  forwardedRef,
73
87
  ) => {
74
88
  const tabsterDOMAttribute = useFocusableGroup({ tabBehavior: 'limited' });
@@ -83,12 +97,7 @@ export const BaseTextEditor = forwardRef<EditorView | null, TextEditorProps>(
83
97
 
84
98
  // Set focus.
85
99
  useEffect(() => {
86
- if (autoFocus && !view?.hasFocus) {
87
- if (view?.state.selection.main?.from === 0) {
88
- // Start at end of line.
89
- const { to } = view.state.doc.lineAt(0);
90
- view?.dispatch({ selection: { anchor: to } });
91
- }
100
+ if (autoFocus) {
92
101
  view?.focus();
93
102
  }
94
103
  }, [view, autoFocus]);
@@ -114,12 +123,15 @@ export const BaseTextEditor = forwardRef<EditorView | null, TextEditorProps>(
114
123
  }),
115
124
 
116
125
  // Theme.
117
- // TODO(burdon): Make configurable.
126
+ // TODO(burdon): Make base theme configurable.
118
127
  EditorView.baseTheme(defaultTheme),
119
- EditorView.theme(theme ?? {}),
128
+ theme && EditorView.theme(theme),
120
129
  EditorView.darkTheme.of(themeMode === 'dark'),
121
- EditorView.editorAttributes.of({ class: slots.editor?.className ?? '' }),
122
- EditorView.contentAttributes.of({ class: slots.content?.className ?? '' }),
130
+ slots.editor?.className && EditorView.editorAttributes.of({ class: slots.editor.className }),
131
+ slots.content?.className && EditorView.contentAttributes.of({ class: slots.content.className }),
132
+
133
+ // NOTE: Assumes default line height.
134
+ _scrollPastEnd && scrollPastEnd(),
123
135
 
124
136
  // Focus.
125
137
  EditorView.updateListener.of((update) => {
@@ -147,7 +159,7 @@ export const BaseTextEditor = forwardRef<EditorView | null, TextEditorProps>(
147
159
  // EditorView
148
160
  // https://codemirror.net/docs/ref/#view.EditorViewConfig
149
161
  //
150
- const newView = new EditorView({
162
+ const view = new EditorView({
151
163
  state,
152
164
  parent: rootRef.current,
153
165
  scrollTo,
@@ -161,8 +173,12 @@ export const BaseTextEditor = forwardRef<EditorView | null, TextEditorProps>(
161
173
  },
162
174
  });
163
175
 
164
- view?.destroy();
165
- setView(newView);
176
+ setView(view);
177
+
178
+ if (moveToEndOfLine) {
179
+ const { to } = view.state.doc.lineAt(0);
180
+ view?.dispatch({ selection: { anchor: to } });
181
+ }
166
182
 
167
183
  // Remove tabster attribute (rely on custom keymap).
168
184
  if (state.facet(editorMode).noTabster) {
@@ -170,7 +186,7 @@ export const BaseTextEditor = forwardRef<EditorView | null, TextEditorProps>(
170
186
  }
171
187
 
172
188
  return () => {
173
- newView?.destroy();
189
+ view?.destroy();
174
190
  setView(null);
175
191
  };
176
192
  // TODO(wittjosiah): Does `rootRef` ever change? Only `.current` changes?
@@ -215,6 +231,7 @@ export const TextEditor = forwardRef<EditorView | null, TextEditorProps>(
215
231
  () => [createBasicBundle({ themeMode, placeholder, lineWrapping }), ...(_extensions ?? [])],
216
232
  [themeMode, placeholder, lineWrapping, _extensions],
217
233
  );
234
+
218
235
  return (
219
236
  <BaseTextEditor
220
237
  ref={forwardedRef}
@@ -236,6 +253,7 @@ export const MarkdownEditor = forwardRef<EditorView | null, TextEditorProps>(
236
253
  () => [createMarkdownExtensions({ themeMode, placeholder }), ...(_extensions ?? [])],
237
254
  [themeMode, placeholder, _extensions],
238
255
  );
256
+
239
257
  return (
240
258
  <BaseTextEditor
241
259
  ref={forwardedRef}
@@ -254,7 +272,7 @@ export const defaultSlots: TextEditorSlots = {
254
272
  className: focusRing,
255
273
  },
256
274
  editor: {
257
- className: 'min-bs-full',
275
+ className: 'bs-full',
258
276
  },
259
277
  };
260
278
 
@@ -7,11 +7,9 @@ import '@dxosTheme';
7
7
  import { BroadcastChannelNetworkAdapter } from '@automerge/automerge-repo-network-broadcastchannel';
8
8
  import '@preact/signals-react';
9
9
  import { EditorView } from '@codemirror/view'; // Register react integration.
10
- import get from 'lodash.get';
11
10
  import React, { useEffect, useMemo, useState } from 'react';
12
11
 
13
- import { type Prop } from '@dxos/automerge/automerge';
14
- import { Repo, type DocHandle } from '@dxos/automerge/automerge-repo';
12
+ import { Repo } from '@dxos/automerge/automerge-repo';
15
13
  import { Filter } from '@dxos/echo-schema';
16
14
  import { type PublicKey } from '@dxos/keys';
17
15
  import { Expando, TextObject, useSpace } from '@dxos/react-client/echo';
@@ -20,7 +18,8 @@ import { ClientRepeater } from '@dxos/react-client/testing';
20
18
  import { withTheme } from '@dxos/storybook-utils';
21
19
 
22
20
  import { MarkdownEditor } from './TextEditor';
23
- import { type IDocHandle, automerge, awareness, createBasicBundle } from '../../extensions';
21
+ import { automerge, awareness, createBasicBundle } from '../../extensions';
22
+ import { DocAccessor } from '../../extensions/automerge/defs';
24
23
  import { useTextEditor, useTextModel } from '../../hooks';
25
24
  import { defaultTheme } from '../../themes';
26
25
  import translations from '../../translations';
@@ -32,24 +31,24 @@ type TestObject = {
32
31
  };
33
32
 
34
33
  type EditorProps = {
35
- handle: IDocHandle;
36
- path?: Prop[];
34
+ source: DocAccessor;
37
35
  };
38
36
 
39
- const Editor = ({ handle, path = ['text'] }: EditorProps) => {
37
+ const Editor = ({ source }: EditorProps) => {
40
38
  const extensions = useMemo(
41
39
  () => [
42
40
  EditorView.baseTheme(defaultTheme),
43
41
  EditorView.editorAttributes.of({ class: 'p-2 bg-white' }),
44
42
  createBasicBundle({ placeholder: 'Type here...' }),
45
- automerge({ handle, path }),
43
+ automerge(source),
46
44
  awareness(),
47
45
  ],
48
- [handle, path],
46
+ [source],
49
47
  );
48
+
50
49
  const { parentRef } = useTextEditor({
51
50
  autoFocus: true,
52
- doc: get(handle.docSync()!, path),
51
+ doc: DocAccessor.getValue(source),
53
52
  extensions,
54
53
  });
55
54
 
@@ -57,8 +56,8 @@ const Editor = ({ handle, path = ['text'] }: EditorProps) => {
57
56
  };
58
57
 
59
58
  const Story = () => {
60
- const [object1, setObject1] = useState<DocHandle<TestObject>>();
61
- const [object2, setObject2] = useState<DocHandle<TestObject>>();
59
+ const [object1, setObject1] = useState<DocAccessor<TestObject>>();
60
+ const [object2, setObject2] = useState<DocAccessor<TestObject>>();
62
61
 
63
62
  useEffect(() => {
64
63
  queueMicrotask(async () => {
@@ -77,8 +76,8 @@ const Story = () => {
77
76
  const object2 = repo2.find(object1.url);
78
77
  await object2.whenReady();
79
78
 
80
- setObject1(object1);
81
- setObject2(object2);
79
+ setObject1({ handle: object1, path: ['text'] });
80
+ setObject2({ handle: object2, path: ['text'] });
82
81
  });
83
82
  }, []);
84
83
 
@@ -88,8 +87,8 @@ const Story = () => {
88
87
 
89
88
  return (
90
89
  <div role='none' className='grid grid-cols-2 bs-full is-full gap-2'>
91
- <Editor handle={object1} path={['text']} />
92
- <Editor handle={object2} path={['text']} />
90
+ <Editor source={object1} />
91
+ <Editor source={object2} />
93
92
  </div>
94
93
  );
95
94
  };
@@ -6,7 +6,7 @@ import '@dxosTheme';
6
6
 
7
7
  import React, { useMemo, useState } from 'react';
8
8
 
9
- import { Toolbar as NaturalToolbar, Select, useThemeContext } from '@dxos/react-ui';
9
+ import { Toolbar as NaturalToolbar, Select, useThemeContext, Tooltip } from '@dxos/react-ui';
10
10
  import { attentionSurface, mx, textBlockWidth } from '@dxos/react-ui-theme';
11
11
  import { withTheme } from '@dxos/storybook-utils';
12
12
 
@@ -22,7 +22,6 @@ import {
22
22
  useFormattingState,
23
23
  } from '../../extensions';
24
24
  import { createDataExtensions, createThemeExtensions, useActionHandler, useTextEditor } from '../../hooks';
25
- import { editorFillLayoutEditor, editorWithToolbarLayout } from '../../styles';
26
25
  import { markdownTheme } from '../../themes';
27
26
  import translations from '../../translations';
28
27
  import { Toolbar } from '../Toolbar';
@@ -52,7 +51,7 @@ const Story = ({ autoFocus, placeholder, doc, readonly }: StoryProps) => {
52
51
  themeMode,
53
52
  theme: markdownTheme,
54
53
  slots: {
55
- content: { className: '!p-4' },
54
+ editor: { className: 'p-2' },
56
55
  },
57
56
  }),
58
57
  // TODO(burdon): Move lineWrapping.
@@ -70,16 +69,16 @@ const Story = ({ autoFocus, placeholder, doc, readonly }: StoryProps) => {
70
69
 
71
70
  const handleAction = useActionHandler(view);
72
71
 
73
- // TODO(marijn) This doesn't update the state on view changes.
72
+ // TODO(marijn): This doesn't update the state on view changes.
74
73
  // Also not sure if view is even guaranteed to exist at this point.
75
74
  return (
76
- <div role='none' className={mx('fixed inset-0', editorWithToolbarLayout)}>
75
+ <div role='none' className={mx('fixed inset-0 flex flex-col')}>
77
76
  <Toolbar.Root onAction={handleAction} state={formattingState} classNames={textBlockWidth}>
78
77
  <Toolbar.Markdown />
79
78
  <EditorModeToolbar editorMode={editorMode} setEditorMode={setEditorMode} />
80
79
  </Toolbar.Root>
81
- <div role='none' className='overflow-y-auto'>
82
- <div role='textbox' className={mx(textBlockWidth, attentionSurface, editorFillLayoutEditor)} ref={parentRef} />
80
+ <div role='none' className='grow overflow-hidden'>
81
+ <div role='textbox' className={mx(textBlockWidth, attentionSurface)} ref={parentRef} />
83
82
  </div>
84
83
  </div>
85
84
  );
@@ -119,7 +118,11 @@ export default {
119
118
  title: 'react-ui-editor/useTextEditor',
120
119
  component: TextEditor,
121
120
  decorators: [withTheme],
122
- render: (args: StoryProps) => <Story {...args} />,
121
+ render: (args: StoryProps) => (
122
+ <Tooltip.Provider>
123
+ <Story {...args} />
124
+ </Tooltip.Provider>
125
+ ),
123
126
  parameters: { translations, layout: 'fullscreen' },
124
127
  };
125
128
 
@@ -9,6 +9,7 @@ import React, { type FC, useMemo, useState } from 'react';
9
9
  import { TextObject } from '@dxos/echo-schema';
10
10
  import { PublicKey } from '@dxos/keys';
11
11
  import { faker } from '@dxos/random';
12
+ import { Tooltip } from '@dxos/react-ui';
12
13
  import { mx, textBlockWidth } from '@dxos/react-ui-theme';
13
14
  import { withTheme } from '@dxos/storybook-utils';
14
15
 
@@ -23,7 +24,6 @@ import {
23
24
  useFormattingState,
24
25
  } from '../../extensions';
25
26
  import { type Comment, useActionHandler, useEditorView, useTextModel } from '../../hooks';
26
- import { editorFillLayoutEditor, editorFillLayoutRoot, editorWithToolbarLayout } from '../../styles';
27
27
  import translations from '../../translations';
28
28
  import { MarkdownEditor } from '../TextEditor';
29
29
 
@@ -61,7 +61,7 @@ const Story: FC<{ id?: string; content: string }> = ({ id = 'test', content }) =
61
61
  }
62
62
 
63
63
  return (
64
- <div role='none' className={mx('fixed inset-0', editorWithToolbarLayout)}>
64
+ <div role='none' className={mx('fixed inset-0 flex flex-col')}>
65
65
  <Toolbar.Root onAction={handleAction} state={formattingState} classNames={textBlockWidth}>
66
66
  <Toolbar.Markdown />
67
67
  <Toolbar.Separator />
@@ -72,9 +72,8 @@ const Story: FC<{ id?: string; content: string }> = ({ id = 'test', content }) =
72
72
  model={model}
73
73
  extensions={extensions}
74
74
  slots={{
75
- root: { className: mx(textBlockWidth, editorFillLayoutRoot) },
76
- editor: { className: editorFillLayoutEditor },
77
- content: { className: '!pli-2' },
75
+ root: { className: mx(textBlockWidth) },
76
+ editor: { className: 'p-2' },
78
77
  }}
79
78
  />
80
79
  </div>
@@ -84,7 +83,11 @@ const Story: FC<{ id?: string; content: string }> = ({ id = 'test', content }) =
84
83
  export default {
85
84
  title: 'react-ui-editor/Toolbar',
86
85
  component: Toolbar,
87
- render: (args: any) => <Story {...args} />,
86
+ render: (args: any) => (
87
+ <Tooltip.Provider>
88
+ <Story {...args} />
89
+ </Tooltip.Provider>
90
+ ),
88
91
  decorators: [withTheme],
89
92
  parameters: { translations, layout: 'fullscreen' },
90
93
  };
@@ -25,7 +25,7 @@ import {
25
25
  Quotes,
26
26
  } from '@phosphor-icons/react';
27
27
  import { createContext } from '@radix-ui/react-context';
28
- import React, { type PropsWithChildren } from 'react';
28
+ import React, { type PropsWithChildren, useRef, useState } from 'react';
29
29
 
30
30
  import {
31
31
  DensityProvider,
@@ -141,11 +141,31 @@ const MarkdownHeading = () => {
141
141
  const header = blockType && /heading(\d)/.exec(blockType);
142
142
  const value = header ? header[1] : blockType === 'paragraph' || !blockType ? '0' : undefined;
143
143
  const HeadingIcon = HeadingIcons[value ?? '0'];
144
+ const suppressNextTooltip = useRef<boolean>(false);
145
+ const [tooltipOpen, setTooltipOpen] = useState<boolean>(false);
146
+ const [selectOpen, setSelectOpen] = useState<boolean>(false);
144
147
  return (
145
- <Tooltip.Root>
148
+ <Tooltip.Root
149
+ open={tooltipOpen}
150
+ onOpenChange={(nextOpen) => {
151
+ if (nextOpen && suppressNextTooltip.current) {
152
+ suppressNextTooltip.current = false;
153
+ return setTooltipOpen(false);
154
+ } else {
155
+ return setTooltipOpen(nextOpen);
156
+ }
157
+ }}
158
+ >
146
159
  <Select.Root
147
160
  disabled={value === null}
148
161
  value={value ?? '0'}
162
+ open={selectOpen}
163
+ onOpenChange={(nextOpen: boolean) => {
164
+ if (!nextOpen) {
165
+ suppressNextTooltip.current = true;
166
+ }
167
+ return setSelectOpen(nextOpen);
168
+ }}
149
169
  onValueChange={(value) => onAction?.({ type: 'heading', data: parseInt(value) })}
150
170
  >
151
171
  <Tooltip.Trigger asChild>
@@ -7,29 +7,24 @@
7
7
  import { StateField, type Extension } from '@codemirror/state';
8
8
  import { EditorView, ViewPlugin } from '@codemirror/view';
9
9
 
10
- import { next as A, type Prop } from '@dxos/automerge/automerge';
10
+ import { next as A } from '@dxos/automerge/automerge';
11
11
 
12
12
  import { cursorConverter } from './cursor';
13
- import { updateHeadsEffect, type IDocHandle, isReconcile, type State } from './defs';
13
+ import { updateHeadsEffect, isReconcile, type State, type DocAccessor } from './defs';
14
14
  import { Syncer } from './sync';
15
15
  import { Cursor } from '../cursor';
16
16
 
17
- export type AutomergeOptions = {
18
- handle: IDocHandle;
19
- path: Prop[];
20
- };
21
-
22
- export const automerge = ({ handle, path }: AutomergeOptions): Extension => {
17
+ export const automerge = (accessor: DocAccessor): Extension => {
23
18
  const syncState = StateField.define<State>({
24
19
  create: () => ({
25
- path: path.slice(),
26
- lastHeads: A.getHeads(handle.docSync()!),
20
+ path: accessor.path.slice(),
21
+ lastHeads: A.getHeads(accessor.handle.docSync()!),
27
22
  unreconciledTransactions: [],
28
23
  }),
29
24
 
30
25
  update: (value, tr) => {
31
26
  const result: State = {
32
- path: path.slice(),
27
+ path: accessor.path.slice(),
33
28
  lastHeads: value.lastHeads,
34
29
  unreconciledTransactions: value.unreconciledTransactions.slice(),
35
30
  };
@@ -54,21 +49,21 @@ export const automerge = ({ handle, path }: AutomergeOptions): Extension => {
54
49
  },
55
50
  });
56
51
 
57
- const syncer = new Syncer(handle, syncState);
52
+ const syncer = new Syncer(accessor.handle, syncState);
58
53
 
59
54
  return [
60
- Cursor.converter.of(cursorConverter(handle, path)),
55
+ Cursor.converter.of(cursorConverter(accessor)),
61
56
  syncState,
62
57
 
63
58
  // Reconcile external updates.
64
59
  ViewPlugin.fromClass(
65
60
  class {
66
61
  constructor(private readonly _view: EditorView) {
67
- handle.addListener('change', this._handleChange.bind(this));
62
+ accessor.handle.addListener('change', this._handleChange.bind(this));
68
63
  }
69
64
 
70
65
  destroy() {
71
- handle.removeListener('change', this._handleChange.bind(this));
66
+ accessor.handle.removeListener('change', this._handleChange.bind(this));
72
67
  }
73
68
 
74
69
  _handleChange() {
@@ -4,14 +4,13 @@
4
4
 
5
5
  import get from 'lodash.get';
6
6
 
7
- import type { Prop } from '@dxos/automerge/automerge';
8
7
  import { next as A } from '@dxos/automerge/automerge';
9
8
  import { log } from '@dxos/log';
10
9
 
11
- import { type IDocHandle } from './defs';
10
+ import { type DocAccessor } from './defs';
12
11
  import { type CursorConverter } from '../cursor';
13
12
 
14
- export const cursorConverter = (handle: IDocHandle, path: Prop[]): CursorConverter => ({
13
+ export const cursorConverter = ({ handle, path }: DocAccessor): CursorConverter => ({
15
14
  // TODO(burdon): Handle assoc to associate with a previous character.
16
15
  toCursor: (pos) => {
17
16
  const doc = handle.docSync();
@@ -5,6 +5,7 @@
5
5
  //
6
6
 
7
7
  import { Annotation, StateEffect, type StateField, type EditorState, type Transaction } from '@codemirror/state';
8
+ import get from 'lodash.get';
8
9
 
9
10
  import { type ChangeFn, type ChangeOptions, type Doc, type Heads, type Prop } from '@dxos/automerge/automerge';
10
11
 
@@ -31,10 +32,22 @@ export const isReconcile = (tr: Transaction): boolean => {
31
32
  return !!tr.annotation(reconcileAnnotation);
32
33
  };
33
34
 
34
- export type IDocHandle<T = any> = {
35
+ // TODO(burdon): Reconcile with echo.
36
+ export interface DocAccessor<T = any> {
37
+ handle: IDocHandle<T>;
38
+ get path(): string[]; // TODO(burdon): Getter or prop?
39
+ }
40
+
41
+ // TODO(burdon): Reconcile with echo.
42
+ export const DocAccessor = {
43
+ getValue: (accessor: DocAccessor) => get(accessor.handle, accessor.path),
44
+ };
45
+
46
+ // TODO(burdon): Reconcile with echo.
47
+ export interface IDocHandle<T = any> {
35
48
  docSync(): Doc<T> | undefined;
36
49
  change(callback: ChangeFn<T>, options?: ChangeOptions<T>): void;
37
50
  changeAt(heads: Heads, callback: ChangeFn<T>, options?: ChangeOptions<T>): string[] | undefined;
38
51
  addListener(event: 'change', listener: () => void): void;
39
52
  removeListener(event: 'change', listener: () => void): void;
40
- };
53
+ }