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

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 (58) hide show
  1. package/dist/lib/browser/index.mjs +82 -190
  2. package/dist/lib/browser/index.mjs.map +4 -4
  3. package/dist/lib/browser/meta.json +1 -1
  4. package/dist/lib/browser/testing/index.mjs +71 -1
  5. package/dist/lib/browser/testing/index.mjs.map +4 -4
  6. package/dist/lib/node-esm/index.mjs +82 -190
  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/testing/index.mjs +71 -1
  10. package/dist/lib/node-esm/testing/index.mjs.map +4 -4
  11. package/dist/types/src/components/{Popover → CommandMenu}/CommandMenu.d.ts +10 -6
  12. package/dist/types/src/components/CommandMenu/CommandMenu.d.ts.map +1 -0
  13. package/dist/types/src/components/CommandMenu/index.d.ts +2 -0
  14. package/dist/types/src/components/CommandMenu/index.d.ts.map +1 -0
  15. package/dist/types/src/components/index.d.ts +1 -1
  16. package/dist/types/src/components/index.d.ts.map +1 -1
  17. package/dist/types/src/extensions/autoscroll.d.ts.map +1 -1
  18. package/dist/types/src/extensions/command/action.d.ts.map +1 -1
  19. package/dist/types/src/extensions/command/floating-menu.d.ts.map +1 -1
  20. package/dist/types/src/extensions/command/useCommandMenu.d.ts +1 -2
  21. package/dist/types/src/extensions/command/useCommandMenu.d.ts.map +1 -1
  22. package/dist/types/src/extensions/markdown/link.d.ts.map +1 -1
  23. package/dist/types/src/extensions/preview/preview.d.ts +0 -1
  24. package/dist/types/src/extensions/preview/preview.d.ts.map +1 -1
  25. package/dist/types/src/hooks/useTextEditor.d.ts.map +1 -1
  26. package/dist/types/src/stories/CommandMenu.stories.d.ts.map +1 -1
  27. package/dist/types/src/stories/Outliner.stories.d.ts.map +1 -1
  28. package/dist/types/src/testing/PreviewPopover.d.ts +20 -0
  29. package/dist/types/src/testing/PreviewPopover.d.ts.map +1 -0
  30. package/dist/types/src/testing/index.d.ts +1 -0
  31. package/dist/types/src/testing/index.d.ts.map +1 -1
  32. package/dist/types/tsconfig.tsbuildinfo +1 -1
  33. package/package.json +33 -33
  34. package/src/components/{Popover → CommandMenu}/CommandMenu.tsx +93 -26
  35. package/src/components/{Popover → CommandMenu}/index.ts +0 -2
  36. package/src/components/index.ts +1 -1
  37. package/src/extensions/autoscroll.ts +14 -8
  38. package/src/extensions/command/action.ts +0 -1
  39. package/src/extensions/command/command-menu.ts +1 -1
  40. package/src/extensions/command/floating-menu.ts +9 -14
  41. package/src/extensions/command/useCommandMenu.ts +3 -7
  42. package/src/extensions/markdown/link.ts +3 -0
  43. package/src/extensions/outliner/outliner.ts +1 -1
  44. package/src/extensions/preview/preview.ts +0 -3
  45. package/src/hooks/useTextEditor.ts +0 -12
  46. package/src/stories/CommandMenu.stories.tsx +5 -7
  47. package/src/stories/Outliner.stories.tsx +28 -19
  48. package/src/stories/Preview.stories.tsx +4 -4
  49. package/src/{components/Popover/RefDropdownMenu.tsx → testing/PreviewPopover.tsx} +19 -30
  50. package/src/testing/index.ts +1 -0
  51. package/dist/types/src/components/Popover/CommandMenu.d.ts.map +0 -1
  52. package/dist/types/src/components/Popover/RefDropdownMenu.d.ts +0 -14
  53. package/dist/types/src/components/Popover/RefDropdownMenu.d.ts.map +0 -1
  54. package/dist/types/src/components/Popover/RefPopover.d.ts +0 -37
  55. package/dist/types/src/components/Popover/RefPopover.d.ts.map +0 -1
  56. package/dist/types/src/components/Popover/index.d.ts +0 -4
  57. package/dist/types/src/components/Popover/index.d.ts.map +0 -1
  58. package/src/components/Popover/RefPopover.tsx +0 -117
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dxos/react-ui-editor",
3
- "version": "0.8.4-main.dedc0f3",
3
+ "version": "0.8.4-main.e098934",
4
4
  "description": "Document editing experience within a DXOS shell.",
