@dxos/react-ui-editor 0.8.2 → 0.8.3-main.7f5a14c

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 (83) hide show
  1. package/dist/lib/browser/index.mjs +936 -274
  2. package/dist/lib/browser/index.mjs.map +4 -4
  3. package/dist/lib/browser/meta.json +1 -1
  4. package/dist/lib/node/index.cjs +981 -314
  5. package/dist/lib/node/index.cjs.map +4 -4
  6. package/dist/lib/node/meta.json +1 -1
  7. package/dist/lib/node-esm/index.mjs +936 -274
  8. package/dist/lib/node-esm/index.mjs.map +4 -4
  9. package/dist/lib/node-esm/meta.json +1 -1
  10. package/dist/types/src/components/EditorToolbar/util.d.ts +2 -2
  11. package/dist/types/src/components/Popover/CommandMenu.d.ts +34 -0
  12. package/dist/types/src/components/Popover/CommandMenu.d.ts.map +1 -0
  13. package/dist/types/src/components/Popover/RefPopover.d.ts +19 -6
  14. package/dist/types/src/components/Popover/RefPopover.d.ts.map +1 -1
  15. package/dist/types/src/components/Popover/index.d.ts +1 -0
  16. package/dist/types/src/components/Popover/index.d.ts.map +1 -1
  17. package/dist/types/src/defaults.d.ts.map +1 -1
  18. package/dist/types/src/extensions/command/menu.d.ts +40 -0
  19. package/dist/types/src/extensions/command/menu.d.ts.map +1 -1
  20. package/dist/types/src/extensions/factories.d.ts +1 -0
  21. package/dist/types/src/extensions/factories.d.ts.map +1 -1
  22. package/dist/types/src/extensions/hashtag.d.ts +3 -0
  23. package/dist/types/src/extensions/hashtag.d.ts.map +1 -0
  24. package/dist/types/src/extensions/index.d.ts +2 -0
  25. package/dist/types/src/extensions/index.d.ts.map +1 -1
  26. package/dist/types/src/extensions/json.d.ts.map +1 -1
  27. package/dist/types/src/extensions/markdown/debug.d.ts +2 -2
  28. package/dist/types/src/extensions/markdown/debug.d.ts.map +1 -1
  29. package/dist/types/src/extensions/outliner/outliner.d.ts +1 -3
  30. package/dist/types/src/extensions/outliner/outliner.d.ts.map +1 -1
  31. package/dist/types/src/extensions/placeholder.d.ts +4 -0
  32. package/dist/types/src/extensions/placeholder.d.ts.map +1 -0
  33. package/dist/types/src/extensions/preview/preview.d.ts.map +1 -1
  34. package/dist/types/src/hooks/useTextEditor.d.ts +8 -9
  35. package/dist/types/src/hooks/useTextEditor.d.ts.map +1 -1
  36. package/dist/types/src/stories/Command.stories.d.ts +1 -1
  37. package/dist/types/src/stories/Command.stories.d.ts.map +1 -1
  38. package/dist/types/src/stories/CommandMenu.stories.d.ts +12 -0
  39. package/dist/types/src/stories/CommandMenu.stories.d.ts.map +1 -0
  40. package/dist/types/src/stories/Comments.stories.d.ts +1 -1
  41. package/dist/types/src/stories/Comments.stories.d.ts.map +1 -1
  42. package/dist/types/src/stories/Experimental.stories.d.ts +1 -1
  43. package/dist/types/src/stories/Experimental.stories.d.ts.map +1 -1
  44. package/dist/types/src/stories/Markdown.stories.d.ts +1 -1
  45. package/dist/types/src/stories/Markdown.stories.d.ts.map +1 -1
  46. package/dist/types/src/stories/Outliner.stories.d.ts.map +1 -1
  47. package/dist/types/src/stories/Preview.stories.d.ts +1 -1
  48. package/dist/types/src/stories/Preview.stories.d.ts.map +1 -1
  49. package/dist/types/src/stories/TextEditor.stories.d.ts +1 -1
  50. package/dist/types/src/stories/TextEditor.stories.d.ts.map +1 -1
  51. package/dist/types/src/stories/components/EditorStory.d.ts +43 -0
  52. package/dist/types/src/stories/components/EditorStory.d.ts.map +1 -0
  53. package/dist/types/src/stories/components/index.d.ts +3 -0
  54. package/dist/types/src/stories/components/index.d.ts.map +1 -0
  55. package/dist/types/src/stories/{util.d.ts → components/util.d.ts} +3 -18
  56. package/dist/types/src/stories/components/util.d.ts.map +1 -0
  57. package/package.json +31 -27
  58. package/src/components/Popover/CommandMenu.tsx +279 -0
  59. package/src/components/Popover/RefPopover.tsx +44 -22
  60. package/src/components/Popover/index.ts +1 -0
  61. package/src/defaults.ts +1 -0
  62. package/src/extensions/command/menu.ts +334 -23
  63. package/src/extensions/factories.ts +4 -1
  64. package/src/extensions/hashtag.tsx +68 -0
  65. package/src/extensions/index.ts +2 -0
  66. package/src/extensions/json.ts +2 -1
  67. package/src/extensions/markdown/debug.ts +2 -2
  68. package/src/extensions/outliner/outliner.ts +6 -8
  69. package/src/extensions/placeholder.ts +82 -0
  70. package/src/extensions/preview/preview.ts +3 -6
  71. package/src/hooks/useTextEditor.ts +11 -12
  72. package/src/stories/Command.stories.tsx +1 -1
  73. package/src/stories/CommandMenu.stories.tsx +143 -0
  74. package/src/stories/Comments.stories.tsx +2 -2
  75. package/src/stories/Experimental.stories.tsx +2 -2
  76. package/src/stories/Markdown.stories.tsx +2 -2
  77. package/src/stories/Outliner.stories.tsx +19 -7
  78. package/src/stories/Preview.stories.tsx +34 -32
  79. package/src/stories/TextEditor.stories.tsx +3 -3
  80. package/src/stories/components/EditorStory.tsx +135 -0
  81. package/src/stories/components/index.ts +6 -0
  82. package/src/stories/{util.tsx → components/util.tsx} +5 -100
  83. package/dist/types/src/stories/util.d.ts.map +0 -1
