@dxos/react-ui-editor 0.8.4-main.3eb6e50203 → 0.8.4-main.3fbcb4aa9b

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 (147) hide show
  1. package/dist/lib/browser/index.mjs +793 -752
  2. package/dist/lib/browser/index.mjs.map +4 -4
  3. package/dist/lib/browser/meta.json +1 -1
  4. package/dist/lib/browser/translations.mjs +39 -0
  5. package/dist/lib/browser/translations.mjs.map +7 -0
  6. package/dist/lib/node-esm/index.mjs +793 -752
  7. package/dist/lib/node-esm/index.mjs.map +4 -4
  8. package/dist/lib/node-esm/meta.json +1 -1
  9. package/dist/lib/node-esm/translations.mjs +41 -0
  10. package/dist/lib/node-esm/translations.mjs.map +7 -0
  11. package/dist/types/src/components/Editor/Editor.d.ts +36 -25
  12. package/dist/types/src/components/Editor/Editor.d.ts.map +1 -1
  13. package/dist/types/src/components/Editor/Editor.stories.d.ts +4 -4
  14. package/dist/types/src/components/Editor/Editor.stories.d.ts.map +1 -1
  15. package/dist/types/src/components/{EditorContent/EditorContent.d.ts → Editor/EditorView.d.ts} +5 -5
  16. package/dist/types/src/components/Editor/EditorView.d.ts.map +1 -0
  17. package/dist/types/src/components/Editor/controller.d.ts.map +1 -0
  18. package/dist/types/src/components/EditorMenuProvider/EditorMenuProvider.d.ts +1 -3
  19. package/dist/types/src/components/EditorMenuProvider/EditorMenuProvider.d.ts.map +1 -1
  20. package/dist/types/src/components/EditorMenuProvider/menu-presets.d.ts.map +1 -1
  21. package/dist/types/src/components/EditorMenuProvider/menu.d.ts.map +1 -1
  22. package/dist/types/src/components/EditorMenuProvider/popover.d.ts +2 -1
  23. package/dist/types/src/components/EditorMenuProvider/popover.d.ts.map +1 -1
  24. package/dist/types/src/components/EditorMenuProvider/useEditorMenu.d.ts.map +1 -1
  25. package/dist/types/src/components/EditorPreviewProvider/EditorPreviewProvider.d.ts.map +1 -1
  26. package/dist/types/src/components/EditorToolbar/EditorToolbar.d.ts +2 -2
  27. package/dist/types/src/components/EditorToolbar/EditorToolbar.d.ts.map +1 -1
  28. package/dist/types/src/components/EditorToolbar/blocks.d.ts +4 -18
  29. package/dist/types/src/components/EditorToolbar/blocks.d.ts.map +1 -1
  30. package/dist/types/src/components/EditorToolbar/formatting.d.ts +4 -18
  31. package/dist/types/src/components/EditorToolbar/formatting.d.ts.map +1 -1
  32. package/dist/types/src/components/EditorToolbar/headings.d.ts +4 -18
  33. package/dist/types/src/components/EditorToolbar/headings.d.ts.map +1 -1
  34. package/dist/types/src/components/EditorToolbar/image.d.ts +3 -8
  35. package/dist/types/src/components/EditorToolbar/image.d.ts.map +1 -1
  36. package/dist/types/src/components/EditorToolbar/index.d.ts +1 -2
  37. package/dist/types/src/components/EditorToolbar/index.d.ts.map +1 -1
  38. package/dist/types/src/components/EditorToolbar/lists.d.ts +6 -0
  39. package/dist/types/src/components/EditorToolbar/lists.d.ts.map +1 -0
  40. package/dist/types/src/components/EditorToolbar/search.d.ts +3 -8
  41. package/dist/types/src/components/EditorToolbar/search.d.ts.map +1 -1
  42. package/dist/types/src/components/EditorToolbar/types.d.ts +6 -0
  43. package/dist/types/src/components/EditorToolbar/types.d.ts.map +1 -0
  44. package/dist/types/src/components/EditorToolbar/view-mode.d.ts +5 -19
  45. package/dist/types/src/components/EditorToolbar/view-mode.d.ts.map +1 -1
  46. package/dist/types/src/components/index.d.ts +0 -2
  47. package/dist/types/src/components/index.d.ts.map +1 -1
  48. package/dist/types/src/extensions/Assistant.stories.d.ts +10 -0
  49. package/dist/types/src/extensions/Assistant.stories.d.ts.map +1 -0
  50. package/dist/types/src/extensions/assistant-extension.d.ts +24 -0
  51. package/dist/types/src/extensions/assistant-extension.d.ts.map +1 -0
  52. package/dist/types/src/extensions/index.d.ts +2 -0
  53. package/dist/types/src/extensions/index.d.ts.map +1 -0
  54. package/dist/types/src/hooks/index.d.ts +1 -0
  55. package/dist/types/src/hooks/index.d.ts.map +1 -1
  56. package/dist/types/src/hooks/useBasicMarkdownExtensions.d.ts +25 -0
  57. package/dist/types/src/hooks/useBasicMarkdownExtensions.d.ts.map +1 -0
  58. package/dist/types/src/hooks/useTextEditor.d.ts.map +1 -1
  59. package/dist/types/src/index.d.ts +1 -2
  60. package/dist/types/src/index.d.ts.map +1 -1
  61. package/dist/types/src/stories/Automerge.stories.d.ts +25 -24
  62. package/dist/types/src/stories/Automerge.stories.d.ts.map +1 -1
  63. package/dist/types/src/stories/Comments.stories.d.ts +2 -2
  64. package/dist/types/src/stories/Comments.stories.d.ts.map +1 -1
  65. package/dist/types/src/stories/EditorToolbar.stories.d.ts +28 -26
  66. package/dist/types/src/stories/EditorToolbar.stories.d.ts.map +1 -1
  67. package/dist/types/src/stories/Experimental.stories.d.ts +3 -3
  68. package/dist/types/src/stories/Experimental.stories.d.ts.map +1 -1
  69. package/dist/types/src/stories/Markdown.stories.d.ts +2 -2
  70. package/dist/types/src/stories/Markdown.stories.d.ts.map +1 -1
  71. package/dist/types/src/stories/Outliner.stories.d.ts +2 -2
  72. package/dist/types/src/stories/Outliner.stories.d.ts.map +1 -1
  73. package/dist/types/src/stories/Popover.stories.d.ts +2 -2
  74. package/dist/types/src/stories/Popover.stories.d.ts.map +1 -1
  75. package/dist/types/src/stories/Preview.stories.d.ts +2 -2
  76. package/dist/types/src/stories/Preview.stories.d.ts.map +1 -1
  77. package/dist/types/src/stories/Tags.stories.d.ts.map +1 -1
  78. package/dist/types/src/stories/TextEditor.stories.d.ts +2 -2
  79. package/dist/types/src/stories/TextEditor.stories.d.ts.map +1 -1
  80. package/dist/types/src/stories/Theme.stories.d.ts.map +1 -1
  81. package/dist/types/src/stories/components/EditorStory.d.ts +4 -4
  82. package/dist/types/src/stories/components/EditorStory.d.ts.map +1 -1
  83. package/dist/types/src/stories/components/util.d.ts +3 -2
  84. package/dist/types/src/stories/components/util.d.ts.map +1 -1
  85. package/dist/types/src/translations.d.ts +24 -24
  86. package/dist/types/src/translations.d.ts.map +1 -1
  87. package/dist/types/src/util/react.d.ts +2 -5
  88. package/dist/types/src/util/react.d.ts.map +1 -1
  89. package/dist/types/tsconfig.tsbuildinfo +1 -1
  90. package/package.json +59 -50
  91. package/src/components/Editor/Editor.stories.tsx +15 -21
  92. package/src/components/Editor/Editor.tsx +54 -53
  93. package/src/components/Editor/EditorView.tsx +102 -0
  94. package/src/components/EditorMenuProvider/EditorMenuProvider.tsx +17 -18
  95. package/src/components/EditorMenuProvider/menu-presets.ts +1 -0
  96. package/src/components/EditorMenuProvider/popover.ts +3 -1
  97. package/src/components/EditorMenuProvider/useEditorMenu.ts +8 -1
  98. package/src/components/EditorPreviewProvider/EditorPreviewProvider.tsx +1 -1
  99. package/src/components/EditorToolbar/EditorToolbar.tsx +31 -65
  100. package/src/components/EditorToolbar/blocks.ts +54 -46
  101. package/src/components/EditorToolbar/formatting.ts +44 -45
  102. package/src/components/EditorToolbar/headings.ts +44 -50
  103. package/src/components/EditorToolbar/image.ts +16 -21
  104. package/src/components/EditorToolbar/index.ts +2 -3
  105. package/src/components/EditorToolbar/lists.ts +58 -0
  106. package/src/components/EditorToolbar/search.ts +16 -21
  107. package/src/components/EditorToolbar/types.ts +8 -0
  108. package/src/components/EditorToolbar/view-mode.ts +37 -43
  109. package/src/components/index.ts +0 -3
  110. package/src/extensions/Assistant.stories.tsx +112 -0
  111. package/src/extensions/assistant-extension.tsx +223 -0
  112. package/src/extensions/index.ts +5 -0
  113. package/src/hooks/index.ts +1 -0
  114. package/src/hooks/useBasicMarkdownExtensions.ts +55 -0
  115. package/src/index.ts +1 -4
  116. package/src/stories/Automerge.stories.tsx +12 -13
  117. package/src/stories/Comments.stories.tsx +6 -6
  118. package/src/stories/EditorToolbar.stories.tsx +37 -65
  119. package/src/stories/Experimental.stories.tsx +12 -12
  120. package/src/stories/Markdown.stories.tsx +2 -2
  121. package/src/stories/Outliner.stories.tsx +4 -5
  122. package/src/stories/Popover.stories.tsx +9 -10
  123. package/src/stories/Preview.stories.tsx +49 -41
  124. package/src/stories/Tags.stories.tsx +5 -5
  125. package/src/stories/TextEditor.stories.tsx +2 -2
  126. package/src/stories/Theme.stories.tsx +4 -4
  127. package/src/stories/components/EditorStory.tsx +19 -12
  128. package/src/stories/components/util.tsx +49 -50
  129. package/src/translations.ts +29 -24
  130. package/src/util/react.tsx +3 -12
  131. package/dist/types/src/components/EditorContent/EditorContent.d.ts.map +0 -1
  132. package/dist/types/src/components/EditorContent/controller.d.ts.map +0 -1
  133. package/dist/types/src/components/EditorContent/index.d.ts +0 -3
  134. package/dist/types/src/components/EditorContent/index.d.ts.map +0 -1
  135. package/dist/types/src/components/EditorToolbar/actions.d.ts +0 -24
  136. package/dist/types/src/components/EditorToolbar/actions.d.ts.map +0 -1
  137. package/dist/types/src/components/EditorToolbar/useEditorToolbar.d.ts +0 -11
  138. package/dist/types/src/components/EditorToolbar/useEditorToolbar.d.ts.map +0 -1
  139. package/dist/types/src/stories/CommandDialog.stories.d.ts +0 -14
  140. package/dist/types/src/stories/CommandDialog.stories.d.ts.map +0 -1
  141. package/src/components/EditorContent/EditorContent.tsx +0 -83
  142. package/src/components/EditorContent/index.ts +0 -6
  143. package/src/components/EditorToolbar/actions.ts +0 -87
  144. package/src/components/EditorToolbar/useEditorToolbar.ts +0 -20
  145. package/src/stories/CommandDialog.stories.tsx +0 -81
  146. /package/dist/types/src/components/{EditorContent → Editor}/controller.d.ts +0 -0
  147. /package/src/components/{EditorContent → Editor}/controller.ts +0 -0
