@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.
Files changed (113) hide show
  1. package/dist/lib/index.mjs +1477 -0
  2. package/dist/lib/index.mjs.map +1 -0
  3. package/dist/lib/translations.mjs +32 -0
  4. package/dist/lib/translations.mjs.map +1 -0
  5. package/dist/types/src/components/Editor/Editor.d.ts +22 -17
  6. package/dist/types/src/components/Editor/Editor.d.ts.map +1 -1
  7. package/dist/types/src/components/Editor/EditorContext.d.ts +20 -0
  8. package/dist/types/src/components/Editor/EditorContext.d.ts.map +1 -0
  9. package/dist/types/src/components/Editor/EditorView.d.ts +1 -2
  10. package/dist/types/src/components/Editor/EditorView.d.ts.map +1 -1
  11. package/dist/types/src/components/Editor/index.d.ts +2 -0
  12. package/dist/types/src/components/Editor/index.d.ts.map +1 -1
  13. package/dist/types/src/components/EditorMenuProvider/EditorMenuProvider.d.ts +7 -1
  14. package/dist/types/src/components/EditorMenuProvider/EditorMenuProvider.d.ts.map +1 -1
  15. package/dist/types/src/components/EditorMenuProvider/menu.d.ts +4 -2
  16. package/dist/types/src/components/EditorMenuProvider/menu.d.ts.map +1 -1
  17. package/dist/types/src/components/EditorMenuProvider/useEditorMenu.d.ts +7 -2
  18. package/dist/types/src/components/EditorMenuProvider/useEditorMenu.d.ts.map +1 -1
  19. package/dist/types/src/components/EditorPreviewProvider/EditorPreviewContext.d.ts +18 -0
  20. package/dist/types/src/components/EditorPreviewProvider/EditorPreviewContext.d.ts.map +1 -0
  21. package/dist/types/src/components/EditorPreviewProvider/EditorPreviewProvider.d.ts +0 -6
  22. package/dist/types/src/components/EditorPreviewProvider/EditorPreviewProvider.d.ts.map +1 -1
  23. package/dist/types/src/components/EditorPreviewProvider/index.d.ts +1 -0
  24. package/dist/types/src/components/EditorPreviewProvider/index.d.ts.map +1 -1
  25. package/dist/types/src/components/EditorToolbar/EditorToolbar.d.ts +4 -1
  26. package/dist/types/src/components/EditorToolbar/EditorToolbar.d.ts.map +1 -1
  27. package/dist/types/src/components/EditorToolbar/index.d.ts +1 -0
  28. package/dist/types/src/components/EditorToolbar/index.d.ts.map +1 -1
  29. package/dist/types/src/components/EditorToolbar/lists.d.ts.map +1 -1
  30. package/dist/types/src/components/EditorToolbar/view-mode.d.ts +27 -2
  31. package/dist/types/src/components/EditorToolbar/view-mode.d.ts.map +1 -1
  32. package/dist/types/src/components/index.d.ts +1 -0
  33. package/dist/types/src/components/index.d.ts.map +1 -1
  34. package/dist/types/src/index.d.ts +0 -1
  35. package/dist/types/src/index.d.ts.map +1 -1
  36. package/dist/types/src/stories/Assistant.stories.d.ts.map +1 -0
  37. package/dist/types/src/stories/Automerge.stories.d.ts +16 -39
  38. package/dist/types/src/stories/Automerge.stories.d.ts.map +1 -1
  39. package/dist/types/src/stories/Blocks.stories.d.ts +10 -0
  40. package/dist/types/src/stories/Blocks.stories.d.ts.map +1 -0
  41. package/dist/types/src/stories/Comments.stories.d.ts +53 -14
  42. package/dist/types/src/stories/Comments.stories.d.ts.map +1 -1
  43. package/dist/types/src/stories/Diff.stories.d.ts +32 -0
  44. package/dist/types/src/stories/Diff.stories.d.ts.map +1 -0
  45. package/dist/types/src/stories/Experimental.stories.d.ts +1 -0
  46. package/dist/types/src/stories/Experimental.stories.d.ts.map +1 -1
  47. package/dist/types/src/stories/Folding.stories.d.ts +18 -0
  48. package/dist/types/src/stories/Folding.stories.d.ts.map +1 -0
  49. package/dist/types/src/stories/Outliner.stories.d.ts +4 -4
  50. package/dist/types/src/stories/Outliner.stories.d.ts.map +1 -1
  51. package/dist/types/src/stories/Popover.stories.d.ts +3 -3
  52. package/dist/types/src/stories/Popover.stories.d.ts.map +1 -1
  53. package/dist/types/src/stories/Suggest.stories.d.ts +27 -0
  54. package/dist/types/src/stories/Suggest.stories.d.ts.map +1 -0
  55. package/dist/types/src/stories/TrackChanges.stories.d.ts +44 -0
  56. package/dist/types/src/stories/TrackChanges.stories.d.ts.map +1 -0
  57. package/dist/types/src/stories/Widgets.stories.d.ts +12 -0
  58. package/dist/types/src/stories/Widgets.stories.d.ts.map +1 -1
  59. package/dist/types/src/stories/components/EditorStory.d.ts.map +1 -1
  60. package/dist/types/src/stories/components/util.d.ts +16 -0
  61. package/dist/types/src/stories/components/util.d.ts.map +1 -1
  62. package/dist/types/tsconfig.tsbuildinfo +1 -1
  63. package/package.json +40 -83
  64. package/src/components/Editor/Editor.tsx +142 -35
  65. package/src/components/Editor/EditorContext.ts +34 -0
  66. package/src/components/Editor/EditorView.tsx +9 -6
  67. package/src/components/Editor/index.ts +3 -0
  68. package/src/components/EditorMenuProvider/EditorMenuProvider.tsx +98 -8
  69. package/src/components/EditorMenuProvider/menu.ts +4 -2
  70. package/src/components/EditorMenuProvider/useEditorMenu.ts +114 -29
  71. package/src/components/EditorPreviewProvider/EditorPreviewContext.ts +21 -0
  72. package/src/components/EditorPreviewProvider/EditorPreviewProvider.tsx +1 -10
  73. package/src/components/EditorPreviewProvider/index.ts +2 -0
  74. package/src/components/EditorToolbar/EditorToolbar.tsx +6 -3
  75. package/src/components/EditorToolbar/index.ts +1 -0
  76. package/src/components/EditorToolbar/lists.ts +2 -6
  77. package/src/components/EditorToolbar/view-mode.ts +41 -12
  78. package/src/components/index.ts +1 -0
  79. package/src/index.ts +0 -1
  80. package/src/{extensions → stories}/Assistant.stories.tsx +7 -2
  81. package/src/stories/Automerge.stories.tsx +99 -78
  82. package/src/stories/Blocks.stories.tsx +83 -0
  83. package/src/stories/Comments.stories.tsx +273 -74
  84. package/src/stories/Diff.stories.tsx +48 -0
  85. package/src/stories/EditorToolbar.stories.tsx +2 -2
  86. package/src/stories/Experimental.stories.tsx +13 -2
  87. package/src/stories/Folding.stories.tsx +120 -0
  88. package/src/stories/Markdown.stories.tsx +1 -1
  89. package/src/stories/Outliner.stories.tsx +18 -7
  90. package/src/stories/Popover.stories.tsx +27 -5
  91. package/src/stories/Suggest.stories.tsx +199 -0
  92. package/src/stories/TrackChanges.stories.tsx +226 -0
  93. package/src/stories/Widgets.stories.tsx +162 -10
  94. package/src/stories/components/EditorStory.tsx +15 -6
  95. package/src/stories/components/util.tsx +40 -2
  96. package/dist/lib/browser/index.mjs +0 -1676
  97. package/dist/lib/browser/index.mjs.map +0 -7
  98. package/dist/lib/browser/meta.json +0 -1
  99. package/dist/lib/browser/translations.mjs +0 -39
  100. package/dist/lib/browser/translations.mjs.map +0 -7
  101. package/dist/lib/node-esm/index.mjs +0 -1678
  102. package/dist/lib/node-esm/index.mjs.map +0 -7
  103. package/dist/lib/node-esm/meta.json +0 -1
  104. package/dist/lib/node-esm/translations.mjs +0 -41
  105. package/dist/lib/node-esm/translations.mjs.map +0 -7
  106. package/dist/types/src/extensions/Assistant.stories.d.ts.map +0 -1
  107. package/dist/types/src/extensions/assistant-extension.d.ts +0 -24
  108. package/dist/types/src/extensions/assistant-extension.d.ts.map +0 -1
  109. package/dist/types/src/extensions/index.d.ts +0 -2
  110. package/dist/types/src/extensions/index.d.ts.map +0 -1
  111. package/src/extensions/assistant-extension.tsx +0 -223
  112. package/src/extensions/index.ts +0 -5
  113. /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