@@ -0,0 +1,279 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ import { type EditorView } from '@codemirror/view';
6
+ import React, { useCallback, useEffect, useRef } from 'react';
7
+
8
+ import { Icon, type Label, Popover, toLocalizedString, useThemeContext, useTranslation } from '@dxos/react-ui';
9
+ import { type MaybePromise } from '@dxos/util';
10
+
11
+ import { commandRangeEffect } from '../../extensions';
12
+
13
+ export type CommandMenuGroup = {
14
+ id: string;
15
+ label?: Label;
16
+ items: CommandMenuItem[];
17
+ };
18
+
19
+ export type CommandMenuItem = {
20
+ id: string;
21
+ label: Label;
22
+ icon?: string;
23
+ onSelect?: (view: EditorView, head: number) => MaybePromise<void>;
24
+ };
25
+
26
+ export type CommandMenuProps = {
27
+ groups: CommandMenuGroup[];
28
+ currentItem?: string;
29
+ onSelect: (item: CommandMenuItem) => void;
30
+ };
31
+
32
+ // NOTE: Not using DropdownMenu because the command menu needs to manage focus explicitly.
33
+ export const CommandMenu = ({ groups, currentItem, onSelect }: CommandMenuProps) => {
34
+ const { tx } = useThemeContext();
35
+ const groupsWithItems = groups.filter((group) => group.items.length > 0);
36
+ return (
37
+ <Popover.Portal>
38
+ <Popover.Content
39
+ align='start'
40
+ onOpenAutoFocus={(event) => event.preventDefault()}
41
+ classNames={tx('menu.content', 'menu--exotic-unfocusable', { elevation: 'positioned' }, [
42
+ 'max-h-[300px] overflow-y-auto',
43
+ ])}
44
+ >
45
+ <Popover.Viewport classNames={tx('menu.viewport', 'menu__viewport--exotic-unfocusable', {})}>
46
+ <ul>
47
+ {groupsWithItems.map((group, index) => (
48
+ <React.Fragment key={group.id}>
49
+ <CommandGroup group={group} currentItem={currentItem} onSelect={onSelect} />
50
+ {index < groupsWithItems.length - 1 && <div className={tx('menu.separator', 'menu__item', {})} />}
51
+ </React.Fragment>
52
+ ))}
53
+ </ul>
54
+ </Popover.Viewport>
55
+ </Popover.Content>
56
+ </Popover.Portal>
57
+ );
58
+ };
59
+
60
+ const CommandGroup = ({
61
+ group,
62
+ currentItem,
63
+ onSelect,
64
+ }: {
65
+ group: CommandMenuGroup;
66
+ currentItem?: string;
67
+ onSelect: (item: CommandMenuItem) => void;
68
+ }) => {
69
+ const { tx } = useThemeContext();
70
+ const { t } = useTranslation();
71
+ return (
72
+ <>
73
+ {group.label && (
74
+ <div className={tx('menu.groupLabel', 'menu__group__label', {})}>
75
+ <span>{toLocalizedString(group.label, t)}</span>
76
+ </div>
77
+ )}
78
+ {group.items.map((item) => (
79
+ <CommandItem key={item.id} item={item} current={currentItem === item.id} onSelect={onSelect} />
80
+ ))}
81
+ </>
82
+ );
83
+ };
84
+
85
+ const CommandItem = ({
86
+ item,
87
+ current,
88
+ onSelect,
89
+ }: {
90
+ item: CommandMenuItem;
91
+ current: boolean;
92
+ onSelect: (item: CommandMenuItem) => void;
93
+ }) => {
94
+ const ref = useRef<HTMLLIElement>(null);
95
+ const { tx } = useThemeContext();
96
+ const { t } = useTranslation();
97
+ const handleSelect = useCallback(() => onSelect(item), [item, onSelect]);
98
+
99
+ useEffect(() => {
100
+ if (current && ref.current) {
101
+ ref.current.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
102
+ }
103
+ }, [current]);
104
+
105
+ return (
106
+ <li
107
+ ref={ref}
108
+ className={tx('menu.item', 'menu__item--exotic-unfocusable', {}, [current && 'bg-hoverSurface'])}
109
+ onClick={handleSelect}
110
+ >
111
+ {item.icon && <Icon icon={item.icon} size={5} />}
112
+ <span className='grow truncate'>{toLocalizedString(item.label, t)}</span>
113
+ </li>
114
+ );
115
+ };
116
+
117
+ // TODO(wittjosiah): Factor out into a separate file.
118
+
119
+ //
120
+ // Helpers
121
+ //
122
+
123
+ export const getItem = (groups: CommandMenuGroup[], id?: string): CommandMenuItem | undefined => {
124
+ return groups.flatMap((group) => group.items).find((item) => item.id === id);
125
+ };
126
+
127
+ export const getNextItem = (groups: CommandMenuGroup[], id?: string): CommandMenuItem => {
128
+ const items = groups.flatMap((group) => group.items);
129
+ const index = items.findIndex((item) => item.id === id);
130
+ return items[(index + 1) % items.length];
131
+ };
132
+
133
+ export const getPreviousItem = (groups: CommandMenuGroup[], id?: string): CommandMenuItem => {
134
+ const items = groups.flatMap((group) => group.items);
135
+ const index = items.findIndex((item) => item.id === id);
136
+ return items[(index - 1 + items.length) % items.length];
137
+ };
138
+
139
+ export const filterItems = (
140
+ groups: CommandMenuGroup[],
141
+ filter: (item: CommandMenuItem) => boolean,
142
+ ): CommandMenuGroup[] => {
143
+ return groups.map((group) => ({
144
+ ...group,
145
+ items: group.items.filter(filter),
146
+ }));
147
+ };
148
+
149
+ export const insertAtCursor = (view: EditorView, head: number, insert: string) => {
150
+ view.dispatch({
151
+ changes: { from: head, to: head, insert },
152
+ selection: { anchor: head + insert.length, head: head + insert.length },
153
+ });
154
+ };
155
+
156
+ /**
157
+ * If the cursor is at the start of a line, insert the text at the cursor.
158
+ * Otherwise, insert the text on a new line.
159
+ */
160
+ export const insertAtLineStart = (view: EditorView, head: number, insert: string) => {
161
+ const line = view.state.doc.lineAt(head);
162
+ if (line.from === head) {
163
+ insertAtCursor(view, head, insert);
164
+ } else {
165
+ insert = '\n' + insert;
166
+ view.dispatch({
167
+ changes: { from: line.to, to: line.to, insert },
168
+ selection: { anchor: line.to + insert.length, head: line.to + insert.length },
169
+ });
170
+ }
171
+ };
172
+
173
+ export const coreSlashCommands: CommandMenuGroup = {
174
+ id: 'markdown',
175
+ label: 'Markdown',
176
+ items: [
177
+ {
178
+ id: 'heading-1',
179
+ label: 'Heading 1',
180
+ icon: 'ph--text-h-one--regular',
181
+ onSelect: (view, head) => insertAtLineStart(view, head, '# '),
182
+ },
183
+ {
184
+ id: 'heading-2',
185
+ label: 'Heading 2',
186
+ icon: 'ph--text-h-two--regular',
187
+ onSelect: (view, head) => insertAtLineStart(view, head, '## '),
188
+ },
189
+ {
190
+ id: 'heading-3',
191
+ label: 'Heading 3',
192
+ icon: 'ph--text-h-three--regular',
193
+ onSelect: (view, head) => insertAtLineStart(view, head, '### '),
194
+ },
195
+ {
196
+ id: 'heading-4',
197
+ label: 'Heading 4',
198
+ icon: 'ph--text-h-four--regular',
199
+ onSelect: (view, head) => insertAtLineStart(view, head, '#### '),
200
+ },
201
+ {
202
+ id: 'heading-5',
203
+ label: 'Heading 5',
204
+ icon: 'ph--text-h-five--regular',
205
+ onSelect: (view, head) => insertAtLineStart(view, head, '##### '),
206
+ },
207
+ {
208
+ id: 'heading-6',
209
+ label: 'Heading 6',
210
+ icon: 'ph--text-h-six--regular',
211
+ onSelect: (view, head) => insertAtLineStart(view, head, '###### '),
212
+ },
213
+ {
214
+ id: 'bullet-list',
215
+ label: 'Bullet List',
216
+ icon: 'ph--list-bullets--regular',
217
+ onSelect: (view, head) => insertAtLineStart(view, head, '- '),
218
+ },
219
+ {
220
+ id: 'numbered-list',
221
+ label: 'Numbered List',
222
+ icon: 'ph--list-numbers--regular',
223
+ onSelect: (view, head) => insertAtLineStart(view, head, '1. '),
224
+ },
225
+ {
226
+ id: 'task-list',
227
+ label: 'Task List',
228
+ icon: 'ph--list-checks--regular',
229
+ onSelect: (view, head) => insertAtLineStart(view, head, '- [ ] '),
230
+ },
231
+ {
232
+ id: 'quote',
233
+ label: 'Quote',
234
+ icon: 'ph--quotes--regular',
235
+ onSelect: (view, head) => insertAtLineStart(view, head, '> '),
236
+ },
237
+ {
238
+ id: 'code-block',
239
+ label: 'Code Block',
240
+ icon: 'ph--code-block--regular',
241
+ onSelect: (view, head) => insertAtLineStart(view, head, '```\n\n```'),
242
+ },
243
+ {
244
+ id: 'table',
245
+ label: 'Table',
246
+ icon: 'ph--table--regular',
247
+ onSelect: (view, head) => insertAtLineStart(view, head, '| | | |\n|---|---|---|\n| | | |'),
248
+ },
249
+ ],
250
+ };
251
+
252
+ export const linkSlashCommands: CommandMenuGroup = {
253
+ id: 'link',
254
+ label: 'Link',
255
+ items: [
256
+ {
257
+ id: 'inline-link',
258
+ label: 'Inline link',
259
+ icon: 'ph--link--regular',
260
+ onSelect: (view, head) =>
261
+ view.dispatch({
262
+ changes: { from: head, insert: '@' },
263
+ selection: { anchor: head + 1, head: head + 1 },
264
+ effects: commandRangeEffect.of({ trigger: '@', range: { from: head, to: head + 1 } }),
265
+ }),
266
+ },
267
+ {
268
+ id: 'block-embed',
269
+ label: 'Block embed',
270
+ icon: 'ph--lego--regular',
271
+ onSelect: (view, head) =>
272
+ view.dispatch({
273
+ changes: { from: head, insert: '@@' },
274
+ selection: { anchor: head + 2, head: head + 2 },
275
+ effects: commandRangeEffect.of({ trigger: '@', range: { from: head, to: head + 2 } }),
276
+ }),
277
+ },
278
+ ],
279
+ };
@@ -3,7 +3,15 @@
3
3
  //