5
5
  "homepage": "https://dxos.org",
6
6
  "bugs": "https://github.com/dxos/dxos/issues",
@@ -59,6 +59,7 @@
59
59
  "@preact-signals/safe-react": "^0.9.0",
60
60
  "@preact/signals-react": "^3.2.0",
61
61
  "@radix-ui/react-context": "1.1.1",
62
+ "@radix-ui/react-use-controllable-state": "1.1.0",
62
63
  "@replit/codemirror-vim": "^6.2.1",
63
64
  "@replit/codemirror-vscode-keymap": "^6.0.2",
64
65
  "ajv": "^8.17.1",
@@ -68,22 +69,21 @@
68
69
  "lodash.merge": "^4.6.2",
69
70
  "lodash.sortby": "^4.7.0",
70
71
  "style-mod": "^4.1.0",
71
- "@dxos/app-graph": "0.8.4-main.dedc0f3",
72
- "@dxos/async": "0.8.4-main.dedc0f3",
73
- "@dxos/context": "0.8.4-main.dedc0f3",
74
- "@dxos/display-name": "0.8.4-main.dedc0f3",
75
- "@dxos/echo-schema": "0.8.4-main.dedc0f3",
76
- "@dxos/invariant": "0.8.4-main.dedc0f3",
77
- "@dxos/debug": "0.8.4-main.dedc0f3",
78
- "@dxos/lit-ui": "0.8.4-main.dedc0f3",
79
- "@dxos/live-object": "0.8.4-main.dedc0f3",
80
- "@dxos/log": "0.8.4-main.dedc0f3",
81
- "@dxos/protocols": "0.8.4-main.dedc0f3",
82
- "@dxos/react-hooks": "0.8.4-main.dedc0f3",
83
- "@dxos/react-ui-stack": "0.8.4-main.dedc0f3",
84
- "@dxos/react-ui-menu": "0.8.4-main.dedc0f3",
85
- "@dxos/react-ui-types": "0.8.4-main.dedc0f3",
86
- "@dxos/util": "0.8.4-main.dedc0f3"
72
+ "@dxos/app-graph": "0.8.4-main.e098934",
73
+ "@dxos/async": "0.8.4-main.e098934",
74
+ "@dxos/context": "0.8.4-main.e098934",
75
+ "@dxos/display-name": "0.8.4-main.e098934",
76
+ "@dxos/echo-schema": "0.8.4-main.e098934",
77
+ "@dxos/invariant": "0.8.4-main.e098934",
78
+ "@dxos/live-object": "0.8.4-main.e098934",
79
+ "@dxos/log": "0.8.4-main.e098934",
80
+ "@dxos/protocols": "0.8.4-main.e098934",
81
+ "@dxos/react-hooks": "0.8.4-main.e098934",
82
+ "@dxos/react-ui-stack": "0.8.4-main.e098934",
83
+ "@dxos/react-ui-menu": "0.8.4-main.e098934",
84
+ "@dxos/react-ui-types": "0.8.4-main.e098934",
85
+ "@dxos/debug": "0.8.4-main.e098934",
86
+ "@dxos/util": "0.8.4-main.e098934"
87
87
  },