@@ -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
- };
@@ -2,26 +2,21 @@
2
2
  // Copyright 2025 DXOS.org
3
3
  //
4
4
 
5
- import { type Node } from '@dxos/app-graph';
5
+ import { type ActionGroupBuilderFn } from '@dxos/react-ui-menu';
6
6
 
7
- import { createEditorAction } from './actions';
7
+ import { translationKey } from '#translations';
8
8
 
9
- const createImageUploadAction = (onImageUpload: () => void) =>
10
- createEditorAction(
11
- 'image',
12
- {
13
- testId: 'editor.toolbar.image',
14
- icon: 'ph--image-square--regular',
15
- },
16
- onImageUpload,
17
- );
18
-
19
- export const createImageUpload = (
20
- onImageUpload: () => void,
21
- ): {
22
- nodes: Node.NodeArg<any>[];
23
- edges: Array<{ source: string; target: string }>;
24
- } => ({
25
- nodes: [createImageUploadAction(onImageUpload)],
26
- edges: [{ source: 'root', target: 'image' }],
27
- });
9
+ /** Add image upload action to the builder. */
10
+ export const addImageUpload =
11
+ (onImageUpload: () => void): ActionGroupBuilderFn =>
12
+ (builder) => {
13
+ builder.action(
14
+ 'image',
15
+ {
16
+ label: ['image.label', { ns: translationKey }],
17
+ testId: 'editor.toolbar.image',
18
+ icon: 'ph--image-square--regular',
19
+ },
20
+ onImageUpload,
21
+ );
22
+ };
@@ -2,7 +2,6 @@
2
2
  // Copyright 2024 DXOS.org