4
4
 
5
5
  import { createContext } from '@radix-ui/react-context';
6
- import React, { type PropsWithChildren, useRef, useState, useEffect, useCallback, type RefObject } from 'react';
6
+ import React, {
7
+ type PropsWithChildren,
8
+ useRef,
9
+ useState,
10
+ useEffect,
11
+ useCallback,
12
+ type RefObject,
13
+ forwardRef,
14
+ } from 'react';
7
15
 
8
16
  import { addEventListener } from '@dxos/async';
9
17
  import { type DxRefTag, type DxRefTagActivate } from '@dxos/lit-ui';
@@ -13,18 +21,45 @@ import { type PreviewLinkRef, type PreviewLinkTarget, type PreviewLookup } from
13
21
 
14
22
  const customEventOptions = { capture: true, passive: false };
15
23
 
24
+ export type RefPopoverProps = PropsWithChildren<{
25
+ modal?: boolean;
26
+ open?: boolean;
27
+ onOpenChange?: (open: boolean) => void;
28
+ onActivate?: (event: DxRefTagActivate) => void;
29
+ }>;
30
+
31
+ export const RefPopover = forwardRef<DxRefTag | null, RefPopoverProps>(
32
+ ({ children, open, onOpenChange, modal, onActivate }, ref) => {
33
+ const [rootRef, setRootRef] = useState<HTMLDivElement | null>(null);
34
+
35
+ useEffect(() => {
36
+ return rootRef && onActivate
37
+ ? addEventListener(rootRef, 'dx-ref-tag-activate', onActivate, customEventOptions)
38
+ : undefined;
39
+ }, [rootRef, onActivate]);
40
+
41
+ return (
42
+ <Popover.Root open={open} onOpenChange={onOpenChange} modal={modal}>
43
+ <Popover.VirtualTrigger virtualRef={ref as unknown as RefObject<HTMLButtonElement>} />
44
+ <div role='none' className='contents' ref={setRootRef}>
45
+ {children}
46
+ </div>
47
+ </Popover.Root>
48
+ );
49
+ },
50
+ );
51
+
16
52
  // Create a context for the dxn value.
