@dxos/react-ui-editor 0.8.1 → 0.8.2-main.f081794

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 +499 -371
  2. package/dist/lib/browser/index.mjs.map +4 -4
  3. package/dist/lib/browser/meta.json +1 -1
  4. package/dist/lib/browser/testing/index.mjs +67 -0
  5. package/dist/lib/browser/testing/index.mjs.map +7 -0
  6. package/dist/lib/node/index.cjs +515 -379
  7. package/dist/lib/node/index.cjs.map +4 -4
  8. package/dist/lib/node/meta.json +1 -1
  9. package/dist/lib/node/testing/index.cjs +101 -0
  10. package/dist/lib/node/testing/index.cjs.map +7 -0
  11. package/dist/lib/node-esm/index.mjs +499 -371
  12. package/dist/lib/node-esm/index.mjs.map +4 -4
  13. package/dist/lib/node-esm/meta.json +1 -1
  14. package/dist/lib/node-esm/testing/index.mjs +69 -0
  15. package/dist/lib/node-esm/testing/index.mjs.map +7 -0
  16. package/dist/types/src/components/EditorToolbar/util.d.ts +3 -3
  17. package/dist/types/src/components/EditorToolbar/util.d.ts.map +1 -1
  18. package/dist/types/src/components/EditorToolbar/{viewMode.d.ts → view-mode.d.ts} +1 -1
  19. package/dist/types/src/components/EditorToolbar/view-mode.d.ts.map +1 -0
  20. package/dist/types/src/defaults.d.ts +1 -0
  21. package/dist/types/src/defaults.d.ts.map +1 -1
  22. package/dist/types/src/extensions/automerge/automerge.stories.d.ts.map +1 -1
  23. package/dist/types/src/extensions/command/action.d.ts +17 -0
  24. package/dist/types/src/extensions/command/action.d.ts.map +1 -0
  25. package/dist/types/src/extensions/command/command.d.ts +5 -10
  26. package/dist/types/src/extensions/command/command.d.ts.map +1 -1
  27. package/dist/types/src/extensions/command/hint.d.ts +4 -2
  28. package/dist/types/src/extensions/command/hint.d.ts.map +1 -1
  29. package/dist/types/src/extensions/command/index.d.ts +1 -0
  30. package/dist/types/src/extensions/command/index.d.ts.map +1 -1
  31. package/dist/types/src/extensions/command/menu.d.ts +7 -2
  32. package/dist/types/src/extensions/command/menu.d.ts.map +1 -1
  33. package/dist/types/src/extensions/command/state.d.ts +9 -11
  34. package/dist/types/src/extensions/command/state.d.ts.map +1 -1
  35. package/dist/types/src/extensions/comments.d.ts +9 -7
  36. package/dist/types/src/extensions/comments.d.ts.map +1 -1
  37. package/dist/types/src/extensions/index.d.ts +1 -0
  38. package/dist/types/src/extensions/index.d.ts.map +1 -1
  39. package/dist/types/src/extensions/markdown/decorate.d.ts +4 -1
  40. package/dist/types/src/extensions/markdown/decorate.d.ts.map +1 -1
  41. package/dist/types/src/extensions/markdown/formatting.d.ts +2 -2
  42. package/dist/types/src/extensions/markdown/formatting.d.ts.map +1 -1
  43. package/dist/types/src/extensions/markdown/link.d.ts +4 -1
  44. package/dist/types/src/extensions/markdown/link.d.ts.map +1 -1
  45. package/dist/types/src/extensions/preview/index.d.ts +2 -0
  46. package/dist/types/src/extensions/preview/index.d.ts.map +1 -0
  47. package/dist/types/src/extensions/preview/preview.d.ts +39 -0
  48. package/dist/types/src/extensions/preview/preview.d.ts.map +1 -0
  49. package/dist/types/src/hooks/useTextEditor.d.ts +2 -1
  50. package/dist/types/src/hooks/useTextEditor.d.ts.map +1 -1
  51. package/dist/types/src/{InputMode.stories.d.ts → stories/InputMode.stories.d.ts} +1 -1
  52. package/dist/types/src/stories/InputMode.stories.d.ts.map +1 -0
  53. package/dist/types/src/{TextEditor.stories.d.ts → stories/TextEditorBasic.stories.d.ts} +2 -35
  54. package/dist/types/src/stories/TextEditorBasic.stories.d.ts.map +1 -0
  55. package/dist/types/src/stories/TextEditorComments.stories.d.ts +13 -0
  56. package/dist/types/src/stories/TextEditorComments.stories.d.ts.map +1 -0
  57. package/dist/types/src/stories/TextEditorPreview.stories.d.ts +13 -0
  58. package/dist/types/src/stories/TextEditorPreview.stories.d.ts.map +1 -0
  59. package/dist/types/src/stories/TextEditorSpecial.stories.d.ts +19 -0
  60. package/dist/types/src/stories/TextEditorSpecial.stories.d.ts.map +1 -0
  61. package/dist/types/src/stories/story-utils.d.ts +53 -0
  62. package/dist/types/src/stories/story-utils.d.ts.map +1 -0
  63. package/dist/types/src/testing/RefPopover.d.ts +21 -0
  64. package/dist/types/src/testing/RefPopover.d.ts.map +1 -0
  65. package/dist/types/src/testing/index.d.ts +2 -0
  66. package/dist/types/src/testing/index.d.ts.map +1 -0
  67. package/dist/types/src/types.d.ts +5 -0
  68. package/dist/types/src/types.d.ts.map +1 -1
  69. package/dist/types/src/util/react.d.ts +6 -1
  70. package/dist/types/src/util/react.d.ts.map +1 -1
  71. package/package.json +33 -27
  72. package/src/components/EditorToolbar/EditorToolbar.tsx +2 -2
  73. package/src/components/EditorToolbar/util.ts +3 -3
  74. package/src/defaults.ts +5 -3
  75. package/src/extensions/automerge/automerge.stories.tsx +3 -11
  76. package/src/extensions/command/action.ts +49 -0
  77. package/src/extensions/command/command.ts +9 -27
  78. package/src/extensions/command/hint.ts +33 -30
  79. package/src/extensions/command/index.ts +1 -0
  80. package/src/extensions/command/menu.ts +11 -8
  81. package/src/extensions/command/state.ts +41 -61
  82. package/src/extensions/comments.ts +9 -9
  83. package/src/extensions/folding.tsx +1 -1
  84. package/src/extensions/index.ts +1 -0
  85. package/src/extensions/markdown/decorate.ts +4 -3
  86. package/src/extensions/markdown/formatting.ts +2 -2
  87. package/src/extensions/markdown/image.ts +12 -11
  88. package/src/extensions/markdown/link.ts +33 -24
  89. package/src/extensions/preview/index.ts +5 -0
  90. package/src/extensions/preview/preview.ts +271 -0
  91. package/src/hooks/useTextEditor.ts +4 -3
  92. package/src/{InputMode.stories.tsx → stories/InputMode.stories.tsx} +4 -4
  93. package/src/stories/TextEditorBasic.stories.tsx +289 -0
  94. package/src/stories/TextEditorComments.stories.tsx +99 -0
  95. package/src/stories/TextEditorPreview.stories.tsx +239 -0
  96. package/src/stories/TextEditorSpecial.stories.tsx +107 -0
  97. package/src/stories/story-utils.tsx +329 -0
  98. package/src/testing/RefPopover.tsx +74 -0
  99. package/src/testing/index.ts +5 -0
  100. package/src/types.ts +7 -0
  101. package/src/util/react.tsx +20 -2
  102. package/dist/types/src/InputMode.stories.d.ts.map +0 -1
  103. package/dist/types/src/TextEditor.stories.d.ts.map +0 -1
  104. package/dist/types/src/components/EditorToolbar/viewMode.d.ts.map +0 -1
  105. package/dist/types/src/extensions/command/preview.d.ts +0 -12
  106. package/dist/types/src/extensions/command/preview.d.ts.map +0 -1
  107. package/dist/types/src/fragments.d.ts +0 -3
  108. package/dist/types/src/fragments.d.ts.map +0 -1
  109. package/src/TextEditor.stories.tsx +0 -856
  110. package/src/extensions/command/preview.ts +0 -79
  111. package/src/fragments.ts +0 -19
  112. /package/src/components/EditorToolbar/{viewMode.ts → view-mode.ts} +0 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dxos/react-ui-editor",