3
3
  //
4
4
 
5
- export * from './EditorToolbar';
5
+ export * from './types';
6
6
 
7
- export { createEditorAction, createEditorActionGroup } from './actions';
8
- export { type EditorToolbarState, useEditorToolbar } from './useEditorToolbar';
7
+ export * from './EditorToolbar';
@@ -0,0 +1,58 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ import { type EditorView } from '@codemirror/view';
6
+
7
+ import { type ActionGroupBuilderFn, type ToolbarMenuActionGroupProperties } from '@dxos/react-ui-menu';
8
+ import { List, addList, removeList } from '@dxos/ui-editor';
9
+
10
+ import { translationKey } from '#translations';
11
+
12
+ import { type EditorToolbarState } from './types';
13
+
14
+ const listStyles = {
15
+ bullet: 'ph--list-bullets--regular',
16
+ ordered: 'ph--list-numbers--regular',
17
+ task: 'ph--list-checks--regular',
18
+ };
19
+
20
+ /** Add list actions to the builder. */
21
+ export const addLists =
22
+ (state: EditorToolbarState, getView: () => EditorView): ActionGroupBuilderFn =>
23
+ (builder) => {
24
+ const value = state.listStyle ?? '';
25
+ builder.group(
26
+ 'list',
27
+ {
28
+ label: ['list.label', { ns: translationKey }],
29
+ iconOnly: true,
30
+ variant: 'toggleGroup',
31
+ selectCardinality: 'single',
32
+ value,
33
+ } as ToolbarMenuActionGroupProperties,
34
+ (group) => {
35
+ for (const [listStyle, icon] of Object.entries(listStyles)) {
36
+ const checked = value === listStyle;
37
+ group.action(
38
+ `list-${listStyle}`,
39
+ { label: [`list.${listStyle}.label`, { ns: translationKey }], checked, icon },
40
+ () => {
41
+ const view = getView();
42
+ if (!view) {
43
+ return;
44
+ }
45
+
46
+ const listType =
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
+ }
53
+ },
54
+ );
55
+ }
56
+ },
57
+ );
58
+ };