@dxos/react-ui-editor 0.8.4-main.dedc0f3 → 0.8.4-main.ead640a

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 (218) hide show
  1. package/dist/lib/browser/{chunk-22UMM3QJ.mjs → chunk-HL3YF6WC.mjs} +2 -2
  2. package/dist/lib/browser/chunk-HL3YF6WC.mjs.map +7 -0
  3. package/dist/lib/browser/index.mjs +5482 -5519
  4. package/dist/lib/browser/index.mjs.map +4 -4
  5. package/dist/lib/browser/meta.json +1 -1
  6. package/dist/lib/browser/testing/index.mjs +71 -1
  7. package/dist/lib/browser/testing/index.mjs.map +4 -4
  8. package/dist/lib/browser/types/index.mjs +1 -1
  9. package/dist/lib/node-esm/{chunk-YXYQPV6R.mjs → chunk-YJZGD3LY.mjs} +2 -2
  10. package/dist/lib/node-esm/chunk-YJZGD3LY.mjs.map +7 -0
  11. package/dist/lib/node-esm/index.mjs +5482 -5519
  12. package/dist/lib/node-esm/index.mjs.map +4 -4
  13. package/dist/lib/node-esm/meta.json +1 -1
  14. package/dist/lib/node-esm/testing/index.mjs +71 -1
  15. package/dist/lib/node-esm/testing/index.mjs.map +4 -4
  16. package/dist/lib/node-esm/types/index.mjs +1 -1
  17. package/dist/types/src/components/Editor/Editor.d.ts +24 -9
  18. package/dist/types/src/components/Editor/Editor.d.ts.map +1 -1
  19. package/dist/types/src/components/Editor/Editor.stories.d.ts +27 -0
  20. package/dist/types/src/components/Editor/Editor.stories.d.ts.map +1 -0
  21. package/dist/types/src/components/EditorToolbar/EditorToolbar.d.ts.map +1 -1
  22. package/dist/types/src/components/EditorToolbar/util.d.ts +1 -1
  23. package/dist/types/src/components/index.d.ts +0 -1
  24. package/dist/types/src/components/index.d.ts.map +1 -1
  25. package/dist/types/src/extensions/{autocomplete.d.ts → autocomplete/autocomplete.d.ts} +1 -1
  26. package/dist/types/src/extensions/autocomplete/autocomplete.d.ts.map +1 -0
  27. package/dist/types/src/extensions/autocomplete/index.d.ts +5 -0
  28. package/dist/types/src/extensions/autocomplete/index.d.ts.map +1 -0
  29. package/dist/types/src/extensions/autocomplete/match.d.ts +13 -0
  30. package/dist/types/src/extensions/autocomplete/match.d.ts.map +1 -0
  31. package/dist/types/src/extensions/autocomplete/placeholder.d.ts +20 -0
  32. package/dist/types/src/extensions/autocomplete/placeholder.d.ts.map +1 -0
  33. package/dist/types/src/extensions/autocomplete/typeahead.d.ts +10 -0
  34. package/dist/types/src/extensions/autocomplete/typeahead.d.ts.map +1 -0
  35. package/dist/types/src/extensions/automerge/automerge.d.ts +1 -1
  36. package/dist/types/src/extensions/automerge/automerge.d.ts.map +1 -1
  37. package/dist/types/src/extensions/automerge/automerge.stories.d.ts +1 -1
  38. package/dist/types/src/extensions/automerge/automerge.stories.d.ts.map +1 -1
  39. package/dist/types/src/extensions/automerge/sync.d.ts +2 -2
  40. package/dist/types/src/extensions/automerge/sync.d.ts.map +1 -1
  41. package/dist/types/src/extensions/autoscroll.d.ts +2 -2
  42. package/dist/types/src/extensions/autoscroll.d.ts.map +1 -1
  43. package/dist/types/src/extensions/factories.d.ts +7 -2
  44. package/dist/types/src/extensions/factories.d.ts.map +1 -1
  45. package/dist/types/src/extensions/focus.d.ts.map +1 -1
  46. package/dist/types/src/extensions/folding.d.ts.map +1 -1
  47. package/dist/types/src/extensions/index.d.ts +2 -1
  48. package/dist/types/src/extensions/index.d.ts.map +1 -1
  49. package/dist/types/src/extensions/json.d.ts +1 -1
  50. package/dist/types/src/extensions/json.d.ts.map +1 -1
  51. package/dist/types/src/extensions/markdown/bundle.d.ts.map +1 -1
  52. package/dist/types/src/extensions/markdown/decorate.d.ts.map +1 -1
  53. package/dist/types/src/extensions/markdown/link.d.ts.map +1 -1
  54. package/dist/types/src/extensions/modes.d.ts +1 -1
  55. package/dist/types/src/extensions/modes.d.ts.map +1 -1
  56. package/dist/types/src/extensions/outliner/menu.d.ts +8 -0
  57. package/dist/types/src/extensions/outliner/menu.d.ts.map +1 -0
  58. package/dist/types/src/extensions/popover/PopoverMenuProvider.d.ts +36 -0
  59. package/dist/types/src/extensions/popover/PopoverMenuProvider.d.ts.map +1 -0
  60. package/dist/types/src/extensions/popover/index.d.ts +8 -0
  61. package/dist/types/src/extensions/popover/index.d.ts.map +1 -0
  62. package/dist/types/src/extensions/popover/menu-presets.d.ts +4 -0
  63. package/dist/types/src/extensions/popover/menu-presets.d.ts.map +1 -0
  64. package/dist/types/src/extensions/popover/menu.d.ts +24 -0
  65. package/dist/types/src/extensions/popover/menu.d.ts.map +1 -0
  66. package/dist/types/src/extensions/popover/modal.d.ts +7 -0
  67. package/dist/types/src/extensions/popover/modal.d.ts.map +1 -0
  68. package/dist/types/src/extensions/popover/popover.d.ts +47 -0
  69. package/dist/types/src/extensions/popover/popover.d.ts.map +1 -0
  70. package/dist/types/src/extensions/popover/usePopoverMenu.d.ts +34 -0
  71. package/dist/types/src/extensions/popover/usePopoverMenu.d.ts.map +1 -0
  72. package/dist/types/src/extensions/popover/util.d.ts +8 -0
  73. package/dist/types/src/extensions/popover/util.d.ts.map +1 -0
  74. package/dist/types/src/extensions/preview/preview.d.ts +0 -2
  75. package/dist/types/src/extensions/preview/preview.d.ts.map +1 -1
  76. package/dist/types/src/extensions/state.d.ts +2 -0
  77. package/dist/types/src/extensions/state.d.ts.map +1 -0
  78. package/dist/types/src/extensions/tags/streamer.d.ts.map +1 -1
  79. package/dist/types/src/extensions/tags/xml-tags.d.ts +1 -0
  80. package/dist/types/src/extensions/tags/xml-tags.d.ts.map +1 -1
  81. package/dist/types/src/hooks/useTextEditor.d.ts +4 -8
  82. package/dist/types/src/hooks/useTextEditor.d.ts.map +1 -1
  83. package/dist/types/src/stories/{Command.stories.d.ts → CommandDialog.stories.d.ts} +2 -3
  84. package/dist/types/src/stories/CommandDialog.stories.d.ts.map +1 -0
  85. package/dist/types/src/stories/Comments.stories.d.ts +3 -4
  86. package/dist/types/src/stories/Comments.stories.d.ts.map +1 -1
  87. package/dist/types/src/stories/EditorToolbar.stories.d.ts +1 -2
  88. package/dist/types/src/stories/EditorToolbar.stories.d.ts.map +1 -1
  89. package/dist/types/src/stories/Experimental.stories.d.ts +3 -4
  90. package/dist/types/src/stories/Experimental.stories.d.ts.map +1 -1
  91. package/dist/types/src/stories/Markdown.stories.d.ts +3 -4
  92. package/dist/types/src/stories/Markdown.stories.d.ts.map +1 -1
  93. package/dist/types/src/stories/Outliner.stories.d.ts +0 -1
  94. package/dist/types/src/stories/Outliner.stories.d.ts.map +1 -1
  95. package/dist/types/src/stories/{CommandMenu.stories.d.ts → Popover.stories.d.ts} +6 -6
  96. package/dist/types/src/stories/Popover.stories.d.ts.map +1 -0
  97. package/dist/types/src/stories/Preview.stories.d.ts +3 -4
  98. package/dist/types/src/stories/Preview.stories.d.ts.map +1 -1
  99. package/dist/types/src/stories/Tags.stories.d.ts +0 -1
  100. package/dist/types/src/stories/Tags.stories.d.ts.map +1 -1
  101. package/dist/types/src/stories/TextEditor.stories.d.ts +3 -5
  102. package/dist/types/src/stories/TextEditor.stories.d.ts.map +1 -1
  103. package/dist/types/src/stories/components/EditorStory.d.ts +5 -5
  104. package/dist/types/src/stories/components/EditorStory.d.ts.map +1 -1
  105. package/dist/types/src/styles/theme.d.ts.map +1 -1
  106. package/dist/types/src/testing/PreviewPopover.d.ts +20 -0
  107. package/dist/types/src/testing/PreviewPopover.d.ts.map +1 -0
  108. package/dist/types/src/testing/index.d.ts +1 -0
  109. package/dist/types/src/testing/index.d.ts.map +1 -1
  110. package/dist/types/src/types/types.d.ts +1 -1
  111. package/dist/types/src/types/types.d.ts.map +1 -1
  112. package/dist/types/src/util/index.d.ts +0 -1
  113. package/dist/types/src/util/index.d.ts.map +1 -1
  114. package/dist/types/tsconfig.tsbuildinfo +1 -1
  115. package/package.json +55 -52
  116. package/src/components/Editor/Editor.stories.tsx +69 -0
  117. package/src/components/Editor/Editor.tsx +57 -14
  118. package/src/components/EditorToolbar/EditorToolbar.tsx +1 -0
  119. package/src/components/index.ts +0 -1
  120. package/src/extensions/{autocomplete.ts → autocomplete/autocomplete.ts} +2 -1
  121. package/src/extensions/autocomplete/index.ts +8 -0
  122. package/src/extensions/autocomplete/match.ts +46 -0
  123. package/src/extensions/{command → autocomplete}/placeholder.ts +21 -17
  124. package/src/extensions/{command → autocomplete}/typeahead.ts +6 -48
  125. package/src/extensions/automerge/automerge.stories.tsx +8 -8
  126. package/src/extensions/automerge/automerge.ts +28 -9
  127. package/src/extensions/automerge/sync.ts +7 -3
  128. package/src/extensions/autoscroll.ts +43 -37
  129. package/src/extensions/factories.ts +41 -12
  130. package/src/extensions/focus.ts +5 -4
  131. package/src/extensions/folding.tsx +4 -6
  132. package/src/extensions/hashtag.tsx +2 -2
  133. package/src/extensions/index.ts +2 -1
  134. package/src/extensions/json.ts +1 -1
  135. package/src/extensions/markdown/bundle.ts +16 -4
  136. package/src/extensions/markdown/decorate.ts +1 -0
  137. package/src/extensions/markdown/link.ts +3 -0
  138. package/src/extensions/modes.ts +2 -2
  139. package/src/extensions/{command/floating-menu.ts → outliner/menu.ts} +15 -20
  140. package/src/extensions/outliner/outliner.ts +3 -3
  141. package/src/extensions/popover/PopoverMenuProvider.tsx +221 -0
  142. package/src/extensions/popover/index.ts +12 -0
  143. package/src/extensions/popover/menu-presets.ts +124 -0
  144. package/src/extensions/popover/menu.ts +67 -0
  145. package/src/extensions/popover/modal.ts +24 -0
  146. package/src/extensions/popover/popover.ts +291 -0
  147. package/src/extensions/popover/usePopoverMenu.ts +173 -0
  148. package/src/extensions/popover/util.ts +29 -0
  149. package/src/extensions/preview/index.ts +1 -1
  150. package/src/extensions/preview/preview.ts +0 -5
  151. package/src/extensions/selection.ts +2 -2
  152. package/src/extensions/state.ts +7 -0
  153. package/src/extensions/tags/streamer.ts +4 -5
  154. package/src/extensions/tags/xml-tags.ts +59 -1
  155. package/src/hooks/useTextEditor.ts +27 -39
  156. package/src/stories/{Command.stories.tsx → CommandDialog.stories.tsx} +10 -22
  157. package/src/stories/Comments.stories.tsx +5 -5
  158. package/src/stories/EditorToolbar.stories.tsx +6 -5
  159. package/src/stories/Experimental.stories.tsx +6 -6
  160. package/src/stories/Markdown.stories.tsx +5 -5
  161. package/src/stories/Outliner.stories.tsx +42 -26
  162. package/src/stories/Popover.stories.tsx +163 -0
  163. package/src/stories/Preview.stories.tsx +9 -9
  164. package/src/stories/Tags.stories.tsx +5 -5
  165. package/src/stories/TextEditor.stories.tsx +7 -32
  166. package/src/stories/components/EditorStory.tsx +7 -5
  167. package/src/styles/theme.ts +12 -10
  168. package/src/{components/Popover/RefDropdownMenu.tsx → testing/PreviewPopover.tsx} +20 -29
  169. package/src/testing/index.ts +1 -0
  170. package/src/types/types.ts +1 -1
  171. package/src/util/index.ts +0 -1
  172. package/dist/lib/browser/chunk-22UMM3QJ.mjs.map +0 -7
  173. package/dist/lib/node-esm/chunk-YXYQPV6R.mjs.map +0 -7
  174. package/dist/types/src/components/Popover/CommandMenu.d.ts +0 -34
  175. package/dist/types/src/components/Popover/CommandMenu.d.ts.map +0 -1
  176. package/dist/types/src/components/Popover/RefDropdownMenu.d.ts +0 -14
  177. package/dist/types/src/components/Popover/RefDropdownMenu.d.ts.map +0 -1
  178. package/dist/types/src/components/Popover/RefPopover.d.ts +0 -37
  179. package/dist/types/src/components/Popover/RefPopover.d.ts.map +0 -1
  180. package/dist/types/src/components/Popover/index.d.ts +0 -4
  181. package/dist/types/src/components/Popover/index.d.ts.map +0 -1
  182. package/dist/types/src/extensions/autocomplete.d.ts.map +0 -1
  183. package/dist/types/src/extensions/command/action.d.ts +0 -17
  184. package/dist/types/src/extensions/command/action.d.ts.map +0 -1
  185. package/dist/types/src/extensions/command/command-menu.d.ts +0 -20
  186. package/dist/types/src/extensions/command/command-menu.d.ts.map +0 -1
  187. package/dist/types/src/extensions/command/command.d.ts +0 -6
  188. package/dist/types/src/extensions/command/command.d.ts.map +0 -1
  189. package/dist/types/src/extensions/command/floating-menu.d.ts +0 -7
  190. package/dist/types/src/extensions/command/floating-menu.d.ts.map +0 -1
  191. package/dist/types/src/extensions/command/hint.d.ts +0 -19
  192. package/dist/types/src/extensions/command/hint.d.ts.map +0 -1
  193. package/dist/types/src/extensions/command/index.d.ts +0 -7
  194. package/dist/types/src/extensions/command/index.d.ts.map +0 -1
  195. package/dist/types/src/extensions/command/placeholder.d.ts +0 -10
  196. package/dist/types/src/extensions/command/placeholder.d.ts.map +0 -1
  197. package/dist/types/src/extensions/command/state.d.ts +0 -16
  198. package/dist/types/src/extensions/command/state.d.ts.map +0 -1
  199. package/dist/types/src/extensions/command/typeahead.d.ts +0 -22
  200. package/dist/types/src/extensions/command/typeahead.d.ts.map +0 -1
  201. package/dist/types/src/extensions/command/useCommandMenu.d.ts +0 -26
  202. package/dist/types/src/extensions/command/useCommandMenu.d.ts.map +0 -1
  203. package/dist/types/src/stories/Command.stories.d.ts.map +0 -1
  204. package/dist/types/src/stories/CommandMenu.stories.d.ts.map +0 -1
  205. package/dist/types/src/util/domino.d.ts +0 -18
  206. package/dist/types/src/util/domino.d.ts.map +0 -1
  207. package/src/components/Popover/CommandMenu.tsx +0 -279
  208. package/src/components/Popover/RefPopover.tsx +0 -117
  209. package/src/components/Popover/index.ts +0 -7
  210. package/src/extensions/command/action.ts +0 -56
  211. package/src/extensions/command/command-menu.ts +0 -211
  212. package/src/extensions/command/command.ts +0 -34
  213. package/src/extensions/command/hint.ts +0 -103
  214. package/src/extensions/command/index.ts +0 -10
  215. package/src/extensions/command/state.ts +0 -90
  216. package/src/extensions/command/useCommandMenu.ts +0 -119
  217. package/src/stories/CommandMenu.stories.tsx +0 -160
  218. package/src/util/domino.ts +0 -51
