@dxos/react-ui-editor 0.8.4-main.e098934 → 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 (205) 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 +3555 -3484
  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.map +2 -2
  7. package/dist/lib/browser/types/index.mjs +1 -1
  8. package/dist/lib/node-esm/{chunk-YXYQPV6R.mjs → chunk-YJZGD3LY.mjs} +2 -2
  9. package/dist/lib/node-esm/chunk-YJZGD3LY.mjs.map +7 -0
  10. package/dist/lib/node-esm/index.mjs +3555 -3484
  11. package/dist/lib/node-esm/index.mjs.map +4 -4
  12. package/dist/lib/node-esm/meta.json +1 -1
  13. package/dist/lib/node-esm/testing/index.mjs.map +2 -2
  14. package/dist/lib/node-esm/types/index.mjs +1 -1
  15. package/dist/types/src/components/Editor/Editor.d.ts +24 -9
  16. package/dist/types/src/components/Editor/Editor.d.ts.map +1 -1
  17. package/dist/types/src/components/Editor/Editor.stories.d.ts +27 -0
  18. package/dist/types/src/components/Editor/Editor.stories.d.ts.map +1 -0
  19. package/dist/types/src/components/EditorToolbar/EditorToolbar.d.ts.map +1 -1
  20. package/dist/types/src/components/EditorToolbar/util.d.ts +1 -1
  21. package/dist/types/src/components/index.d.ts +0 -1
  22. package/dist/types/src/components/index.d.ts.map +1 -1
  23. package/dist/types/src/extensions/{autocomplete.d.ts → autocomplete/autocomplete.d.ts} +1 -1
  24. package/dist/types/src/extensions/autocomplete/autocomplete.d.ts.map +1 -0
  25. package/dist/types/src/extensions/autocomplete/index.d.ts +5 -0
  26. package/dist/types/src/extensions/autocomplete/index.d.ts.map +1 -0
  27. package/dist/types/src/extensions/autocomplete/match.d.ts +13 -0
  28. package/dist/types/src/extensions/autocomplete/match.d.ts.map +1 -0
  29. package/dist/types/src/extensions/autocomplete/placeholder.d.ts +20 -0
  30. package/dist/types/src/extensions/autocomplete/placeholder.d.ts.map +1 -0
  31. package/dist/types/src/extensions/autocomplete/typeahead.d.ts +10 -0
  32. package/dist/types/src/extensions/autocomplete/typeahead.d.ts.map +1 -0
  33. package/dist/types/src/extensions/automerge/automerge.d.ts +1 -1
  34. package/dist/types/src/extensions/automerge/automerge.d.ts.map +1 -1
  35. package/dist/types/src/extensions/automerge/automerge.stories.d.ts +1 -1
  36. package/dist/types/src/extensions/automerge/automerge.stories.d.ts.map +1 -1
  37. package/dist/types/src/extensions/automerge/sync.d.ts +2 -2
  38. package/dist/types/src/extensions/automerge/sync.d.ts.map +1 -1
  39. package/dist/types/src/extensions/autoscroll.d.ts +2 -2
  40. package/dist/types/src/extensions/autoscroll.d.ts.map +1 -1
  41. package/dist/types/src/extensions/factories.d.ts +7 -2
  42. package/dist/types/src/extensions/factories.d.ts.map +1 -1
  43. package/dist/types/src/extensions/focus.d.ts.map +1 -1
  44. package/dist/types/src/extensions/folding.d.ts.map +1 -1
  45. package/dist/types/src/extensions/index.d.ts +2 -1
  46. package/dist/types/src/extensions/index.d.ts.map +1 -1
  47. package/dist/types/src/extensions/json.d.ts +1 -1
  48. package/dist/types/src/extensions/json.d.ts.map +1 -1
  49. package/dist/types/src/extensions/markdown/bundle.d.ts.map +1 -1
  50. package/dist/types/src/extensions/markdown/decorate.d.ts.map +1 -1
  51. package/dist/types/src/extensions/modes.d.ts +1 -1
  52. package/dist/types/src/extensions/modes.d.ts.map +1 -1
  53. package/dist/types/src/extensions/outliner/menu.d.ts +8 -0
  54. package/dist/types/src/extensions/outliner/menu.d.ts.map +1 -0
  55. package/dist/types/src/extensions/popover/PopoverMenuProvider.d.ts +36 -0
  56. package/dist/types/src/extensions/popover/PopoverMenuProvider.d.ts.map +1 -0
  57. package/dist/types/src/extensions/popover/index.d.ts +8 -0
  58. package/dist/types/src/extensions/popover/index.d.ts.map +1 -0
  59. package/dist/types/src/extensions/popover/menu-presets.d.ts +4 -0
  60. package/dist/types/src/extensions/popover/menu-presets.d.ts.map +1 -0
  61. package/dist/types/src/extensions/popover/menu.d.ts +24 -0
  62. package/dist/types/src/extensions/popover/menu.d.ts.map +1 -0
  63. package/dist/types/src/extensions/popover/modal.d.ts +7 -0
  64. package/dist/types/src/extensions/popover/modal.d.ts.map +1 -0
  65. package/dist/types/src/extensions/popover/popover.d.ts +47 -0
  66. package/dist/types/src/extensions/popover/popover.d.ts.map +1 -0
  67. package/dist/types/src/extensions/popover/usePopoverMenu.d.ts +34 -0
  68. package/dist/types/src/extensions/popover/usePopoverMenu.d.ts.map +1 -0
  69. package/dist/types/src/extensions/popover/util.d.ts +8 -0
  70. package/dist/types/src/extensions/popover/util.d.ts.map +1 -0
  71. package/dist/types/src/extensions/preview/preview.d.ts +0 -1
  72. package/dist/types/src/extensions/preview/preview.d.ts.map +1 -1
  73. package/dist/types/src/extensions/state.d.ts +2 -0
  74. package/dist/types/src/extensions/state.d.ts.map +1 -0
  75. package/dist/types/src/extensions/tags/streamer.d.ts.map +1 -1
  76. package/dist/types/src/extensions/tags/xml-tags.d.ts +1 -0
  77. package/dist/types/src/extensions/tags/xml-tags.d.ts.map +1 -1
  78. package/dist/types/src/hooks/useTextEditor.d.ts +4 -8
  79. package/dist/types/src/hooks/useTextEditor.d.ts.map +1 -1
  80. package/dist/types/src/stories/{Command.stories.d.ts → CommandDialog.stories.d.ts} +2 -3
  81. package/dist/types/src/stories/CommandDialog.stories.d.ts.map +1 -0
  82. package/dist/types/src/stories/Comments.stories.d.ts +3 -4
  83. package/dist/types/src/stories/Comments.stories.d.ts.map +1 -1
  84. package/dist/types/src/stories/EditorToolbar.stories.d.ts +1 -2
  85. package/dist/types/src/stories/EditorToolbar.stories.d.ts.map +1 -1
  86. package/dist/types/src/stories/Experimental.stories.d.ts +3 -4
  87. package/dist/types/src/stories/Experimental.stories.d.ts.map +1 -1
  88. package/dist/types/src/stories/Markdown.stories.d.ts +3 -4
  89. package/dist/types/src/stories/Markdown.stories.d.ts.map +1 -1
  90. package/dist/types/src/stories/Outliner.stories.d.ts +0 -1
  91. package/dist/types/src/stories/Outliner.stories.d.ts.map +1 -1
  92. package/dist/types/src/stories/{CommandMenu.stories.d.ts → Popover.stories.d.ts} +6 -6
  93. package/dist/types/src/stories/Popover.stories.d.ts.map +1 -0
  94. package/dist/types/src/stories/Preview.stories.d.ts +3 -4
  95. package/dist/types/src/stories/Preview.stories.d.ts.map +1 -1
  96. package/dist/types/src/stories/Tags.stories.d.ts +0 -1
  97. package/dist/types/src/stories/Tags.stories.d.ts.map +1 -1
  98. package/dist/types/src/stories/TextEditor.stories.d.ts +3 -5
  99. package/dist/types/src/stories/TextEditor.stories.d.ts.map +1 -1
  100. package/dist/types/src/stories/components/EditorStory.d.ts +5 -5
  101. package/dist/types/src/stories/components/EditorStory.d.ts.map +1 -1
  102. package/dist/types/src/styles/theme.d.ts.map +1 -1
  103. package/dist/types/src/testing/PreviewPopover.d.ts.map +1 -1
  104. package/dist/types/src/types/types.d.ts +1 -1
  105. package/dist/types/src/types/types.d.ts.map +1 -1
  106. package/dist/types/src/util/index.d.ts +0 -1
  107. package/dist/types/src/util/index.d.ts.map +1 -1
  108. package/dist/types/tsconfig.tsbuildinfo +1 -1
  109. package/package.json +54 -51
  110. package/src/components/Editor/Editor.stories.tsx +69 -0
  111. package/src/components/Editor/Editor.tsx +57 -14
  112. package/src/components/EditorToolbar/EditorToolbar.tsx +1 -0
  113. package/src/components/index.ts +0 -1
  114. package/src/extensions/{autocomplete.ts → autocomplete/autocomplete.ts} +2 -1
  115. package/src/extensions/autocomplete/index.ts +8 -0
  116. package/src/extensions/autocomplete/match.ts +46 -0
  117. package/src/extensions/{command → autocomplete}/placeholder.ts +21 -17
  118. package/src/extensions/{command → autocomplete}/typeahead.ts +6 -48
  119. package/src/extensions/automerge/automerge.stories.tsx +8 -8
  120. package/src/extensions/automerge/automerge.ts +28 -9
  121. package/src/extensions/automerge/sync.ts +7 -3
  122. package/src/extensions/autoscroll.ts +29 -29
  123. package/src/extensions/factories.ts +41 -12
  124. package/src/extensions/focus.ts +5 -4
  125. package/src/extensions/folding.tsx +4 -6
  126. package/src/extensions/hashtag.tsx +2 -2
  127. package/src/extensions/index.ts +2 -1
  128. package/src/extensions/json.ts +1 -1
  129. package/src/extensions/markdown/bundle.ts +16 -4
  130. package/src/extensions/markdown/decorate.ts +1 -0
  131. package/src/extensions/modes.ts +2 -2
  132. package/src/extensions/{command/floating-menu.ts → outliner/menu.ts} +9 -9
  133. package/src/extensions/outliner/outliner.ts +2 -2
  134. package/src/extensions/popover/PopoverMenuProvider.tsx +221 -0
  135. package/src/extensions/popover/index.ts +12 -0
  136. package/src/extensions/popover/menu-presets.ts +124 -0
  137. package/src/extensions/popover/menu.ts +67 -0
  138. package/src/extensions/popover/modal.ts +24 -0
  139. package/src/extensions/popover/popover.ts +291 -0
  140. package/src/extensions/popover/usePopoverMenu.ts +173 -0
  141. package/src/extensions/popover/util.ts +29 -0
  142. package/src/extensions/preview/index.ts +1 -1
  143. package/src/extensions/preview/preview.ts +0 -2
  144. package/src/extensions/selection.ts +2 -2
  145. package/src/extensions/state.ts +7 -0
  146. package/src/extensions/tags/streamer.ts +4 -5
  147. package/src/extensions/tags/xml-tags.ts +59 -1
  148. package/src/hooks/useTextEditor.ts +27 -27
  149. package/src/stories/{Command.stories.tsx → CommandDialog.stories.tsx} +10 -22
  150. package/src/stories/Comments.stories.tsx +5 -5
  151. package/src/stories/EditorToolbar.stories.tsx +6 -5
  152. package/src/stories/Experimental.stories.tsx +6 -6
  153. package/src/stories/Markdown.stories.tsx +5 -5
  154. package/src/stories/Outliner.stories.tsx +21 -14
  155. package/src/stories/Popover.stories.tsx +163 -0
  156. package/src/stories/Preview.stories.tsx +5 -5
  157. package/src/stories/Tags.stories.tsx +5 -5
  158. package/src/stories/TextEditor.stories.tsx +7 -32
  159. package/src/stories/components/EditorStory.tsx +7 -5
  160. package/src/styles/theme.ts +12 -10
  161. package/src/testing/PreviewPopover.tsx +2 -0
  162. package/src/types/types.ts +1 -1
  163. package/src/util/index.ts +0 -1
  164. package/dist/lib/browser/chunk-22UMM3QJ.mjs.map +0 -7
  165. package/dist/lib/node-esm/chunk-YXYQPV6R.mjs.map +0 -7
  166. package/dist/types/src/components/CommandMenu/CommandMenu.d.ts +0 -38
  167. package/dist/types/src/components/CommandMenu/CommandMenu.d.ts.map +0 -1
  168. package/dist/types/src/components/CommandMenu/index.d.ts +0 -2
  169. package/dist/types/src/components/CommandMenu/index.d.ts.map +0 -1
  170. package/dist/types/src/extensions/autocomplete.d.ts.map +0 -1
  171. package/dist/types/src/extensions/command/action.d.ts +0 -17
  172. package/dist/types/src/extensions/command/action.d.ts.map +0 -1
  173. package/dist/types/src/extensions/command/command-menu.d.ts +0 -20
  174. package/dist/types/src/extensions/command/command-menu.d.ts.map +0 -1
  175. package/dist/types/src/extensions/command/command.d.ts +0 -6
  176. package/dist/types/src/extensions/command/command.d.ts.map +0 -1
  177. package/dist/types/src/extensions/command/floating-menu.d.ts +0 -7
  178. package/dist/types/src/extensions/command/floating-menu.d.ts.map +0 -1
  179. package/dist/types/src/extensions/command/hint.d.ts +0 -19
  180. package/dist/types/src/extensions/command/hint.d.ts.map +0 -1
  181. package/dist/types/src/extensions/command/index.d.ts +0 -7
  182. package/dist/types/src/extensions/command/index.d.ts.map +0 -1
  183. package/dist/types/src/extensions/command/placeholder.d.ts +0 -10
  184. package/dist/types/src/extensions/command/placeholder.d.ts.map +0 -1
  185. package/dist/types/src/extensions/command/state.d.ts +0 -16
  186. package/dist/types/src/extensions/command/state.d.ts.map +0 -1
  187. package/dist/types/src/extensions/command/typeahead.d.ts +0 -22
  188. package/dist/types/src/extensions/command/typeahead.d.ts.map +0 -1
  189. package/dist/types/src/extensions/command/useCommandMenu.d.ts +0 -25
  190. package/dist/types/src/extensions/command/useCommandMenu.d.ts.map +0 -1
  191. package/dist/types/src/stories/Command.stories.d.ts.map +0 -1
  192. package/dist/types/src/stories/CommandMenu.stories.d.ts.map +0 -1
  193. package/dist/types/src/util/domino.d.ts +0 -18
  194. package/dist/types/src/util/domino.d.ts.map +0 -1
  195. package/src/components/CommandMenu/CommandMenu.tsx +0 -346
  196. package/src/components/CommandMenu/index.ts +0 -5
  197. package/src/extensions/command/action.ts +0 -55
  198. package/src/extensions/command/command-menu.ts +0 -211
  199. package/src/extensions/command/command.ts +0 -34
  200. package/src/extensions/command/hint.ts +0 -103
  201. package/src/extensions/command/index.ts +0 -10
  202. package/src/extensions/command/state.ts +0 -90
  203. package/src/extensions/command/useCommandMenu.ts +0 -115
  204. package/src/stories/CommandMenu.stories.tsx +0 -158
  205. package/src/util/domino.ts +0 -51