88
88
  "devDependencies": {
89
89
  "@automerge/automerge": "3.1.1",
@@ -111,19 +111,19 @@
111
111
  "vite": "7.1.1",
112
112
  "vite-plugin-top-level-await": "^1.6.0",
113
113
  "vite-plugin-wasm": "^3.5.0",
114
- "@dxos/config": "0.8.4-main.dedc0f3",
115
- "@dxos/echo": "0.8.4-main.dedc0f3",
116
- "@dxos/echo-signals": "0.8.4-main.dedc0f3",
117
- "@dxos/keyboard": "0.8.4-main.dedc0f3",
118
- "@dxos/random": "0.8.4-main.dedc0f3",
119
- "@dxos/react-client": "0.8.4-main.dedc0f3",
120
- "@dxos/react-ui": "0.8.4-main.dedc0f3",
121
- "@dxos/react-ui-stack": "0.8.4-main.dedc0f3",
122
- "@dxos/react-ui-theme": "0.8.4-main.dedc0f3",
123
- "@dxos/react-ui-attention": "0.8.4-main.dedc0f3",
124
- "@dxos/schema": "0.8.4-main.dedc0f3",
125
- "@dxos/storybook-utils": "0.8.4-main.dedc0f3",
126
- "@dxos/react-ui-syntax-highlighter": "0.8.4-main.dedc0f3"
114
+ "@dxos/echo": "0.8.4-main.e098934",
115
+ "@dxos/echo-signals": "0.8.4-main.e098934",
116
+ "@dxos/keyboard": "0.8.4-main.e098934",
117
+ "@dxos/random": "0.8.4-main.e098934",
118
+ "@dxos/react-ui": "0.8.4-main.e098934",
119
+ "@dxos/react-ui-attention": "0.8.4-main.e098934",
120
+ "@dxos/react-client": "0.8.4-main.e098934",
121
+ "@dxos/react-ui-stack": "0.8.4-main.e098934",
122
+ "@dxos/react-ui-syntax-highlighter": "0.8.4-main.e098934",
123
+ "@dxos/react-ui-theme": "0.8.4-main.e098934",
124
+ "@dxos/config": "0.8.4-main.e098934",
125
+ "@dxos/storybook-utils": "0.8.4-main.e098934",
126
+ "@dxos/schema": "0.8.4-main.e098934"
127
127
  },
128
128
  "peerDependencies": {
129
129
  "@effect-rx/rx-react": "^0.34.1",
@@ -131,9 +131,9 @@
131
131
  "effect": "^3.13.3",
132
132
  "react": "~18.2.0",
133
133
  "react-dom": "~18.2.0",
134
- "@dxos/react-client": "0.8.4-main.dedc0f3",
135
- "@dxos/react-ui": "0.8.4-main.dedc0f3",
136
- "@dxos/react-ui-theme": "0.8.4-main.dedc0f3"
134
+ "@dxos/react-client": "0.8.4-main.e098934",
135
+ "@dxos/react-ui-theme": "0.8.4-main.e098934",
136
+ "@dxos/react-ui": "0.8.4-main.e098934"
137
137
  },