@@ -7,11 +7,11 @@ import { Decoration, type DecorationSet, EditorView, ViewPlugin, type ViewUpdate
7
7
 
8
8
  import { mx } from '@dxos/react-ui-theme';
9
9
 
10
- import { floatingMenu } from '../command';
11
10
  import { decorateMarkdown } from '../markdown';
12
11
 
13
12
  import { commands } from './commands';
14
13
  import { editor } from './editor';
14
+ import { menu } from './menu';
15
15
  import { selectionCompartment, selectionEquals, selectionFacet } from './selection';
16
16
  import { outlinerTree, treeFacet } from './tree';
17
17
 
@@ -52,7 +52,7 @@ export const outliner = (_options: OutlinerProps = {}): Extension => [
52
52
  editor(),
53
53
 
54
54
  // Floating menu.
55
- floatingMenu(),
55
+ menu(),
56
56
 
57
57
  // Line decorations.
58
58
  decorations(),
@@ -159,7 +159,7 @@ const decorations = () => [
159
159
  '.cm-list-item-focused': {
160
160
  borderColor: 'var(--dx-accentFocusIndicator)',
161
161
  },
162
- '[data-has-focus] & .cm-list-item-selected': {
162
+ '&:focus-within .cm-list-item-selected': {
163
163
  borderColor: 'var(--dx-separator)',
164
164
  },
165
165
  }),
@@ -0,0 +1,221 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ import { type EditorView } from '@codemirror/view';
6
+ import { useControllableState } from '@radix-ui/react-use-controllable-state';
7
+ import React, { Fragment, type PropsWithChildren, useCallback, useEffect, useRef, useState } from 'react';
8
+
9
+ import { addEventListener } from '@dxos/async';
10
+ import { invariant } from '@dxos/invariant';
11
+ import {
12
+ type DxAnchorActivate,
13
+ Icon,
14
+ Popover,
15
+ toLocalizedString,
16
+ useDynamicRef,
17
+ useThemeContext,
18
+ useTranslation,
19
+ } from '@dxos/react-ui';
20
+
21
+ import { type PopoverMenuGroup, type PopoverMenuItem } from './menu';
22
+
23
+ export type PopoverMenuProviderProps = PropsWithChildren<{
24
+ view?: EditorView | null;
25
+ groups: PopoverMenuGroup[];
26
+ currentItem?: string;
27
+ open?: boolean;
28
+ defaultOpen?: boolean;
29
+ numItems?: number;
30
+ onOpenChange?: (event: { view: EditorView; open: boolean; trigger?: string }) => void;
31
+ onActivate?: (event: { view: EditorView; trigger?: string }) => void;
32
+ onSelect?: (event: { view: EditorView; item: PopoverMenuItem }) => void;
33
+ onCancel?: (event: { view: EditorView }) => void;
34
+ }>;
35
+
36
+ /**
37
+ * Implements the Popover and listens for the `dx-anchor-activate` event from the
38
+ * `popover` extension's decoration.
39
+ *
40
+ * NOTE: We don't use DropdownMenu because the command menu needs to manage focus explicitly.
41
+ * I.e., focus must remain in the editor while displaying the menu (for type-ahead).
42
+ */
43
+ export const PopoverMenuProvider = ({
44
+ children,
45
+ view,
46
+ groups,
47
+ currentItem,
48
+ open: openParam,
49
+ defaultOpen,
50
+ numItems = 8,
51
+ onOpenChange,
52
+ onActivate,
53
+ onSelect,
54
+ onCancel,
55
+ }: PopoverMenuProviderProps) => {
56
+ const { tx } = useThemeContext();
57
+ const triggerRef = useRef<HTMLButtonElement | null>(null);
58
+ const [root, setRoot] = useState<HTMLDivElement | null>(null);
59
+ const viewRef = useDynamicRef(view);
60
+ const [open, setOpen] = useControllableState({
61
+ prop: openParam,
62
+ defaultProp: defaultOpen,
63
+ onChange: (open) => {
64
+ invariant(viewRef.current);
65
+ onOpenChange?.({ view: viewRef.current, open });
66
+ },
67
+ });
68
+
69
+ useEffect(() => {
70
+ if (!root) {
71
+ return;
72
+ }
73
+
74
+ // Listen for trigger.
75
+ return addEventListener(
76
+ root,
77
+ 'dx-anchor-activate' as any,
78
+ (event: DxAnchorActivate) => {
79
+ const { trigger, refId } = event;
80
+ console.log('update', trigger, refId);
81
+
82
+ // If this has a `refId`, then it’s probably a URL or DXN and out of scope for this component.
83
+ if (!refId) {
84
+ triggerRef.current = trigger as HTMLButtonElement;
85
+ if (onActivate) {
86
+ onActivate({ view: viewRef.current!, trigger: trigger.getAttribute('data-trigger') ?? undefined });
87
+ } else {
88
+ requestAnimationFrame(() => setOpen(true));
89
+ }
90
+ }
91
+ },
92
+ {
93
+ capture: true,
94
+ passive: false,
95
+ },
96
+ );
97
+ }, [root, onActivate]);
98
+
99
+ const handleSelect = useCallback<NonNullable<MenuProps['onSelect']>>(
100
+ (item) => {
101
+ invariant(viewRef.current);
102
+ onSelect?.({ view: viewRef.current, item });
103
+ },
104
+ [viewRef, onSelect],
105
+ );
106
+
107
+ const menuGroups = groups.filter((group) => group.items.length > 0);
108
+
109
+ return (
110
+ <Popover.Root modal={false} open={open} onOpenChange={setOpen}>
111
+ <Popover.VirtualTrigger virtualRef={triggerRef} />
112
+ <Popover.Portal>
113
+ <Popover.Content
114
+ align='start'
115
+ classNames={tx('menu.content', 'menu--exotic-unfocusable', { elevation: 'positioned' }, [
116
+ 'overflow-y-auto',
117
+ !menuGroups.length && 'hidden',
118
+ ])}
119
+ style={{
120
+ maxBlockSize: 36 * numItems + 10,
121
+ }}
122
+ /**
123
+ * NOTE: We keep the focus in the editor, but Radix routes escape key.
124
+ */
125
+ onEscapeKeyDown={() => {
126
+ // NOTE: Able to cancel if not in valid state.
127
+ // event.preventDefault();
128
+ onCancel?.({ view: view! });
129
+ }}
130
+ onOpenAutoFocus={(event) => event.preventDefault()}
131
+ >
132
+ <Popover.Viewport classNames={tx('menu.viewport', 'menu__viewport--exotic-unfocusable', {})}>
133
+ <Menu groups={menuGroups} currentItem={currentItem} onSelect={handleSelect} />
134
+ </Popover.Viewport>
135
+ <Popover.Arrow />
136
+ </Popover.Content>
137
+ </Popover.Portal>
138
+
139
+ <div ref={setRoot} role='none' className='contents'>
140
+ {children}
141
+ </div>
142
+ </Popover.Root>
143
+ );
144
+ };
145
+
146
+ //
147
+ // Menu
148
+ //
149
+
150
+ type MenuProps = {
151
+ groups: PopoverMenuGroup[];
152
+ } & Pick<MenuGroupProps, 'currentItem' | 'onSelect'>;
153
+
154
+ const Menu = ({ groups, currentItem, onSelect }: MenuProps) => {
155
+ const { tx } = useThemeContext();
156
+ return (
157
+ <ul>
158
+ {groups.map((group, index) => (
159
+ <Fragment key={group.id}>
160
+ <MenuGroup group={group} currentItem={currentItem} onSelect={onSelect} />
161
+ {index < groups.length - 1 && <div className={tx('menu.separator', 'menu__item', {})} />}
162
+ </Fragment>
163
+ ))}
164
+ </ul>
165
+ );
166
+ };
167
+
168
+ type MenuGroupProps = {
169
+ group: PopoverMenuGroup;
170
+ currentItem?: string;
171
+ } & Pick<MenuItemProps, 'onSelect'>;
172
+
173
+ const MenuGroup = ({ group, currentItem, onSelect }: MenuGroupProps) => {
174
+ const { tx } = useThemeContext();
175
+ const { t } = useTranslation();
176
+
177
+ return (
178
+ <>
179
+ {group.label && (
180
+ <div className={tx('menu.groupLabel', 'menu__group__label', {})}>
181
+ <span>{toLocalizedString(group.label, t)}</span>
182
+ </div>
183
+ )}
184
+
185
+ {group.items.map((item) => (
186
+ <MenuItem key={item.id} item={item} current={currentItem === item.id} onSelect={onSelect} />
187
+ ))}
188
+ </>
189
+ );
190
+ };
191
+
192
+ type MenuItemProps = {
193
+ item: PopoverMenuItem;
194
+ current: boolean;
195
+ onSelect?: (item: PopoverMenuItem) => void;
196
+ };
197
+
198
+ const MenuItem = ({ item, current, onSelect }: MenuItemProps) => {
199
+ const { tx } = useThemeContext();
200
+ const { t } = useTranslation();
201
+
202
+ const listRef = useRef<HTMLLIElement>(null);
203
+ useEffect(() => {
204
+ if (current && listRef.current) {
205
+ listRef.current.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
206
+ }
207
+ }, [current]);
208
+
209
+ const handleSelect = useCallback(() => onSelect?.(item), [item, onSelect]);
210
+
211
+ return (
212
+ <li
213
+ ref={listRef}
214
+ className={tx('menu.item', 'menu__item--exotic-unfocusable', {}, [current && 'bg-hoverSurface'])}
215
+ onClick={handleSelect}
216
+ >
217
+ {item.icon && <Icon icon={item.icon} size={5} />}
218
+ <span className='grow truncate'>{toLocalizedString(item.label, t)}</span>
219
+ </li>
220
+ );
221
+ };
@@ -0,0 +1,12 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ export * from './menu';
6
+ export * from './menu-presets';
7
+ export * from './modal';
8
+ export * from './popover';
9
+ export * from './util';
10
+
11
+ export * from './PopoverMenuProvider';
12
+ export * from './usePopoverMenu';
@@ -0,0 +1,124 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ import { popoverRangeEffect } from '../../extensions';
6
+
7
+ import { type PopoverMenuGroup } from './menu';
8
+ import { insertAtLineStart } from './util';
9
+
10
+ export const formattingCommands: PopoverMenuGroup = {
11
+ id: 'markdown',
12
+ label: 'Markdown',
13
+ items: [
14
+ {
15
+ id: 'heading-1',
16
+ label: 'Heading 1',
17
+ icon: 'ph--text-h-one--regular',
18
+ onSelect: (view, head) => insertAtLineStart(view, head, '# '),
19
+ },
20
+ {
21
+ id: 'heading-2',
22
+ label: 'Heading 2',
23
+ icon: 'ph--text-h-two--regular',
24
+ onSelect: (view, head) => insertAtLineStart(view, head, '## '),
25
+ },
26
+ {
27
+ id: 'heading-3',
28
+ label: 'Heading 3',
29
+ icon: 'ph--text-h-three--regular',
30
+ onSelect: (view, head) => insertAtLineStart(view, head, '### '),
31
+ },
32
+ {
33
+ id: 'heading-4',
34
+ label: 'Heading 4',
35
+ icon: 'ph--text-h-four--regular',
36
+ onSelect: (view, head) => insertAtLineStart(view, head, '#### '),
37
+ },
38
+ {
39
+ id: 'heading-5',
40
+ label: 'Heading 5',
41
+ icon: 'ph--text-h-five--regular',
42
+ onSelect: (view, head) => insertAtLineStart(view, head, '##### '),
43
+ },
44
+ {
45
+ id: 'heading-6',
46
+ label: 'Heading 6',
47
+ icon: 'ph--text-h-six--regular',
48
+ onSelect: (view, head) => insertAtLineStart(view, head, '###### '),
49
+ },
50
+ {
51
+ id: 'bullet-list',
52
+ label: 'Bullet List',
53
+ icon: 'ph--list-bullets--regular',
54
+ onSelect: (view, head) => insertAtLineStart(view, head, '- '),
55
+ },
56
+ {
57
+ id: 'numbered-list',
58
+ label: 'Numbered List',
59
+ icon: 'ph--list-numbers--regular',
60
+ onSelect: (view, head) => insertAtLineStart(view, head, '1. '),
61
+ },
62
+ {
63
+ id: 'task-list',
64
+ label: 'Task List',
65
+ icon: 'ph--list-checks--regular',
66
+ onSelect: (view, head) => insertAtLineStart(view, head, '- [ ] '),
67
+ },
68
+ {
69
+ id: 'quote',
70
+ label: 'Quote',
71
+ icon: 'ph--quotes--regular',
72
+ onSelect: (view, head) => insertAtLineStart(view, head, '> '),
73
+ },
74
+ {
75
+ id: 'code-block',
76
+ label: 'Code Block',
77
+ icon: 'ph--code-block--regular',
78
+ onSelect: (view, head) => insertAtLineStart(view, head, '```\n\n```'),
79
+ },
80
+ {
81
+ id: 'table',
82
+ label: 'Table',
83
+ icon: 'ph--table--regular',
84
+ onSelect: (view, head) => insertAtLineStart(view, head, '| | | |\n|---|---|---|\n| | | |'),
85
+ },
86
+ ],
87
+ };
88
+
89
+ export const linkSlashCommands: PopoverMenuGroup = {
90
+ id: 'link',
91
+ label: 'Link',
92
+ items: [
93
+ {
94
+ id: 'inline-link',
95
+ label: 'Inline link',
96
+ icon: 'ph--link--regular',
97
+ onSelect: (view, head) => {
98
+ view.dispatch({
99
+ changes: { from: head, insert: '@' },
100
+ selection: { anchor: head + 1, head: head + 1 },
101
+ effects: popoverRangeEffect.of({
102
+ trigger: '@',
103
+ range: { from: head, to: head + 1 },
104
+ }),
105
+ });
106
+ },
107
+ },
108
+ {
109
+ id: 'block-embed',
110
+ label: 'Block embed',
111
+ icon: 'ph--lego--regular',
112
+ onSelect: (view, head) => {
113
+ view.dispatch({
114
+ changes: { from: head, insert: '@@' },
115
+ selection: { anchor: head + 2, head: head + 2 },
116
+ effects: popoverRangeEffect.of({
117
+ trigger: '@',
118
+ range: { from: head, to: head + 2 },
119
+ }),
120
+ });
121
+ },
122
+ },
123
+ ],
124
+ };
@@ -0,0 +1,67 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ import { type EditorView } from '@codemirror/view';
6
+
7
+ import { type Label } from '@dxos/react-ui';
8
+ import { type MaybePromise } from '@dxos/util';
9
+
10
+ import { insertAtCursor } from './util';
11
+
12
+ export type PopoverMenuGroup = {
13
+ id: string;
14
+ label?: Label;
15
+ items: PopoverMenuItem[];
16
+ };
17
+
18
+ export type PopoverMenuItem = {
19
+ id: string;
20
+ label: Label;
21
+ icon?: string;
22
+ onSelect?: (view: EditorView, head: number) => MaybePromise<void>;
23
+ };
24
+
25
+ export const getMenuItem = (groups: PopoverMenuGroup[], id?: string): PopoverMenuItem | undefined => {
26
+ return groups.flatMap((group) => group.items).find((item) => item.id === id);
27
+ };
28
+
29
+ export const getNextMenuItem = (groups: PopoverMenuGroup[], id?: string): PopoverMenuItem => {
30
+ const items = groups.flatMap((group) => group.items);
31
+ const index = items.findIndex((item) => item.id === id);
32
+ return index < items.length - 1 ? items[index + 1] : items[index];
33
+ };
34
+
35
+ export const getPreviousMenuItem = (groups: PopoverMenuGroup[], id?: string): PopoverMenuItem => {
36
+ const items = groups.flatMap((group) => group.items);
37
+ const index = items.findIndex((item) => item.id === id);
38
+ return index > 0 ? items[index - 1] : items[index];
39
+ };
40
+
41
+ export const createMenuGroup = ({
42
+ id = 'menu',
43
+ label,
44
+ items,
45
+ }: {
46
+ id?: string;
47
+ label?: Label;
48
+ items: string[];
49
+ }): PopoverMenuGroup => ({
50
+ id,
51
+ label,
52
+ items: items.map((item, i) => ({
53
+ id: `${id}-${i}`,
54
+ label: item,
55
+ onSelect: (view, head) => insertAtCursor(view, head, item),
56
+ })),
57
+ });
58
+
59
+ export const filterMenuGroups = (
60
+ groups: PopoverMenuGroup[],
61
+ filter: (item: PopoverMenuItem) => boolean,
62
+ ): PopoverMenuGroup[] => {
63
+ return groups.map((group) => ({
64
+ ...group,
65
+ items: group.items.filter(filter),
66
+ }));
67
+ };
@@ -0,0 +1,24 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ import { StateEffect, StateField } from '@codemirror/state';
6
+
7
+ export const modalStateEffect = StateEffect.define<boolean>();
8
+
9
+ /**
10
+ * Determines if a modal dialog (e.g., popover) is active.
11
+ */
12
+ export const modalStateField = StateField.define<boolean>({
13
+ create: () => false,
14
+ update: (value, tr) => {
15
+ let newValue = value;
16
+ for (const effect of tr.effects) {
17
+ if (effect.is(modalStateEffect)) {
18
+ newValue = effect.value;
19
+ }
20
+ }
21
+
22
+ return newValue;
23
+ },
24
+ });