3
- "version": "0.8.1",
3
+ "version": "0.8.2-main.f081794",
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",
@@ -13,6 +13,11 @@
13
13
  "types": "./dist/types/src/index.d.ts",
14
14
  "browser": "./dist/lib/browser/index.mjs",
15
15
  "node": "./dist/lib/node-esm/index.mjs"
16
+ },
17
+ "./testing": {
18
+ "types": "./dist/types/src/testing/index.d.ts",
19
+ "browser": "./dist/lib/browser/testing/index.mjs",
20
+ "node": "./dist/lib/node-esm/testing/index.mjs"
16
21
  }
17
22
  },
18
23
  "types": "dist/types/src/index.d.ts",
@@ -51,20 +56,21 @@
51
56
  "lodash.merge": "^4.6.2",
52
57
  "lodash.sortby": "^4.7.0",
53
58
  "style-mod": "^4.1.0",
54
- "@dxos/app-graph": "0.8.1",
55
- "@dxos/async": "0.8.1",
56
- "@dxos/automerge": "0.8.1",
57
- "@dxos/context": "0.8.1",
58
- "@dxos/debug": "0.8.1",
59
- "@dxos/display-name": "0.8.1",
60
- "@dxos/echo-schema": "0.8.1",
61
- "@dxos/invariant": "0.8.1",
62
- "@dxos/live-object": "0.8.1",
63
- "@dxos/log": "0.8.1",
64
- "@dxos/protocols": "0.8.1",
65
- "@dxos/react-ui-menu": "0.8.1",
66
- "@dxos/react-hooks": "0.8.1",
67
- "@dxos/util": "0.8.1"
59
+ "@dxos/app-graph": "0.8.2-main.f081794",
60
+ "@dxos/automerge": "0.8.2-main.f081794",
61
+ "@dxos/async": "0.8.2-main.f081794",
62
+ "@dxos/context": "0.8.2-main.f081794",
63
+ "@dxos/debug": "0.8.2-main.f081794",
64
+ "@dxos/display-name": "0.8.2-main.f081794",
65
+ "@dxos/invariant": "0.8.2-main.f081794",
66
+ "@dxos/live-object": "0.8.2-main.f081794",
67
+ "@dxos/lit-ui": "0.8.2-main.f081794",
68
+ "@dxos/log": "0.8.2-main.f081794",
69
+ "@dxos/protocols": "0.8.2-main.f081794",
70
+ "@dxos/echo-schema": "0.8.2-main.f081794",
71
+ "@dxos/react-hooks": "0.8.2-main.f081794",
72
+ "@dxos/util": "0.8.2-main.f081794",
73
+ "@dxos/react-ui-menu": "0.8.2-main.f081794"
68
74
  },