- export const getNextMenuItem = (groups: EditorMenuGroup[], id?: string): EditorMenuItem => {
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
- export const getPreviousMenuItem = (groups: EditorMenuGroup[], id?: string): EditorMenuItem => {
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<EditorMenuProviderProps, 'currentItem' | 'open' | 'onOpenChange' | 'onActivate' | 'onSelect' | 'onCancel'>;
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
- setCurrentItem((currentItem) => {
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
- groupsRef.current = (await getMenuOptions({ state: view.state, pos, text, trigger })) ?? [];
163
- const firstItem = groupsRef.current.filter((group) => group.items.length > 0)[0]?.items[0];
164
- if (firstItem) {
165
- setCurrentItem(firstItem.id);
166
- currentRef.current = firstItem;
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
- }, [handleOpenChange, getMenuOptions, serializedTrigger, placeholder, activateOnTyping]);
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 = Partial<{
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 };
@@ -2,4 +2,6 @@
2
2
  // Copyright 2025 DXOS.org
3
3
  //
4
4
 
5
+ export { useEditorPreview } from './EditorPreviewContext';
6
+
5
7
  export * from './EditorPreviewProvider';
@@ -3,7 +3,7 @@
3
3
  //
4
4
 
5
5
  import { type EditorView } from '@codemirror/view';
6
- import { Atom } from '@effect-atom/atom-react';
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,3 +5,4 @@
5
5
  export * from './types';
6
6
 
7
7
  export * from './EditorToolbar';
8
+ export * from './view-mode';
@@ -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, addList, removeList } from '@dxos/ui-editor';
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
- if (checked) {
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
- const viewModes = {
13
- preview: 'ph--eye--regular',
14
- source: 'ph--pencil-simple--regular',
15
- readonly: 'ph--pencil-slash--regular',
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
- /** Add view mode actions to the builder. */
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
- (state: EditorToolbarState, onViewModeChange: (mode: EditorViewMode) => void): ActionGroupBuilderFn =>
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 [viewMode, icon] of Object.entries(viewModes)) {
36
- const checked = viewMode === value;
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--${viewMode}`,
67
+ `view-mode--${item.id}`,
39
68
  {
40
- label: [`view-mode.${viewMode}.label`, { ns: translationKey }],
69
+ label: item.label ?? [`view-mode.${item.id}.label`, { ns: translationKey }],
41
70
  checked,
42
- icon,
71
+ icon: item.icon,
43
72
  },
44
- () => onViewModeChange(viewMode as EditorViewMode),
73
+ () => (item.onSelect ? item.onSelect() : onViewModeChange(item.id as EditorViewMode)),
45
74
  );
46
75
  }
47
76
  },
@@ -6,3 +6,4 @@ export * from './Editor';
6
6
 
7
7
  export * from './EditorMenuProvider';
8
8
  export * from './EditorPreviewProvider';
9
+ export * from './EditorToolbar/view-mode';
package/src/index.ts CHANGED
@@ -3,5 +3,4 @@
3
3
  //
4
4
 
5
5
  export * from './components';
6
- export * from './extensions';
7
6
  export * from './hooks';
@@ -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 { compactSlots, createBasicExtensions, createThemeExtensions } from '@dxos/ui-editor';
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 = () => {