@@ -1,346 +0,0 @@
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 {
11
- type DxAnchorActivate,
12
- Icon,
13
- type Label,
14
- Popover,
15
- toLocalizedString,
16
- useThemeContext,
17
- useTranslation,
18
- } from '@dxos/react-ui';
19
- import { type MaybePromise } from '@dxos/util';
20
-
21
- import { commandRangeEffect } from '../../extensions';
22
-
23
- export type CommandMenuGroup = {
24
- id: string;
25
- label?: Label;
26
- items: CommandMenuItem[];
27
- };
28
-
29
- export type CommandMenuItem = {
30
- id: string;
31
- label: Label;
32
- icon?: string;
33
- onSelect?: (view: EditorView, head: number) => MaybePromise<void>;
34
- };
35
-
36
- export type CommandMenuProps = PropsWithChildren<{
37
- groups: CommandMenuGroup[];
38
- onSelect: (item: CommandMenuItem) => void;
39
- onActivate?: (event: DxAnchorActivate) => void;
40
- currentItem?: string;
41
- open?: boolean;
42
- onOpenChange?: (nextOpen: boolean) => void;
43
- defaultOpen?: boolean;
44
- }>;
45
-
46
- // NOTE: Not using DropdownMenu because the command menu needs to manage focus explicitly.
47
- export const CommandMenuProvider = ({
48
- groups,
49
- onSelect,
50
- onActivate,
51
- currentItem,
52
- children,
53
- open: propsOpen,
54
- onOpenChange,
55
- defaultOpen,
56
- }: CommandMenuProps) => {
57
- const { tx } = useThemeContext();
58
- const groupsWithItems = groups.filter((group) => group.items.length > 0);
59
- const trigger = useRef<HTMLButtonElement | null>(null);
60
-
61
- const [open, setOpen] = useControllableState({
62
- prop: propsOpen,
63
- onChange: onOpenChange,
64
- defaultProp: defaultOpen,
65
- });
66
-
67
- const handleDxAnchorActivate = useCallback(
68
- (event: DxAnchorActivate) => {
69
- const { trigger: dxTrigger, refId } = event;
70
- // If this has a `refId`, then it’s probably a URL or DXN and out of scope for this component.
71
- if (!refId) {
72
- trigger.current = dxTrigger as HTMLButtonElement;
73
- if (onActivate) {
74
- onActivate(event);
75
- } else {
76
- queueMicrotask(() => setOpen(true));
77
- }
78
- }
79
- },
80
- [onActivate],
81
- );
82
-
83
- const [rootRef, setRootRef] = useState<HTMLDivElement | null>(null);
84
-
85
- useEffect(() => {
86
- if (!rootRef || !handleDxAnchorActivate) {
87
- return;
88
- }
89
-
90
- return addEventListener(rootRef, 'dx-anchor-activate' as any, handleDxAnchorActivate, {
91
- capture: true,
92
- passive: false,
93
- });
94
- }, [rootRef, handleDxAnchorActivate]);
95
-
96
- return (
97
- <Popover.Root modal={false} open={open} onOpenChange={setOpen}>
98
- <Popover.Portal>
99
- <Popover.Content
100
- align='start'
101
- onOpenAutoFocus={(event) => event.preventDefault()}
102
- classNames={tx('menu.content', 'menu--exotic-unfocusable', { elevation: 'positioned' }, [
103
- 'max-bs-80 overflow-y-auto',
104
- ])}
105
- >
106
- <Popover.Viewport classNames={tx('menu.viewport', 'menu__viewport--exotic-unfocusable', {})}>
107
- <ul>
108
- {groupsWithItems.map((group, index) => (
109
- <Fragment key={group.id}>
110
- <CommandGroup group={group} currentItem={currentItem} onSelect={onSelect} />
111
- {index < groupsWithItems.length - 1 && <div className={tx('menu.separator', 'menu__item', {})} />}
112
- </Fragment>
113
- ))}
114
- </ul>
115
- </Popover.Viewport>
116
- <Popover.Arrow />
117
- </Popover.Content>
118
- </Popover.Portal>
119
- <Popover.VirtualTrigger virtualRef={trigger} />
120
- <div role='none' className='contents' ref={setRootRef}>
121
- {children}
122
- </div>
123
- </Popover.Root>
124
- );
125
- };
126
-
127
- const CommandGroup = ({
128
- group,
129
- currentItem,
130
- onSelect,
131
- }: {
132
- group: CommandMenuGroup;
133
- currentItem?: string;
134
- onSelect: (item: CommandMenuItem) => void;
135
- }) => {
136
- const { tx } = useThemeContext();
137
- const { t } = useTranslation();
138
- return (
139
- <>
140
- {group.label && (
141
- <div className={tx('menu.groupLabel', 'menu__group__label', {})}>
142
- <span>{toLocalizedString(group.label, t)}</span>
143
- </div>
144
- )}
145
- {group.items.map((item) => (
146
- <CommandItem key={item.id} item={item} current={currentItem === item.id} onSelect={onSelect} />
147
- ))}
148
- </>
149
- );
150
- };
151
-
152
- const CommandItem = ({
153
- item,
154
- current,
155
- onSelect,
156
- }: {
157
- item: CommandMenuItem;
158
- current: boolean;
159
- onSelect: (item: CommandMenuItem) => void;
160
- }) => {
161
- const ref = useRef<HTMLLIElement>(null);
162
- const { tx } = useThemeContext();
163
- const { t } = useTranslation();
164
- const handleSelect = useCallback(() => onSelect(item), [item, onSelect]);
165
-
166
- useEffect(() => {
167
- if (current && ref.current) {
168
- ref.current.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
169
- }
170
- }, [current]);
171
-
172
- return (
173
- <li
174
- ref={ref}
175
- className={tx('menu.item', 'menu__item--exotic-unfocusable', {}, [current && 'bg-hoverSurface'])}
176
- onClick={handleSelect}
177
- >
178
- {item.icon && <Icon icon={item.icon} size={5} />}
179
- <span className='grow truncate'>{toLocalizedString(item.label, t)}</span>
180
- </li>
181
- );
182
- };
183
-
184
- // TODO(wittjosiah): Factor out into a separate file.
185
-
186
- //
187
- // Helpers
188
- //
189
-
190
- export const getItem = (groups: CommandMenuGroup[], id?: string): CommandMenuItem | undefined => {
191
- return groups.flatMap((group) => group.items).find((item) => item.id === id);
192
- };
193
-
194
- export const getNextItem = (groups: CommandMenuGroup[], id?: string): CommandMenuItem => {
195
- const items = groups.flatMap((group) => group.items);
196
- const index = items.findIndex((item) => item.id === id);
197
- return items[(index + 1) % items.length];
198
- };
199
-
200
- export const getPreviousItem = (groups: CommandMenuGroup[], id?: string): CommandMenuItem => {
201
- const items = groups.flatMap((group) => group.items);
202
- const index = items.findIndex((item) => item.id === id);
203
- return items[(index - 1 + items.length) % items.length];
204
- };
205
-
206
- export const filterItems = (
207
- groups: CommandMenuGroup[],
208
- filter: (item: CommandMenuItem) => boolean,
209
- ): CommandMenuGroup[] => {
210
- return groups.map((group) => ({
211
- ...group,
212
- items: group.items.filter(filter),
213
- }));
214
- };
215
-
216
- export const insertAtCursor = (view: EditorView, head: number, insert: string) => {
217
- view.dispatch({
218
- changes: { from: head, to: head, insert },
219
- selection: { anchor: head + insert.length, head: head + insert.length },
220
- });
221
- };
222
-
223
- /**
224
- * If the cursor is at the start of a line, insert the text at the cursor.
225
- * Otherwise, insert the text on a new line.
226
- */
227
- export const insertAtLineStart = (view: EditorView, head: number, insert: string) => {
228
- const line = view.state.doc.lineAt(head);
229
- if (line.from === head) {
230
- insertAtCursor(view, head, insert);
231
- } else {
232
- insert = '\n' + insert;
233
- view.dispatch({
234
- changes: { from: line.to, to: line.to, insert },
235
- selection: { anchor: line.to + insert.length, head: line.to + insert.length },
236
- });
237
- }
238
- };
239
-
240
- export const coreSlashCommands: CommandMenuGroup = {
241
- id: 'markdown',
242
- label: 'Markdown',
243
- items: [
244
- {
245
- id: 'heading-1',
246
- label: 'Heading 1',
247
- icon: 'ph--text-h-one--regular',
248
- onSelect: (view, head) => insertAtLineStart(view, head, '# '),
249
- },
250
- {
251
- id: 'heading-2',
252
- label: 'Heading 2',
253
- icon: 'ph--text-h-two--regular',
254
- onSelect: (view, head) => insertAtLineStart(view, head, '## '),
255
- },
256
- {
257
- id: 'heading-3',
258
- label: 'Heading 3',
259
- icon: 'ph--text-h-three--regular',
260
- onSelect: (view, head) => insertAtLineStart(view, head, '### '),
261
- },
262
- {
263
- id: 'heading-4',
264
- label: 'Heading 4',
265
- icon: 'ph--text-h-four--regular',
266
- onSelect: (view, head) => insertAtLineStart(view, head, '#### '),
267
- },
268
- {
269
- id: 'heading-5',
270
- label: 'Heading 5',
271
- icon: 'ph--text-h-five--regular',
272
- onSelect: (view, head) => insertAtLineStart(view, head, '##### '),
273
- },
274
- {
275
- id: 'heading-6',
276
- label: 'Heading 6',
277
- icon: 'ph--text-h-six--regular',
278
- onSelect: (view, head) => insertAtLineStart(view, head, '###### '),
279
- },
280
- {
281
- id: 'bullet-list',
282
- label: 'Bullet List',
283
- icon: 'ph--list-bullets--regular',
284
- onSelect: (view, head) => insertAtLineStart(view, head, '- '),
285
- },
286
- {
287
- id: 'numbered-list',
288
- label: 'Numbered List',
289
- icon: 'ph--list-numbers--regular',
290
- onSelect: (view, head) => insertAtLineStart(view, head, '1. '),
291
- },
292
- {
293
- id: 'task-list',
294
- label: 'Task List',
295
- icon: 'ph--list-checks--regular',
296
- onSelect: (view, head) => insertAtLineStart(view, head, '- [ ] '),
297
- },
298
- {
299
- id: 'quote',
300
- label: 'Quote',
301
- icon: 'ph--quotes--regular',
302
- onSelect: (view, head) => insertAtLineStart(view, head, '> '),
303
- },
304
- {
305
- id: 'code-block',
306
- label: 'Code Block',
307
- icon: 'ph--code-block--regular',
308
- onSelect: (view, head) => insertAtLineStart(view, head, '```\n\n```'),
309
- },
310
- {
311
- id: 'table',
312
- label: 'Table',
313
- icon: 'ph--table--regular',
314
- onSelect: (view, head) => insertAtLineStart(view, head, '| | | |\n|---|---|---|\n| | | |'),
315
- },
316
- ],
317
- };
318
-
319
- export const linkSlashCommands: CommandMenuGroup = {
320
- id: 'link',
321
- label: 'Link',
322
- items: [
323
- {
324
- id: 'inline-link',
325
- label: 'Inline link',
326
- icon: 'ph--link--regular',
327
- onSelect: (view, head) =>
328
- view.dispatch({
329
- changes: { from: head, insert: '@' },
330
- selection: { anchor: head + 1, head: head + 1 },
331
- effects: commandRangeEffect.of({ trigger: '@', range: { from: head, to: head + 1 } }),
332
- }),
333
- },
334
- {
335
- id: 'block-embed',
336
- label: 'Block embed',
337
- icon: 'ph--lego--regular',
338
- onSelect: (view, head) =>
339
- view.dispatch({
340
- changes: { from: head, insert: '@@' },
341
- selection: { anchor: head + 2, head: head + 2 },
342
- effects: commandRangeEffect.of({ trigger: '@', range: { from: head, to: head + 2 } }),
343
- }),
344
- },
345
- ],
346
- };
@@ -1,5 +0,0 @@
1
- //
2
- // Copyright 2022 DXOS.org
3
- //
4
-
5
- export * from './CommandMenu';
@@ -1,55 +0,0 @@
1
- //
2
- // Copyright 2025 DXOS.org
3
- //
4
-
5
- import { StateEffect } from '@codemirror/state';
6
- import { type Command, type EditorView, type KeyBinding } from '@codemirror/view';
7
-
8
- import { commandState } from './state';
9
-
10
- export type Action =
11
- | {
12
- type: 'insert';
13
- text: string;
14
- }
15
- | {
16
- type: 'cancel';
17
- };
18
-
19
- export type ActionHandler = (action: Action) => void;
20
-
21
- export const openEffect = StateEffect.define<{ pos: number; fullWidth?: boolean }>();
22
- export const closeEffect = StateEffect.define<null>();
23
-
24
- export const openCommand: Command = (view: EditorView) => {
25
- if (view.state.field(commandState, false)) {
26
- const selection = view.state.selection.main;
27
- const line = view.state.doc.lineAt(selection.from);
28
- if (line.from === selection.from && line.from === line.to) {
29
- view.dispatch({ effects: openEffect.of({ pos: selection.anchor, fullWidth: true }) });
30
- return true;
31
- }
32
- }
33
-
34
- return false;
35
- };
36
-
37
- export const closeCommand: Command = (view: EditorView) => {
38
- if (view.state.field(commandState, false)) {
39
- view.dispatch({ effects: closeEffect.of(null) });
40
- return true;
41
- }
42
-
43
- return false;
44
- };
45
-
46
- export const commandKeyBindings: readonly KeyBinding[] = [
47
- {
48
- key: '/',
49
- run: openCommand,
50
- },
51
- {
52
- key: 'Escape',
53
- run: closeCommand,
54
- },
55
- ];
@@ -1,211 +0,0 @@
1
- //
2
- // Copyright 2024 DXOS.org
3
- //
4
-
5
- import { Prec, RangeSetBuilder, StateEffect, StateField } from '@codemirror/state';
6
- import { Decoration, type DecorationSet, EditorView, ViewPlugin, type ViewUpdate, keymap } from '@codemirror/view';
7
-
8
- import { type Range } from '../../types';
9
-
10
- import { type PlaceholderOptions, placeholder } from './placeholder';
11
-
12
- export type CommandMenuOptions = {
13
- trigger: string | string[];
14
- placeholder?: Partial<PlaceholderOptions>;
15
-
16
- // TODO(burdon): Replace with onKey?
17
- onClose?: () => void;
18
- onArrowDown?: () => void;
19
- onArrowUp?: () => void;
20
- onEnter?: () => void;
21
-
22
- onTextChange?: (trigger: string, text: string) => void;
23
- };
24
-
25
- export const commandMenu = (options: CommandMenuOptions) => {
26
- const commandMenuPlugin = ViewPlugin.fromClass(
27
- class {
28
- decorations: DecorationSet = Decoration.none;
29
-
30
- constructor(readonly view: EditorView) {}
31
-
32
- // TODO(wittjosiah): The decorations are repainted on every update, this occasionally causes menu to flicker.
33
- update(update: ViewUpdate) {
34
- const builder = new RangeSetBuilder<Decoration>();
35
- const selection = update.view.state.selection.main;
36
- const { range: activeRange, trigger } = update.view.state.field(commandMenuState) ?? {};
37
-
38
- // Check if we should show the widget - only if cursor is within the active command range.
39
- const shouldShowWidget = activeRange && selection.head >= activeRange.from && selection.head <= activeRange.to;
40
- if (shouldShowWidget) {
41
- // Create mark decoration that wraps the entire line content in a dx-anchor.
42
- builder.add(
43
- activeRange.from,
44
- activeRange.to,
45
- Decoration.mark({
46
- tagName: 'dx-anchor',
47
- class: 'cm-floating-menu-trigger',
48
- attributes: {
49
- 'data-auto-trigger': 'true',
50
- 'data-trigger': trigger!,
51
- },
52
- }),
53
- );
54
- }
55
-
56
- const activeRangeChanged = update.transactions.some((tr) =>
57
- tr.effects.some((effect) => effect.is(commandRangeEffect)),
58
- );
59
- if (activeRange && activeRangeChanged && trigger) {
60
- const content = update.view.state.sliceDoc(
61
- activeRange.from + 1, // Skip the trigger character.
62
- activeRange.to,
63
- );
64
- options.onTextChange?.(trigger, content);
65
- }
66
-
67
- this.decorations = builder.finish();
68
- }
69
- },
70
- {
71
- decorations: (v) => v.decorations,
72
- },
73
- );
74
-
75
- const triggers = Array.isArray(options.trigger) ? options.trigger : [options.trigger];
76
-
77
- const commandKeymap = keymap.of([
78
- ...triggers.map((trigger) => ({
79
- key: trigger,
80
- preventDefault: true,
81
- run: (view: EditorView) => {
82
- const selection = view.state.selection.main;
83
- const line = view.state.doc.lineAt(selection.head);
84
-
85
- // Check if we should trigger the command menu:
86
- // 1. Empty lines or at the beginning of a line
87
- // 2. When there's a preceding space
88
- if (
89
- line.text.trim() === '' ||
90
- selection.head === line.from ||
91
- (selection.head > line.from && line.text[selection.head - line.from - 1] === ' ')
92
- ) {
93
- // Insert and select the trigger.
94
- view.dispatch({
95
- changes: { from: selection.head, insert: trigger },
96
- selection: { anchor: selection.head + 1, head: selection.head + 1 },
97
- effects: commandRangeEffect.of({ trigger, range: { from: selection.head, to: selection.head + 1 } }),
98
- });
99
-
100
- return true;
101
- }
102
-
103
- return false;
104
- },
105
- })),
106
- {
107
- key: 'Enter',
108
- run: (view) => {
109
- const activeRange = view.state.field(commandMenuState)?.range;
110
- if (activeRange) {
111
- view.dispatch({ changes: { from: activeRange.from, to: activeRange.to, insert: '' } });
112
- options.onEnter?.();
113
- return true;
114
- }
115
-
116
- return false;
117
- },
118
- },
119
- {
120
- key: 'ArrowDown',
121
- run: (view) => {
122
- const activeRange = view.state.field(commandMenuState)?.range;
123
- if (activeRange) {
124
- options.onArrowDown?.();
125
- return true;
126
- }
127
-
128
- return false;
129
- },
130
- },
131
- {
132
- key: 'ArrowUp',
133
- run: (view) => {
134
- const activeRange = view.state.field(commandMenuState)?.range;
135
- if (activeRange) {
136
- options.onArrowUp?.();
137
- return true;
138
- }
139
-
140
- return false;
141
- },
142
- },
143
- ]);
144
-
145
- // Listen for selection and document changes to clean up the command menu.
146
- const updateListener = EditorView.updateListener.of((update) => {
147
- const { trigger, range: activeRange } = update.view.state.field(commandMenuState) ?? {};
148
- if (!activeRange || !trigger) {
149
- return;
150
- }
151
-
152
- const selection = update.view.state.selection.main;
153
- const firstChar = update.view.state.doc.sliceString(activeRange.from, activeRange.from + 1);
154
- const shouldRemove =
155
- firstChar !== trigger || // Trigger deleted.
156
- selection.head < activeRange.from || // Cursor moved before the range.
157
- selection.head > activeRange.to + 1; // Cursor moved after the range (+1 to handle selection changing before doc).
158
-
159
- const nextRange = shouldRemove
160
- ? null
161
- : update.docChanged
162
- ? { from: activeRange.from, to: selection.head }
163
- : activeRange;
164
- if (nextRange !== activeRange) {
165
- update.view.dispatch({ effects: commandRangeEffect.of(nextRange ? { trigger, range: nextRange } : null) });
166
- }
167
-
168
- // TODO(burdon): Should delete if user presses escape? How else to insert the trigger character?
169
- if (shouldRemove) {
170
- options.onClose?.();
171
- }
172
- });
173
-
174
- return [
175
- Prec.highest(commandKeymap),
176
- placeholder(
177
- Object.assign(
178
- {
179
- content: `Press '${Array.isArray(options.trigger) ? options.trigger[0] : options.trigger}' for commands`,
180
- },
181
- options.placeholder,
182
- ),
183
- ),
184
- updateListener,
185
- commandMenuState,
186
- commandMenuPlugin,
187
- ];
188
- };
189
-
190
- type CommandState = {
191
- trigger: string;
192
- range: Range;
193
- };
194
-
195
- // State effects for managing command menu state.
196
- export const commandRangeEffect = StateEffect.define<CommandState | null>();
197
-
198
- // State field to track the active command menu range.
199
- const commandMenuState = StateField.define<CommandState | null>({
200
- create: () => null,
201
- update: (value, tr) => {
202
- let newValue = value;
203
- for (const effect of tr.effects) {
204
- if (effect.is(commandRangeEffect)) {
205
- newValue = effect.value;
206
- }
207
- }
208
-
209
- return newValue;
210
- },
211
- });
@@ -1,34 +0,0 @@
1
- //
2
- // Copyright 2024 DXOS.org
3
- //
4
-
5
- import { type Extension, Prec } from '@codemirror/state';
6
- import { EditorView, keymap } from '@codemirror/view';
7
-
8
- import { isNonNullable } from '@dxos/util';
9
-
10
- import { closeEffect, commandKeyBindings } from './action';
11
- import { type HintOptions, hint } from './hint';
12
- import { type PopupOptions, commandConfig, commandState } from './state';
13
-
14
- // TODO(burdon): Create knowledge base for CM notes and ideas.
15
- // https://discuss.codemirror.net/t/inline-code-hints-like-vscode/5533/4
16
- // https://github.com/saminzadeh/codemirror-extension-inline-suggestion
17
- // https://github.com/ChromeDevTools/devtools-frontend/blob/main/front_end/ui/components/text_editor/config.ts#L370
18
-
19
- export type CommandOptions = Partial<PopupOptions & HintOptions>;
20
-
21
- export const command = (options: CommandOptions = {}): Extension => {
22
- return [
23
- Prec.highest(keymap.of(commandKeyBindings)),
24
- commandConfig.of(options),
25
- commandState,
26
- options.onHint && hint(options),
27
- EditorView.focusChangeEffect.of((_, focusing) => (focusing ? closeEffect.of(null) : null)),
28
- EditorView.theme({
29
- '.cm-tooltip': {
30
- background: 'transparent',
31
- },
32
- }),
33
- ].filter(isNonNullable);
34
- };