69
75
  "devDependencies": {
70
76
  "@phosphor-icons/react": "^2.1.5",
@@ -88,23 +94,23 @@
88
94
  "vite": "5.4.7",
89
95
  "vite-plugin-top-level-await": "^1.4.1",
90
96
  "vite-plugin-wasm": "^3.3.0",
91
- "@dxos/automerge": "0.8.1",
92
- "@dxos/echo-signals": "0.8.1",
93
- "@dxos/config": "0.8.1",
94
- "@dxos/keyboard": "0.8.1",
95
- "@dxos/random": "0.8.1",
96
- "@dxos/react-client": "0.8.1",
97
- "@dxos/storybook-utils": "0.8.1",
98
- "@dxos/react-ui-theme": "0.8.1",
99
- "@dxos/react-ui": "0.8.1"
97
+ "@dxos/automerge": "0.8.2-main.f081794",
98
+ "@dxos/config": "0.8.2-main.f081794",
99
+ "@dxos/echo-signals": "0.8.2-main.f081794",
100
+ "@dxos/keyboard": "0.8.2-main.f081794",
101
+ "@dxos/random": "0.8.2-main.f081794",
102
+ "@dxos/react-client": "0.8.2-main.f081794",
103
+ "@dxos/react-ui": "0.8.2-main.f081794",
104
+ "@dxos/storybook-utils": "0.8.2-main.f081794",
105
+ "@dxos/react-ui-theme": "0.8.2-main.f081794"
100
106
  },
101
107
  "peerDependencies": {
102
108
  "@phosphor-icons/react": "^2.1.5",
103
109
  "react": "~18.2.0",
104
110
  "react-dom": "~18.2.0",
105
- "@dxos/react-ui-theme": "0.8.1",
106
- "@dxos/react-client": "0.8.1",
107
- "@dxos/react-ui": "0.8.1"
111
+ "@dxos/react-client": "0.8.2-main.f081794",
112
+ "@dxos/react-ui-theme": "0.8.2-main.f081794",
113
+ "@dxos/react-ui": "0.8.2-main.f081794"
108
114
  },
