@dxos/react-ui-editor 0.8.2-main.f11618f → 0.8.2-staging.7ac8446

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 (112) hide show
  1. package/dist/lib/browser/index.mjs +371 -499
  2. package/dist/lib/browser/index.mjs.map +4 -4
  3. package/dist/lib/browser/meta.json +1 -1
  4. package/dist/lib/node/index.cjs +379 -515
  5. package/dist/lib/node/index.cjs.map +4 -4
  6. package/dist/lib/node/meta.json +1 -1
  7. package/dist/lib/node-esm/index.mjs +371 -499
  8. package/dist/lib/node-esm/index.mjs.map +4 -4
  9. package/dist/lib/node-esm/meta.json +1 -1
  10. package/dist/types/src/{stories/InputMode.stories.d.ts → InputMode.stories.d.ts} +1 -1
  11. package/dist/types/src/InputMode.stories.d.ts.map +1 -0
  12. package/dist/types/src/{stories/TextEditorBasic.stories.d.ts → TextEditor.stories.d.ts} +35 -2
  13. package/dist/types/src/TextEditor.stories.d.ts.map +1 -0
  14. package/dist/types/src/components/EditorToolbar/util.d.ts +3 -3
  15. package/dist/types/src/components/EditorToolbar/util.d.ts.map +1 -1
  16. package/dist/types/src/components/EditorToolbar/{view-mode.d.ts → viewMode.d.ts} +1 -1
  17. package/dist/types/src/components/EditorToolbar/viewMode.d.ts.map +1 -0
  18. package/dist/types/src/defaults.d.ts +0 -1
  19. package/dist/types/src/defaults.d.ts.map +1 -1
  20. package/dist/types/src/extensions/automerge/automerge.stories.d.ts.map +1 -1
  21. package/dist/types/src/extensions/command/command.d.ts +10 -5
  22. package/dist/types/src/extensions/command/command.d.ts.map +1 -1
  23. package/dist/types/src/extensions/command/hint.d.ts +2 -4
  24. package/dist/types/src/extensions/command/hint.d.ts.map +1 -1
  25. package/dist/types/src/extensions/command/index.d.ts +0 -1
  26. package/dist/types/src/extensions/command/index.d.ts.map +1 -1
  27. package/dist/types/src/extensions/command/menu.d.ts +2 -7
  28. package/dist/types/src/extensions/command/menu.d.ts.map +1 -1
  29. package/dist/types/src/extensions/command/preview.d.ts +12 -0
  30. package/dist/types/src/extensions/command/preview.d.ts.map +1 -0
  31. package/dist/types/src/extensions/command/state.d.ts +11 -9
  32. package/dist/types/src/extensions/command/state.d.ts.map +1 -1
  33. package/dist/types/src/extensions/comments.d.ts +7 -9
  34. package/dist/types/src/extensions/comments.d.ts.map +1 -1
  35. package/dist/types/src/extensions/index.d.ts +0 -1
  36. package/dist/types/src/extensions/index.d.ts.map +1 -1
  37. package/dist/types/src/extensions/markdown/decorate.d.ts +1 -4
  38. package/dist/types/src/extensions/markdown/decorate.d.ts.map +1 -1
  39. package/dist/types/src/extensions/markdown/formatting.d.ts +2 -2
  40. package/dist/types/src/extensions/markdown/formatting.d.ts.map +1 -1
  41. package/dist/types/src/extensions/markdown/link.d.ts +1 -4
  42. package/dist/types/src/extensions/markdown/link.d.ts.map +1 -1
  43. package/dist/types/src/fragments.d.ts +3 -0
  44. package/dist/types/src/fragments.d.ts.map +1 -0
  45. package/dist/types/src/hooks/useTextEditor.d.ts +1 -2
  46. package/dist/types/src/hooks/useTextEditor.d.ts.map +1 -1
  47. package/dist/types/src/types.d.ts +0 -5
  48. package/dist/types/src/types.d.ts.map +1 -1
  49. package/dist/types/src/util/react.d.ts +1 -6
  50. package/dist/types/src/util/react.d.ts.map +1 -1
  51. package/package.json +27 -33
  52. package/src/{stories/InputMode.stories.tsx → InputMode.stories.tsx} +4 -4
  53. package/src/TextEditor.stories.tsx +856 -0
  54. package/src/components/EditorToolbar/EditorToolbar.tsx +2 -2
  55. package/src/components/EditorToolbar/util.ts +3 -3
  56. package/src/defaults.ts +3 -5
  57. package/src/extensions/automerge/automerge.stories.tsx +11 -3
  58. package/src/extensions/command/command.ts +27 -9
  59. package/src/extensions/command/hint.ts +30 -33
  60. package/src/extensions/command/index.ts +0 -1
  61. package/src/extensions/command/menu.ts +8 -11
  62. package/src/extensions/command/preview.ts +79 -0
  63. package/src/extensions/command/state.ts +61 -41
  64. package/src/extensions/comments.ts +9 -9
  65. package/src/extensions/folding.tsx +1 -1
  66. package/src/extensions/index.ts +0 -1
  67. package/src/extensions/markdown/decorate.ts +3 -4
  68. package/src/extensions/markdown/formatting.ts +2 -2
  69. package/src/extensions/markdown/image.ts +11 -12
  70. package/src/extensions/markdown/link.ts +24 -33
  71. package/src/fragments.ts +19 -0
  72. package/src/hooks/useTextEditor.ts +3 -4
  73. package/src/types.ts +0 -7
  74. package/src/util/react.tsx +2 -20
  75. package/dist/lib/browser/testing/index.mjs +0 -67
  76. package/dist/lib/browser/testing/index.mjs.map +0 -7
  77. package/dist/lib/node/testing/index.cjs +0 -101
  78. package/dist/lib/node/testing/index.cjs.map +0 -7
  79. package/dist/lib/node-esm/testing/index.mjs +0 -69
  80. package/dist/lib/node-esm/testing/index.mjs.map +0 -7
  81. package/dist/types/src/components/EditorToolbar/view-mode.d.ts.map +0 -1
  82. package/dist/types/src/extensions/command/action.d.ts +0 -17
  83. package/dist/types/src/extensions/command/action.d.ts.map +0 -1
  84. package/dist/types/src/extensions/preview/index.d.ts +0 -2
  85. package/dist/types/src/extensions/preview/index.d.ts.map +0 -1
  86. package/dist/types/src/extensions/preview/preview.d.ts +0 -39
  87. package/dist/types/src/extensions/preview/preview.d.ts.map +0 -1
  88. package/dist/types/src/stories/InputMode.stories.d.ts.map +0 -1
  89. package/dist/types/src/stories/TextEditorBasic.stories.d.ts.map +0 -1
  90. package/dist/types/src/stories/TextEditorComments.stories.d.ts +0 -13
  91. package/dist/types/src/stories/TextEditorComments.stories.d.ts.map +0 -1
  92. package/dist/types/src/stories/TextEditorPreview.stories.d.ts +0 -13
  93. package/dist/types/src/stories/TextEditorPreview.stories.d.ts.map +0 -1
  94. package/dist/types/src/stories/TextEditorSpecial.stories.d.ts +0 -19
  95. package/dist/types/src/stories/TextEditorSpecial.stories.d.ts.map +0 -1
  96. package/dist/types/src/stories/story-utils.d.ts +0 -53
  97. package/dist/types/src/stories/story-utils.d.ts.map +0 -1
  98. package/dist/types/src/testing/RefPopover.d.ts +0 -21
  99. package/dist/types/src/testing/RefPopover.d.ts.map +0 -1
  100. package/dist/types/src/testing/index.d.ts +0 -2
  101. package/dist/types/src/testing/index.d.ts.map +0 -1
  102. package/src/extensions/command/action.ts +0 -49
  103. package/src/extensions/preview/index.ts +0 -5
  104. package/src/extensions/preview/preview.ts +0 -271
  105. package/src/stories/TextEditorBasic.stories.tsx +0 -289
  106. package/src/stories/TextEditorComments.stories.tsx +0 -99
  107. package/src/stories/TextEditorPreview.stories.tsx +0 -239
  108. package/src/stories/TextEditorSpecial.stories.tsx +0 -107
  109. package/src/stories/story-utils.tsx +0 -329
  110. package/src/testing/RefPopover.tsx +0 -74
  111. package/src/testing/index.ts +0 -5
  112. /package/src/components/EditorToolbar/{view-mode.ts → viewMode.ts} +0 -0
