@dxos/react-ui-editor 0.8.4-main.ef1bc66f44 → 0.8.4-main.effb148878

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 (149) hide show
  1. package/LICENSE +102 -5
  2. package/README.md +1 -1
  3. package/dist/lib/browser/index.mjs +796 -755
  4. package/dist/lib/browser/index.mjs.map +4 -4
  5. package/dist/lib/browser/meta.json +1 -1
  6. package/dist/lib/browser/translations.mjs +39 -0
  7. package/dist/lib/browser/translations.mjs.map +7 -0
  8. package/dist/lib/node-esm/index.mjs +796 -755
  9. package/dist/lib/node-esm/index.mjs.map +4 -4
  10. package/dist/lib/node-esm/meta.json +1 -1
  11. package/dist/lib/node-esm/translations.mjs +41 -0
  12. package/dist/lib/node-esm/translations.mjs.map +7 -0
  13. package/dist/types/src/components/Editor/Editor.d.ts +36 -25
  14. package/dist/types/src/components/Editor/Editor.d.ts.map +1 -1
  15. package/dist/types/src/components/Editor/Editor.stories.d.ts +4 -4
  16. package/dist/types/src/components/Editor/Editor.stories.d.ts.map +1 -1
  17. package/dist/types/src/components/{EditorContent/EditorContent.d.ts → Editor/EditorView.d.ts} +5 -5
  18. package/dist/types/src/components/Editor/EditorView.d.ts.map +1 -0
  19. package/dist/types/src/components/Editor/controller.d.ts.map +1 -0
  20. package/dist/types/src/components/EditorMenuProvider/EditorMenuProvider.d.ts +1 -3
  21. package/dist/types/src/components/EditorMenuProvider/EditorMenuProvider.d.ts.map +1 -1
  22. package/dist/types/src/components/EditorMenuProvider/menu-presets.d.ts.map +1 -1
  23. package/dist/types/src/components/EditorMenuProvider/menu.d.ts.map +1 -1
  24. package/dist/types/src/components/EditorMenuProvider/popover.d.ts +2 -1
  25. package/dist/types/src/components/EditorMenuProvider/popover.d.ts.map +1 -1
  26. package/dist/types/src/components/EditorMenuProvider/useEditorMenu.d.ts.map +1 -1
  27. package/dist/types/src/components/EditorPreviewProvider/EditorPreviewProvider.d.ts.map +1 -1
  28. package/dist/types/src/components/EditorToolbar/EditorToolbar.d.ts +2 -2
  29. package/dist/types/src/components/EditorToolbar/EditorToolbar.d.ts.map +1 -1
  30. package/dist/types/src/components/EditorToolbar/blocks.d.ts +4 -18
  31. package/dist/types/src/components/EditorToolbar/blocks.d.ts.map +1 -1
  32. package/dist/types/src/components/EditorToolbar/formatting.d.ts +4 -18
  33. package/dist/types/src/components/EditorToolbar/formatting.d.ts.map +1 -1
  34. package/dist/types/src/components/EditorToolbar/headings.d.ts +4 -18
  35. package/dist/types/src/components/EditorToolbar/headings.d.ts.map +1 -1
  36. package/dist/types/src/components/EditorToolbar/image.d.ts +3 -8
  37. package/dist/types/src/components/EditorToolbar/image.d.ts.map +1 -1
  38. package/dist/types/src/components/EditorToolbar/index.d.ts +1 -2
  39. package/dist/types/src/components/EditorToolbar/index.d.ts.map +1 -1
  40. package/dist/types/src/components/EditorToolbar/lists.d.ts +6 -0
  41. package/dist/types/src/components/EditorToolbar/lists.d.ts.map +1 -0
  42. package/dist/types/src/components/EditorToolbar/search.d.ts +3 -8
  43. package/dist/types/src/components/EditorToolbar/search.d.ts.map +1 -1
  44. package/dist/types/src/components/EditorToolbar/types.d.ts +6 -0
  45. package/dist/types/src/components/EditorToolbar/types.d.ts.map +1 -0
  46. package/dist/types/src/components/EditorToolbar/view-mode.d.ts +5 -19
  47. package/dist/types/src/components/EditorToolbar/view-mode.d.ts.map +1 -1
  48. package/dist/types/src/components/index.d.ts +0 -2
  49. package/dist/types/src/components/index.d.ts.map +1 -1
  50. package/dist/types/src/extensions/Assistant.stories.d.ts +10 -0
  51. package/dist/types/src/extensions/Assistant.stories.d.ts.map +1 -0
  52. package/dist/types/src/extensions/assistant-extension.d.ts +24 -0
  53. package/dist/types/src/extensions/assistant-extension.d.ts.map +1 -0
  54. package/dist/types/src/extensions/index.d.ts +2 -0
  55. package/dist/types/src/extensions/index.d.ts.map +1 -0
  56. package/dist/types/src/hooks/index.d.ts +1 -0
  57. package/dist/types/src/hooks/index.d.ts.map +1 -1
  58. package/dist/types/src/hooks/useBasicMarkdownExtensions.d.ts +25 -0
  59. package/dist/types/src/hooks/useBasicMarkdownExtensions.d.ts.map +1 -0
  60. package/dist/types/src/hooks/useTextEditor.d.ts.map +1 -1
  61. package/dist/types/src/index.d.ts +1 -2
  62. package/dist/types/src/index.d.ts.map +1 -1
  63. package/dist/types/src/stories/Automerge.stories.d.ts +25 -24
  64. package/dist/types/src/stories/Automerge.stories.d.ts.map +1 -1
  65. package/dist/types/src/stories/Comments.stories.d.ts +2 -2
  66. package/dist/types/src/stories/Comments.stories.d.ts.map +1 -1
  67. package/dist/types/src/stories/EditorToolbar.stories.d.ts +28 -26
  68. package/dist/types/src/stories/EditorToolbar.stories.d.ts.map +1 -1
  69. package/dist/types/src/stories/Experimental.stories.d.ts +3 -3
  70. package/dist/types/src/stories/Experimental.stories.d.ts.map +1 -1
  71. package/dist/types/src/stories/Markdown.stories.d.ts +2 -2
  72. package/dist/types/src/stories/Markdown.stories.d.ts.map +1 -1
  73. package/dist/types/src/stories/Outliner.stories.d.ts +2 -2
  74. package/dist/types/src/stories/Outliner.stories.d.ts.map +1 -1
  75. package/dist/types/src/stories/Popover.stories.d.ts +2 -2
  76. package/dist/types/src/stories/Popover.stories.d.ts.map +1 -1
  77. package/dist/types/src/stories/Preview.stories.d.ts +2 -2
  78. package/dist/types/src/stories/Preview.stories.d.ts.map +1 -1
  79. package/dist/types/src/stories/Tags.stories.d.ts.map +1 -1
  80. package/dist/types/src/stories/TextEditor.stories.d.ts +2 -2
  81. package/dist/types/src/stories/TextEditor.stories.d.ts.map +1 -1
  82. package/dist/types/src/stories/Theme.stories.d.ts.map +1 -1
  83. package/dist/types/src/stories/components/EditorStory.d.ts +4 -4
  84. package/dist/types/src/stories/components/EditorStory.d.ts.map +1 -1
  85. package/dist/types/src/stories/components/util.d.ts +3 -2
  86. package/dist/types/src/stories/components/util.d.ts.map +1 -1
  87. package/dist/types/src/translations.d.ts +24 -24
  88. package/dist/types/src/translations.d.ts.map +1 -1
  89. package/dist/types/src/util/react.d.ts +2 -5
  90. package/dist/types/src/util/react.d.ts.map +1 -1
  91. package/dist/types/tsconfig.tsbuildinfo +1 -1
  92. package/package.json +77 -68
  93. package/src/components/Editor/Editor.stories.tsx +15 -21
  94. package/src/components/Editor/Editor.tsx +54 -53
  95. package/src/components/Editor/EditorView.tsx +102 -0
  96. package/src/components/EditorMenuProvider/EditorMenuProvider.tsx +19 -24
  97. package/src/components/EditorMenuProvider/menu-presets.ts +1 -0
  98. package/src/components/EditorMenuProvider/popover.ts +3 -1
  99. package/src/components/EditorMenuProvider/useEditorMenu.ts +8 -1
  100. package/src/components/EditorPreviewProvider/EditorPreviewProvider.tsx +1 -1
  101. package/src/components/EditorToolbar/EditorToolbar.tsx +31 -65
  102. package/src/components/EditorToolbar/blocks.ts +54 -46
  103. package/src/components/EditorToolbar/formatting.ts +44 -45
  104. package/src/components/EditorToolbar/headings.ts +44 -50
  105. package/src/components/EditorToolbar/image.ts +16 -21
  106. package/src/components/EditorToolbar/index.ts +2 -3
  107. package/src/components/EditorToolbar/lists.ts +58 -0
  108. package/src/components/EditorToolbar/search.ts +16 -21
  109. package/src/components/EditorToolbar/types.ts +8 -0
  110. package/src/components/EditorToolbar/view-mode.ts +37 -43
  111. package/src/components/index.ts +0 -3
  112. package/src/extensions/Assistant.stories.tsx +112 -0
  113. package/src/extensions/assistant-extension.tsx +223 -0
  114. package/src/extensions/index.ts +5 -0
  115. package/src/hooks/index.ts +1 -0
  116. package/src/hooks/useBasicMarkdownExtensions.ts +55 -0
  117. package/src/index.ts +1 -4
  118. package/src/stories/Automerge.stories.tsx +18 -16
  119. package/src/stories/Comments.stories.tsx +6 -6
  120. package/src/stories/EditorToolbar.stories.tsx +37 -65
  121. package/src/stories/Experimental.stories.tsx +12 -12
  122. package/src/stories/Markdown.stories.tsx +2 -2
  123. package/src/stories/Outliner.stories.tsx +4 -5
  124. package/src/stories/Popover.stories.tsx +10 -11
  125. package/src/stories/Preview.stories.tsx +51 -43
  126. package/src/stories/Tags.stories.tsx +5 -5
  127. package/src/stories/TextEditor.stories.tsx +2 -2
  128. package/src/stories/Theme.stories.tsx +4 -4
  129. package/src/stories/components/EditorStory.tsx +19 -12
  130. package/src/stories/components/util.tsx +49 -50
  131. package/src/translations.ts +29 -24
  132. package/src/util/react.tsx +4 -13
  133. package/dist/types/src/components/EditorContent/EditorContent.d.ts.map +0 -1
  134. package/dist/types/src/components/EditorContent/controller.d.ts.map +0 -1
  135. package/dist/types/src/components/EditorContent/index.d.ts +0 -3
  136. package/dist/types/src/components/EditorContent/index.d.ts.map +0 -1
  137. package/dist/types/src/components/EditorToolbar/actions.d.ts +0 -24
  138. package/dist/types/src/components/EditorToolbar/actions.d.ts.map +0 -1
  139. package/dist/types/src/components/EditorToolbar/useEditorToolbar.d.ts +0 -11
  140. package/dist/types/src/components/EditorToolbar/useEditorToolbar.d.ts.map +0 -1
  141. package/dist/types/src/stories/CommandDialog.stories.d.ts +0 -14
  142. package/dist/types/src/stories/CommandDialog.stories.d.ts.map +0 -1
  143. package/src/components/EditorContent/EditorContent.tsx +0 -83
  144. package/src/components/EditorContent/index.ts +0 -6
  145. package/src/components/EditorToolbar/actions.ts +0 -87
  146. package/src/components/EditorToolbar/useEditorToolbar.ts +0 -20
  147. package/src/stories/CommandDialog.stories.tsx +0 -81
  148. /package/dist/types/src/components/{EditorContent → Editor}/controller.d.ts +0 -0
  149. /package/src/components/{EditorContent → Editor}/controller.ts +0 -0