109
115
  "publishConfig": {
110
116
  "access": "public"
@@ -26,8 +26,8 @@ import {
26
26
  type EditorToolbarProps,
27
27
  editorToolbarSearch,
28
28
  } from './util';
29
- import { createViewMode } from './viewMode';
30
- import { stackItemContentToolbarClassNames } from '../../fragments';
29
+ import { createViewMode } from './view-mode';
30
+ import { stackItemContentToolbarClassNames } from '../../defaults';
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 { create, type ReactiveObject } from '@dxos/live-object';
7
+ import { live, type Live } 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(() => create<EditorToolbarState>(initialState), []);
26
+ return useMemo(() => live<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: ReactiveObject<EditorToolbarState>;
40
+ state: Live<EditorToolbarState>;
41
41
  // TODO(wittjosiah): Control positioning.
42
42
  customActions?: () => ActionGraphProps;
43
43
  onAction: (action: EditorAction) => void;
package/src/defaults.ts CHANGED
@@ -17,7 +17,9 @@ 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 editorContent = mx(margin, '!mli-auto w-full max-w-[min(50rem,100%-4rem)]');
20
+ export const editorWidth = '!mli-auto is-full max-is-[min(50rem,100%-4rem)]';
21
+
22
+ export const editorContent = mx(margin, editorWidth);
21
23
 
22
24
  /**
23
25
  * Margin for numbers.
@@ -50,6 +52,6 @@ export const stackItemContentEditorClassNames = (role?: string) =>
50
52
 
51
53
  export const stackItemContentToolbarClassNames = (role?: string) =>
52
54
  mx(
53
- 'attention-surface is-full border-be !border-separator',
54
- role === 'section' && 'sticky block-start-0 z-[1] -mbe-px min-is-0',
55
+ 'attention-surface is-full border-be !border-separator relative z-[1]',
56
+ role === 'section' && 'sticky block-start-0 -mbe-px min-is-0',
55
57
  );
@@ -10,15 +10,7 @@ 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 {
14
- DocAccessor,
15
- Filter,
16
- create,
17
- createDocAccessor,
18
- useQuery,
19
- useSpace,
20
- type Space,
21
- } from '@dxos/react-client/echo';
13
+ import { DocAccessor, Filter, live, createDocAccessor, useQuery, useSpace, type Space } from '@dxos/react-client/echo';
22
14
  import { useIdentity, type Identity } from '@dxos/react-client/halo';
23
15
  import { ClientRepeater, type ClientRepeatedComponentProps } from '@dxos/react-client/testing';
24
16
  import { useThemeContext } from '@dxos/react-ui';
@@ -139,9 +131,9 @@ export const WithEcho = {
139
131
  createSpace
140
132
  onSpaceCreated={async ({ space }) => {
141
133
  space.db.add(
142
- create({
134
+ live({
143
135
  type: 'test',
144
- content: create(Expando, { content: initialContent }),
136
+ content: live(Expando, { content: initialContent }),
145
137
  }),
146
138
  );
147
139
  }}
@@ -0,0 +1,49 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ import { StateEffect } from '@codemirror/state';
6
+ import { type KeyBinding, type Command, type EditorView } from '@codemirror/view';
7
+
8
+ import { commandState } from './state';
9
+
10
+ export type Action =
11
+ | {
12
+ type: 'insert';
13
+ text: string;
14
+ }
15
+ | {
16
+ type: 'cancel';
17
+ };
18
+
19
+ export type ActionHandler = (action: Action) => void;
20
+
21
+ export const openEffect = StateEffect.define<{ pos: number; fullWidth?: boolean }>();
22
+ export const closeEffect = StateEffect.define<null>();
23
+
24
+ export const openCommand: Command = (view: EditorView) => {
25
+ if (view.state.field(commandState, false)) {
26
+ const selection = view.state.selection.main;
27
+ const line = view.state.doc.lineAt(selection.from);
28
+ if (line.from === selection.from && line.from === line.to) {
29
+ view.dispatch({ effects: openEffect.of({ pos: selection.anchor, fullWidth: true }) });
30
+ return true;
31
+ }
32
+ }
33
+
34
+ return false;
35
+ };
36
+
37
+ export const closeCommand: Command = (view: EditorView) => {
38
+ if (view.state.field(commandState, false)) {
39
+ view.dispatch({ effects: closeEffect.of(null) });
40
+ return true;
41
+ }
42
+
43
+ return false;
44
+ };
45
+
46
+ export const commandKeyBindings: readonly KeyBinding[] = [
47
+ { key: '/', run: openCommand },
48
+ { key: 'Escape', run: closeCommand },
49
+ ];
@@ -5,35 +5,25 @@
5
5
  import { type Extension } from '@codemirror/state';
6
6
  import { EditorView, keymap } from '@codemirror/view';
7
7
 
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';
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';
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
- // 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'>;
18
+ export type CommandOptions = Partial<PopupOptions & FloatingMenuOptions & HintOptions>;
28
19
 
29
- export const command = (options: CommandOptions): Extension => {
20
+ export const command = (options: CommandOptions = {}): Extension => {
30
21
  return [
22
+ keymap.of(commandKeyBindings),
31
23
  commandConfig.of(options),
32
24
  commandState,
33
- keymap.of(commandKeyBindings),
34
- preview(options),
35
- floatingMenu(options),
36
- hintViewPlugin(options),
25
+ options.renderMenu ? floatingMenu({ renderMenu: options.renderMenu }) : [],
26
+ options.onHint ? hintViewPlugin({ onHint: options.onHint }) : [],
37
27
  EditorView.focusChangeEffect.of((_, focusing) => {
38
28
  return focusing ? closeEffect.of(null) : null;
39
29
  }),
@@ -41,14 +31,6 @@ export const command = (options: CommandOptions): Extension => {
41
31
  '.cm-tooltip': {
42
32
  background: 'transparent',
43
33
  },
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
- },
52
34
  }),
53
35
  ];
54
36
  };
@@ -5,10 +5,42 @@
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';
9
8
  import { commandState } from './state';
10
9
  import { clientRectsFor, flattenRect } from '../../util';
11
10
 
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
+
12
44
  class CommandHint extends WidgetType {
13
45
  constructor(readonly content: string | HTMLElement) {
14
46
  super();
@@ -48,32 +80,3 @@ class CommandHint extends WidgetType {
48
80
  return false;
49
81
  }
50
82
  }
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,4 +2,5 @@
2
2
  // Copyright 2024 DXOS.org
3
3
  //
4
4
 
5
+ export * from './action';
5
6
  export * from './command';
@@ -4,12 +4,16 @@
4
4
 
5
5
  import { type BlockInfo, type EditorView, ViewPlugin, type ViewUpdate } from '@codemirror/view';
6
6
 
7
- import { type CommandOptions } from './command';
8
- import { closeEffect, openCommand, openEffect } from './state';
7
+ import { closeEffect, openCommand, openEffect } from './action';
8
+ import { type RenderCallback } from '../../types';
9
+
10
+ export type FloatingMenuOptions = {
11
+ renderMenu: RenderCallback<{ onAction: () => void }>;
12
+ };
9
13
 
10
14
  // TODO(burdon): Trigger completion on click.
11
15
  // TODO(burdon): Hide when dialog is open.
12
- export const floatingMenu = (options: CommandOptions) =>
16
+ export const floatingMenu = (options: FloatingMenuOptions) =>
13
17
  ViewPlugin.fromClass(
14
18
  class {
15
19
  button: HTMLElement;
@@ -31,13 +35,11 @@ export const floatingMenu = (options: CommandOptions) =>
31
35
  this.button.style.zIndex = '10';
32
36
  this.button.style.display = 'none';
33
37
 
34
- options.onRenderMenu(this.button, () => {
35
- openCommand(view);
36
- });
38
+ options.renderMenu(this.button, { onAction: () => openCommand(view) }, view);
37
39
  container.appendChild(this.button);
38
40
 
39
41
  // Listen for scroll events.
40
- container.addEventListener('scroll', this.scheduleUpdate);
42
+ container.addEventListener('scroll', this.scheduleUpdate.bind(this));
41
43
  this.scheduleUpdate();
42
44
  }
43
45
 
@@ -56,7 +58,8 @@ export const floatingMenu = (options: CommandOptions) =>
56
58
  if (this.rafId != null) {
57
59
  cancelAnimationFrame(this.rafId);
58
60
  }
59
- this.rafId = requestAnimationFrame(() => this.updateButtonPosition());
61
+
62
+ this.rafId = requestAnimationFrame(this.updateButtonPosition.bind(this));
60
63
  }
61
64
 
62
65
  updateButtonPosition() {
@@ -2,25 +2,24 @@
2
2
  // Copyright 2024 DXOS.org
3
3
  //
4
4
 
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';
5
+ import { StateField } from '@codemirror/state';
6
+ import { showTooltip, type EditorView, type Tooltip, type TooltipView } from '@codemirror/view';
14
7
 
8
+ import { closeEffect, type Action, openEffect } from './action';
15
9
  import { type CommandOptions } from './command';
10
+ import { type RenderCallback } from '../../types';
16
11
  import { singleValueFacet } from '../../util';
17
12
 
13
+ export const commandConfig = singleValueFacet<CommandOptions>();
14
+
15
+ export type PopupOptions = {
16
+ renderDialog: RenderCallback<{ onAction: (action?: Action) => void }>;
17
+ };
18
+
18
19
  type CommandState = {
19
20
  tooltip?: Tooltip | null;
20
21
  };
21
22
 
22
- export const commandConfig = singleValueFacet<CommandOptions>();
23
-
24
23
  export const commandState = StateField.define<CommandState>({
25
24
  create: () => ({}),
26
25
  update: (state, tr) => {
@@ -29,8 +28,8 @@ export const commandState = StateField.define<CommandState>({
29
28
  return {};
30
29
  }
31
30
 
32
- if (effect.is(openEffect)) {
33
- const options = tr.state.facet(commandConfig);
31
+ const { renderDialog } = tr.state.facet(commandConfig);
32
+ if (effect.is(openEffect) && renderDialog) {
34
33
  const { pos, fullWidth } = effect.value;
35
34
  const tooltip: Tooltip = {
36
35
  pos,
@@ -38,38 +37,49 @@ export const commandState = StateField.define<CommandState>({
38
37
  arrow: false,
39
38
  strictSide: true,
40
39
  create: (view: EditorView) => {
41
- const dom = document.createElement('div');
40
+ const root = document.createElement('div');
41
+
42
42
  const tooltipView: TooltipView = {
43
- dom,
43
+ dom: root,
44
44
  mount: (view: EditorView) => {
45
45
  if (fullWidth) {
46
- const parent = dom.parentElement!;
46
+ const parent = root.parentElement!;
47
47
  const { paddingLeft, paddingRight } = window.getComputedStyle(parent);
48
48
  const widthWithoutPadding = parent.clientWidth - parseFloat(paddingLeft) - parseFloat(paddingRight);
49
- dom.style.width = `${widthWithoutPadding}px`;
49
+ root.style.width = `${widthWithoutPadding}px`;
50
50
  }
51
51
 
52
52
  // Render react component.
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
- });
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
+ );
67
76
  },
68
77
  };
69
78
 
70
79
  return tooltipView;
71
80
  },
72
81
  };
82
+
73
83
  return { tooltip };
74
84
  }
75
85
  }
@@ -78,33 +88,3 @@ export const commandState = StateField.define<CommandState>({
78
88
  },
79
89
  provide: (field) => [showTooltip.from(field, (value) => value.tooltip ?? null)],
80
90
  });
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 ReactiveObject } from '@dxos/live-object';
28
+ import { type Live } 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 Comment, type Range } from '../types';
34
+ import { type RenderCallback, type Comment, type Range } from '../types';
35
35
  import { Cursor, overlap, singleValueFacet, callbackWrapper } from '../util';
36
36
 
37
37
  //
@@ -345,6 +345,10 @@ 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 }>;
348
352
  /**
349
353
  * Called to create a new thread and return the thread id.
350
354
  */
@@ -361,10 +365,6 @@ export type CommentsOptions = {
361
365
  * Called to notify which thread is currently closest to the cursor.
362
366
  */
363
367
  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.onHover &&
411
+ options.renderTooltip &&
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.onHover!(el, shortcut);
422
+ options.renderTooltip!(el, { shortcut }, view);
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: ReactiveObject<EditorToolbarState>): Extension => {
609
+ export const useCommentState = (state: Live<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--regular' size={3} classNames={['mx-3 cursor-pointer', open && 'rotate-90']} />,
32
+ <Icon icon='ph--caret-right--bold' size={3} classNames={['mx-3 cursor-pointer', open && 'rotate-90']} />,
33
33
  );
34
34
  },
35
35
  }),
@@ -18,5 +18,6 @@ export * from './listener';
18
18
  export * from './markdown';
19
19
  export * from './mention';
20
20
  export * from './modes';
21
+ export * from './preview';
21
22
  export * from './selection';
22
23
  export * from './typewriter';