138
138
  "publishConfig": {
139
139
  "access": "public"
@@ -3,9 +3,19 @@
3
3
  //
4
4
 
5
5
  import { type EditorView } from '@codemirror/view';
6
- import React, { Fragment, useCallback, useEffect, useRef } from 'react';
6
+ import { useControllableState } from '@radix-ui/react-use-controllable-state';
7
+ import React, { Fragment, type PropsWithChildren, useCallback, useEffect, useRef, useState } from 'react';
7
8
 
8
- import { Icon, type Label, Popover, toLocalizedString, useThemeContext, useTranslation } from '@dxos/react-ui';
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';
9
19
  import { type MaybePromise } from '@dxos/util';
10
20
 
11
21
  import { commandRangeEffect } from '../../extensions';
@@ -23,37 +33,94 @@ export type CommandMenuItem = {
23
33
  onSelect?: (view: EditorView, head: number) => MaybePromise<void>;
24
34
  };
25
35
 
26
- export type CommandMenuProps = {
36
+ export type CommandMenuProps = PropsWithChildren<{
27
37
  groups: CommandMenuGroup[];
28
- currentItem?: string;
29
38
  onSelect: (item: CommandMenuItem) => void;
30
- };
39
+ onActivate?: (event: DxAnchorActivate) => void;
40
+ currentItem?: string;
41
+ open?: boolean;
42
+ onOpenChange?: (nextOpen: boolean) => void;
43
+ defaultOpen?: boolean;
44
+ }>;
31
45
 
32
46
  // NOTE: Not using DropdownMenu because the command menu needs to manage focus explicitly.
33
- export const CommandMenu = ({ groups, currentItem, onSelect }: CommandMenuProps) => {
47
+ export const CommandMenuProvider = ({
48
+ groups,
49
+ onSelect,
50
+ onActivate,
51
+ currentItem,
52
+ children,
53
+ open: propsOpen,
54
+ onOpenChange,
55
+ defaultOpen,
56
+ }: CommandMenuProps) => {
34
57
  const { tx } = useThemeContext();
35
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
+
36
96
  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
- <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
- </Fragment>
52
- ))}
53
- </ul>
54
- </Popover.Viewport>
55
- </Popover.Content>
56
- </Popover.Portal>
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>
57
124
  );
58
125
  };
59
126
 
@@ -3,5 +3,3 @@
3
3
  //
4
4
 
5
5
  export * from './CommandMenu';
6
- export * from './RefPopover';
7
- export * from './RefDropdownMenu';
@@ -4,4 +4,4 @@
4
4
 
5
5
  export * from './Editor';
6
6
  export * from './EditorToolbar';
