@dxos/react-ui-editor 0.8.3-main.672df60 → 0.8.3-staging.0fa589b

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 +981 -377
  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 +1025 -420
  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 +981 -377
  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/EditorToolbar.d.ts.map +1 -1
  11. package/dist/types/src/components/EditorToolbar/util.d.ts +2 -2
  12. package/dist/types/src/components/Popover/CommandMenu.d.ts +34 -0
  13. package/dist/types/src/components/Popover/CommandMenu.d.ts.map +1 -0
  14. package/dist/types/src/components/Popover/RefDropdownMenu.d.ts.map +1 -1
  15. package/dist/types/src/components/Popover/RefPopover.d.ts +19 -6
  16. package/dist/types/src/components/Popover/RefPopover.d.ts.map +1 -1
  17. package/dist/types/src/components/Popover/index.d.ts +1 -0
  18. package/dist/types/src/components/Popover/index.d.ts.map +1 -1
  19. package/dist/types/src/defaults.d.ts +0 -1
  20. package/dist/types/src/defaults.d.ts.map +1 -1
  21. package/dist/types/src/extensions/automerge/automerge.stories.d.ts.map +1 -1
  22. package/dist/types/src/extensions/command/action.d.ts.map +1 -1
  23. package/dist/types/src/extensions/command/command-menu.d.ts +20 -0
  24. package/dist/types/src/extensions/command/command-menu.d.ts.map +1 -0
  25. package/dist/types/src/extensions/command/command.d.ts.map +1 -1
  26. package/dist/types/src/extensions/command/{menu.d.ts → floating-menu.d.ts} +1 -1
  27. package/dist/types/src/extensions/command/floating-menu.d.ts.map +1 -0
  28. package/dist/types/src/extensions/command/hint.d.ts +5 -2
  29. package/dist/types/src/extensions/command/hint.d.ts.map +1 -1
  30. package/dist/types/src/extensions/command/index.d.ts +3 -1
  31. package/dist/types/src/extensions/command/index.d.ts.map +1 -1
  32. package/dist/types/src/extensions/command/placeholder.d.ts +10 -0
  33. package/dist/types/src/extensions/command/placeholder.d.ts.map +1 -0
  34. package/dist/types/src/extensions/command/state.d.ts +1 -1
  35. package/dist/types/src/extensions/command/state.d.ts.map +1 -1
  36. package/dist/types/src/extensions/command/useCommandMenu.d.ts +26 -0
  37. package/dist/types/src/extensions/command/useCommandMenu.d.ts.map +1 -0
  38. package/dist/types/src/extensions/factories.d.ts +1 -0
  39. package/dist/types/src/extensions/factories.d.ts.map +1 -1
  40. package/dist/types/src/extensions/markdown/bundle.d.ts.map +1 -1
  41. package/dist/types/src/extensions/outliner/tree.d.ts.map +1 -1
  42. package/dist/types/src/extensions/preview/preview.d.ts +12 -19
  43. package/dist/types/src/extensions/preview/preview.d.ts.map +1 -1
  44. package/dist/types/src/hooks/useTextEditor.d.ts +8 -9
  45. package/dist/types/src/hooks/useTextEditor.d.ts.map +1 -1
  46. package/dist/types/src/stories/CommandMenu.stories.d.ts +13 -0
  47. package/dist/types/src/stories/CommandMenu.stories.d.ts.map +1 -0
  48. package/dist/types/src/stories/Preview.stories.d.ts.map +1 -1
  49. package/dist/types/src/util/dom.d.ts +5 -0
  50. package/dist/types/src/util/dom.d.ts.map +1 -1
  51. package/dist/types/src/util/react.d.ts +2 -4
  52. package/dist/types/src/util/react.d.ts.map +1 -1
  53. package/package.json +31 -28
  54. package/src/components/EditorToolbar/EditorToolbar.tsx +5 -9
  55. package/src/components/Popover/CommandMenu.tsx +279 -0
  56. package/src/components/Popover/RefDropdownMenu.tsx +5 -3
  57. package/src/components/Popover/RefPopover.tsx +46 -22
  58. package/src/components/Popover/index.ts +1 -0
  59. package/src/defaults.ts +1 -6
  60. package/src/extensions/automerge/automerge.stories.tsx +5 -5
  61. package/src/extensions/command/action.ts +9 -2
  62. package/src/extensions/command/command-menu.ts +210 -0
  63. package/src/extensions/command/command.ts +8 -8
  64. package/src/extensions/command/{menu.ts → floating-menu.ts} +0 -4
  65. package/src/extensions/command/hint.ts +29 -9
  66. package/src/extensions/command/index.ts +3 -1
  67. package/src/extensions/command/placeholder.ts +113 -0
  68. package/src/extensions/command/state.ts +1 -2
  69. package/src/extensions/command/useCommandMenu.ts +118 -0
  70. package/src/extensions/factories.ts +4 -1
  71. package/src/extensions/markdown/bundle.ts +0 -2
  72. package/src/extensions/outliner/outliner.ts +0 -3
  73. package/src/extensions/outliner/tree.test.ts +13 -10
  74. package/src/extensions/outliner/tree.ts +5 -3
  75. package/src/extensions/preview/preview.ts +11 -89
  76. package/src/hooks/useTextEditor.ts +11 -12
  77. package/src/stories/Command.stories.tsx +1 -1
  78. package/src/stories/CommandMenu.stories.tsx +159 -0
  79. package/src/stories/Preview.stories.tsx +157 -78
  80. package/src/stories/components/util.tsx +2 -2
  81. package/src/util/dom.ts +20 -0
  82. package/src/util/react.tsx +3 -20
  83. package/dist/types/src/extensions/command/menu.d.ts.map +0 -1
