@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
|
@@ -25,13 +25,15 @@ export const getMenuItem = (groups: EditorMenuGroup[], id?: string): EditorMenuI
|
|
|
25
25
|
return groups.flatMap((group) => group.items).find((item) => item.id === id);
|
|
26
26
|
};
|
|
27
27
|
|
|
28
|
-
|
|
28
|
+
/** Undefined when the menu is empty; at either end the current item is returned unchanged. */
|
|
29
|
+
export const getNextMenuItem = (groups: EditorMenuGroup[], id?: string): EditorMenuItem | undefined => {
|
|
29
30
|
const items = groups.flatMap((group) => group.items);
|
|
30
31
|
const index = items.findIndex((item) => item.id === id);
|
|
31
32
|
return index < items.length - 1 ? items[index + 1] : items[index];
|
|
32
33
|
};
|
|
33
34
|
|
|
34
|
-
|
|
35
|
+
/** Undefined when the menu is empty; at either end the current item is returned unchanged. */
|
|
36
|
+
export const getPreviousMenuItem = (groups: EditorMenuGroup[], id?: string): EditorMenuItem | undefined => {
|
|
35
37
|
const items = groups.flatMap((group) => group.items);
|
|
36
38
|
const index = items.findIndex((item) => item.id === id);
|
|
37
39
|
return index > 0 ? items[index - 1] : items[index];
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
import { type Extension } from '@codemirror/state';
|
|
6
6
|
import { type EditorState } from '@codemirror/state';
|
|
7
|
+
import { type EditorView } from '@codemirror/view';
|
|
7
8
|
import { type RefObject, useCallback, useMemo, useRef, useState } from 'react';
|
|
8
9
|
|
|
9
10
|
import { invariant } from '@dxos/invariant';
|
|
@@ -24,13 +25,30 @@ export type GetMenuContext = {
|
|
|
24
25
|
|
|
25
26
|
export type UseEditorMenuProps = {
|
|
26
27
|
filter?: boolean;
|
|
28
|
+
/**
|
|
29
|
+
* Triggers whose menu is rendered as a combobox: the popover owns a search input and the query is
|
|
30
|
+
* typed there rather than into the document.
|
|
31
|
+
*/
|
|
32
|
+
searchTriggers?: string[];
|
|
27
33
|
getMenu?: (context: GetMenuContext) => MaybePromise<EditorMenuGroup[]>;
|
|
28
34
|
} & Pick<PopoverOptions, 'trigger' | 'triggerKey' | 'placeholder' | 'activateOnTyping'>;
|
|
29
35
|
|
|
30
36
|
export type UseEditorMenu = {
|
|
31
37
|
groupsRef: RefObject<EditorMenuGroup[]>;
|
|
32
38
|
extension: Extension;
|
|
33
|
-
} & Pick<
|
|
39
|
+
} & Pick<
|
|
40
|
+
EditorMenuProviderProps,
|
|
41
|
+
| 'currentItem'
|
|
42
|
+
| 'open'
|
|
43
|
+
| 'search'
|
|
44
|
+
| 'query'
|
|
45
|
+
| 'onOpenChange'
|
|
46
|
+
| 'onActivate'
|
|
47
|
+
| 'onSelect'
|
|
48
|
+
| 'onCancel'
|
|
49
|
+
| 'onQueryChange'
|
|
50
|
+
| 'onNavigate'
|
|
51
|
+
>;
|
|
34
52
|
|
|
35
53
|
/**
|
|
36
54
|
* ```tsx
|
|
@@ -49,14 +67,24 @@ export const useEditorMenu = ({
|
|
|
49
67
|
placeholder,
|
|
50
68
|
activateOnTyping,
|
|
51
69
|
filter = true,
|
|
70
|
+
searchTriggers,
|
|
52
71
|
getMenu,
|
|
53
72
|
}: UseEditorMenuProps): UseEditorMenu => {
|
|
54
73
|
const groupsRef = useRef<EditorMenuGroup[]>([]);
|
|
55
74
|
const currentRef = useRef<EditorMenuItem | null>(null);
|
|
56
75
|
const [currentItem, setCurrentItem] = useState<string>();
|
|
57
76
|
const [open, setOpen] = useState(false);
|
|
77
|
+
const [search, setSearch] = useState(false);
|
|
78
|
+
const [query, setQuery] = useState('');
|
|
58
79
|
const [_, refresh] = useState({});
|
|
59
80
|
|
|
81
|
+
// The document range only supplies the trigger-relative prefix (e.g. the second "@" of a block
|
|
82
|
+
// embed); in search mode the rest of the query comes from the popover's input.
|
|
83
|
+
const contextRef = useRef<{ view: EditorView; pos: number; text: string; trigger?: string } | null>(null);
|
|
84
|
+
|
|
85
|
+
// Monotonic token identifying the latest menu query; bumped per request and on close.
|
|
86
|
+
const requestRef = useRef(0);
|
|
87
|
+
|
|
60
88
|
/**
|
|
61
89
|
* Get filtered options.
|
|
62
90
|
*/
|
|
@@ -73,12 +101,49 @@ export const useEditorMenu = ({
|
|
|
73
101
|
[getMenu, filter],
|
|
74
102
|
);
|
|
75
103
|
|
|
104
|
+
/**
|
|
105
|
+
* Recompute the menu and preselect the first item.
|
|
106
|
+
*/
|
|
107
|
+
const updateGroups = useCallback(
|
|
108
|
+
async ({ view, pos, text, trigger }: { view: EditorView; pos: number; text: string; trigger?: string }) => {
|
|
109
|
+
const request = ++requestRef.current;
|
|
110
|
+
const groups = (await getMenuOptions({ state: view.state, pos, text, trigger })) ?? [];
|
|
111
|
+
// Queries resolve out of order, so a slower earlier one must not overwrite a newer result
|
|
112
|
+
// (or repopulate the menu after it closed).
|
|
113
|
+
if (request !== requestRef.current) {
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
groupsRef.current = groups;
|
|
118
|
+
const firstItem = groupsRef.current.filter((group) => group.items.length > 0)[0]?.items[0];
|
|
119
|
+
if (firstItem) {
|
|
120
|
+
setCurrentItem(firstItem.id);
|
|
121
|
+
currentRef.current = firstItem;
|
|
122
|
+
} else {
|
|
123
|
+
// No matches: clear the selection so Enter falls through instead of
|
|
124
|
+
// selecting a stale item from a previous query.
|
|
125
|
+
setCurrentItem(undefined);
|
|
126
|
+
currentRef.current = null;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
refresh({});
|
|
130
|
+
},
|
|
131
|
+
[getMenuOptions],
|
|
132
|
+
);
|
|
133
|
+
|
|
76
134
|
const handleOpenChange = useCallback<NonNullable<UseEditorMenu['onOpenChange']>>(
|
|
77
135
|
async ({ view, open }) => {
|
|
78
136
|
invariant(view);
|
|
79
137
|
setOpen(open);
|
|
80
138
|
if (!open) {
|
|
81
139
|
setCurrentItem(undefined);
|
|
140
|
+
setSearch(false);
|
|
141
|
+
setQuery('');
|
|
142
|
+
contextRef.current = null;
|
|
143
|
+
// Invalidate in-flight queries so a late response cannot repopulate the closed menu.
|
|
144
|
+
requestRef.current++;
|
|
145
|
+
groupsRef.current = [];
|
|
146
|
+
currentRef.current = null;
|
|
82
147
|
view.dispatch({
|
|
83
148
|
effects: [popoverRangeEffect.of(null)],
|
|
84
149
|
});
|
|
@@ -129,7 +194,35 @@ export const useEditorMenu = ({
|
|
|
129
194
|
}
|
|
130
195
|
}, []);
|
|
131
196
|
|
|
197
|
+
const handleQueryChange = useCallback<NonNullable<UseEditorMenu['onQueryChange']>>(
|
|
198
|
+
(query) => {
|
|
199
|
+
setQuery(query);
|
|
200
|
+
const context = contextRef.current;
|
|
201
|
+
if (context) {
|
|
202
|
+
void updateGroups({ ...context, text: context.text + query });
|
|
203
|
+
}
|
|
204
|
+
},
|
|
205
|
+
[updateGroups],
|
|
206
|
+
);
|
|
207
|
+
|
|
208
|
+
const handleNavigate = useCallback<NonNullable<UseEditorMenu['onNavigate']>>((direction) => {
|
|
209
|
+
setCurrentItem((currentItem) => {
|
|
210
|
+
const next =
|
|
211
|
+
direction === 'up'
|
|
212
|
+
? getPreviousMenuItem(groupsRef.current, currentItem)
|
|
213
|
+
: getNextMenuItem(groupsRef.current, currentItem);
|
|
214
|
+
// A query that matches nothing leaves an empty menu; arrow keys then have nowhere to go.
|
|
215
|
+
if (!next) {
|
|
216
|
+
return currentItem;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
currentRef.current = next;
|
|
220
|
+
return next.id;
|
|
221
|
+
});
|
|
222
|
+
}, []);
|
|
223
|
+
|
|
132
224
|
const serializedTrigger = Array.isArray(trigger) ? trigger.join(',') : trigger;
|
|
225
|
+
const serializedSearchTriggers = searchTriggers?.join(',');
|
|
133
226
|
const extension = useMemo<Extension>(() => {
|
|
134
227
|
return popover({
|
|
135
228
|
trigger,
|
|
@@ -144,46 +237,38 @@ export const useEditorMenu = ({
|
|
|
144
237
|
}
|
|
145
238
|
return false;
|
|
146
239
|
},
|
|
147
|
-
onArrowUp: () =>
|
|
148
|
-
|
|
149
|
-
const previous = getPreviousMenuItem(groupsRef.current, currentItem);
|
|
150
|
-
currentRef.current = previous;
|
|
151
|
-
return previous.id;
|
|
152
|
-
});
|
|
153
|
-
},
|
|
154
|
-
onArrowDown: () => {
|
|
155
|
-
setCurrentItem((currentItem) => {
|
|
156
|
-
const next = getNextMenuItem(groupsRef.current, currentItem);
|
|
157
|
-
currentRef.current = next;
|
|
158
|
-
return next.id;
|
|
159
|
-
});
|
|
160
|
-
},
|
|
240
|
+
onArrowUp: () => handleNavigate('up'),
|
|
241
|
+
onArrowDown: () => handleNavigate('down'),
|
|
161
242
|
onTextChange: async ({ view, pos, text, trigger }) => {
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
} else {
|
|
168
|
-
// No matches: clear the selection so Enter falls through instead of
|
|
169
|
-
// selecting a stale item from a previous query.
|
|
170
|
-
setCurrentItem(undefined);
|
|
171
|
-
currentRef.current = null;
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
refresh({});
|
|
243
|
+
contextRef.current = { view, pos, text, trigger };
|
|
244
|
+
// The input is remounted per activation, so the query always starts empty.
|
|
245
|
+
setSearch(!!trigger && !!searchTriggers?.includes(trigger));
|
|
246
|
+
setQuery('');
|
|
247
|
+
await updateGroups({ view, pos, text, trigger });
|
|
175
248
|
},
|
|
176
249
|
});
|
|
177
|
-
}, [
|
|
250
|
+
}, [
|
|
251
|
+
handleOpenChange,
|
|
252
|
+
handleNavigate,
|
|
253
|
+
updateGroups,
|
|
254
|
+
serializedTrigger,
|
|
255
|
+
serializedSearchTriggers,
|
|
256
|
+
placeholder,
|
|
257
|
+
activateOnTyping,
|
|
258
|
+
]);
|
|
178
259
|
|
|
179
260
|
return {
|
|
180
261
|
groupsRef,
|
|
181
262
|
extension,
|
|
182
263
|
currentItem,
|
|
183
264
|
open,
|
|
265
|
+
search,
|
|
266
|
+
query,
|
|
184
267
|
onOpenChange: handleOpenChange,
|
|
185
268
|
onActivate: handleActivate,
|
|
186
269
|
onSelect: handleSelect,
|
|
187
270
|
onCancel: handleCancel,
|
|
271
|
+
onQueryChange: handleQueryChange,
|
|
272
|
+
onNavigate: handleNavigate,
|
|
188
273
|
};
|
|
189
274
|
};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2025 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { createContext } from '@radix-ui/react-context';
|
|
6
|
+
|
|
7
|
+
import { type PreviewLinkRef, type PreviewLinkTarget } from '@dxos/ui-types';
|
|
8
|
+
|
|
9
|
+
// Kept out of `EditorPreviewProvider.tsx`: react-refresh only fast-refreshes a module whose exports are all
|
|
10
|
+
// components, so a context and its hook exported beside them force a full page reload on every edit.
|
|
11
|
+
|
|
12
|
+
export type EditorPreviewPopoverValue = Partial<{
|
|
13
|
+
link: PreviewLinkRef;
|
|
14
|
+
target: PreviewLinkTarget;
|
|
15
|
+
pending: boolean;
|
|
16
|
+
}>;
|
|
17
|
+
|
|
18
|
+
export const [EditorPreviewContextProvider, useEditorPreview] = createContext<EditorPreviewPopoverValue>(
|
|
19
|
+
'PreviewPopover',
|
|
20
|
+
{},
|
|
21
|
+
);
|
|
@@ -2,20 +2,13 @@
|
|
|
2
2
|
// Copyright 2025 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
-
import { createContext } from '@radix-ui/react-context';
|
|
6
5
|
import React, { type PropsWithChildren, type RefObject, useCallback, useEffect, useRef, useState } from 'react';
|
|
7
6
|
|
|
8
7
|
import { addEventListener } from '@dxos/async';
|
|
9
8
|
import { DX_ANCHOR_ACTIVATE, type DxAnchorActivate, Popover } from '@dxos/react-ui';
|
|
10
9
|
import { type PreviewLinkRef, type PreviewLinkTarget } from '@dxos/ui-types';
|
|
11
10
|
|
|
12
|
-
type EditorPreviewPopoverValue
|
|
13
|
-
link: PreviewLinkRef;
|
|
14
|
-
target: PreviewLinkTarget;
|
|
15
|
-
pending: boolean;
|
|
16
|
-
}>;
|
|
17
|
-
|
|
18
|
-
const [EditorPreviewContextProvider, useEditorPreview] = createContext<EditorPreviewPopoverValue>('PreviewPopover', {});
|
|
11
|
+
import { EditorPreviewContextProvider, type EditorPreviewPopoverValue } from './EditorPreviewContext';
|
|
19
12
|
|
|
20
13
|
export type EditorPreviewProviderProps = PropsWithChildren<{
|
|
21
14
|
onLookup?: (link: PreviewLinkRef) => Promise<PreviewLinkTarget | null | undefined>;
|
|
@@ -76,5 +69,3 @@ export const EditorPreviewProvider = ({ children, onLookup }: EditorPreviewProvi
|
|
|
76
69
|
</EditorPreviewContextProvider>
|
|
77
70
|
);
|
|
78
71
|
};
|
|
79
|
-
|
|
80
|
-
export { useEditorPreview };
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
//
|
|
4
4
|
|
|
5
5
|
import { type EditorView } from '@codemirror/view';
|
|
6
|
-
import { Atom } from '@effect-atom/atom
|
|
6
|
+
import { Atom } from '@effect-atom/atom';
|
|
7
7
|
import React, { memo, useMemo } from 'react';
|
|
8
8
|
|
|
9
9
|
import { type Node } from '@dxos/app-graph';
|
|
@@ -18,7 +18,7 @@ import { addImageUpload } from './image';
|
|
|
18
18
|
import { addLists } from './lists';
|
|
19
19
|
import { addSearch } from './search';
|
|
20
20
|
import { type EditorToolbarState } from './types';
|
|
21
|
-
import { addViewMode } from './view-mode';
|
|
21
|
+
import { type ViewModeItem, addViewMode } from './view-mode';
|
|
22
22
|
|
|
23
23
|
// TODO(burdon): Enable toolbar variants (e.g., markdown, code).
|
|
24
24
|
|
|
@@ -35,6 +35,8 @@ export type EditorToolbarFeatureFlags = Partial<{
|
|
|
35
35
|
// TODO(wittjosiah): Factor out (depends on plugin-level capabilities.)
|
|
36
36
|
onImageUpload: () => void;
|
|
37
37
|
onViewModeChange: (mode: EditorViewMode) => void;
|
|
38
|
+
/** Override the view-mode dropdown entries (default: the three built-in modes). See {@link ViewModeItem}. */
|
|
39
|
+
viewModes: ViewModeItem[];
|
|
38
40
|
}>;
|
|
39
41
|
|
|
40
42
|
export type EditorToolbarActionGraphProps = {
|
|
@@ -86,6 +88,7 @@ const useMarkdownMenuActions = ({ state, getView, customActions, ...features }:
|
|
|
86
88
|
features?.showSearch,
|
|
87
89
|
features?.onImageUpload,
|
|
88
90
|
features?.onViewModeChange,
|
|
91
|
+
features?.viewModes,
|
|
89
92
|
],
|
|
90
93
|
);
|
|
91
94
|
|
|
@@ -110,7 +113,7 @@ const createMarkdownActions = ({
|
|
|
110
113
|
.separator('gap')
|
|
111
114
|
.subgraph(customActions && get(customActions))
|
|
112
115
|
.subgraph(features?.showSearch !== false && addSearch(getView))
|
|
113
|
-
.subgraph(features?.onViewModeChange && addViewMode(stateSnapshot, features.onViewModeChange))
|
|
116
|
+
.subgraph(features?.onViewModeChange && addViewMode(stateSnapshot, features.onViewModeChange, features.viewModes))
|
|
114
117
|
.build();
|
|
115
118
|
});
|
|
116
119
|
};
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
import { type EditorView } from '@codemirror/view';
|
|
6
6
|
|
|
7
7
|
import { type ActionGroupBuilderFn, type ToolbarMenuActionGroupProperties } from '@dxos/react-ui-menu';
|
|
8
|
-
import { List,
|
|
8
|
+
import { List, toggleList } from '@dxos/ui-editor';
|
|
9
9
|
|
|
10
10
|
import { translationKey } from '#translations';
|
|
11
11
|
|
|
@@ -45,11 +45,7 @@ export const addLists =
|
|
|
45
45
|
|
|
46
46
|
const listType =
|
|
47
47
|
listStyle === 'ordered' ? List.Ordered : listStyle === 'bullet' ? List.Bullet : List.Task;
|
|
48
|
-
|
|
49
|
-
removeList(listType)(view);
|
|
50
|
-
} else {
|
|
51
|
-
addList(listType)(view);
|
|
52
|
-
}
|
|
48
|
+
toggleList(listType)(view);
|
|
53
49
|
},
|
|
54
50
|
);
|
|
55
51
|
}
|
|
@@ -4,20 +4,47 @@
|
|
|
4
4
|
|
|
5
5
|
import { type ActionGroupBuilderFn, type ToolbarMenuActionGroupProperties } from '@dxos/react-ui-menu';
|
|
6
6
|
import { type EditorViewMode } from '@dxos/ui-editor/types';
|
|
7
|
+
import { type Label } from '@dxos/ui-types';
|
|
7
8
|
|
|
8
9
|
import { translationKey } from '#translations';
|
|
9
10
|
|
|
10
11
|
import { type EditorToolbarState } from './types';
|
|
11
12
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
13
|
+
/**
|
|
14
|
+
* One entry in the editor's view-mode dropdown. A built-in entry references an {@link EditorViewMode}
|
|
15
|
+
* by `id` and relies on the defaults (label `view-mode.<id>.label`, checked when it is the active view
|
|
16
|
+
* mode, select via `onViewModeChange`). A contributed entry (e.g. a review "Suggesting" mode) supplies
|
|
17
|
+
* its own `label`, `checked`, and `onSelect` so it can drive state other than the editor view mode.
|
|
18
|
+
*/
|
|
19
|
+
export type ViewModeItem = {
|
|
20
|
+
id: string;
|
|
21
|
+
icon: string;
|
|
22
|
+
label?: Label;
|
|
23
|
+
checked?: boolean;
|
|
24
|
+
onSelect?: () => void;
|
|
16
25
|
};
|
|
17
26
|
|
|
18
|
-
/**
|
|
27
|
+
/**
|
|
28
|
+
* The built-in view modes: preview (rendered), source (raw edit), readonly (non-editable source). Typed
|
|
29
|
+
* so each `id` is an {@link EditorViewMode}, letting callers pass `item.id` to `onViewModeChange` without a cast.
|
|
30
|
+
*/
|
|
31
|
+
export const defaultViewModeItems: Array<ViewModeItem & { id: EditorViewMode }> = [
|
|
32
|
+
{ id: 'preview', icon: 'ph--eye--regular' },
|
|
33
|
+
{ id: 'source', icon: 'ph--pencil-simple--regular' },
|
|
34
|
+
{ id: 'readonly', icon: 'ph--pencil-slash--regular' },
|
|
35
|
+
];
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Add view mode actions to the builder. `items` defaults to the three built-in editor view modes;
|
|
39
|
+
* callers pass an extended list to surface contributed modes (see {@link ViewModeItem}) in the same
|
|
40
|
+
* single-select dropdown. The trigger tracks the checked item (via the group's `applyActive`).
|
|
41
|
+
*/
|
|
19
42
|
export const addViewMode =
|
|
20
|
-
(
|
|
43
|
+
(
|
|
44
|
+
state: EditorToolbarState,
|
|
45
|
+
onViewModeChange: (mode: EditorViewMode) => void,
|
|
46
|
+
items: ViewModeItem[] = defaultViewModeItems,
|
|
47
|
+
): ActionGroupBuilderFn =>
|
|
21
48
|
(builder) => {
|
|
22
49
|
const value = state.viewMode ?? 'source';
|
|
23
50
|
builder.group(
|
|
@@ -32,16 +59,18 @@ export const addViewMode =
|
|
|
32
59
|
value,
|
|
33
60
|
} as ToolbarMenuActionGroupProperties,
|
|
34
61
|
(group) => {
|
|
35
|
-
for (const
|
|
36
|
-
|
|
62
|
+
for (const item of items) {
|
|
63
|
+
// A contributed entry may drive non-view-mode state, so honour its explicit `checked`;
|
|
64
|
+
// a built-in entry is checked when it matches the active editor view mode.
|
|
65
|
+
const checked = item.checked ?? item.id === value;
|
|
37
66
|
group.action(
|
|
38
|
-
`view-mode--${
|
|
67
|
+
`view-mode--${item.id}`,
|
|
39
68
|
{
|
|
40
|
-
label: [`view-mode.${
|
|
69
|
+
label: item.label ?? [`view-mode.${item.id}.label`, { ns: translationKey }],
|
|
41
70
|
checked,
|
|
42
|
-
icon,
|
|
71
|
+
icon: item.icon,
|
|
43
72
|
},
|
|
44
|
-
() => onViewModeChange(
|
|
73
|
+
() => (item.onSelect ? item.onSelect() : onViewModeChange(item.id as EditorViewMode)),
|
|
45
74
|
);
|
|
46
75
|
}
|
|
47
76
|
},
|
package/src/components/index.ts
CHANGED
package/src/index.ts
CHANGED
|
@@ -13,13 +13,18 @@ import { AiService } from '@dxos/ai';
|
|
|
13
13
|
import { AiServiceTestingPreset } from '@dxos/ai/testing';
|
|
14
14
|
import { useThemeContext } from '@dxos/react-ui';
|
|
15
15
|
import { Loading, withLayout, withTheme } from '@dxos/react-ui/testing';
|
|
16
|
-
import {
|
|
16
|
+
import {
|
|
17
|
+
type AssistantOptions,
|
|
18
|
+
assistant,
|
|
19
|
+
compactSlots,
|
|
20
|
+
createBasicExtensions,
|
|
21
|
+
createThemeExtensions,
|
|
22
|
+
} from '@dxos/ui-editor';
|
|
17
23
|
import { trim } from '@dxos/util';
|
|
18
24
|
|
|
19
25
|
import { translations } from '#translations';
|
|
20
26
|
|
|
21
27
|
import { Editor, type EditorViewProps } from '../components';
|
|
22
|
-
import { type AssistantOptions, assistant } from './assistant-extension';
|
|
23
28
|
|
|
24
29
|
// TODO(burdon): Factor out.
|
|
25
30
|
const useTestGenerate = () => {
|