@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.
- package/dist/lib/index.mjs +1477 -0
- package/dist/lib/index.mjs.map +1 -0
- package/dist/lib/translations.mjs +32 -0
- package/dist/lib/translations.mjs.map +1 -0
- package/dist/types/src/components/Editor/Editor.d.ts +22 -17
- package/dist/types/src/components/Editor/Editor.d.ts.map +1 -1
- package/dist/types/src/components/Editor/EditorContext.d.ts +20 -0
- package/dist/types/src/components/Editor/EditorContext.d.ts.map +1 -0
- package/dist/types/src/components/Editor/EditorView.d.ts +1 -2
- package/dist/types/src/components/Editor/EditorView.d.ts.map +1 -1
- package/dist/types/src/components/Editor/index.d.ts +2 -0
- package/dist/types/src/components/Editor/index.d.ts.map +1 -1
- package/dist/types/src/components/EditorMenuProvider/EditorMenuProvider.d.ts +7 -1
- package/dist/types/src/components/EditorMenuProvider/EditorMenuProvider.d.ts.map +1 -1
- package/dist/types/src/components/EditorMenuProvider/menu.d.ts +4 -2
- package/dist/types/src/components/EditorMenuProvider/menu.d.ts.map +1 -1
- package/dist/types/src/components/EditorMenuProvider/useEditorMenu.d.ts +7 -2
- package/dist/types/src/components/EditorMenuProvider/useEditorMenu.d.ts.map +1 -1
- package/dist/types/src/components/EditorPreviewProvider/EditorPreviewContext.d.ts +18 -0
- package/dist/types/src/components/EditorPreviewProvider/EditorPreviewContext.d.ts.map +1 -0
- package/dist/types/src/components/EditorPreviewProvider/EditorPreviewProvider.d.ts +0 -6
- package/dist/types/src/components/EditorPreviewProvider/EditorPreviewProvider.d.ts.map +1 -1
- package/dist/types/src/components/EditorPreviewProvider/index.d.ts +1 -0
- package/dist/types/src/components/EditorPreviewProvider/index.d.ts.map +1 -1
- package/dist/types/src/components/EditorToolbar/EditorToolbar.d.ts +4 -1
- package/dist/types/src/components/EditorToolbar/EditorToolbar.d.ts.map +1 -1
- package/dist/types/src/components/EditorToolbar/index.d.ts +1 -0
- package/dist/types/src/components/EditorToolbar/index.d.ts.map +1 -1
- package/dist/types/src/components/EditorToolbar/lists.d.ts.map +1 -1
- package/dist/types/src/components/EditorToolbar/view-mode.d.ts +27 -2
- package/dist/types/src/components/EditorToolbar/view-mode.d.ts.map +1 -1
- package/dist/types/src/components/index.d.ts +1 -0
- package/dist/types/src/components/index.d.ts.map +1 -1
- package/dist/types/src/index.d.ts +0 -1
- package/dist/types/src/index.d.ts.map +1 -1
- package/dist/types/src/stories/Assistant.stories.d.ts.map +1 -0
- package/dist/types/src/stories/Automerge.stories.d.ts +16 -39
- package/dist/types/src/stories/Automerge.stories.d.ts.map +1 -1
- package/dist/types/src/stories/Blocks.stories.d.ts +10 -0
- package/dist/types/src/stories/Blocks.stories.d.ts.map +1 -0
- package/dist/types/src/stories/Comments.stories.d.ts +53 -14
- package/dist/types/src/stories/Comments.stories.d.ts.map +1 -1
- package/dist/types/src/stories/Diff.stories.d.ts +32 -0
- package/dist/types/src/stories/Diff.stories.d.ts.map +1 -0
- package/dist/types/src/stories/Experimental.stories.d.ts +1 -0
- package/dist/types/src/stories/Experimental.stories.d.ts.map +1 -1
- package/dist/types/src/stories/Folding.stories.d.ts +18 -0
- package/dist/types/src/stories/Folding.stories.d.ts.map +1 -0
- package/dist/types/src/stories/Outliner.stories.d.ts +4 -4
- package/dist/types/src/stories/Outliner.stories.d.ts.map +1 -1
- package/dist/types/src/stories/Popover.stories.d.ts +3 -3
- package/dist/types/src/stories/Popover.stories.d.ts.map +1 -1
- package/dist/types/src/stories/Suggest.stories.d.ts +27 -0
- package/dist/types/src/stories/Suggest.stories.d.ts.map +1 -0
- package/dist/types/src/stories/TrackChanges.stories.d.ts +44 -0
- package/dist/types/src/stories/TrackChanges.stories.d.ts.map +1 -0
- package/dist/types/src/stories/Widgets.stories.d.ts +12 -0
- package/dist/types/src/stories/Widgets.stories.d.ts.map +1 -1
- package/dist/types/src/stories/components/EditorStory.d.ts.map +1 -1
- package/dist/types/src/stories/components/util.d.ts +16 -0
- package/dist/types/src/stories/components/util.d.ts.map +1 -1
- package/dist/types/tsconfig.tsbuildinfo +1 -1
- package/package.json +40 -83
- package/src/components/Editor/Editor.tsx +142 -35
- package/src/components/Editor/EditorContext.ts +34 -0
- package/src/components/Editor/EditorView.tsx +9 -6
- package/src/components/Editor/index.ts +3 -0
- package/src/components/EditorMenuProvider/EditorMenuProvider.tsx +98 -8
- package/src/components/EditorMenuProvider/menu.ts +4 -2
- package/src/components/EditorMenuProvider/useEditorMenu.ts +114 -29
- package/src/components/EditorPreviewProvider/EditorPreviewContext.ts +21 -0
- package/src/components/EditorPreviewProvider/EditorPreviewProvider.tsx +1 -10
- package/src/components/EditorPreviewProvider/index.ts +2 -0
- package/src/components/EditorToolbar/EditorToolbar.tsx +6 -3
- package/src/components/EditorToolbar/index.ts +1 -0
- package/src/components/EditorToolbar/lists.ts +2 -6
- package/src/components/EditorToolbar/view-mode.ts +41 -12
- package/src/components/index.ts +1 -0
- package/src/index.ts +0 -1
- package/src/{extensions → stories}/Assistant.stories.tsx +7 -2
- package/src/stories/Automerge.stories.tsx +99 -78
- package/src/stories/Blocks.stories.tsx +83 -0
- package/src/stories/Comments.stories.tsx +273 -74
- package/src/stories/Diff.stories.tsx +48 -0
- package/src/stories/EditorToolbar.stories.tsx +2 -2
- package/src/stories/Experimental.stories.tsx +13 -2
- package/src/stories/Folding.stories.tsx +120 -0
- package/src/stories/Markdown.stories.tsx +1 -1
- package/src/stories/Outliner.stories.tsx +18 -7
- package/src/stories/Popover.stories.tsx +27 -5
- package/src/stories/Suggest.stories.tsx +199 -0
- package/src/stories/TrackChanges.stories.tsx +226 -0
- package/src/stories/Widgets.stories.tsx +162 -10
- package/src/stories/components/EditorStory.tsx +15 -6
- package/src/stories/components/util.tsx +40 -2
- package/dist/lib/browser/index.mjs +0 -1676
- package/dist/lib/browser/index.mjs.map +0 -7
- package/dist/lib/browser/meta.json +0 -1
- package/dist/lib/browser/translations.mjs +0 -39
- package/dist/lib/browser/translations.mjs.map +0 -7
- package/dist/lib/node-esm/index.mjs +0 -1678
- package/dist/lib/node-esm/index.mjs.map +0 -7
- package/dist/lib/node-esm/meta.json +0 -1
- package/dist/lib/node-esm/translations.mjs +0 -41
- package/dist/lib/node-esm/translations.mjs.map +0 -7
- package/dist/types/src/extensions/Assistant.stories.d.ts.map +0 -1
- package/dist/types/src/extensions/assistant-extension.d.ts +0 -24
- package/dist/types/src/extensions/assistant-extension.d.ts.map +0 -1
- package/dist/types/src/extensions/index.d.ts +0 -2
- package/dist/types/src/extensions/index.d.ts.map +0 -1
- package/src/extensions/assistant-extension.tsx +0 -223
- package/src/extensions/index.ts +0 -5
- /package/dist/types/src/{extensions → stories}/Assistant.stories.d.ts +0 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dxos/react-ui-editor",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.11.0",
|
|
4
4
|
"description": "Text editor components.",
|
|
5
5
|
"homepage": "https://dxos.org",
|
|
6
6
|
"bugs": "https://github.com/dxos/dxos/issues",
|
|
@@ -19,14 +19,12 @@
|
|
|
19
19
|
".": {
|
|
20
20
|
"source": "./src/index.ts",
|
|
21
21
|
"types": "./dist/types/src/index.d.ts",
|
|
22
|
-
"
|
|
23
|
-
"node": "./dist/lib/node-esm/index.mjs"
|
|
22
|
+
"import": "./dist/lib/index.mjs"
|
|
24
23
|
},
|
|
25
24
|
"./translations": {
|
|
26
25
|
"source": "./src/translations.ts",
|
|
27
26
|
"types": "./dist/types/src/translations.d.ts",
|
|
28
|
-
"
|
|
29
|
-
"node": "./dist/lib/node-esm/translations.mjs"
|
|
27
|
+
"import": "./dist/lib/translations.mjs"
|
|
30
28
|
}
|
|
31
29
|
},
|
|
32
30
|
"types": "dist/types/src/index.d.ts",
|
|
@@ -35,103 +33,62 @@
|
|
|
35
33
|
"src"
|
|
36
34
|
],
|
|
37
35
|
"dependencies": {
|
|
38
|
-
"@
|
|
39
|
-
"@codemirror/
|
|
40
|
-
"@codemirror/
|
|
41
|
-
"@
|
|
42
|
-
"@codemirror/lang-javascript": "^6.2.5",
|
|
43
|
-
"@codemirror/lang-json": "^6.0.2",
|
|
44
|
-
"@codemirror/lang-markdown": "6.5.0",
|
|
45
|
-
"@codemirror/lang-xml": "^6.1.0",
|
|
46
|
-
"@codemirror/lang-yaml": "^6.1.3",
|
|
47
|
-
"@codemirror/language": "^6.12.3",
|
|
48
|
-
"@codemirror/language-data": "^6.5.2",
|
|
49
|
-
"@codemirror/lint": "6.9.7",
|
|
50
|
-
"@codemirror/search": "^6.7.0",
|
|
51
|
-
"@codemirror/state": "^6.6.0",
|
|
52
|
-
"@codemirror/theme-one-dark": "^6.1.3",
|
|
53
|
-
"@codemirror/view": "^6.43.0",
|
|
54
|
-
"@fluentui/react-tabster": "9.26.11",
|
|
55
|
-
"@lezer/common": "^1.5.2",
|
|
56
|
-
"@lezer/generator": "^1.8.0",
|
|
57
|
-
"@lezer/highlight": "^1.2.3",
|
|
58
|
-
"@lezer/markdown": "^1.6.3",
|
|
36
|
+
"@codemirror/search": "^6.7.1",
|
|
37
|
+
"@codemirror/state": "^6.7.1",
|
|
38
|
+
"@codemirror/view": "^6.43.6",
|
|
39
|
+
"@effect-atom/atom": "^0.5.3",
|
|
59
40
|
"@radix-ui/react-context": "1.1.1",
|
|
60
41
|
"@radix-ui/react-use-controllable-state": "1.1.0",
|
|
61
|
-
"@
|
|
62
|
-
"@
|
|
63
|
-
"@
|
|
64
|
-
"
|
|
65
|
-
"
|
|
66
|
-
"
|
|
67
|
-
"
|
|
68
|
-
"
|
|
69
|
-
"lodash.merge": "^4.6.2",
|
|
70
|
-
"lodash.sortby": "^4.7.0",
|
|
71
|
-
"style-mod": "^4.1.0",
|
|
72
|
-
"@dxos/app-graph": "0.10.0",
|
|
73
|
-
"@dxos/async": "0.10.0",
|
|
74
|
-
"@dxos/context": "0.10.0",
|
|
75
|
-
"@dxos/debug": "0.10.0",
|
|
76
|
-
"@dxos/echo": "0.10.0",
|
|
77
|
-
"@dxos/echo-client": "0.10.0",
|
|
78
|
-
"@dxos/client": "0.10.0",
|
|
79
|
-
"@dxos/echo-doc": "0.10.0",
|
|
80
|
-
"@dxos/invariant": "0.10.0",
|
|
81
|
-
"@dxos/effect": "0.10.0",
|
|
82
|
-
"@dxos/log": "0.10.0",
|
|
83
|
-
"@dxos/lit-ui": "0.10.0",
|
|
84
|
-
"@dxos/keys": "0.10.0",
|
|
85
|
-
"@dxos/protocols": "0.10.0",
|
|
86
|
-
"@dxos/react-hooks": "0.10.0",
|
|
87
|
-
"@dxos/display-name": "0.10.0",
|
|
88
|
-
"@dxos/react-ui-menu": "0.10.0",
|
|
89
|
-
"@dxos/react-ui-mosaic": "0.10.0",
|
|
90
|
-
"@dxos/ui": "0.10.0",
|
|
91
|
-
"@dxos/ui-editor": "0.10.0",
|
|
92
|
-
"@dxos/util": "0.10.0",
|
|
93
|
-
"@dxos/ui-theme": "0.10.0"
|
|
42
|
+
"@dxos/app-graph": "0.11.0",
|
|
43
|
+
"@dxos/async": "0.11.0",
|
|
44
|
+
"@dxos/echo-react": "0.11.0",
|
|
45
|
+
"@dxos/log": "0.11.0",
|
|
46
|
+
"@dxos/invariant": "0.11.0",
|
|
47
|
+
"@dxos/react-ui-menu": "0.11.0",
|
|
48
|
+
"@dxos/ui-editor": "0.11.0",
|
|
49
|
+
"@dxos/util": "0.11.0"
|
|
94
50
|
},
|
|
95
51
|
"devDependencies": {
|
|
96
|
-
"@
|
|
97
|
-
"@
|
|
98
|
-
"@
|
|
52
|
+
"@codemirror/autocomplete": "^6.20.3",
|
|
53
|
+
"@codemirror/lang-javascript": "^6.2.5",
|
|
54
|
+
"@codemirror/lang-markdown": "6.5.1",
|
|
99
55
|
"@effect-atom/atom-react": "^0.5.0",
|
|
100
56
|
"@effect/ai": "0.36.0",
|
|
101
57
|
"@effect/platform": "0.96.2",
|
|
102
58
|
"@types/chai": "^4.2.15",
|
|
103
59
|
"@types/chai-dom": "^1.11.0",
|
|
104
60
|
"@types/lodash.defaultsdeep": "^4.6.6",
|
|
105
|
-
"@types/lodash.merge": "^4.6.6",
|
|
106
|
-
"@types/lodash.sortby": "^4.7.7",
|
|
107
61
|
"@types/react": "~19.2.17",
|
|
108
62
|
"@types/react-dom": "~19.2.3",
|
|
109
|
-
"@types/react-test-renderer": "^17.0.2",
|
|
110
63
|
"chai": "^4.4.1",
|
|
111
64
|
"chai-dom": "^1.11.0",
|
|
112
65
|
"effect": "3.21.4",
|
|
113
66
|
"happy-dom": "^20.0.0",
|
|
114
67
|
"jsdom": "^29.1.1",
|
|
68
|
+
"lodash.defaultsdeep": "^4.6.1",
|
|
115
69
|
"mocha": "^10.6.0",
|
|
116
70
|
"react": "~19.2.7",
|
|
117
71
|
"react-dom": "~19.2.7",
|
|
118
|
-
"
|
|
119
|
-
"vite": "^8.0.16",
|
|
72
|
+
"vite": "^8.1.4",
|
|
120
73
|
"vite-plugin-top-level-await": "^1.6.0",
|
|
121
74
|
"vite-plugin-wasm": "^3.6.0",
|
|
122
|
-
"@dxos/ai": "0.
|
|
123
|
-
"@dxos/
|
|
124
|
-
"@dxos/
|
|
125
|
-
"@dxos/
|
|
126
|
-
"@dxos/
|
|
127
|
-
"@dxos/
|
|
128
|
-
"@dxos/
|
|
129
|
-
"@dxos/react-
|
|
130
|
-
"@dxos/react-ui
|
|
131
|
-
"@dxos/
|
|
132
|
-
"@dxos/
|
|
133
|
-
"@dxos/ui-
|
|
134
|
-
"@dxos/
|
|
75
|
+
"@dxos/ai": "0.11.0",
|
|
76
|
+
"@dxos/echo": "0.11.0",
|
|
77
|
+
"@dxos/echo-client": "0.11.0",
|
|
78
|
+
"@dxos/keys": "0.11.0",
|
|
79
|
+
"@dxos/protocols": "0.11.0",
|
|
80
|
+
"@dxos/echo-doc": "0.11.0",
|
|
81
|
+
"@dxos/random": "0.11.0",
|
|
82
|
+
"@dxos/react-client": "0.11.0",
|
|
83
|
+
"@dxos/react-ui": "0.11.0",
|
|
84
|
+
"@dxos/react-ui-attention": "0.11.0",
|
|
85
|
+
"@dxos/react-ui-list": "0.11.0",
|
|
86
|
+
"@dxos/react-ui-syntax-highlighter": "0.11.0",
|
|
87
|
+
"@dxos/schema": "0.11.0",
|
|
88
|
+
"@dxos/ui-theme": "0.11.0",
|
|
89
|
+
"@dxos/storybook-utils": "0.11.0",
|
|
90
|
+
"@dxos/ui": "0.11.0",
|
|
91
|
+
"@dxos/ui-types": "0.11.0"
|
|
135
92
|
},
|
|
136
93
|
"peerDependencies": {
|
|
137
94
|
"@effect-atom/atom-react": "^0.5.0",
|
|
@@ -139,9 +96,9 @@
|
|
|
139
96
|
"effect": "3.21.4",
|
|
140
97
|
"react": "~19.2.7",
|
|
141
98
|
"react-dom": "~19.2.7",
|
|
142
|
-
"@dxos/react-client": "0.
|
|
143
|
-
"@dxos/ui
|
|
144
|
-
"@dxos/
|
|
99
|
+
"@dxos/react-client": "^0.11.0",
|
|
100
|
+
"@dxos/react-ui": "^0.11.0",
|
|
101
|
+
"@dxos/ui-theme": "^0.11.0"
|
|
145
102
|
},
|
|
146
103
|
"publishConfig": {
|
|
147
104
|
"access": "public"
|
|
@@ -2,10 +2,19 @@
|
|
|
2
2
|
// Copyright 2025 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
-
import { type Extension } from '@codemirror/state';
|
|
6
|
-
import { Atom } from '@effect-atom/atom
|
|
7
|
-
import {
|
|
8
|
-
|
|
5
|
+
import { type EditorState, type Extension } from '@codemirror/state';
|
|
6
|
+
import { Atom } from '@effect-atom/atom';
|
|
7
|
+
import React, {
|
|
8
|
+
type PropsWithChildren,
|
|
9
|
+
forwardRef,
|
|
10
|
+
useCallback,
|
|
11
|
+
useEffect,
|
|
12
|
+
useImperativeHandle,
|
|
13
|
+
useMemo,
|
|
14
|
+
useRef,
|
|
15
|
+
useState,
|
|
16
|
+
} from 'react';
|
|
17
|
+
import { createPortal } from 'react-dom';
|
|
9
18
|
|
|
10
19
|
import { invariant } from '@dxos/invariant';
|
|
11
20
|
import { type ThemedClassName } from '@dxos/react-ui';
|
|
@@ -23,40 +32,18 @@ import {
|
|
|
23
32
|
EditorToolbar as NaturalEditorToolbar,
|
|
24
33
|
type EditorToolbarProps as NaturalEditorToolbarProps,
|
|
25
34
|
} from '../EditorToolbar';
|
|
26
|
-
import {
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
type EditorViewProps as NaturalEditorContentProps,
|
|
30
|
-
createEditorController,
|
|
31
|
-
noopController,
|
|
32
|
-
} from './EditorView';
|
|
33
|
-
|
|
34
|
-
//
|
|
35
|
-
// Context
|
|
36
|
-
//
|
|
37
|
-
|
|
38
|
-
type EditorContextValue = {
|
|
39
|
-
controller?: EditorController;
|
|
40
|
-
setController: (controller: EditorController) => void;
|
|
41
|
-
extensions?: Extension[];
|
|
42
|
-
state: Atom.Writable<EditorToolbarState>;
|
|
43
|
-
};
|
|
44
|
-
|
|
45
|
-
const [EditorContextProvider, useEditorContext] = createContext<EditorContextValue>('Editor');
|
|
46
|
-
|
|
47
|
-
/**
|
|
48
|
-
* Access the editor context. Must be used within `Editor.Root`.
|
|
49
|
-
*/
|
|
50
|
-
export { useEditorContext };
|
|
35
|
+
import { type EditorController, noopController } from './controller';
|
|
36
|
+
import { EditorContextProvider, type EditorContextValue, useEditorContext } from './EditorContext';
|
|
37
|
+
import { EditorView as NaturalEditorContent, type EditorViewProps as NaturalEditorContentProps } from './EditorView';
|
|
51
38
|
|
|
52
39
|
//
|
|
53
40
|
// Root
|
|
54
41
|
//
|
|
55
42
|
|
|
56
43
|
type EditorRootProps = PropsWithChildren<
|
|
57
|
-
Pick<EditorContextValue, 'extensions'> &
|
|
44
|
+
Pick<EditorContextValue, 'extensions' | 'widgets'> &
|
|
58
45
|
Omit<UseEditorMenuProps, 'viewRef'> &
|
|
59
|
-
Pick<EditorMenuProviderProps, 'numItems'> & {
|
|
46
|
+
Pick<EditorMenuProviderProps, 'numItems' | 'searchPlaceholder'> & {
|
|
60
47
|
viewMode?: EditorToolbarState['viewMode'];
|
|
61
48
|
}
|
|
62
49
|
>;
|
|
@@ -66,7 +53,10 @@ type EditorRootProps = PropsWithChildren<
|
|
|
66
53
|
* Provides context for all child components and manages the editor controller state.
|
|
67
54
|
*/
|
|
68
55
|
const EditorRoot = forwardRef<EditorController | null, EditorRootProps>(
|
|
69
|
-
(
|
|
56
|
+
(
|
|
57
|
+
{ children, extensions: extensionsProp, widgets, viewMode, numItems, searchPlaceholder, ...props },
|
|
58
|
+
forwardedRef,
|
|
59
|
+
) => {
|
|
70
60
|
// TODO(wittjosiah): Including initialState in the deps causes reactivity issues.
|
|
71
61
|
const state = useMemo(() => Atom.make<EditorToolbarState>({ viewMode }), [viewMode]);
|
|
72
62
|
|
|
@@ -87,9 +77,16 @@ const EditorRoot = forwardRef<EditorController | null, EditorRootProps>(
|
|
|
87
77
|
controller={controller}
|
|
88
78
|
setController={setController}
|
|
89
79
|
extensions={extensions}
|
|
80
|
+
widgets={widgets}
|
|
90
81
|
state={state}
|
|
91
82
|
>
|
|
92
|
-
<EditorMenuProvider
|
|
83
|
+
<EditorMenuProvider
|
|
84
|
+
getView={getView}
|
|
85
|
+
groups={groupsRef.current}
|
|
86
|
+
numItems={numItems}
|
|
87
|
+
searchPlaceholder={searchPlaceholder}
|
|
88
|
+
{...menuProps}
|
|
89
|
+
>
|
|
93
90
|
{children}
|
|
94
91
|
</EditorMenuProvider>
|
|
95
92
|
</EditorContextProvider>
|
|
@@ -164,6 +161,116 @@ const EditorToolbar = (props: EditorToolbarProps) => {
|
|
|
164
161
|
|
|
165
162
|
EditorToolbar.displayName = EDITOR_TOOLBAR_NAME;
|
|
166
163
|
|
|
164
|
+
//
|
|
165
|
+
// Blocks
|
|
166
|
+
//
|
|
167
|
+
|
|
168
|
+
const EDITOR_BLOCKS_NAME = 'Editor.Blocks';
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Renders the xmlTags widget portals (embedded blocks) contributed via the editor's `setWidgets`
|
|
172
|
+
* callback. Place inside `Editor.Root`; the widgets come from context (pass them to `Editor.Root`).
|
|
173
|
+
*/
|
|
174
|
+
const EditorBlocks = () => {
|
|
175
|
+
const { widgets = [] } = useEditorContext(EDITOR_BLOCKS_NAME);
|
|
176
|
+
return (
|
|
177
|
+
<>
|
|
178
|
+
{widgets.map(({ id, root, Component, props }) => (
|
|
179
|
+
<div key={id}>{createPortal(<Component {...props} />, root)}</div>
|
|
180
|
+
))}
|
|
181
|
+
</>
|
|
182
|
+
);
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
EditorBlocks.displayName = EDITOR_BLOCKS_NAME;
|
|
186
|
+
|
|
187
|
+
//
|
|
188
|
+
// Diagnostics
|
|
189
|
+
//
|
|
190
|
+
|
|
191
|
+
const EDITOR_DIAGNOSTICS_NAME = 'Editor.Diagnostics';
|
|
192
|
+
|
|
193
|
+
/** Rough leaf count of a (recursively nested) CodeMirror extension tree. */
|
|
194
|
+
const countExtensions = (extension: Extension | undefined): number =>
|
|
195
|
+
extension == null
|
|
196
|
+
? 0
|
|
197
|
+
: Array.isArray(extension)
|
|
198
|
+
? extension.reduce((sum, child) => sum + countExtensions(child), 0)
|
|
199
|
+
: 1;
|
|
200
|
+
|
|
201
|
+
type EditorDiagnosticsProps = ThemedClassName<{}>;
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Developer panel showing live editor state read from the CodeMirror `EditorState` (document size,
|
|
205
|
+
* selection, viewport, configured-extension count). A debugging aid — gate it behind a dev/debug
|
|
206
|
+
* setting; unrelated to `@codemirror/lint` diagnostics. Place inside `Editor.Root`, below the content.
|
|
207
|
+
*/
|
|
208
|
+
const EditorDiagnostics = ({ classNames }: EditorDiagnosticsProps) => {
|
|
209
|
+
const { controller, extensions } = useEditorContext(EDITOR_DIAGNOSTICS_NAME);
|
|
210
|
+
const view = controller?.view ?? null;
|
|
211
|
+
// `EditorState` is immutable (a new object per transaction), so a reference change signals an update.
|
|
212
|
+
// Poll on animation frames while mounted — self-contained (no injected updateListener) and re-renders
|
|
213
|
+
// only when the state actually changes; catches programmatic transactions too.
|
|
214
|
+
const [, setRevision] = useState(0);
|
|
215
|
+
const lastState = useRef<EditorState | null>(null);
|
|
216
|
+
useEffect(() => {
|
|
217
|
+
if (!view) {
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
let frame = 0;
|
|
221
|
+
const tick = () => {
|
|
222
|
+
if (view.state !== lastState.current) {
|
|
223
|
+
lastState.current = view.state;
|
|
224
|
+
setRevision((revision) => revision + 1);
|
|
225
|
+
}
|
|
226
|
+
frame = requestAnimationFrame(tick);
|
|
227
|
+
};
|
|
228
|
+
frame = requestAnimationFrame(tick);
|
|
229
|
+
return () => cancelAnimationFrame(frame);
|
|
230
|
+
}, [view]);
|
|
231
|
+
|
|
232
|
+
if (!view) {
|
|
233
|
+
return null;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
const { state } = view;
|
|
237
|
+
const { doc, selection } = state;
|
|
238
|
+
const { main } = selection;
|
|
239
|
+
const head = doc.lineAt(main.head);
|
|
240
|
+
// The context holds the extensions passed to `Root`; count leaves (CodeMirror does not expose the
|
|
241
|
+
// resolved extension set at runtime, so this is the configured, not the active, count).
|
|
242
|
+
const extensionCount = countExtensions(extensions);
|
|
243
|
+
const item = (label: string, value: string | number) => (
|
|
244
|
+
<span>
|
|
245
|
+
<span className='text-subdued'>{label} </span>
|
|
246
|
+
{value}
|
|
247
|
+
</span>
|
|
248
|
+
);
|
|
249
|
+
|
|
250
|
+
return (
|
|
251
|
+
<div
|
|
252
|
+
className={mx(
|
|
253
|
+
'flex flex-wrap gap-x-4 gap-y-0.5 border-bs border-separator px-2 py-1 text-xs font-mono text-description',
|
|
254
|
+
classNames,
|
|
255
|
+
)}
|
|
256
|
+
data-testid='editor.diagnostics'
|
|
257
|
+
>
|
|
258
|
+
{item('chars', doc.length)}
|
|
259
|
+
{item('lines', doc.lines)}
|
|
260
|
+
{item(
|
|
261
|
+
'cursor',
|
|
262
|
+
main.empty ? `${head.number}:${main.head - head.from + 1}` : `${main.from}–${main.to} (${main.to - main.from})`,
|
|
263
|
+
)}
|
|
264
|
+
{selection.ranges.length > 1 && item('ranges', selection.ranges.length)}
|
|
265
|
+
{item('viewport', `${view.viewport.from}–${view.viewport.to}`)}
|
|
266
|
+
{item('mode', state.readOnly ? 'read-only' : 'editable')}
|
|
267
|
+
{item('extensions', extensionCount)}
|
|
268
|
+
</div>
|
|
269
|
+
);
|
|
270
|
+
};
|
|
271
|
+
|
|
272
|
+
EditorDiagnostics.displayName = EDITOR_DIAGNOSTICS_NAME;
|
|
273
|
+
|
|
167
274
|
//
|
|
168
275
|
// Editor
|
|
169
276
|
//
|
|
@@ -173,6 +280,8 @@ export const Editor = {
|
|
|
173
280
|
Toolbar: EditorToolbar,
|
|
174
281
|
Content: EditorContent,
|
|
175
282
|
View: EditorView,
|
|
283
|
+
Blocks: EditorBlocks,
|
|
284
|
+
Diagnostics: EditorDiagnostics,
|
|
176
285
|
};
|
|
177
286
|
|
|
178
287
|
export type {
|
|
@@ -183,5 +292,3 @@ export type {
|
|
|
183
292
|
EditorToolbarState,
|
|
184
293
|
EditorViewProps,
|
|
185
294
|
};
|
|
186
|
-
|
|
187
|
-
export { createEditorController };
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2025 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { type Extension } from '@codemirror/state';
|
|
6
|
+
import { type Atom } from '@effect-atom/atom-react';
|
|
7
|
+
import { createContext } from '@radix-ui/react-context';
|
|
8
|
+
|
|
9
|
+
import { type XmlWidgetState } from '@dxos/ui-editor';
|
|
10
|
+
|
|
11
|
+
import { type EditorToolbarState } from '../EditorToolbar/types';
|
|
12
|
+
import { type EditorController } from './controller';
|
|
13
|
+
|
|
14
|
+
// Kept out of `Editor.tsx`: react-refresh only fast-refreshes a module whose exports are all
|
|
15
|
+
// components, so a context and its hook exported beside them force a full page reload on every edit.
|
|
16
|
+
|
|
17
|
+
//
|
|
18
|
+
// Context
|
|
19
|
+
//
|
|
20
|
+
|
|
21
|
+
export type EditorContextValue = {
|
|
22
|
+
controller?: EditorController;
|
|
23
|
+
setController: (controller: EditorController) => void;
|
|
24
|
+
extensions?: Extension[];
|
|
25
|
+
/** xmlTags widget portals (embedded blocks); rendered by `Editor.Blocks`, fed via `setWidgets`. */
|
|
26
|
+
widgets?: XmlWidgetState[];
|
|
27
|
+
state: Atom.Writable<EditorToolbarState>;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export const [EditorContextProvider, useEditorContext] = createContext<EditorContextValue>('Editor');
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Access the editor context. Must be used within `Editor.Root`.
|
|
34
|
+
*/
|
|
@@ -11,7 +11,7 @@ import { initialSync } from '@dxos/ui-editor';
|
|
|
11
11
|
import { mx } from '@dxos/ui-theme';
|
|
12
12
|
|
|
13
13
|
import { type UseTextEditorProps, useTextEditor } from '../../hooks';
|
|
14
|
-
import { type EditorController, createEditorController
|
|
14
|
+
import { type EditorController, createEditorController } from './controller';
|
|
15
15
|
|
|
16
16
|
export type EditorViewProps = ThemedClassName<
|
|
17
17
|
{
|
|
@@ -62,11 +62,15 @@ export const EditorView = forwardRef<EditorController, EditorViewProps>(
|
|
|
62
62
|
// disagree. After internal typing the prop will already match the editor's
|
|
63
63
|
// doc, and dispatching anyway would race fast keystrokes — a stale rAF
|
|
64
64
|
// closure can replace doc content with an older value, dropping characters.
|
|
65
|
+
//
|
|
66
|
+
// `undefined` means uncontrolled: the doc was seeded once from `value` at creation and the caller
|
|
67
|
+
// isn't driving it, so leave it be (coalescing undefined→'' here would wipe that seed on mount). A
|
|
68
|
+
// controlled caller clears the doc by passing '' explicitly.
|
|
65
69
|
useEffect(() => {
|
|
66
|
-
if (!view) {
|
|
70
|
+
if (!view || value === undefined) {
|
|
67
71
|
return;
|
|
68
72
|
}
|
|
69
|
-
const next = value
|
|
73
|
+
const next = value;
|
|
70
74
|
if (view.state.doc.toString() === next) {
|
|
71
75
|
return;
|
|
72
76
|
}
|
|
@@ -86,14 +90,13 @@ export const EditorView = forwardRef<EditorController, EditorViewProps>(
|
|
|
86
90
|
});
|
|
87
91
|
}, [view, value, selectionEnd]);
|
|
88
92
|
|
|
93
|
+
// Focus chrome (border/ring) is the caller's responsibility (e.g. `dx-input`).
|
|
89
94
|
return (
|
|
90
95
|
<div
|
|
91
|
-
className={mx('w-full outline-hidden
|
|
96
|
+
className={mx('w-full outline-hidden', classNames)}
|
|
92
97
|
{...(focusable ? focusAttributes : {})}
|
|
93
98
|
ref={parentRef}
|
|
94
99
|
/>
|
|
95
100
|
);
|
|
96
101
|
},
|
|
97
102
|
);
|
|
98
|
-
|
|
99
|
-
export { type EditorController, createEditorController, noopController };
|
|
@@ -4,7 +4,15 @@
|
|
|
4
4
|
|
|
5
5
|
import { type EditorView } from '@codemirror/view';
|
|
6
6
|
import { useControllableState } from '@radix-ui/react-use-controllable-state';
|
|
7
|
-
import React, {
|
|
7
|
+
import React, {
|
|
8
|
+
Fragment,
|
|
9
|
+
type KeyboardEvent,
|
|
10
|
+
type PropsWithChildren,
|
|
11
|
+
useCallback,
|
|
12
|
+
useEffect,
|
|
13
|
+
useRef,
|
|
14
|
+
useState,
|
|
15
|
+
} from 'react';
|
|
8
16
|
|
|
9
17
|
import { addEventListener } from '@dxos/async';
|
|
10
18
|
import { invariant } from '@dxos/invariant';
|
|
@@ -12,6 +20,7 @@ import {
|
|
|
12
20
|
DX_ANCHOR_ACTIVATE,
|
|
13
21
|
type DxAnchorActivate,
|
|
14
22
|
Icon,
|
|
23
|
+
Input,
|
|
15
24
|
Popover,
|
|
16
25
|
ScrollArea,
|
|
17
26
|
toLocalizedString,
|
|
@@ -20,7 +29,9 @@ import {
|
|
|
20
29
|
useTranslation,
|
|
21
30
|
} from '@dxos/react-ui';
|
|
22
31
|
|
|
23
|
-
import {
|
|
32
|
+
import { translationKey } from '#translations';
|
|
33
|
+
|
|
34
|
+
import { type EditorMenuGroup, type EditorMenuItem, getMenuItem } from './menu';
|
|
24
35
|
|
|
25
36
|
export type EditorMenuProviderProps = PropsWithChildren<{
|
|
26
37
|
// Provided as a getter (not a value prop) so the live `EditorView` is never carried in a React prop that
|
|
@@ -31,10 +42,16 @@ export type EditorMenuProviderProps = PropsWithChildren<{
|
|
|
31
42
|
open?: boolean;
|
|
32
43
|
defaultOpen?: boolean;
|
|
33
44
|
numItems?: number;
|
|
45
|
+
/** Render the menu as a combobox: the popover owns the query input and takes focus from the editor. */
|
|
46
|
+
search?: boolean;
|
|
47
|
+
query?: string;
|
|
48
|
+
searchPlaceholder?: string;
|
|
34
49
|
onOpenChange?: (event: { view: EditorView; open: boolean; trigger?: string }) => void;
|
|
35
50
|
onActivate?: (event: { view: EditorView; trigger?: string }) => void;
|
|
36
51
|
onSelect?: (event: { view: EditorView; item: EditorMenuItem }) => void;
|
|
37
52
|
onCancel?: (event: { view: EditorView }) => void;
|
|
53
|
+
onQueryChange?: (query: string) => void;
|
|
54
|
+
onNavigate?: (direction: 'up' | 'down') => void;
|
|
38
55
|
}>;
|
|
39
56
|
|
|
40
57
|
/**
|
|
@@ -50,12 +67,17 @@ export const EditorMenuProvider = ({
|
|
|
50
67
|
open: openProp,
|
|
51
68
|
defaultOpen,
|
|
52
69
|
numItems = 8,
|
|
70
|
+
search,
|
|
71
|
+
query = '',
|
|
72
|
+
searchPlaceholder,
|
|
53
73
|
onOpenChange,
|
|
54
74
|
onActivate,
|
|
55
75
|
onSelect,
|
|
56
76
|
onCancel,
|
|
77
|
+
onQueryChange,
|
|
78
|
+
onNavigate,
|
|
57
79
|
}: EditorMenuProviderProps) => {
|
|
58
|
-
const {
|
|
80
|
+
const { t } = useTranslation(translationKey);
|
|
59
81
|
const triggerRef = useRef<HTMLButtonElement | null>(null);
|
|
60
82
|
|
|
61
83
|
// Hold the latest `getView` so callbacks/effects always read the current view without re-subscribing.
|
|
@@ -106,12 +128,59 @@ export const EditorMenuProvider = ({
|
|
|
106
128
|
const view = getViewRef.current?.();
|
|
107
129
|
invariant(view);
|
|
108
130
|
onSelect?.({ view, item });
|
|
131
|
+
// Close the popover after acting (e.g. Delete) and return focus to the editor; otherwise the menu
|
|
132
|
+
// lingers open over the now-changed document.
|
|
133
|
+
setOpen(false);
|
|
134
|
+
view.focus();
|
|
109
135
|
},
|
|
110
|
-
[getViewRef, onSelect],
|
|
136
|
+
[getViewRef, onSelect, setOpen],
|
|
111
137
|
);
|
|
112
138
|
|
|
113
139
|
const menuGroups = groups?.filter((group) => group.items.length > 0) ?? [];
|
|
114
140
|
|
|
141
|
+
// Handing off from the slash menu refocuses the editor and reopens the popover a frame later, so
|
|
142
|
+
// neither `autoFocus` nor `onOpenAutoFocus` lands on the input. Claim focus a frame after the
|
|
143
|
+
// modal-state dispatch `handleOpenChange` schedules, which otherwise hands focus back to CodeMirror.
|
|
144
|
+
const searchInputRef = useRef<HTMLInputElement>(null);
|
|
145
|
+
useEffect(() => {
|
|
146
|
+
if (!search || !open) {
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
let inner: number;
|
|
151
|
+
const outer = requestAnimationFrame(() => {
|
|
152
|
+
inner = requestAnimationFrame(() => searchInputRef.current?.focus());
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
return () => {
|
|
156
|
+
cancelAnimationFrame(outer);
|
|
157
|
+
cancelAnimationFrame(inner);
|
|
158
|
+
};
|
|
159
|
+
}, [search, open]);
|
|
160
|
+
|
|
161
|
+
const handleSearchKeyDown = useCallback(
|
|
162
|
+
(event: KeyboardEvent<HTMLInputElement>) => {
|
|
163
|
+
switch (event.key) {
|
|
164
|
+
case 'ArrowUp':
|
|
165
|
+
case 'ArrowDown': {
|
|
166
|
+
event.preventDefault();
|
|
167
|
+
onNavigate?.(event.key === 'ArrowUp' ? 'up' : 'down');
|
|
168
|
+
break;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
case 'Enter': {
|
|
172
|
+
const item = getMenuItem(menuGroups, currentItem);
|
|
173
|
+
if (item) {
|
|
174
|
+
event.preventDefault();
|
|
175
|
+
handleSelect(item);
|
|
176
|
+
}
|
|
177
|
+
break;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
},
|
|
181
|
+
[menuGroups, currentItem, handleSelect, onNavigate],
|
|
182
|
+
);
|
|
183
|
+
|
|
115
184
|
return (
|
|
116
185
|
<Popover.Root modal={false} open={open} onOpenChange={setOpen}>
|
|
117
186
|
<Popover.VirtualTrigger virtualRef={triggerRef} />
|
|
@@ -120,9 +189,10 @@ export const EditorMenuProvider = ({
|
|
|
120
189
|
<Popover.Portal>
|
|
121
190
|
<Popover.Content
|
|
122
191
|
align='start'
|
|
123
|
-
classNames={['flex flex-col', !menuGroups.length && 'hidden']}
|
|
192
|
+
classNames={['flex flex-col', !search && !menuGroups.length && 'hidden']}
|
|
124
193
|
style={{
|
|
125
|
-
|
|
194
|
+
// The search input shares the box, so `numItems` keeps meaning "items visible".
|
|
195
|
+
maxBlockSize: 36 * numItems + 10 + (search ? 36 : 0),
|
|
126
196
|
}}
|
|
127
197
|
// NOTE: We keep the focus in the editor, but Radix routes escape key.
|
|
128
198
|
onEscapeKeyDown={() => {
|
|
@@ -131,8 +201,26 @@ export const EditorMenuProvider = ({
|
|
|
131
201
|
onCancel?.({ view: currentView });
|
|
132
202
|
}
|
|
133
203
|
}}
|
|
134
|
-
|
|
204
|
+
// In search mode the query is typed into the popover's own input, so it must take focus.
|
|
205
|
+
onOpenAutoFocus={search ? undefined : (event) => event.preventDefault()}
|
|
135
206
|
>
|
|
207
|
+
{search && (
|
|
208
|
+
<Input.Root>
|
|
209
|
+
<Input.TextInput
|
|
210
|
+
ref={searchInputRef}
|
|
211
|
+
density='sm'
|
|
212
|
+
variant='subdued'
|
|
213
|
+
classNames='shrink-0 mbe-1'
|
|
214
|
+
value={query}
|
|
215
|
+
placeholder={searchPlaceholder}
|
|
216
|
+
// Placeholder text is not a persistent accessible name, and it is optional — fall
|
|
217
|
+
// back to the generic label so the input is never anonymous.
|
|
218
|
+
aria-label={searchPlaceholder ?? t('search.label')}
|
|
219
|
+
onChange={(event) => onQueryChange?.(event.target.value)}
|
|
220
|
+
onKeyDown={handleSearchKeyDown}
|
|
221
|
+
/>
|
|
222
|
+
</Input.Root>
|
|
223
|
+
)}
|
|
136
224
|
<Popover.Viewport asChild classNames='dx-container'>
|
|
137
225
|
<ScrollArea.Root thin>
|
|
138
226
|
<ScrollArea.Viewport>
|
|
@@ -219,7 +307,9 @@ const MenuItem = ({ item, current, onSelect }: MenuItemProps) => {
|
|
|
219
307
|
const listRef = useRef<HTMLLIElement>(null);
|
|
220
308
|
useEffect(() => {
|
|
221
309
|
if (current && listRef.current) {
|
|
222
|
-
|
|
310
|
+
// Instant, not smooth: held arrow keys retarget faster than a smooth scroll can settle, and each
|
|
311
|
+
// new animation supersedes the last, so the list stalls then jumps.
|
|
312
|
+
listRef.current.scrollIntoView({ behavior: 'instant', block: 'nearest' });
|
|
223
313
|
}
|
|
224
314
|
}, [current]);
|
|
225
315
|
|