7
- export * from './Popover';
7
+ export * from './CommandMenu';
@@ -23,9 +23,10 @@ export type AutoScrollOptions = {
23
23
  export const autoScroll = ({ overscroll = 4 * lineHeight, throttle = 2_000 }: Partial<AutoScrollOptions> = {}) => {
24
24
  let isThrottled = false;
25
25
  let isPinned = true;
26
- let lastScrollTop = 0;
27
26
  let timeout: NodeJS.Timeout | undefined;
28
27
  let buttonContainer: HTMLDivElement;
28
+ let lastScrollTop = 0;
29
+ let scrollCounter = 0;
29
30
 
30
31
  const hideScrollbar = (view: EditorView) => {
31
32
  view.scrollDOM.classList.add('cm-hide-scrollbar');
@@ -37,6 +38,7 @@ export const autoScroll = ({ overscroll = 4 * lineHeight, throttle = 2_000 }: Pa
37
38
 
38
39
  const scrollToBottom = (view: EditorView) => {
39
40
  isPinned = true;
41
+ scrollCounter = 0;
40
42
  buttonContainer?.classList.add('opacity-0');
41
43
  requestAnimationFrame(() => {
42
44
  hideScrollbar(view);
@@ -84,11 +86,8 @@ export const autoScroll = ({ overscroll = 4 * lineHeight, throttle = 2_000 }: Pa
84
86
  if (update.docChanged && isPinned && !isThrottled) {
85
87
  const distanceFromBottom = calcDistance(update.view.scrollDOM);
86
88
 
87
- // Hide scrollbar even if not scrolling to bottom.
88
- // hideScrollbar(update.view);
89
-
90
89
  // Keep pinned.
91
- if (distanceFromBottom > overscroll) {
90
+ if (distanceFromBottom >= overscroll) {
92
91
  isThrottled = true;
93
92
  requestAnimationFrame(() => {
94
93
  scrollToBottom(update.view);
@@ -102,21 +101,28 @@ export const autoScroll = ({ overscroll = 4 * lineHeight, throttle = 2_000 }: Pa
102
101
  }
103
102
  }),
104
103
 
104
+ // Detect user scroll.
105
+ // NOTE: Multiple scroll events are triggered during programmatic smooth scrolling.
105
106
  EditorView.domEventHandlers({
106
107
  scroll: (event, view) => {
107
108
  const scroller = view.scrollDOM;
109
+ // Suspect delta goes positive when rendering widgets, so count positive deltas.
110
+ // TODO(burdon): Detect user scroll directly (wheel, touch, keys, etc.)
111
+ if (lastScrollTop > scroller.scrollTop) {
112
+ scrollCounter++;
113
+ }
114
+ lastScrollTop = scroller.scrollTop;
108
115
  const distanceFromBottom = calcDistance(scroller);
109
116
  if (distanceFromBottom === 0) {
110
117
  // Pin to bottom.
111
118
  isPinned = true;
112
119
  buttonContainer?.classList.add('opacity-0');
113
- } else if (scroller.scrollTop < lastScrollTop) {
120
+ scrollCounter = 0;
121
+ } else if (scrollCounter > 3) {
114
122
  // Break pin if user scrolls up.
115
123
  isPinned = false;
116
124
  buttonContainer?.classList.remove('opacity-0');
117
125
  }
118
-
119
- lastScrollTop = scroller.scrollTop;
120
126
  },
121
127
  }),
122
128
 
@@ -46,7 +46,6 @@ export const closeCommand: Command = (view: EditorView) => {
46
46
  export const commandKeyBindings: readonly KeyBinding[] = [
47
47
  {
48
48
  key: '/',
49
- preventDefault: true,
50
49
  run: openCommand,
51
50
  },
52
51
  {
@@ -44,7 +44,7 @@ export const commandMenu = (options: CommandMenuOptions) => {
44
44
  activeRange.to,
45
45
  Decoration.mark({
46
46
  tagName: 'dx-anchor',
47
- class: 'cm-ref-tag',
47
+ class: 'cm-floating-menu-trigger',
48
48
  attributes: {
49
49
  'data-auto-trigger': 'true',
50
50
  'data-trigger': trigger!,
@@ -34,12 +34,10 @@ export const floatingMenu = (options: FloatingMenuOptions = {}) => [
34
34
  {
35
35
  const icon = document.createElement('dx-icon');
36
36
  icon.setAttribute('icon', options.icon ?? 'ph--dots-three-vertical--regular');
37
- const button = document.createElement('button');
38
- button.appendChild(icon);
39
37
 
40
38
  this.tag = document.createElement('dx-anchor');
41
- this.tag.classList.add('cm-ref-tag');
42
- this.tag.appendChild(button);
39
+ this.tag.classList.add('cm-floating-menu-trigger');
40
+ this.tag.appendChild(icon);
43
41
  }
44
42
 
45
43
  container.appendChild(this.tag);
@@ -69,7 +67,7 @@ export const floatingMenu = (options: FloatingMenuOptions = {}) => [
69
67
  this.tag.style.display = 'none';
70
68
  this.tag.classList.add('opacity-10');
71
69
  } else if (update.transactions.some((tr) => tr.effects.some((effect) => effect.is(closeEffect)))) {
72
- this.tag.style.display = 'block';
70
+ this.tag.style.display = '';
73
71
  } else if (
74
72
  update.docChanged ||
75
73
  update.focusChanged ||
@@ -99,7 +97,7 @@ export const floatingMenu = (options: FloatingMenuOptions = {}) => [
99
97
 
100
98
  this.tag.style.top = `${offsetTop}px`;
101
99
  this.tag.style.left = `${offsetLeft}px`;
102
- this.tag.style.display = 'block';
100
+ this.tag.style.display = '';
103
101
  }
104
102
 
105
103
  scheduleUpdate() {
@@ -113,21 +111,18 @@ export const floatingMenu = (options: FloatingMenuOptions = {}) => [
113
111
  ),
114
112
 
115
113
  EditorView.theme({
116
- '.cm-ref-tag': {
114
+ '.cm-floating-menu-trigger': {
117
115
  position: 'fixed',
118
116
  padding: '0',
119
117
  border: 'none',
120
118
  opacity: '0',
121
- },
122
- '[data-has-focus] & .cm-ref-tag': {
123
- opacity: '1',
124
- },
125
- '.cm-ref-tag button': {
126
119
  display: 'grid',
127
- alignItems: 'center',
128
- justifyContent: 'center',
120
+ placeContent: 'center',
129
121
  width: '2rem',
130
122
  height: '2rem',
131
123
  },
124
+ '&:focus-within .cm-floating-menu-trigger': {
125
+ opacity: '1',
126
+ },
132
127
  }),
133
128
  ];
@@ -5,7 +5,7 @@
5
5
  import { type EditorView } from '@codemirror/view';
6
6
  import { type RefObject, useCallback, useMemo, useRef, useState } from 'react';
7
7
 
8
- import { type DxAnchor, type DxAnchorActivate } from '@dxos/lit-ui';
8
+ import { type DxAnchorActivate } from '@dxos/react-ui';
9
9
  import { type MaybePromise } from '@dxos/util';
10
10
 
11
11
  import { type CommandMenuGroup, type CommandMenuItem, getItem, getNextItem, getPreviousItem } from '../../components';
@@ -21,7 +21,6 @@ export type UseCommandMenuOptions = {
21
21
  };
22
22
 
23
23
  export const useCommandMenu = ({ viewRef, trigger, placeholder, getMenu }: UseCommandMenuOptions) => {
24
- const triggerRef = useRef<DxAnchor | null>(null);
25
24
  const currentRef = useRef<CommandMenuItem | null>(null);
26
25
  const groupsRef = useRef<CommandMenuGroup[]>([]);
27
26
  const [currentItem, setCurrentItem] = useState<string>();
@@ -35,7 +34,6 @@ export const useCommandMenu = ({ viewRef, trigger, placeholder, getMenu }: UseCo
35
34
  }
36
35
  setOpen(open);
37
36
  if (!open) {
38
- triggerRef.current = null;
39
37
  setCurrentItem(undefined);
40
38
  viewRef.current?.dispatch({ effects: [commandRangeEffect.of(null)] });
41
39
  }
@@ -50,7 +48,6 @@ export const useCommandMenu = ({ viewRef, trigger, placeholder, getMenu }: UseCo
50
48
  currentRef.current = item;
51
49
  }
52
50
 
53
- triggerRef.current = event.trigger;
54
51
  const triggerKey = event.trigger.getAttribute('data-trigger');
55
52
  if (!open && triggerKey) {
56
53
  await handleOpenChange(true, triggerKey);
@@ -70,7 +67,7 @@ export const useCommandMenu = ({ viewRef, trigger, placeholder, getMenu }: UseCo
70
67
  }, []);
71
68
 
72
69
  const serializedTrigger = Array.isArray(trigger) ? trigger.join(',') : trigger;
73
- const _commandMenu = useMemo(() => {
70
+ const memoizedCommandMenu = useMemo(() => {
74
71
  return commandMenu({
75
72
  trigger,
76
73
  placeholder,
@@ -107,10 +104,9 @@ export const useCommandMenu = ({ viewRef, trigger, placeholder, getMenu }: UseCo
107
104
  }, [handleOpenChange, getMenu, serializedTrigger, placeholder]);
108
105
 
109
106
  return {
110
- commandMenu: _commandMenu,
107
+ commandMenu: memoizedCommandMenu,
111
108
  currentItem,
112
109
  groupsRef,
113
- ref: triggerRef,
114
110
  open,
115
111
  onActivate: handleActivate,
116
112
  onOpenChange: setOpen,
@@ -26,6 +26,9 @@ export const linkTooltip = (renderTooltip: RenderCallback<{ url: string }>) => {
26
26
  }
27
27
 
28
28
  const urlText = view.state.sliceDoc(url.from, url.to);
29
+ if (urlText.startsWith('dxn')) {
30
+ return null;
31
+ }
29
32
  return {
30
33
  pos: link.from,
31
34
  end: link.to,
@@ -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
  }),
@@ -22,9 +22,6 @@ export type PreviewLinkTarget = {
22
22
  object?: any;
23
23
  };
24
24
 
25
- // TODO(wittjosiah): Remove.
26
- export type PreviewLookup = (link: PreviewLinkRef) => Promise<PreviewLinkTarget | null | undefined>;
27
-
28
25
  export type PreviewOptions = {
29
26
  addBlockContainer?: (link: PreviewLinkRef, el: HTMLElement) => void;
30
27
  removeBlockContainer?: (link: PreviewLinkRef) => void;
@@ -96,18 +96,6 @@ export const useTextEditor = (
96
96
  EditorView.exceptionSink.of((err) => {
97
97
  log.catch(err);
98
98
  }),
99
- // TODO(burdon): Factor out debug inspector.
100
- // ViewPlugin.fromClass(
101
- // class {
102
- // constructor(_view: EditorView) {
103
- // log('construct', { id });
104
- // }
105
- //
106
- // destroy() {
107
- // log('destroy', { id });
108
- // }
109
- // },
110
- // ),
111
99
  ].filter(isNotFalsy),
112
100
  });
113
101
 
@@ -15,10 +15,9 @@ import { Testing, type ValueGenerator, createObjectFactory } from '@dxos/schema/
15
15
  import { withLayout, withTheme } from '@dxos/storybook-utils';
16
16
 
17
17
  import {
18
- CommandMenu,
19
18
  type CommandMenuGroup,
20
19
  type CommandMenuItem,
21
- RefPopover,
20
+ CommandMenuProvider,
22
21
  coreSlashCommands,
23
22
  filterItems,
24
23
  insertAtCursor,
@@ -37,13 +36,12 @@ type StoryProps = Omit<UseCommandMenuOptions, 'viewRef'> & { text: string };
37
36
 
38
37
  const DefaultStory = ({ text, ...options }: StoryProps) => {
39
38
  const viewRef = useRef<EditorView>();
40
- const { commandMenu, groupsRef, currentItem, onSelect, ...props } = useCommandMenu({ viewRef, ...options });
39
+ const { commandMenu, groupsRef, ...commandMenuProps } = useCommandMenu({ viewRef, ...options });
41
40
 
42
41
  return (
43
- <RefPopover modal={false} {...props}>
42
+ <CommandMenuProvider groups={groupsRef.current} {...commandMenuProps}>
44
43
  <EditorStory ref={viewRef} text={text} placeholder={''} extensions={commandMenu} />
45
- <CommandMenu groups={groupsRef.current} currentItem={currentItem} onSelect={onSelect} />
46
- </RefPopover>
44
+ </CommandMenuProvider>
47
45
  );
48
46
  };
49
47
 
@@ -123,7 +121,7 @@ export const Link: Story = {
123
121
  label: object.name,
124
122
  icon: 'ph--user--regular',
125
123
  onSelect: (view, head) => {
126
- const link = `[${object.name}][${Obj.getDXN(object)}]`;
124
+ const link = `[${object.name}](${Obj.getDXN(object)})`;
127
125
  if (query?.startsWith('@')) {
128
126
  insertAtLineStart(view, head, `!${link}\n`);
129
127
  } else {
@@ -6,13 +6,12 @@ import '@dxos-theme';
6
6
 
7
7
  import { type EditorView } from '@codemirror/view';
8
8
  import { type Meta, type StoryObj } from '@storybook/react-vite';
9
- import React, { useRef } from 'react';
9
+ import React, { useMemo, useRef } from 'react';
10
10
 
11
- import { DropdownMenu } from '@dxos/react-ui';
12
11
  import { withAttention } from '@dxos/react-ui-attention/testing';
13
12
  import { withLayout, withTheme } from '@dxos/storybook-utils';
14
13
 
15
- import { RefDropdownMenuProvider } from '../components';
14
+ import { type CommandMenuGroup, type CommandMenuItem, CommandMenuProvider } from '../components';
16
15
  import { deleteItem, hashtag, listItemToString, outliner, treeFacet } from '../extensions';
17
16
  import { str } from '../testing';
18
17
 
@@ -25,14 +24,33 @@ type StoryProps = {
25
24
  const DefaultStory = ({ text }: StoryProps) => {
26
25
  const viewRef = useRef<EditorView>(null);
27
26
 
28
- const handleDelete = () => {
29
- if (viewRef.current) {
30
- deleteItem(viewRef.current);
31
- }
32
- };
27
+ const commandGroups: CommandMenuGroup[] = useMemo(
28
+ () => [
29
+ {
30
+ id: 'outliner-actions',
31
+ items: [
32
+ {
33
+ id: 'delete-row',
34
+ label: 'Delete',
35
+ onSelect: (view: EditorView) => {
36
+ deleteItem(view);
37
+ },
38
+ },
39
+ ],
40
+ },
41
+ ],
42
+ [],
43
+ );
33
44
 
34
45
  return (
35
- <RefDropdownMenuProvider>
46
+ <CommandMenuProvider
47
+ groups={commandGroups}
48
+ onSelect={(item: CommandMenuItem) => {
49
+ if (viewRef.current && item.onSelect) {
50
+ return item.onSelect(viewRef.current, viewRef.current.state.selection.main.head);
51
+ }
52
+ }}
53
+ >
36
54
  <EditorStory
37
55
  ref={viewRef}
38
56
  text={text}
@@ -46,16 +64,7 @@ const DefaultStory = ({ text }: StoryProps) => {
46
64
  return <pre className='p-1 overflow-auto text-xs text-green-800 dark:text-green-200'>{lines.join('\n')}</pre>;
47
65
  }}
48
66
  />
49
-
50
- <DropdownMenu.Portal>
51
- <DropdownMenu.Content>
52
- <DropdownMenu.Viewport>
53
- <DropdownMenu.Item onClick={handleDelete}>Delete</DropdownMenu.Item>
54
- </DropdownMenu.Viewport>
55
- <DropdownMenu.Arrow />
56
- </DropdownMenu.Content>
57
- </DropdownMenu.Portal>
58
- </RefDropdownMenuProvider>
67
+ </CommandMenuProvider>
59
68
  );
60
69
  };
61
70
 
@@ -18,8 +18,8 @@ import { hoverableControlItem, hoverableControlItemTransition, hoverableControls
18
18
  import { withLayout, withTheme } from '@dxos/storybook-utils';
19
19
  import { trim } from '@dxos/util';
20
20
 
21
- import { PreviewProvider, useRefPopover } from '../components';
22
21
  import { type PreviewLinkRef, type PreviewLinkTarget, getLinkRef, image, preview } from '../extensions';
22
+ import { PreviewPopoverProvider, usePreviewPopover } from '../testing';
23
23
 
24
24
  import { EditorStory } from './components';
25
25
 
@@ -45,7 +45,7 @@ const useRefTarget = (link: PreviewLinkRef): PreviewLinkTarget | undefined => {
45
45
  };
46
46
 
47
47
  const PreviewCard = () => {
48
- const { target } = useRefPopover('PreviewCard');
48
+ const { target } = usePreviewPopover('PreviewCard');
49
49
  return (
50
50
  <Popover.Portal>
51
51
  <Popover.Content onOpenAutoFocus={(event) => event.preventDefault()}>
@@ -200,7 +200,7 @@ export const Default: Story = {
200
200
  }, []);
201
201
 
202
202
  return (
203
- <PreviewProvider onLookup={handlePreviewLookup}>
203
+ <PreviewPopoverProvider onLookup={handlePreviewLookup}>
204
204
  <EditorStory
205
205
  ref={handleViewRef}
206
206
  text={trim`
@@ -223,7 +223,7 @@ export const Default: Story = {
223
223
  {previewBlocks.map(({ link, el }) => (
224
224
  <PreviewBlock key={link.ref} link={link} el={el} view={view} />
225
225
  ))}
226
- </PreviewProvider>
226
+ </PreviewPopoverProvider>
227
227
  );
228
228
  },
229
229
  };