@@ -16,8 +16,6 @@ import {
16
16
  import { Decoration, type DecorationSet, EditorView, WidgetType } from '@codemirror/view';
17
17
  import { type SyntaxNode } from '@lezer/common';
18
18
 
19
- import { type RenderCallback } from '../../types';
20
-
21
19
  export type PreviewLinkRef = {
22
20
  suggest?: boolean;
23
21
  block?: boolean;
@@ -31,32 +29,13 @@ export type PreviewLinkTarget = {
31
29
  object?: any;
32
30
  };
33
31
 
34
- export type PreviewAction =
35
- | {
36
- type: 'insert';
37
- link: PreviewLinkRef;
38
- target: PreviewLinkTarget;
39
- }
40
- | {
41
- type: 'delete';
42
- link: PreviewLinkRef;
43
- };
44
-
32
+ // TODO(wittjosiah): Remove.
45
33
  // TODO(burdon): Handle error.
46
34
  export type PreviewLookup = (link: PreviewLinkRef) => Promise<PreviewLinkTarget | null | undefined>;
47
35
 
48
- export type PreviewActionHandler = (action: PreviewAction) => void;
49
-
50
- export type PreviewRenderProps = {
51
- readonly: boolean;
52
- link: PreviewLinkRef;
53
- onAction: PreviewActionHandler;
54
- onLookup?: PreviewLookup;
55
- };
56
-
57
36
  export type PreviewOptions = {
58
- renderBlock?: RenderCallback<PreviewRenderProps>;
59
- onLookup?: PreviewLookup;
37
+ addBlockContainer?: (link: PreviewLinkRef, el: HTMLElement) => void;
38
+ removeBlockContainer?: (link: PreviewLinkRef) => void;
60
39
  };
61
40
 
62
41
  /**
@@ -74,17 +53,6 @@ export const preview = (options: PreviewOptions = {}): Extension => {
74
53
  EditorView.atomicRanges.of((view) => view.state.field(field)),
75
54
  ],
76
55
  }),
77
-
78
- EditorView.theme({
79
- '.cm-preview-block': {
80
- marginLeft: '-1rem',
81
- marginRight: '-1rem',
82
- padding: '1rem',
83
- borderRadius: '0.5rem',
84
- background: 'var(--dx-modalSurface)',
85
- border: '1px solid var(--dx-separator)',
86
- },
87
- }),
88
56
  ];
89
57
  };
90
58
 
@@ -95,7 +63,7 @@ export const preview = (options: PreviewOptions = {}): Extension => {
95
63
  * ![Label][dxn:echo:123] Block reference
96
64
  * ![Label][?dxn:echo:123] Suggestion
97
65
  */
98
- const getLinkRef = (state: EditorState, node: SyntaxNode): PreviewLinkRef | undefined => {
66
+ export const getLinkRef = (state: EditorState, node: SyntaxNode): PreviewLinkRef | undefined => {
99
67
  const mark = node.getChild('LinkMark');
100
68
  const label = node.getChild('LinkLabel');
101
69
  if (mark && label) {
@@ -144,7 +112,7 @@ const buildDecorations = (state: EditorState, options: PreviewOptions) => {
144
112
  //
145
113
  case 'Image': {
146
114
  const link = getLinkRef(state, node.node);
147
- if (options.renderBlock && link) {
115
+ if (options.addBlockContainer && options.removeBlockContainer && link) {
148
116
  builder.add(
149
117
  node.from,
150
118
  node.to,
@@ -214,58 +182,12 @@ class PreviewBlockWidget extends WidgetType {
214
182
 
215
183
  override toDOM(view: EditorView): HTMLDivElement {
216
184
  const root = document.createElement('div');
217
- root.classList.add('cm-preview-block');
218
-
219
- // TODO(burdon): Inject handler.
220
- const handleAction: PreviewActionHandler = (action) => {
221
- const pos = view.posAtDOM(root);
222
- const node = syntaxTree(view.state).resolve(pos + 1).node.parent;
223
- if (!node) {
224
- return;
225
- }
226
-
227
- const link = getLinkRef(view.state, node);
228
- if (link?.ref !== action.link.ref) {
229
- return;
230
- }
231
-
232
- switch (action.type) {
233
- // TODO(burdon): Should we dispatch to the view or mutate the document? (i.e., handle externally?)
234
- // Insert ref text.
235
- case 'insert': {
236
- view.dispatch({
237
- changes: {
238
- from: node.from,
239
- to: node.to,
240
- insert: action.target.text,
241
- },
242
- });
243
- break;
244
- }
245
- // Remove ref.
246
- case 'delete': {
247
- view.dispatch({
248
- changes: {
249
- from: node.from,
250
- to: node.to,
251
- },
252
- });
253
- break;
254
- }
255
- }
256
- };
257
-
258
- this._options.renderBlock!(
259
- root,
260
- {
261
- readonly: view.state.readOnly,
262
- link: this._link,
263
- onAction: handleAction,
264
- onLookup: this._options.onLookup,
265
- },
266
- view,
267
- );
268
-
185
+ root.classList.add('cm-preview-block', 'density-coarse');
186
+ this._options.addBlockContainer?.(this._link, root);
269
187
  return root;
270
188
  }
189
+
190
+ override destroy() {
191
+ this._options.removeBlockContainer?.(this._link);
192
+ }
271
193
  }
@@ -22,15 +22,7 @@ import { getProviderValue, isNotFalsy, type MaybeProvider } from '@dxos/util';
22
22
  import { type EditorSelection, documentId, createEditorStateTransaction, editorInputMode } from '../extensions';
23
23
  import { debugDispatcher } from '../util';
24
24
 
25
- export type UseTextEditor = {
26
- // TODO(burdon): Rename.
27
- parentRef: RefObject<HTMLDivElement>;
28
- view?: EditorView;
29
- focusAttributes?: TabsterTypes.TabsterDOMAttribute & {
30
- tabIndex: 0;
31
- onKeyUp: KeyboardEventHandler<HTMLDivElement>;
32
- };
33
- };
25
+ let instanceCount = 0;
34
26
 
35
27
  export type CursorInfo = {
36
28
  from: number;
@@ -41,11 +33,20 @@ export type CursorInfo = {
41
33
  after?: string;
42
34
  };
43
35
 
36
+ export type UseTextEditor = {
37
+ // TODO(burdon): Rename.
38
+ parentRef: RefObject<HTMLDivElement>;
39
+ view?: EditorView;
40
+ focusAttributes?: TabsterTypes.TabsterDOMAttribute & {
41
+ tabIndex: 0;
42
+ onKeyUp: KeyboardEventHandler<HTMLDivElement>;
43
+ };
44
+ };
45
+
44
46
  export type UseTextEditorProps = Pick<EditorStateConfig, 'extensions'> & {
45
47
  id?: string;
46
48
  doc?: Text;
47
49
  initialValue?: string;
48
- className?: string;
49
50
  autoFocus?: boolean;
50
51
  scrollTo?: number;
51
52
  selection?: EditorSelection;
@@ -53,8 +54,6 @@ export type UseTextEditorProps = Pick<EditorStateConfig, 'extensions'> & {
53
54
  debug?: boolean;
54
55
  };
55
56
 
56
- let instanceCount = 0;
57
-
58
57
  /**
59
58
  * Creates codemirror text editor.
60
59
  */
@@ -75,7 +75,7 @@ const meta: Meta<typeof EditorStory> = {
75
75
  floatingMenu(),
76
76
  command({
77
77
  renderDialog: createRenderer(CommandDialog),
78
- onHint: () => 'Press / for commands.',
78
+ onHint: () => "Press '/' for commands",
79
79
  }),
80
80
  ]}
81
81
  />
@@ -0,0 +1,159 @@
1
+ //
2
+ // Copyright 2023 DXOS.org
3
+ //
4
+
5
+ import '@dxos-theme';
6
+
7
+ import { type EditorView } from '@codemirror/view';
8
+ import { type StoryObj } from '@storybook/react';
9
+ import React, { useCallback, useRef } from 'react';
10
+
11
+ import { Obj, Query } from '@dxos/echo';
12
+ import { faker } from '@dxos/random';
13
+ import { useClientProvider, withClientProvider } from '@dxos/react-client/testing';
14
+ import { createObjectFactory, Testing, type ValueGenerator } from '@dxos/schema/testing';
15
+ import { withLayout, withTheme, type Meta } from '@dxos/storybook-utils';
16
+
17
+ import { EditorStory, names } from './components';
18
+ import {
19
+ CommandMenu,
20
+ type CommandMenuGroup,
21
+ type CommandMenuItem,
22
+ RefPopover,
23
+ coreSlashCommands,
24
+ filterItems,
25
+ insertAtCursor,
26
+ insertAtLineStart,
27
+ linkSlashCommands,
28
+ } from '../components';
29
+ import { useCommandMenu, type UseCommandMenuOptions } from '../extensions';
30
+ import { str } from '../testing';
31
+ import { createElement } from '../util';
32
+
33
+ const generator: ValueGenerator = faker as any;
34
+
35
+ type StoryProps = Omit<UseCommandMenuOptions, 'viewRef'> & { text: string };
36
+
37
+ const DefaultStory = ({ text, ...options }: StoryProps) => {
38
+ const viewRef = useRef<EditorView>();
39
+ const { commandMenu, groupsRef, currentItem, onSelect, ...props } = useCommandMenu({ viewRef, ...options });
40
+
41
+ return (
42
+ <RefPopover modal={false} {...props}>
43
+ <EditorStory ref={viewRef} text={text} placeholder={''} extensions={commandMenu} />
44
+ <CommandMenu groups={groupsRef.current} currentItem={currentItem} onSelect={onSelect} />
45
+ </RefPopover>
46
+ );
47
+ };
48
+
49
+ const groups: CommandMenuGroup[] = [
50
+ coreSlashCommands,
51
+ linkSlashCommands,
52
+ {
53
+ id: 'custom',
54
+ label: 'Custom',
55
+ items: [
56
+ {
57
+ id: 'custom-1',
58
+ label: 'Log',
59
+ icon: 'ph--log--regular',
60
+ onSelect: console.log,
61
+ },
62
+ ],
63
+ },
64
+ ];
65
+
66
+ const meta: Meta<StoryProps> = {
67
+ title: 'ui/react-ui-editor/CommandMenu',
68
+ decorators: [withTheme, withLayout({ fullscreen: true })],
69
+ render: (args) => <DefaultStory {...args} />,
70
+ parameters: {
71
+ layout: 'fullscreen',
72
+ },
73
+ };
74
+
75
+ export default meta;
76
+
77
+ type Story = StoryObj<StoryProps>;
78
+
79
+ // TODO(burdon): Not working.
80
+ export const Slash: Story = {
81
+ args: {
82
+ text: str('# Slash', '', names.join(' '), ''),
83
+ trigger: '/',
84
+ placeholder: {
85
+ content: () => {
86
+ return createElement('div', undefined, [
87
+ createElement('span', { text: 'Press' }),
88
+ createElement('span', { className: 'border border-separator rounded-sm mx-1 px-1', text: '/' }),
89
+ createElement('span', { text: 'for commands' }),
90
+ ]);
91
+ },
92
+ },
93
+ getMenu: (text) => {
94
+ return filterItems(groups, (item) =>
95
+ text ? (item.label as string).toLowerCase().includes(text.toLowerCase()) : true,
96
+ );
97
+ },
98
+ },
99
+ };
100
+
101
+ export const Link: Story = {
102
+ render: (args) => {
103
+ const { space } = useClientProvider();
104
+ const getMenu = useCallback(
105
+ async (trigger: string, query?: string): Promise<CommandMenuGroup[]> => {
106
+ if (trigger === '/') {
107
+ return filterItems(groups, (item) =>
108
+ query ? (item.label as string).toLowerCase().includes(query.toLowerCase()) : true,
109
+ );
110
+ }
111
+
112
+ if (!space) {
113
+ return [];
114
+ }
115
+
116
+ const name = query?.startsWith('@') ? query.slice(1).toLowerCase() : query?.toLowerCase() ?? '';
117
+ const result = await space?.db.query(Query.type(Testing.Contact)).run();
118
+ const items = result.objects
119
+ .filter((object) => object.name.toLowerCase().includes(name))
120
+ .map(
121
+ (object): CommandMenuItem => ({
122
+ id: object.id,
123
+ label: object.name,
124
+ icon: 'ph--user--regular',
125
+ onSelect: (view, head) => {
126
+ const link = `[${object.name}][${Obj.getDXN(object)}]`;
127
+ if (query?.startsWith('@')) {
128
+ insertAtLineStart(view, head, `!${link}\n`);
129
+ } else {
130
+ insertAtCursor(view, head, `${link} `);
131
+ }
132
+ },
133
+ }),
134
+ );
135
+ return [{ id: 'echo', items }];
136
+ },
137
+ [space],
138
+ );
139
+
140
+ return <DefaultStory {...args} getMenu={getMenu} />;
141
+ },
142
+ args: {
143
+ text: str('# Link', '', names.join(' '), ''),
144
+ trigger: ['/', '@'],
145
+ },
146
+ decorators: [
147
+ withClientProvider({
148
+ createSpace: true,
149
+ onInitialized: async (client) => {
150
+ client.addTypes([Testing.Contact]);
151
+ },
152
+ onSpaceCreated: async ({ space }) => {
153
+ const createObjects = createObjectFactory(space.db, generator);
154
+ await createObjects([{ type: Testing.Contact, count: 10 }]);
155
+ await space.db.flush({ indexes: true });
156
+ },
157
+ }),
158
+ ],
159
+ };
@@ -4,25 +4,22 @@
4
4
 
5
5
  import '@dxos-theme';
6
6
 
7
- import React, { useState, useEffect, type FC } from 'react';
7
+ import { syntaxTree } from '@codemirror/language';
8
+ import { type EditorView } from '@codemirror/view';
9
+ import React, { useState, useEffect, useMemo, useCallback } from 'react';
10
+ import { createPortal } from 'react-dom';
8
11
 
12
+ import { invariant } from '@dxos/invariant';
9
13
  import { faker } from '@dxos/random';
10
- import { IconButton, Popover } from '@dxos/react-ui';
11
- import { hoverableHidden } from '@dxos/react-ui-theme';
14
+ import { Popover } from '@dxos/react-ui';
15
+ import { Card } from '@dxos/react-ui-stack';
16
+ import { hoverableControlItem, hoverableControlItemTransition, hoverableControls } from '@dxos/react-ui-theme';
12
17
  import { withLayout, withTheme, type Meta } from '@dxos/storybook-utils';
13
18
 
14
19
  import { EditorStory } from './components';
15
- import { RefPopover, useRefPopover } from '../components';
16
- import {
17
- preview,
18
- image,
19
- type PreviewOptions,
20
- type PreviewLinkRef,
21
- type PreviewLinkTarget,
22
- type PreviewRenderProps,
23
- } from '../extensions';
20
+ import { PreviewProvider, useRefPopover } from '../components';
21
+ import { preview, image, type PreviewLinkRef, type PreviewLinkTarget, getLinkRef } from '../extensions';
24
22
  import { str } from '../testing';
25
- import { createRenderer } from '../util';
26
23
 
27
24
  const handlePreviewLookup = async ({ label, ref }: PreviewLinkRef): Promise<PreviewLinkTarget> => {
28
25
  // Random text.
@@ -36,11 +33,11 @@ const handlePreviewLookup = async ({ label, ref }: PreviewLinkRef): Promise<Prev
36
33
 
37
34
  // Async lookup.
38
35
  // TODO(burdon): Handle errors.
39
- const useRefTarget = (link: PreviewLinkRef, onLookup: PreviewOptions['onLookup']): PreviewLinkTarget | undefined => {
36
+ const useRefTarget = (link: PreviewLinkRef): PreviewLinkTarget | undefined => {
40
37
  const [target, setTarget] = useState<PreviewLinkTarget | undefined>();
41
38
  useEffect(() => {
42
- void onLookup?.(link).then((target) => setTarget(target ?? undefined));
43
- }, [link, onLookup]);
39
+ void handlePreviewLookup(link).then((target) => setTarget(target ?? undefined));
40
+ }, [link]);
44
41
 
45
42
  return target;
46
43
  };
@@ -49,10 +46,12 @@ const PreviewCard = () => {
49
46
  const { target } = useRefPopover('PreviewCard');
50
47
  return (
51
48
  <Popover.Portal>
52
- <Popover.Content classNames='popover-card-width p-2' onOpenAutoFocus={(event) => event.preventDefault()}>
49
+ <Popover.Content onOpenAutoFocus={(event) => event.preventDefault()}>
53
50
  <Popover.Viewport>
54
- <h2 className='grow truncate'>{target?.label}</h2>
55
- {target && <div className='line-clamp-3'>{target.text}</div>}
51
+ <Card.Container role='popover'>
52
+ <Card.Heading>{target?.label}</Card.Heading>
53
+ {target && <Card.Text classNames='line-clamp-3'>{target.text}</Card.Text>}
54
+ </Card.Container>
56
55
  </Popover.Viewport>
57
56
  <Popover.Arrow />
58
57
  </Popover.Content>
@@ -60,49 +59,108 @@ const PreviewCard = () => {
60
59
  );
61
60
  };
62
61
 
63
- // TODO(burdon): Replace with card.
64
- const PreviewBlock: FC<PreviewRenderProps> = ({ readonly, link, onAction, onLookup }) => {
65
- const target = useRefTarget(link, onLookup);
66
- return (
67
- <div className='group flex flex-col gap-2'>
68
- <div className='flex items-center gap-4'>
69
- <div className='grow truncate'>
70
- {/* <span className='text-xs text-subdued mie-2'>Prompt</span> */}
71
- {link.label}
72
- </div>
73
- {!readonly && (
74
- <div className='flex gap-1'>
62
+ type PreviewAction =
63
+ | {
64
+ type: 'insert';
65
+ link: PreviewLinkRef;
66
+ target: PreviewLinkTarget;
67
+ }
68
+ | {
69
+ type: 'delete';
70
+ link: PreviewLinkRef;
71
+ };
72
+
73
+ const PreviewBlock = ({ link, el, view }: { link: PreviewLinkRef; el: HTMLElement; view?: EditorView }) => {
74
+ const target = useRefTarget(link);
75
+
76
+ const handleAction = useCallback(
77
+ (action: PreviewAction) => {
78
+ invariant(view, 'View not found');
79
+ const pos = view.posAtDOM(el);
80
+ const node = syntaxTree(view.state).resolve(pos + 1).node.parent;
81
+ if (!node) {
82
+ return;
83
+ }
84
+
85
+ const link = getLinkRef(view.state, node);
86
+ if (link?.ref !== action.link.ref) {
87
+ return;
88
+ }
89
+
90
+ switch (action.type) {
91
+ // TODO(burdon): Should we dispatch to the view or mutate the document? (i.e., handle externally?)
92
+ // Insert ref text.
93
+ case 'insert': {
94
+ view.dispatch({
95
+ changes: {
96
+ from: node.from,
97
+ to: node.to,
98
+ insert: action.target.text,
99
+ },
100
+ });
101
+ break;
102
+ }
103
+ // Remove ref.
104
+ case 'delete': {
105
+ view.dispatch({
106
+ changes: {
107
+ from: node.from,
108
+ to: node.to,
109
+ },
110
+ });
111
+ break;
112
+ }
113
+ }
114
+ },
115
+ [view, el],
116
+ );
117
+
118
+ const handleDelete = useCallback(() => {
119
+ handleAction({ type: 'delete', link });
120
+ }, [handleAction, link]);
121
+
122
+ const handleInsert = useCallback(() => {
123
+ if (target) {
124
+ handleAction({ type: 'insert', link, target });
125
+ }
126
+ }, [handleAction, link, target]);
127
+
128
+ return createPortal(
129
+ <Card.Content classNames={hoverableControls}>
130
+ <div className='flex items-start'>
131
+ {!view?.state.readOnly && (
132
+ <Card.Toolbar classNames='is-min p-[--dx-cardSpacingInline]'>
75
133
  {(link.suggest && (
76
134
  <>
135
+ <Card.ToolbarIconButton label='Discard' icon={'ph--x--regular'} onClick={handleDelete} />
77
136
  {target && (
78
- <IconButton
79
- classNames='text-green-500'
137
+ <Card.ToolbarIconButton
138
+ classNames='bg-successSurface text-successSurfaceText'
80
139
  label='Apply'
81
- icon={'ph--check--regular'}
82
- onClick={() => onAction({ type: 'insert', link, target })}
140
+ icon='ph--check--regular'
141
+ onClick={handleInsert}
83
142
  />
84
143
  )}
85
- <IconButton
86
- classNames='text-red-500'
87
- label='Cancel'
88
- icon={'ph--x--regular'}
89
- onClick={() => onAction({ type: 'delete', link })}
90
- />
91
144
  </>
92
145
  )) || (
93
- <IconButton
146
+ <Card.ToolbarIconButton
94
147
  iconOnly
95
148
  label='Delete'
96
- icon={'ph--x--regular'}
97
- classNames={hoverableHidden}
98
- onClick={() => onAction({ type: 'delete', link })}
149
+ icon='ph--x--regular'
150
+ classNames={[hoverableControlItem, hoverableControlItemTransition]}
151
+ onClick={handleDelete}
99
152
  />
100
153
  )}
101
- </div>
154
+ </Card.Toolbar>
102
155
  )}
156
+ <Card.Heading classNames='grow order-first mie-0'>
157
+ {/* <span className='text-xs text-subdued mie-2'>Prompt</span> */}
158
+ {link.label}
159
+ </Card.Heading>
103
160
  </div>
104
- {target && <div className='line-clamp-3'>{target.text}</div>}
105
- </div>
161
+ {target && <Card.Text classNames='line-clamp-3 mbs-0'>{target.text}</Card.Text>}
162
+ </Card.Content>,
163
+ el,
106
164
  );
107
165
  };
108
166
 
@@ -116,34 +174,55 @@ const meta: Meta<typeof EditorStory> = {
116
174
  export default meta;
117
175
 
118
176
  export const Default = {
119
- render: () => (
120
- <RefPopover.Provider onLookup={handlePreviewLookup}>
121
- <EditorStory
122
- text={str(
123
- '# Preview',
124
- '',
125
- 'This project is part of the [DXOS][dxn:queue:data:123] SDK.',
126
- '',
127
- '![DXOS][?dxn:queue:data:123]',
128
- '',
129
- 'It consists of [ECHO][dxn:queue:data:echo], [HALO][dxn:queue:data:halo], and [MESH][dxn:queue:data:mesh].',
130
- '',
131
- '## Deep dive',
132
- '',
133
- '![ECHO][dxn:queue:data:echo]',
134
- '',
135
- '',
136
- '',
137
- )}
138
- extensions={[
139
- image(),
140
- preview({
141
- renderBlock: createRenderer(PreviewBlock),
142
- onLookup: handlePreviewLookup,
143
- }),
144
- ]}
145
- />
146
- <PreviewCard />
147
- </RefPopover.Provider>
148
- ),
177
+ render: () => {
178
+ const [view, setView] = useState<EditorView>();
179
+ const [previewBlocks, setPreviewBlocks] = useState<{ link: PreviewLinkRef; el: HTMLElement }[]>([]);
180
+
181
+ const extensions = useMemo(() => {
182
+ return [
183
+ image(),
184
+ preview({
185
+ addBlockContainer: (link, el) => {
186
+ setPreviewBlocks((prev) => [...prev, { link, el }]);
187
+ },
188
+ removeBlockContainer: (link) => {
189
+ setPreviewBlocks((prev) => prev.filter(({ link: prevLink }) => prevLink.ref !== link.ref));
190
+ },
191
+ }),
192
+ ];
193
+ }, []);
194
+
195
+ const handleViewRef = useCallback((instance?: EditorView | null) => {
196
+ setView(instance ?? undefined);
197
+ }, []);
198
+
199
+ return (
200
+ <PreviewProvider onLookup={handlePreviewLookup}>
201
+ <EditorStory
202
+ ref={handleViewRef}
203
+ text={str(
204
+ '# Preview',
205
+ '',
206
+ 'This project is part of the [DXOS][dxn:queue:data:123] SDK.',
207
+ '',
208
+ '![DXOS][?dxn:queue:data:123]',
209
+ '',
210
+ 'It consists of [ECHO][dxn:queue:data:echo], [HALO][dxn:queue:data:halo], and [MESH][dxn:queue:data:mesh].',
211
+ '',
212
+ '## Deep dive',
213
+ '',
214
+ '![ECHO][dxn:queue:data:echo]',
215
+ '',
216
+ '',
217
+ '',
218
+ )}
219
+ extensions={extensions}
220
+ />
221
+ <PreviewCard />
222
+ {previewBlocks.map(({ link, el }) => (
223
+ <PreviewBlock key={link.ref} link={link} el={el} view={view} />
224
+ ))}
225
+ </PreviewProvider>
226
+ );
227
+ },
149
228
  };
@@ -196,7 +196,7 @@ const LinkButton: FC<{ url: string }> = ({ url }) => {
196
196
 
197
197
  export const renderLinkButton = createRenderer(LinkButton);
198
198
 
199
- // Shared extensions
199
+ // Shared extensions.
200
200
  export const defaultExtensions: Extension[] = [
201
201
  decorateMarkdown({ renderLinkButton, selectionChangeDelay: 100 }),
202
202
  formattingKeymap(),
@@ -212,7 +212,7 @@ export const allExtensions: Extension[] = [
212
212
  folding(),
213
213
  ];
214
214
 
215
- // Long text for scrolling stories
215
+ // Long text for scrolling stories.
216
216
  export const longText = faker.helpers
217
217
  .multiple(() => faker.lorem.paragraph({ min: 8, max: 16 }), { count: 20 })
218
218
  .join('\n\n');