@@ -13,6 +13,7 @@ import {
13
13
  type DxAnchorActivate,
14
14
  Icon,
15
15
  Popover,
16
+ ScrollArea,
16
17
  toLocalizedString,
17
18
  useDynamicRef,
18
19
  useThemeContext,
@@ -35,9 +36,7 @@ export type EditorMenuProviderProps = PropsWithChildren<{
35
36
  }>;
36
37
 
37
38
  /**
38
- * Implements the Popover and listens for the `dx-anchor-activate` event from the
39
- * `popover` extension's decoration.
40
- *
39
+ * Implements the Popover and listens for the `dx-anchor-activate` event from the `popover` extension's decoration.
41
40
  * NOTE: We don't use DropdownMenu because the command menu needs to manage focus explicitly.
42
41
  * I.e., focus must remain in the editor while displaying the menu (for type-ahead).
43
42
  */
@@ -113,32 +112,32 @@ export const EditorMenuProvider = ({
113
112
  <Popover.Portal>
114
113
  <Popover.Content
115
114
  align='start'
116
- classNames={tx('menu.content', 'menu--exotic-unfocusable', { elevation: 'positioned' }, [
117
- 'overflow-y-auto',
118
- !menuGroups.length && 'hidden',
119
- ])}
115
+ classNames={['flex flex-col', !menuGroups.length && 'hidden']}
120
116
  style={{
121
117
  maxBlockSize: 36 * numItems + 10,
122
118
  }}
123
- /**
124
- * NOTE: We keep the focus in the editor, but Radix routes escape key.
125
- */
119
+ // NOTE: We keep the focus in the editor, but Radix routes escape key.
126
120
  onEscapeKeyDown={() => {
127
- // NOTE: Able to cancel if not in valid state.
128
- // event.preventDefault();
129
- onCancel?.({ view: view! });
121
+ const currentView = viewRef.current;
122
+ if (currentView) {
123
+ onCancel?.({ view: currentView });
124
+ }
130
125
  }}
131
126
  onOpenAutoFocus={(event) => event.preventDefault()}
132
127
  >
133
- <Popover.Viewport classNames={tx('menu.viewport', 'menu__viewport--exotic-unfocusable', {})}>
134
- <Menu groups={menuGroups} currentItem={currentItem} onSelect={handleSelect} />
128
+ <Popover.Viewport asChild classNames='dx-container'>
129
+ <ScrollArea.Root thin>
130
+ <ScrollArea.Viewport>
131
+ <Menu groups={menuGroups} currentItem={currentItem} onSelect={handleSelect} />
132
+ </ScrollArea.Viewport>
133
+ </ScrollArea.Root>
135
134
  </Popover.Viewport>
136
135
  <Popover.Arrow />
137
136
  </Popover.Content>
138
137
  </Popover.Portal>
139
138
 
140
139
  {/* Content */}
141
- <div ref={setRoot} role='none' className='contents'>
140
+ <div className='contents' ref={setRoot}>
142
141
  {children}
143
142
  </div>
144
143
  </Popover.Root>
@@ -160,7 +159,7 @@ const Menu = ({ groups, currentItem, onSelect }: MenuProps) => {
160
159
  {groups.map((group, index) => (
161
160
  <Fragment key={group.id}>
162
161
  <MenuGroup group={group} currentItem={currentItem} onSelect={onSelect} />
163
- {index < groups.length - 1 && <div className={tx('menu.separator', 'menu__item', {})} />}
162
+ {index < groups.length - 1 && <div className={tx('menu.separator', {})} />}
164
163
  </Fragment>
165
164
  ))}
166
165
  </ul>
@@ -183,7 +182,7 @@ const MenuGroup = ({ group, currentItem, onSelect }: MenuGroupProps) => {
183
182
  return (
184
183
  <>
185
184
  {group.label && (
186
- <div className={tx('menu.groupLabel', 'menu__group__label', {})}>
185
+ <div className={tx('menu.groupLabel', {})}>
187
186
  <span>{toLocalizedString(group.label, t)}</span>
188
187
  </div>
189
188
  )}
@@ -219,12 +218,8 @@ const MenuItem = ({ item, current, onSelect }: MenuItemProps) => {
219
218
  const handleSelect = useCallback(() => onSelect?.(item), [item, onSelect]);
220
219
 
221
220
  return (
222
- <li
223
- ref={listRef}
224
- className={tx('menu.item', 'menu__item--exotic-unfocusable', {}, [current && 'bg-hoverSurface'])}
225
- onClick={handleSelect}
226
- >
227
- {item.icon && <Icon icon={item.icon} size={5} />}
221
+ <li ref={listRef} className={tx('menu.item', {}, [current && 'bg-hover-surface'])} onClick={handleSelect}>
222
+ {item.icon && <Icon icon={item.icon} />}
228
223
  <span className='grow truncate'>{toLocalizedString(item.label, t)}</span>
229
224
  </li>
230
225
  );
@@ -110,6 +110,7 @@ export const linkSlashCommands: EditorMenuGroup = {
110
110
  label: 'Block embed',
111
111
  icon: 'ph--lego--regular',
112
112
  onSelect: ({ view, head }) => {
113
+ // Seed the same query shape as typing "@@" manually.
113
114
  view.dispatch({
114
115
  changes: { from: head, insert: '@@' },
115
116
  selection: { anchor: head + 2, head: head + 2 },
@@ -13,7 +13,8 @@ import {
13
13
  keymap,
14
14
  } from '@codemirror/view';
15
15
 
16
- import { type PlaceholderOptions, type Range, modalStateField, placeholder } from '@dxos/ui-editor';
16
+ import { type PlaceholderOptions, modalStateField, placeholder } from '@dxos/ui-editor';
17
+ import { type Range } from '@dxos/ui-editor/types';
17
18
  import { isNonNullable, isTruthy } from '@dxos/util';
18
19
 
19
20
  const DELIMITERS = [' ', ':'];
@@ -52,6 +53,7 @@ export const popover = (options: PopoverOptions = {}): Extension => {
52
53
  placeholder({
53
54
  // TODO(burdon): Translations.
54
55
  content: `Press '${Array.isArray(options.trigger) ? options.trigger[0] : options.trigger}' for commands`,
56
+ focusOnly: true,
55
57
  ...options.placeholder,
56
58
  }),
57
59
  ].filter(isTruthy);
@@ -62,7 +62,8 @@ export const useEditorMenu = ({
62
62
  const getMenuOptions = useCallback<NonNullable<UseEditorMenuProps['getMenu']>>(
63
63
  async ({ text, trigger, ...props }) => {
64
64
  const groups = (await getMenu?.({ text, trigger, ...props })) ?? [];
65
- return filter
65
+ // The "@" menu can use "@@" as syntax for block embeds, so it owns its own query filtering.
66
+ return filter && trigger !== '@'
66
67
  ? filterMenuGroups(groups, (item) =>
67
68
  text ? (item.label as string).toLowerCase().startsWith(text.toLowerCase()) : true,
68
69
  )
@@ -108,7 +109,13 @@ export const useEditorMenu = ({
108
109
  );
109
110
 
110
111
  const handleSelect = useCallback<NonNullable<UseEditorMenu['onSelect']>>(({ view, item }) => {
112
+ // Delete trigger range (e.g., "/" and any typed filter text).
113
+ const { range } = view.state.field(popoverStateField) ?? {};
114
+ if (range) {
115
+ view.dispatch({ changes: { from: range.from, to: range.to, insert: '' } });
116
+ }
111
117
  void item.onSelect?.({ view, head: view.state.selection.main.head });
118
+ view.focus();
112
119
  }, []);
113
120
 
114
121
  const handleCancel = useCallback<NonNullable<UseEditorMenu['onCancel']>>(({ view }) => {
@@ -68,7 +68,7 @@ export const EditorPreviewProvider = ({ children, onLookup }: EditorPreviewProvi
68
68
  <EditorPreviewContextProvider pending={value.pending} link={value.link} target={value.target}>
69
69
  <Popover.Root open={open} onOpenChange={setOpen}>
70
70
  <Popover.VirtualTrigger virtualRef={triggerRef as unknown as RefObject<HTMLButtonElement>} />
71
- <div role='none' className='contents' ref={setRoot}>
71
+ <div className='contents' ref={setRoot}>
72
72
  {children}
73
73
  </div>
74
74
  </Popover.Root>
@@ -8,24 +8,19 @@ import React, { memo, useMemo } from 'react';
8
8
 
9
9
  import { type Node } from '@dxos/app-graph';
10
10
  import { ElevationProvider, type ThemedClassName } from '@dxos/react-ui';
11
- import {
12
- type ActionGraphProps,
13
- type MenuAction,
14
- MenuProvider,
15
- ToolbarMenu,
16
- createGapSeparator,
17
- useMenuActions,
18
- } from '@dxos/react-ui-menu';
19
- import { type EditorViewMode } from '@dxos/ui-editor';
11
+ import { type ActionGraphProps, Menu, type MenuAction, MenuBuilder, useMenuActions } from '@dxos/react-ui-menu';
12
+ import { type EditorViewMode } from '@dxos/ui-editor/types';
20
13
 
21
- import { createLists } from './actions';
22
- import { createBlocks } from './blocks';
23
- import { createFormatting } from './formatting';
24
- import { createHeadings } from './headings';
25
- import { createImageUpload } from './image';
26
- import { createSearch } from './search';
27
- import { type EditorToolbarState } from './useEditorToolbar';
28
- import { createViewMode } from './view-mode';
14
+ import { addBlocks } from './blocks';
15
+ import { addFormatting } from './formatting';
16
+ import { addHeadings } from './headings';
17
+ import { addImageUpload } from './image';
18
+ import { addLists } from './lists';
19
+ import { addSearch } from './search';
20
+ import { type EditorToolbarState } from './types';
21
+ import { addViewMode } from './view-mode';
22
+
23
+ // TODO(burdon): Enable toolbar variants (e.g., markdown, code).
29
24
 
30
25
  export type EditorToolbarFeatureFlags = Partial<{
31
26
  showHeadings: boolean;
@@ -56,13 +51,13 @@ export type EditorToolbarProps = ThemedClassName<
56
51
  >;
57
52
 
58
53
  export const EditorToolbar = memo(({ classNames, role, attendableId, onAction, ...props }: EditorToolbarProps) => {
59
- const menuProps = useEditorToolbarActionGraph(props);
54
+ const menuActions = useMarkdownMenuActions(props);
60
55
 
61
56
  return (
62
57
  <ElevationProvider elevation={role === 'section' ? 'positioned' : 'base'}>
63
- <MenuProvider {...menuProps} attendableId={attendableId} onAction={onAction}>
64
- <ToolbarMenu classNames={classNames} textBlockWidth />
65
- </MenuProvider>
58
+ <Menu.Root {...menuActions} attendableId={attendableId} onAction={onAction}>
59
+ <Menu.Toolbar classNames={classNames} />
60
+ </Menu.Root>
66
61
  </ElevationProvider>
67
62
  );
68
63
  });
@@ -70,12 +65,13 @@ export const EditorToolbar = memo(({ classNames, role, attendableId, onAction, .
70
65
  type ToolbarActionsProps = Pick<EditorToolbarActionGraphProps, 'state' | 'getView' | 'customActions'> &
71
66
  EditorToolbarFeatureFlags;
72
67
 
68
+ // TODO(burdon): Some actions should toggle the state (e.g., toggle bullets on/off depending on the current state).
73
69
  // TODO(wittjosiah): Toolbar re-rendering is causing this graph to be recreated and breaking reactivity in some cases.
74
70
  // E.g. for toolbar dropdowns which use active icon, the icon is not updated when the active item changes.
75
71
  // This is currently only happening in the markdown plugin usage and should be reproduced in an editor story.
76
- const useEditorToolbarActionGraph = ({ state, getView, customActions, ...features }: ToolbarActionsProps) => {
72
+ const useMarkdownMenuActions = ({ state, getView, customActions, ...features }: ToolbarActionsProps) => {
77
73
  const menuCreator = useMemo(
78
- () => createToolbarActions({ state, getView, customActions, ...features }),
74
+ () => createMarkdownActions({ state, getView, customActions, ...features }),
79
75
  [
80
76
  state,
81
77
  getView,
@@ -93,55 +89,25 @@ const useEditorToolbarActionGraph = ({ state, getView, customActions, ...feature
93
89
  return useMenuActions(menuCreator);
94
90
  };
95
91
 
96
- const createToolbarActions = ({
92
+ const createMarkdownActions = ({
97
93
  state,
98
94
  getView,
99
95
  customActions,
100
96
  ...features
101
97
  }: ToolbarActionsProps): Atom.Atom<ActionGraphProps> => {
102
98
  return Atom.make((get) => {
103
- const graph: ActionGraphProps = {
104
- nodes: [],
105
- edges: [],
106
- };
107
-
108
- // TODO(burdon): Builder pattern?
109
- const addSubGraph = (graph: ActionGraphProps, subGraph: ActionGraphProps) => {
110
- graph.nodes.push(...subGraph.nodes);
111
- graph.edges.push(...subGraph.edges);
112
- };
113
-
114
99
  // Subscribe to state changes.
115
100
  const stateSnapshot = get(state);
116
-
117
- if (features?.showHeadings ?? true) {
118
- addSubGraph(graph, createHeadings(stateSnapshot, getView));
119
- }
120
- if (features?.showFormatting ?? true) {
121
- addSubGraph(graph, createFormatting(stateSnapshot, getView));
122
- }
123
- if (features?.showLists ?? true) {
124
- addSubGraph(graph, createLists(stateSnapshot, getView));
125
- }
126
- if (features?.showBlocks ?? true) {
127
- addSubGraph(graph, createBlocks(stateSnapshot, getView));
128
- }
129
- if (features?.onImageUpload) {
130
- addSubGraph(graph, createImageUpload(features.onImageUpload!));
131
- }
132
-
133
- addSubGraph(graph, createGapSeparator());
134
-
135
- if (customActions) {
136
- addSubGraph(graph, get(customActions));
137
- }
138
- if (features?.showSearch ?? true) {
139
- addSubGraph(graph, createSearch(getView));
140
- }
141
- if (features?.onViewModeChange) {
142
- addSubGraph(graph, createViewMode(stateSnapshot, features.onViewModeChange!));
143
- }
144
-
145
- return graph;
101
+ return MenuBuilder.make()
102
+ .subgraph(features?.showHeadings !== false && addHeadings(stateSnapshot, getView))
103
+ .subgraph(features?.showFormatting !== false && addFormatting(stateSnapshot, getView))
104
+ .subgraph(features?.showLists !== false && addLists(stateSnapshot, getView))
105
+ .subgraph(features?.showBlocks !== false && addBlocks(stateSnapshot, getView))
106
+ .subgraph(features?.onImageUpload && addImageUpload(features.onImageUpload))
107
+ .separator('gap')
108
+ .subgraph(customActions && get(customActions))
109
+ .subgraph(features?.showSearch !== false && addSearch(getView))
110
+ .subgraph(features?.onViewModeChange && addViewMode(stateSnapshot, features.onViewModeChange))
111
+ .build();
146
112
  });
147
113
  };
@@ -4,56 +4,64 @@
4
4
 
5
5
  import { type EditorView } from '@codemirror/view';
6
6
 
7
- import { type Node } from '@dxos/app-graph';
8
- import { type ToolbarMenuActionGroupProperties } from '@dxos/react-ui-menu';
7
+ import { type ActionGroupBuilderFn, type ToolbarMenuActionGroupProperties } from '@dxos/react-ui-menu';
9
8
  import { addBlockquote, addCodeblock, insertTable, removeBlockquote, removeCodeblock } from '@dxos/ui-editor';
10
9
 
11
- import { createEditorAction, createEditorActionGroup } from './actions';
12
- import { type EditorToolbarState } from './useEditorToolbar';
10
+ import { translationKey } from '#translations';
13
11
 
14
- const createBlockGroupAction = (value: string) =>
15
- createEditorActionGroup('block', {
16
- variant: 'toggleGroup',
17
- selectCardinality: 'single',
18
- value,
19
- } as ToolbarMenuActionGroupProperties);
12
+ import { type EditorToolbarState } from './types';
20
13
 
21
- const createBlockActions = (value: string, getView: () => EditorView, blankLine?: boolean) =>
22
- Object.entries({
23
- blockquote: 'ph--quotes--regular',
24
- codeblock: 'ph--code-block--regular',
25
- table: 'ph--table--regular',
26
- }).map(([type, icon]) => {
27
- const checked = type === value;
28
- return createEditorAction(type, { checked, ...(type === 'table' && { disabled: !!blankLine }), icon }, () => {
29
- const view = getView();
30
- if (!view) {
31
- return;
32
- }
14
+ const blockTypes = {
15
+ blockquote: 'ph--quotes--regular',
16
+ codeblock: 'ph--code-block--regular',
17
+ table: 'ph--table--regular',
18
+ };
33
19
 
34
- switch (type) {
35
- case 'blockquote':
36
- checked ? removeBlockquote(view) : addBlockquote(view);
37
- break;
38
- case 'codeblock':
39
- checked ? removeCodeblock(view) : addCodeblock(view);
40
- break;
41
- case 'table':
42
- insertTable(view);
43
- break;
44
- }
45
- });
46
- });
20
+ /** Add block actions to the builder. */
21
+ export const addBlocks =
22
+ (state: EditorToolbarState, getView: () => EditorView): ActionGroupBuilderFn =>
23
+ (builder) => {
24
+ const value = state?.blockQuote ? 'blockquote' : (state.blockType ?? '');
25
+ builder.group(
26
+ 'block',
27
+ {
28
+ label: ['block.label', { ns: translationKey }],
29
+ iconOnly: true,
30
+ variant: 'toggleGroup',
31
+ selectCardinality: 'single',
32
+ value,
33
+ } as ToolbarMenuActionGroupProperties,
34
+ (group) => {
35
+ for (const [type, icon] of Object.entries(blockTypes)) {
36
+ const checked = type === value;
37
+ group.action(
38
+ type,
39
+ {
40
+ label: [`block.${type}.label`, { ns: translationKey }],
41
+ checked,
42
+ ...(type === 'table' && { disabled: !!state.blankLine }),
43
+ icon,
44
+ },
45
+ () => {
46
+ const view = getView();
47
+ if (!view) {
48
+ return;
49
+ }
47
50
 
48
- export const createBlocks = (state: EditorToolbarState, getView: () => EditorView) => {
49
- const value = state?.blockQuote ? 'blockquote' : (state.blockType ?? '');
50
- const blockGroupAction = createBlockGroupAction(value);
51
- const blockActions = createBlockActions(value, getView, state.blankLine);
52
- return {
53
- nodes: [blockGroupAction as Node.NodeArg<any>, ...blockActions],
54
- edges: [
55
- { source: 'root', target: 'block' },
56
- ...blockActions.map(({ id }) => ({ source: blockGroupAction.id, target: id })),
57
- ],
51
+ switch (type) {
52
+ case 'blockquote':
53
+ checked ? removeBlockquote(view) : addBlockquote(view);
54
+ break;
55
+ case 'codeblock':
56
+ checked ? removeCodeblock(view) : addCodeblock(view);
57
+ break;
58
+ case 'table':
59
+ insertTable(view);
60
+ break;
61
+ }
62
+ },
63
+ );
64
+ }
65
+ },
66
+ );
58
67
  };
59
- };
@@ -4,12 +4,12 @@
4
4
 
5
5
  import { type EditorView } from '@codemirror/view';
6
6
 
7
- import { type Node } from '@dxos/app-graph';
8
- import { type ToolbarMenuActionGroupProperties } from '@dxos/react-ui-menu';
7
+ import { type ActionGroupBuilderFn, type ToolbarMenuActionGroupProperties } from '@dxos/react-ui-menu';
9
8
  import { type Formatting, Inline, addLink, removeLink, setStyle } from '@dxos/ui-editor';
10
9
 
11
- import { createEditorAction, createEditorActionGroup } from './actions';
12
- import { type EditorToolbarState } from './useEditorToolbar';
10
+ import { translationKey } from '#translations';
11
+
12
+ import { type EditorToolbarState } from './types';
13
13
 
14
14
  const formats = {
15
15
  strong: 'ph--text-b--regular',
@@ -19,47 +19,46 @@ const formats = {
19
19
  link: 'ph--link--regular',
20
20
  };
21
21
 
22
- const createFormattingGroup = (formatting: Formatting) =>
23
- createEditorActionGroup('formatting', {
24
- variant: 'toggleGroup',
25
- selectCardinality: 'multiple',
26
- value: Object.keys(formats).filter((key) => !!formatting[key as keyof Formatting]),
27
- } as ToolbarMenuActionGroupProperties);
28
-
29
- const createFormattingActions = (formatting: Formatting, getView: () => EditorView) =>
30
- Object.entries(formats).map(([type, icon]) => {
31
- const checked = !!formatting[type as keyof Formatting];
32
- return createEditorAction(type, { checked, icon }, () => {
33
- const view = getView();
34
- if (!view) {
35
- return;
36
- }
22
+ /** Add formatting actions to the builder. */
23
+ export const addFormatting =
24
+ (state: EditorToolbarState, getView: () => EditorView): ActionGroupBuilderFn =>
25
+ (builder) => {
26
+ const formatting: Formatting = state;
27
+ builder.group(
28
+ 'formatting',
29
+ {
30
+ label: ['formatting.label', { ns: translationKey }],
31
+ iconOnly: true,
32
+ variant: 'toggleGroup',
33
+ selectCardinality: 'multiple',
34
+ value: Object.keys(formats).filter((key) => !!formatting[key as keyof Formatting]),
35
+ } as ToolbarMenuActionGroupProperties,
36
+ (group) => {
37
+ for (const [type, icon] of Object.entries(formats)) {
38
+ const checked = !!formatting[type as keyof Formatting];
39
+ group.action(type, { label: [`formatting.${type}.label`, { ns: translationKey }], checked, icon }, () => {
40
+ const view = getView();
41
+ if (!view) {
42
+ return;
43
+ }
37
44
 
38
- if (type === 'link') {
39
- checked ? removeLink(view) : addLink()(view);
40
- return;
41
- }
45
+ if (type === 'link') {
46
+ checked ? removeLink(view) : addLink()(view);
47
+ return;
48
+ }
42
49
 
43
- const inlineType =
44
- type === 'strong'
45
- ? Inline.Strong
46
- : type === 'emphasis'
47
- ? Inline.Emphasis
48
- : type === 'strikethrough'
49
- ? Inline.Strikethrough
50
- : Inline.Code;
51
- setStyle(inlineType, !checked)(view);
52
- });
53
- });
54
-
55
- export const createFormatting = (state: EditorToolbarState, getView: () => EditorView) => {
56
- const formattingGroupAction = createFormattingGroup(state);
57
- const formattingActions = createFormattingActions(state, getView);
58
- return {
59
- nodes: [formattingGroupAction as Node.NodeArg<any>, ...formattingActions],
60
- edges: [
61
- { source: 'root', target: 'formatting' },
62
- ...formattingActions.map(({ id }) => ({ source: formattingGroupAction.id, target: id })),
63
- ],
50
+ setStyle(
51
+ type === 'strong'
52
+ ? Inline.Strong
53
+ : type === 'emphasis'
54
+ ? Inline.Emphasis
55
+ : type === 'strikethrough'
56
+ ? Inline.Strikethrough
57
+ : Inline.Code,
58
+ !checked,
59
+ )(view);
60
+ });
61
+ }
62
+ },
63
+ );
64
64
  };
65
- };
@@ -4,49 +4,22 @@
4
4
 
5
5
  import { type EditorView } from '@codemirror/view';
6
6
 
7
- import { type Node } from '@dxos/app-graph';
8
- import { type ToolbarMenuActionGroupProperties } from '@dxos/react-ui-menu';
7
+ import { type ActionGroupBuilderFn, type ToolbarMenuActionGroupProperties } from '@dxos/react-ui-menu';
9
8
  import { setHeading } from '@dxos/ui-editor';
10
9
 
11
- import { translationKey } from '../../translations';
10
+ import { translationKey } from '#translations';
12
11
 
13
- import { createEditorAction, createEditorActionGroup } from './actions';
14
- import { type EditorToolbarState } from './useEditorToolbar';
12
+ import { type EditorToolbarState } from './types';
15
13
 
16
- const createHeadingGroupAction = (value: string) =>
17
- createEditorActionGroup(
18
- 'heading',
19
- {
20
- variant: 'dropdownMenu',
21
- applyActive: true,
22
- selectCardinality: 'single',
23
- // TODO(wittjosiah): Remove? Not sure this does anything.
24
- value,
25
- } as ToolbarMenuActionGroupProperties,
26
- 'ph--text-h--regular',
27
- );
28
-
29
- const createHeadingActions = (currentLevel: string, getView: () => EditorView) =>
30
- Object.entries({
31
- '0': 'ph--paragraph--regular',
32
- '1': 'ph--text-h-one--regular',
33
- '2': 'ph--text-h-two--regular',
34
- '3': 'ph--text-h-three--regular',
35
- '4': 'ph--text-h-four--regular',
36
- '5': 'ph--text-h-five--regular',
37
- '6': 'ph--text-h-six--regular',
38
- }).map(([levelStr, icon]) => {
39
- const level = parseInt(levelStr);
40
- return createEditorAction(
41
- `heading--${levelStr}`,
42
- {
43
- label: ['heading level label', { count: level, ns: translationKey }],
44
- icon,
45
- checked: levelStr === currentLevel,
46
- },
47
- () => setHeading(level)(getView()),
48
- );
49
- });
14
+ const headingIcons: Record<string, string> = {
15
+ 0: 'ph--paragraph--regular',
16
+ 1: 'ph--text-h-one--regular',
17
+ 2: 'ph--text-h-two--regular',
18
+ 3: 'ph--text-h-three--regular',
19
+ 4: 'ph--text-h-four--regular',
20
+ 5: 'ph--text-h-five--regular',
21
+ 6: 'ph--text-h-six--regular',
22
+ };
50
23
 
51
24
  const computeHeadingValue = (state: EditorToolbarState) => {
52
25
  const blockType = state ? state.blockType : 'paragraph';
@@ -54,15 +27,36 @@ const computeHeadingValue = (state: EditorToolbarState) => {
54
27
  return heading ? heading[1] : blockType === 'paragraph' || !blockType ? '0' : '';
55
28
  };
56
29
 
57
- export const createHeadings = (state: EditorToolbarState, getView: () => EditorView) => {
58
- const headingValue = computeHeadingValue(state);
59
- const headingGroupAction = createHeadingGroupAction(headingValue);
60
- const headingActions = createHeadingActions(headingValue, getView);
61
- return {
62
- nodes: [headingGroupAction as Node.NodeArg<any>, ...headingActions],
63
- edges: [
64
- { source: 'root', target: 'heading' },
65
- ...headingActions.map(({ id }) => ({ source: headingGroupAction.id, target: id })),
66
- ],
30
+ /** Add heading actions to the builder. */
31
+ export const addHeadings =
32
+ (state: EditorToolbarState, getView: () => EditorView): ActionGroupBuilderFn =>
33
+ (builder) => {
34
+ const headingValue = computeHeadingValue(state);
35
+ builder.group(
36
+ 'heading',
37
+ {
38
+ label: ['heading.label', { ns: translationKey }],
39
+ icon: 'ph--text-h--regular',
40
+ iconOnly: true,
41
+ variant: 'dropdownMenu',
42
+ applyActive: true,
43
+ selectCardinality: 'single',
44
+ // TODO(wittjosiah): Remove? Not sure this does anything.
45
+ value: headingValue,
46
+ } as ToolbarMenuActionGroupProperties,
47
+ (group) => {
48
+ for (const [levelStr, icon] of Object.entries(headingIcons)) {
49
+ const level = parseInt(levelStr);
50
+ group.action(
51
+ `heading--${levelStr}`,
52
+ {
53
+ label: ['heading-level.label', { count: level, ns: translationKey }],
54
+ icon,
55
+ checked: levelStr === headingValue,
56
+ },
57
+ () => setHeading(level)(getView()),
58
+ );
59
+ }
60
+ },
61
+ );
67
62
  };
68
- };