@@ -26,8 +26,8 @@ import {
26
26
  type EditorToolbarProps,
27
27
  editorToolbarSearch,
28
28
  } from './util';
29
- import { createViewMode } from './view-mode';
30
- import { stackItemContentToolbarClassNames } from '../../defaults';
29
+ import { createViewMode } from './viewMode';
30
+ import { stackItemContentToolbarClassNames } from '../../fragments';
31
31
 
32
32
  const createToolbar = ({
33
33
  state,
@@ -4,7 +4,7 @@
4
4
 
5
5
  import { useMemo } from 'react';
6
6
 
7
- import { live, type Live } from '@dxos/live-object';
7
+ import { create, type ReactiveObject } from '@dxos/live-object';
8
8
  import { type Label, type ThemedClassName } from '@dxos/react-ui';
9
9
  import {
10
10
  type MenuSeparator,
@@ -23,7 +23,7 @@ export type EditorToolbarState = Formatting &
23
23
  Partial<{ comment: boolean; viewMode: EditorViewMode; selection: boolean }>;
24
24
 
25
25
  export const useEditorToolbarState = (initialState: Partial<EditorToolbarState> = {}) => {
26
- return useMemo(() => live<EditorToolbarState>(initialState), []);
26
+ return useMemo(() => create<EditorToolbarState>(initialState), []);
27
27
  };
28
28
 
29
29
  export type EditorToolbarFeatureFlags = Partial<{
@@ -37,7 +37,7 @@ export type EditorToolbarFeatureFlags = Partial<{
37
37
  }>;
38
38
 
39
39
  export type EditorToolbarActionGraphProps = {
40
- state: Live<EditorToolbarState>;
40
+ state: ReactiveObject<EditorToolbarState>;
41
41
  // TODO(wittjosiah): Control positioning.
42
42
  customActions?: () => ActionGraphProps;
43
43
  onAction: (action: EditorAction) => void;
package/src/defaults.ts CHANGED
@@ -17,9 +17,7 @@ const margin = '!mt-[1rem]';
17
17
  * NOTE: Max width - 4rem = 2rem left/right margin (or 2rem gutter plus 1rem left/right margin).
18
18
  */
19
19
  // TOOD(burdon): Adjust depending on
20
- export const editorWidth = '!mli-auto is-full max-is-[min(50rem,100%-4rem)]';
21
-
22
- export const editorContent = mx(margin, editorWidth);
20
+ export const editorContent = mx(margin, '!mli-auto w-full max-w-[min(50rem,100%-4rem)]');
23
21
 
24
22
  /**
25
23
  * Margin for numbers.
@@ -52,6 +50,6 @@ export const stackItemContentEditorClassNames = (role?: string) =>
52
50
 
53
51
  export const stackItemContentToolbarClassNames = (role?: string) =>
54
52
  mx(
55
- 'attention-surface is-full border-be !border-separator relative z-[1]',
56
- role === 'section' && 'sticky block-start-0 -mbe-px min-is-0',
53
+ 'attention-surface is-full border-be !border-separator',
54
+ role === 'section' && 'sticky block-start-0 z-[1] -mbe-px min-is-0',
57
55
  );
@@ -10,7 +10,15 @@ import React, { useEffect, useState } from 'react';
10
10
  import { Repo } from '@dxos/automerge/automerge-repo';
11
11
  import { BroadcastChannelNetworkAdapter } from '@dxos/automerge/automerge-repo-network-broadcastchannel';
12
12
  import { Expando } from '@dxos/echo-schema';
13
- import { DocAccessor, Filter, live, createDocAccessor, useQuery, useSpace, type Space } from '@dxos/react-client/echo';
13
+ import {
14
+ DocAccessor,
15
+ Filter,
16
+ create,
17
+ createDocAccessor,
18
+ useQuery,
19
+ useSpace,
20
+ type Space,
21
+ } from '@dxos/react-client/echo';
14
22
  import { useIdentity, type Identity } from '@dxos/react-client/halo';
15
23
  import { ClientRepeater, type ClientRepeatedComponentProps } from '@dxos/react-client/testing';
16
24
  import { useThemeContext } from '@dxos/react-ui';
@@ -131,9 +139,9 @@ export const WithEcho = {
131
139
  createSpace
132
140
  onSpaceCreated={async ({ space }) => {
133
141
  space.db.add(
134
- live({
142
+ create({
135
143
  type: 'test',
136
- content: live(Expando, { content: initialContent }),
144
+ content: create(Expando, { content: initialContent }),
137
145
  }),
138
146
  );
139
147
  }}
@@ -5,25 +5,35 @@
5
5
  import { type Extension } from '@codemirror/state';
6
6
  import { EditorView, keymap } from '@codemirror/view';
7
7
 
8
- import { closeEffect, commandKeyBindings } from './action';
9
- import { hintViewPlugin, type HintOptions } from './hint';
10
- import { floatingMenu, type FloatingMenuOptions } from './menu';
11
- import { commandConfig, commandState, type PopupOptions } from './state';
8
+ import { hintViewPlugin } from './hint';
9
+ import { floatingMenu } from './menu';
10
+ import { preview, type PreviewOptions } from './preview';
11
+ import { closeEffect, commandConfig, commandKeyBindings, commandState } from './state';
12
12
 
13
13
  // TODO(burdon): Create knowledge base for CM notes and ideas.
14
14
  // https://discuss.codemirror.net/t/inline-code-hints-like-vscode/5533/4
15
15
  // https://github.com/saminzadeh/codemirror-extension-inline-suggestion
16
16
  // https://github.com/ChromeDevTools/devtools-frontend/blob/main/front_end/ui/components/text_editor/config.ts#L370
17
17
 
18
- export type CommandOptions = Partial<PopupOptions & FloatingMenuOptions & HintOptions>;
18
+ // TODO(burdon): Discriminated union.
19
+ export type CommandAction = {
20
+ insert?: string;
21
+ };
22
+
23
+ export type CommandOptions = {
24
+ onHint: () => string | undefined;
25
+ onRenderDialog: (el: HTMLElement, cb: (action?: CommandAction) => void) => void;
26
+ onRenderMenu: (el: HTMLElement, cb: () => void) => void;
27
+ } & Pick<PreviewOptions, 'onRenderPreview'>;
19
28
 
20
- export const command = (options: CommandOptions = {}): Extension => {
29
+ export const command = (options: CommandOptions): Extension => {
21
30
  return [
22
- keymap.of(commandKeyBindings),
23
31
  commandConfig.of(options),
24
32
  commandState,
25
- options.renderMenu ? floatingMenu({ renderMenu: options.renderMenu }) : [],
26
- options.onHint ? hintViewPlugin({ onHint: options.onHint }) : [],
33
+ keymap.of(commandKeyBindings),
34
+ preview(options),
35
+ floatingMenu(options),
36
+ hintViewPlugin(options),
27
37
  EditorView.focusChangeEffect.of((_, focusing) => {
28
38
  return focusing ? closeEffect.of(null) : null;
29
39
  }),
@@ -31,6 +41,14 @@ export const command = (options: CommandOptions = {}): Extension => {
31
41
  '.cm-tooltip': {
32
42
  background: 'transparent',
33
43
  },
44
+ '.cm-preview': {
45
+ marginLeft: '-1rem',
46
+ marginRight: '-1rem',
47
+ padding: '1rem',
48
+ borderRadius: '1rem',
49
+ background: 'var(--dx-modalSurface)',
50
+ border: '1px solid var(--dx-separator)',
51
+ },
34
52
  }),
35
53
  ];
36
54
  };
@@ -5,42 +5,10 @@
5
5
  import { RangeSetBuilder } from '@codemirror/state';
6
6
  import { Decoration, EditorView, ViewPlugin, type ViewUpdate, WidgetType } from '@codemirror/view';
7
7
 
8
+ import { type CommandOptions } from './command';
8
9
  import { commandState } from './state';
9
10
  import { clientRectsFor, flattenRect } from '../../util';
10
11
 
11
- export type HintOptions = {
12
- onHint: () => string | undefined;
13
- };
14
-
15
- export const hintViewPlugin = ({ onHint }: HintOptions) =>
16
- ViewPlugin.fromClass(
17
- class {
18
- deco = Decoration.none;
19
- update(update: ViewUpdate) {
20
- const builder = new RangeSetBuilder<Decoration>();
21
- const cState = update.view.state.field(commandState, false);
22
- if (!cState?.tooltip) {
23
- const selection = update.view.state.selection.main;
24
- const line = update.view.state.doc.lineAt(selection.from);
25
- // Only show if blank line.
26
- // TODO(burdon): Clashes with placeholder if pos === 0.
27
- // TODO(burdon): Show after delay or if blank line above?
28
- if (selection.from === selection.to && line.from === line.to) {
29
- const hint = onHint();
30
- if (hint) {
31
- builder.add(selection.from, selection.to, Decoration.widget({ widget: new CommandHint(hint) }));
32
- }
33
- }
34
- }
35
-
36
- this.deco = builder.finish();
37
- }
38
- },
39
- {
40
- provide: (plugin) => [EditorView.decorations.of((view) => view.plugin(plugin)?.deco ?? Decoration.none)],
41
- },
42
- );
43
-
44
12
  class CommandHint extends WidgetType {
45
13
  constructor(readonly content: string | HTMLElement) {
46
14
  super();
@@ -80,3 +48,32 @@ class CommandHint extends WidgetType {
80
48
  return false;
81
49
  }
82
50
  }
51
+
52
+ export const hintViewPlugin = ({ onHint }: CommandOptions) =>
53
+ ViewPlugin.fromClass(
54
+ class {
55
+ deco = Decoration.none;
56
+ update(update: ViewUpdate) {
57
+ const builder = new RangeSetBuilder<Decoration>();
58
+ const cState = update.view.state.field(commandState, false);
59
+ if (!cState?.tooltip) {
60
+ const selection = update.view.state.selection.main;
61
+ const line = update.view.state.doc.lineAt(selection.from);
62
+ // Only show if blank line.
63
+ // TODO(burdon): Clashes with placeholder if pos === 0.
64
+ // TODO(burdon): Show after delay or if blank line above?
65
+ if (selection.from === selection.to && line.from === line.to) {
66
+ const hint = onHint();
67
+ if (hint) {
68
+ builder.add(selection.from, selection.to, Decoration.widget({ widget: new CommandHint(hint) }));
69
+ }
70
+ }
71
+ }
72
+
73
+ this.deco = builder.finish();
74
+ }
75
+ },
76
+ {
77
+ provide: (plugin) => [EditorView.decorations.of((view) => view.plugin(plugin)?.deco ?? Decoration.none)],
78
+ },
79
+ );
@@ -2,5 +2,4 @@
2
2
  // Copyright 2024 DXOS.org
3
3
  //
4
4
 
5
- export * from './action';
6
5
  export * from './command';
@@ -4,16 +4,12 @@
4
4
 
5
5
  import { type BlockInfo, type EditorView, ViewPlugin, type ViewUpdate } from '@codemirror/view';
6
6
 
7
- import { closeEffect, openCommand, openEffect } from './action';
8
- import { type RenderCallback } from '../../types';
9
-
10
- export type FloatingMenuOptions = {
11
- renderMenu: RenderCallback<{ onAction: () => void }>;
12
- };
7
+ import { type CommandOptions } from './command';
8
+ import { closeEffect, openCommand, openEffect } from './state';
13
9
 
14
10
  // TODO(burdon): Trigger completion on click.
15
11
  // TODO(burdon): Hide when dialog is open.
16
- export const floatingMenu = (options: FloatingMenuOptions) =>
12
+ export const floatingMenu = (options: CommandOptions) =>
17
13
  ViewPlugin.fromClass(
18
14
  class {
19
15
  button: HTMLElement;
@@ -35,11 +31,13 @@ export const floatingMenu = (options: FloatingMenuOptions) =>
35
31
  this.button.style.zIndex = '10';
36
32
  this.button.style.display = 'none';
37
33
 
38
- options.renderMenu(this.button, { onAction: () => openCommand(view) }, view);
34
+ options.onRenderMenu(this.button, () => {
35
+ openCommand(view);
36
+ });
39
37
  container.appendChild(this.button);
40
38
 
41
39
  // Listen for scroll events.
42
- container.addEventListener('scroll', this.scheduleUpdate.bind(this));
40
+ container.addEventListener('scroll', this.scheduleUpdate);
43
41
  this.scheduleUpdate();
44
42
  }
45
43
 
@@ -58,8 +56,7 @@ export const floatingMenu = (options: FloatingMenuOptions) =>
58
56
  if (this.rafId != null) {
59
57
  cancelAnimationFrame(this.rafId);
60
58
  }
61
-
62
- this.rafId = requestAnimationFrame(this.updateButtonPosition.bind(this));
59
+ this.rafId = requestAnimationFrame(() => this.updateButtonPosition());
63
60
  }
64
61
 
65
62
  updateButtonPosition() {
@@ -0,0 +1,79 @@
1
+ //
2
+ // Copyright 2023 DXOS.org
3
+ //
4
+
5
+ import { syntaxTree } from '@codemirror/language';
6
+ import {
7
+ type EditorState,
8
+ type Extension,
9
+ type RangeSet,
10
+ RangeSetBuilder,
11
+ StateField,
12
+ type Transaction,
13
+ } from '@codemirror/state';
14
+ import { Decoration, type DecorationSet, EditorView, WidgetType } from '@codemirror/view';
15
+
16
+ export type PreviewOptions = {
17
+ onRenderPreview: (el: HTMLElement, props: { url: string; text: string }) => void;
18
+ };
19
+
20
+ /**
21
+ * Create image decorations.
22
+ */
23
+ export const preview = (options: PreviewOptions): Extension => {
24
+ return [
25
+ StateField.define<DecorationSet>({
26
+ create: (state) => buildDecorations(state, options),
27
+ update: (_: RangeSet<Decoration>, tr: Transaction) => buildDecorations(tr.state, options),
28
+ // TODO(burdon): Make atomic.
29
+ provide: (field) => EditorView.decorations.from(field),
30
+ }),
31
+ ];
32
+ };
33
+
34
+ // TODO(burdon): Make atomic.
35
+ const buildDecorations = (state: EditorState, options: PreviewOptions) => {
36
+ const builder = new RangeSetBuilder<Decoration>();
37
+ syntaxTree(state).iterate({
38
+ enter: (node) => {
39
+ if (node.name === 'Link') {
40
+ const urlNode = node.node.getChild('URL');
41
+ if (urlNode) {
42
+ const text = state.sliceDoc(node.from + 1, urlNode.from - 2);
43
+ const url = state.sliceDoc(urlNode.from, urlNode.to);
44
+ builder.add(
45
+ node.from,
46
+ node.to,
47
+ Decoration.replace({
48
+ block: true, // Prevent cursor from entering.
49
+ widget: new PreviewWidget(options.onRenderPreview, url, text),
50
+ }),
51
+ );
52
+ }
53
+ }
54
+ },
55
+ });
56
+
57
+ return builder.finish();
58
+ };
59
+
60
+ class PreviewWidget extends WidgetType {
61
+ constructor(
62
+ readonly _onRenderPreview: PreviewOptions['onRenderPreview'],
63
+ readonly _url: string,
64
+ readonly _text: string,
65
+ ) {
66
+ super();
67
+ }
68
+
69
+ override eq(other: this) {
70
+ return this._url === (other as any as PreviewWidget)._url;
71
+ }
72
+
73
+ override toDOM(view: EditorView) {
74
+ const root = document.createElement('div');
75
+ root.classList.add('cm-preview');
76
+ this._onRenderPreview(root, { url: this._url, text: this._text });
77
+ return root;
78
+ }
79
+ }
@@ -2,24 +2,25 @@
2
2
  // Copyright 2024 DXOS.org
3
3
  //
4
4
 
5
- import { StateField } from '@codemirror/state';
6
- import { showTooltip, type EditorView, type Tooltip, type TooltipView } from '@codemirror/view';
5
+ import { StateEffect, StateField } from '@codemirror/state';
6
+ import {
7
+ showTooltip,
8
+ type Command,
9
+ type EditorView,
10
+ type KeyBinding,
11
+ type Tooltip,
12
+ type TooltipView,
13
+ } from '@codemirror/view';
7
14
 
8
- import { closeEffect, type Action, openEffect } from './action';
9
15
  import { type CommandOptions } from './command';
10
- import { type RenderCallback } from '../../types';
11
16
  import { singleValueFacet } from '../../util';
12
17
 
13
- export const commandConfig = singleValueFacet<CommandOptions>();
14
-
15
- export type PopupOptions = {
16
- renderDialog: RenderCallback<{ onAction: (action?: Action) => void }>;
17
- };
18
-
19
18
  type CommandState = {
20
19
  tooltip?: Tooltip | null;
21
20
  };
22
21
 
22
+ export const commandConfig = singleValueFacet<CommandOptions>();
23
+
23
24
  export const commandState = StateField.define<CommandState>({
24
25
  create: () => ({}),
25
26
  update: (state, tr) => {
@@ -28,8 +29,8 @@ export const commandState = StateField.define<CommandState>({
28
29
  return {};
29
30
  }
30
31
 
31
- const { renderDialog } = tr.state.facet(commandConfig);
32
- if (effect.is(openEffect) && renderDialog) {
32
+ if (effect.is(openEffect)) {
33
+ const options = tr.state.facet(commandConfig);
33
34
  const { pos, fullWidth } = effect.value;
34
35
  const tooltip: Tooltip = {
35
36
  pos,
@@ -37,49 +38,38 @@ export const commandState = StateField.define<CommandState>({
37
38
  arrow: false,
38
39
  strictSide: true,
39
40
  create: (view: EditorView) => {
40
- const root = document.createElement('div');
41
-
41
+ const dom = document.createElement('div');
42
42
  const tooltipView: TooltipView = {
43
- dom: root,
43
+ dom,
44
44
  mount: (view: EditorView) => {
45
45
  if (fullWidth) {
46
- const parent = root.parentElement!;
46
+ const parent = dom.parentElement!;
47
47
  const { paddingLeft, paddingRight } = window.getComputedStyle(parent);
48
48
  const widthWithoutPadding = parent.clientWidth - parseFloat(paddingLeft) - parseFloat(paddingRight);
49
- root.style.width = `${widthWithoutPadding}px`;
49
+ dom.style.width = `${widthWithoutPadding}px`;
50
50
  }
51
51
 
52
52
  // Render react component.
53
- renderDialog(
54
- root,
55
- {
56
- onAction: (action) => {
57
- view.dispatch({ effects: closeEffect.of(null) });
58
- switch (action?.type) {
59
- case 'insert': {
60
- // Insert into editor.
61
- const text = action.text + '\n';
62
- view.dispatch({
63
- changes: { from: pos, insert: text },
64
- selection: { anchor: pos + text.length },
65
- });
66
- break;
67
- }
68
- }
69
-
70
- // NOTE: Truncates text if set focus immediately.
71
- requestAnimationFrame(() => view.focus());
72
- },
73
- },
74
- view,
75
- );
53
+ options.onRenderDialog(dom, (action) => {
54
+ view.dispatch({ effects: closeEffect.of(null) });
55
+ if (action?.insert?.length) {
56
+ // Insert into editor.
57
+ const text = action.insert + '\n';
58
+ view.dispatch({
59
+ changes: { from: pos, insert: text },
60
+ selection: { anchor: pos + text.length },
61
+ });
62
+ }
63
+
64
+ // NOTE: Truncates text if set focus immediately.
65
+ requestAnimationFrame(() => view.focus());
66
+ });
76
67
  },
77
68
  };
78
69
 
79
70
  return tooltipView;
80
71
  },
81
72
  };
82
-
83
73
  return { tooltip };
84
74
  }
85
75
  }
@@ -88,3 +78,33 @@ export const commandState = StateField.define<CommandState>({
88
78
  },
89
79
  provide: (field) => [showTooltip.from(field, (value) => value.tooltip ?? null)],
90
80
  });
81
+
82
+ export const openEffect = StateEffect.define<{ pos: number; fullWidth?: boolean }>();
83
+ export const closeEffect = StateEffect.define<null>();
84
+
85
+ export const openCommand: Command = (view: EditorView) => {
86
+ if (view.state.field(commandState, false)) {
87
+ const selection = view.state.selection.main;
88
+ const line = view.state.doc.lineAt(selection.from);
89
+ if (line.from === selection.from && line.from === line.to) {
90
+ view.dispatch({ effects: openEffect.of({ pos: selection.anchor, fullWidth: true }) });
91
+ return true;
92
+ }
93
+ }
94
+
95
+ return false;
96
+ };
97
+
98
+ export const closeCommand: Command = (view: EditorView) => {
99
+ if (view.state.field(commandState, false)) {
100
+ view.dispatch({ effects: closeEffect.of(null) });
101
+ return true;
102
+ }
103
+
104
+ return false;
105
+ };
106
+
107
+ export const commandKeyBindings: readonly KeyBinding[] = [
108
+ { key: '/', run: openCommand },
109
+ { key: 'Escape', run: closeCommand },
110
+ ];
@@ -25,13 +25,13 @@ import sortBy from 'lodash.sortby';
25
25
  import { useEffect, useMemo } from 'react';
26
26
 
27
27
  import { debounce, type CleanupFn } from '@dxos/async';
28
- import { type Live } from '@dxos/live-object';
28
+ import { type ReactiveObject } from '@dxos/live-object';
29
29
  import { log } from '@dxos/log';
30
30
  import { isNonNullable } from '@dxos/util';
31
31
 
32
32
  import { documentId } from './selection';
33
33
  import { type EditorToolbarState } from '../components';
34
- import { type RenderCallback, type Comment, type Range } from '../types';
34
+ import { type Comment, type Range } from '../types';
35
35
  import { Cursor, overlap, singleValueFacet, callbackWrapper } from '../util';
36
36
 
37
37
  //
@@ -345,10 +345,6 @@ export type CommentsOptions = {
345
345
  * Key shortcut to create a new thread.
346
346
  */
347
347
  key?: string;
348
- /**
349
- * Called to render tooltip.
350
- */
351
- renderTooltip?: RenderCallback<{ shortcut: string }>;
352
348
  /**
353
349
  * Called to create a new thread and return the thread id.
354
350
  */
@@ -365,6 +361,10 @@ export type CommentsOptions = {
365
361
  * Called to notify which thread is currently closest to the cursor.
366
362
  */
367
363
  onSelect?: (state: CommentsState) => void;
364
+ /**
365
+ * Called to render tooltip.
366
+ */
367
+ onHover?: (el: Element, shortcut: string) => void;
368
368
  };
369
369
 
370
370
  const optionsFacet = singleValueFacet<CommentsOptions>();
@@ -408,7 +408,7 @@ export const comments = (options: CommentsOptions = {}): Extension => {
408
408
  // Hover tooltip (for key shortcut hints, etc.)
409
409
  // TODO(burdon): Factor out to generic hints extension for current selection/line.
410
410
  //
411
- options.renderTooltip &&
411
+ options.onHover &&
412
412
  hoverTooltip(
413
413
  (view, pos) => {
414
414
  const selection = view.state.selection.main;
@@ -419,7 +419,7 @@ export const comments = (options: CommentsOptions = {}): Extension => {
419
419
  above: true,
420
420
  create: () => {
421
421
  const el = document.createElement('div');
422
- options.renderTooltip!(el, { shortcut }, view);
422
+ options.onHover!(el, shortcut);
423
423
  return { dom: el, offset: { x: 0, y: 8 } };
424
424
  },
425
425
  };
@@ -606,7 +606,7 @@ export const createExternalCommentSync = (
606
606
  },
607
607
  );
608
608
 
609
- export const useCommentState = (state: Live<EditorToolbarState>): Extension => {
609
+ export const useCommentState = (state: ReactiveObject<EditorToolbarState>): Extension => {
610
610
  return useMemo(
611
611
  () =>
612
612
  EditorView.updateListener.of((update) => {
@@ -29,7 +29,7 @@ export const folding = (_props: FoldingOptions = {}): Extension => [
29
29
  const el = createElement('div', { className: 'flex h-full items-center' });
30
30
  return renderRoot(
31
31
  el,
32
- <Icon icon='ph--caret-right--bold' size={3} classNames={['mx-3 cursor-pointer', open && 'rotate-90']} />,
32
+ <Icon icon='ph--caret-right--regular' size={3} classNames={['mx-3 cursor-pointer', open && 'rotate-90']} />,
33
33
  );
34
34
  },
35
35
  }),
@@ -18,6 +18,5 @@ export * from './listener';
18
18
  export * from './markdown';
19
19
  export * from './mention';
20
20
  export * from './modes';
21
- export * from './preview';
22
21
  export * from './selection';
23
22
  export * from './typewriter';
@@ -15,7 +15,6 @@ import { image } from './image';
15
15
  import { formattingStyles, bulletListIndentationWidth, orderedListIndentationWidth } from './styles';
16
16
  import { table } from './table';
17
17
  import { theme, type HeadingLevel } from '../../styles';
18
- import { type RenderCallback } from '../../types';
19
18
  import { wrapWithCatch } from '../../util';
20
19
 
21
20
  /**
@@ -46,7 +45,7 @@ class HorizontalRuleWidget extends WidgetType {
46
45
  class LinkButton extends WidgetType {
47
46
  constructor(
48
47
  private readonly url: string,
49
- private readonly render: RenderCallback<{ url: string }>,
48
+ private readonly render: (el: HTMLElement, url: string) => void,
50
49
  ) {
51
50
  super();
52
51
  }
@@ -58,7 +57,7 @@ class LinkButton extends WidgetType {
58
57
  // TODO(burdon): Create icon and link directly without react?
59
58
  override toDOM(view: EditorView) {
60
59
  const el = document.createElement('span');
61
- this.render(el, { url: this.url }, view);
60
+ this.render(el, this.url);
62
61
  return el;
63
62
  }
64
63
  }
@@ -520,7 +519,7 @@ export interface DecorateOptions {
520
519
  */
521
520
  selectionChangeDelay?: number;
522
521
  numberedHeadings?: { from: number; to?: number };
523
- renderLinkButton?: RenderCallback<{ url: string }>;
522
+ renderLinkButton?: (el: Element, url: string) => void;
524
523
  }
525
524
 
526
525
  export const decorateMarkdown = (options: DecorateOptions = {}) => {
@@ -17,7 +17,7 @@ import { EditorView, keymap } from '@codemirror/view';
17
17
  import { type SyntaxNodeRef, type SyntaxNode } from '@lezer/common';
18
18
  import { useMemo } from 'react';
19
19
 
20
- import { type Live } from '@dxos/live-object';
20
+ import { type ReactiveObject } from '@dxos/live-object';
21
21
 
22
22
  import { type EditorToolbarState } from '../../components';
23
23
 
@@ -1250,7 +1250,7 @@ export const getFormatting = (state: EditorState): Formatting => {
1250
1250
  /**
1251
1251
  * Hook provides an extension to compute the current formatting state.
1252
1252
  */
1253
- export const useFormattingState = (state: Live<EditorToolbarState>): Extension => {
1253
+ export const useFormattingState = (state: ReactiveObject<EditorToolbarState>): Extension => {
1254
1254
  return useMemo(
1255
1255
  () =>
1256
1256
  EditorView.updateListener.of((update) => {