17
53
  type RefPopoverValue = Partial<{ link: PreviewLinkRef; target: PreviewLinkTarget; pending: boolean }>;
18
54
 
19
55
  const REF_POPOVER = 'RefPopover';
20
56
  const [RefPopoverContextProvider, useRefPopover] = createContext<RefPopoverValue>(REF_POPOVER, {});
21
57
 
22
- type RefPopoverProviderProps = PropsWithChildren<{ onLookup?: PreviewLookup }>;
58
+ type PreviewProviderProps = PropsWithChildren<{ onLookup?: PreviewLookup }>;
23
59
 
24
- const RefPopoverProvider = ({ children, onLookup }: RefPopoverProviderProps) => {
60
+ const PreviewProvider = ({ children, onLookup }: PreviewProviderProps) => {
25
61
  const trigger = useRef<DxRefTag | null>(null);
26
62
  const [value, setValue] = useState<RefPopoverValue>({});
27
- const [rootRef, setRootRef] = useState<HTMLDivElement | null>(null);
28
63
  const [open, setOpen] = useState(false);
29
64
 
30
65
  const handleDxRefTagActivate = useCallback(
@@ -48,28 +83,15 @@ const RefPopoverProvider = ({ children, onLookup }: RefPopoverProviderProps) =>
48
83
  [onLookup],
49
84
  );
50
85
 
51
- useEffect(() => {
52
- return rootRef
53
- ? addEventListener(rootRef, 'dx-ref-tag-activate', handleDxRefTagActivate, customEventOptions)
54
- : undefined;
55
- }, [rootRef]);
56
-
57
86
  return (
58
87
  <RefPopoverContextProvider pending={value.pending} link={value.link} target={value.target}>
59
- <Popover.Root open={open} onOpenChange={setOpen}>
60
- <Popover.VirtualTrigger virtualRef={trigger as unknown as RefObject<HTMLButtonElement>} />
61
- <div role='none' className='contents' ref={setRootRef}>
62
- {children}
63
- </div>
64
- </Popover.Root>
88
+ <RefPopover ref={trigger} open={open} onOpenChange={setOpen} onActivate={handleDxRefTagActivate}>
89
+ {children}
90
+ </RefPopover>
65
91
  </RefPopoverContextProvider>
66
92
  );
67
93
  };
68
94
 
69
- export const RefPopover = {
70
- Provider: RefPopoverProvider,
71
- };
72
-
73
- export { useRefPopover };
95
+ export { PreviewProvider, useRefPopover };
74
96
 
75
- export type { RefPopoverProviderProps, RefPopoverValue };
97
+ export type { PreviewProviderProps, RefPopoverValue };
@@ -2,5 +2,6 @@
2
2
  // Copyright 2022 DXOS.org
3
3
  //
4
4
 
5
+ export * from './CommandMenu';
5
6
  export * from './RefPopover';
6
7
  export * from './RefDropdownMenu';
package/src/defaults.ts CHANGED
@@ -28,6 +28,7 @@ export const editorSlots: ThemeExtensionsOptions['slots'] = {
28
28
 
29
29
  export const editorGutter = EditorView.theme({
30
30
  '.cm-gutters': {
31
+ background: 'var(--dx-baseSurface)',
31
32
  paddingRight: '1rem',
32
33
  },
33
34
  });