@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
@@ -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
  */
@@ -10,7 +10,7 @@ import { Button, Icon, Input, DropdownMenu } from '@dxos/react-ui';
10
10
  import { mx } from '@dxos/react-ui-theme';
11
11
  import { withLayout, withTheme, type Meta } from '@dxos/storybook-utils';
12
12
 
13
- import { EditorStory } from './util';
13
+ import { EditorStory } from './components';
14
14
  import { RefDropdownMenu } from '../components';
15
15
  import { editorWidth } from '../defaults';
16
16
  import { command, type Action, floatingMenu } from '../extensions';
@@ -0,0 +1,143 @@
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
+ coreSlashCommands,
20
+ filterItems,
21
+ RefPopover,
22
+ type CommandMenuGroup,
23
+ CommandMenu,
24
+ type CommandMenuItem,
25
+ insertAtCursor,
26
+ insertAtLineStart,
27
+ linkSlashCommands,
28
+ } from '../components';
29
+ import { useCommandMenu, type UseCommandMenuOptions } from '../extensions';
30
+ import { str } from '../testing';
31
+
32
+ const generator: ValueGenerator = faker as any;
33
+
34
+ type Args = Omit<UseCommandMenuOptions, 'viewRef'> & { text: string };
35
+
36
+ const Story = ({ text, ...options }: Args) => {
37
+ const viewRef = useRef<EditorView>();
38
+ const { commandMenu, groupsRef, currentItem, onSelect, ...props } = useCommandMenu({ viewRef, ...options });
39
+
40
+ return (
41
+ <RefPopover modal={false} {...props}>
42
+ <EditorStory ref={viewRef} text={text} placeholder={''} extensions={commandMenu} />
43
+ <CommandMenu groups={groupsRef.current} currentItem={currentItem} onSelect={onSelect} />
44
+ </RefPopover>
45
+ );
46
+ };
47
+
48
+ const groups: CommandMenuGroup[] = [
49
+ coreSlashCommands,
50
+ linkSlashCommands,
51
+ {
52
+ id: 'custom',
53
+ label: 'Custom',
54
+ items: [
55
+ {
56
+ id: 'custom-1',
57
+ label: 'Log',
58
+ icon: 'ph--log--regular',
59
+ onSelect: console.log,
60
+ },
61
+ ],
62
+ },
63
+ ];
64
+
65
+ const meta: Meta<Args> = {
66
+ title: 'ui/react-ui-editor/CommandMenu',
67
+ decorators: [withTheme, withLayout({ fullscreen: true })],
68
+ render: (args) => <Story {...args} />,
69
+ parameters: { layout: 'fullscreen' },
70
+ };
71
+
72
+ export default meta;
73
+
74
+ export const Slash: StoryObj<Args> = {
75
+ args: {
76
+ trigger: '/',
77
+ getGroups: (query) =>
78
+ filterItems(groups, (item) =>
79
+ query ? (item.label as string).toLowerCase().includes(query.toLowerCase()) : true,
80
+ ),
81
+ text: str('# Slash', '', names.join(' '), ''),
82
+ },
83
+ };
84
+
85
+ export const Link: StoryObj<Args> = {
86
+ render: (args) => {
87
+ const { space } = useClientProvider();
88
+ const getGroups = useCallback(
89
+ async (trigger: string, query?: string): Promise<CommandMenuGroup[]> => {
90
+ if (trigger === '/') {
91
+ return filterItems(groups, (item) =>
92
+ query ? (item.label as string).toLowerCase().includes(query.toLowerCase()) : true,
93
+ );
94
+ }
95
+
96
+ if (!space) {
97
+ return [];
98
+ }
99
+
100
+ const name = query?.startsWith('@') ? query.slice(1).toLowerCase() : query?.toLowerCase() ?? '';
101
+ const result = await space?.db.query(Query.type(Testing.Contact)).run();
102
+ const items = result.objects
103
+ .filter((object) => object.name.toLowerCase().includes(name))
104
+ .map(
105
+ (object): CommandMenuItem => ({
106
+ id: object.id,
107
+ label: object.name,
108
+ icon: 'ph--user--regular',
109
+ onSelect: (view, head) => {
110
+ const link = `[${object.name}][${Obj.getDXN(object)}]`;
111
+ if (query?.startsWith('@')) {
112
+ insertAtLineStart(view, head, `!${link}\n`);
113
+ } else {
114
+ insertAtCursor(view, head, `${link} `);
115
+ }
116
+ },
117
+ }),
118
+ );
119
+ return [{ id: 'echo', items }];
120
+ },
121
+ [space],
122
+ );
123
+
124
+ return <Story {...args} getGroups={getGroups} />;
125
+ },
126
+ args: {
127
+ trigger: ['/', '@'],
128
+ text: str('# Link', '', names.join(' '), ''),
129
+ },
130
+ decorators: [
131
+ withClientProvider({
132
+ createSpace: true,
133
+ onInitialized: async (client) => {
134
+ client.addTypes([Testing.Contact]);
135
+ },
136
+ onSpaceCreated: async ({ space }) => {
137
+ const createObjects = createObjectFactory(space.db, generator);
138
+ await createObjects([{ type: Testing.Contact, count: 10 }]);
139
+ await space.db.flush({ indexes: true });
140
+ },
141
+ }),
142
+ ],
143
+ };
@@ -12,7 +12,7 @@ import { PublicKey } from '@dxos/keys';
12
12
  import { log } from '@dxos/log';
13
13
  import { withLayout, withTheme, type Meta } from '@dxos/storybook-utils';
14
14
 
15
- import { EditorStory, content, longText } from './util';
15
+ import { EditorStory, content, longText } from './components';
16
16
  import { annotations, comments, createExternalCommentSync } from '../extensions';
17
17
  import { str } from '../testing';
18
18
  import { type Comment } from '../types';
@@ -20,8 +20,8 @@ import { createRenderer } from '../util';
20
20
 
21
21
  const meta: Meta<typeof EditorStory> = {
22
22
  title: 'ui/react-ui-editor/Comments',
23
+ component: EditorStory,
23
24
  decorators: [withTheme, withLayout({ fullscreen: true })],
24
- render: EditorStory,
25
25
  parameters: { layout: 'fullscreen' },
26
26
  };
27
27
 
@@ -11,14 +11,14 @@ import { log } from '@dxos/log';
11
11
  import { faker } from '@dxos/random';
12
12
  import { withLayout, withTheme, type Meta } from '@dxos/storybook-utils';
13
13
 
14
- import { EditorStory, content } from './util';
14
+ import { EditorStory, content } from './components';
15
15
  import { typewriter, blast, defaultOptions, dropFile } from '../extensions';
16
16
  import { str } from '../testing';
17
17
 
18
18
  const meta: Meta<typeof EditorStory> = {
19
19
  title: 'ui/react-ui-editor/Experimental',
20
+ component: EditorStory,
20
21
  decorators: [withTheme, withLayout({ fullscreen: true })],
21
- render: EditorStory,
22
22
  parameters: { layout: 'fullscreen' },
23
23
  };
24
24
 
@@ -9,14 +9,14 @@ import React from 'react';
9
9
 
10
10
  import { withLayout, withTheme, type Meta } from '@dxos/storybook-utils';
11
11
 
12
- import { EditorStory, content, defaultExtensions, headings, renderLinkTooltip, text } from './util';
12
+ import { EditorStory, content, defaultExtensions, headings, renderLinkTooltip, text } from './components';
13
13
  import { decorateMarkdown, image, linkTooltip, table } from '../extensions';
14
14
  import { str } from '../testing';
15
15
 
16
16
  const meta: Meta<typeof EditorStory> = {
17
17
  title: 'ui/react-ui-editor/Markdown',
18
+ component: EditorStory,
18
19
  decorators: [withTheme, withLayout({ fullscreen: true })],
19
- render: EditorStory,
20
20
  parameters: { layout: 'fullscreen' },
21
21
  };
22
22
 
@@ -4,14 +4,16 @@
4
4
 
5
5
  import '@dxos-theme';
6
6
 
7
- import React from 'react';
7
+ import { type EditorView } from '@codemirror/view';
8
+ import React, { useRef } from 'react';
8
9
 
9
10
  import { DropdownMenu } from '@dxos/react-ui';
11
+ import { withAttention } from '@dxos/react-ui-attention/testing';
10
12
  import { withLayout, withTheme, type Meta } from '@dxos/storybook-utils';
11
13
 
12
- import { EditorStory } from './util';
14
+ import { EditorStory } from './components';
13
15
  import { RefDropdownMenu } from '../components';
14
- import { outliner, listItemToString, treeFacet } from '../extensions';
16
+ import { outliner, listItemToString, treeFacet, deleteItem, hashtag } from '../extensions';
15
17
  import { str } from '../testing';
16
18
 
17
19
  type StoryProps = {
@@ -19,11 +21,20 @@ type StoryProps = {
19
21
  };
20
22
 
21
23
  const DefaultStory = ({ text }: StoryProps) => {
24
+ const viewRef = useRef<EditorView>(null);
25
+
26
+ const handleDelete = () => {
27
+ if (viewRef.current) {
28
+ deleteItem(viewRef.current);
29
+ }
30
+ };
31
+
22
32
  return (
23
33
  <RefDropdownMenu.Provider>
24
34
  <EditorStory
35
+ ref={viewRef}
25
36
  text={text}
26
- extensions={[outliner()]}
37
+ extensions={[outliner(), hashtag()]}
27
38
  placeholder=''
28
39
  slots={{}}
29
40
  debug='raw+tree'
@@ -31,13 +42,14 @@ const DefaultStory = ({ text }: StoryProps) => {
31
42
  const tree = view.state.facet(treeFacet);
32
43
  const lines: string[] = [];
33
44
  tree.traverse((item) => lines.push(listItemToString(item)));
34
- return <pre className='p-1 text-xs text-green-800 dark:text-green-200 overflow-auto'>{lines.join('\n')}</pre>;
45
+ return <pre className='p-1 overflow-auto text-xs text-green-800 dark:text-green-200'>{lines.join('\n')}</pre>;
35
46
  }}
36
47
  />
48
+
37
49
  <DropdownMenu.Portal>
38
50
  <DropdownMenu.Content>
39
51
  <DropdownMenu.Viewport>
40
- <DropdownMenu.Item onClick={() => console.log('test')}>Test</DropdownMenu.Item>
52
+ <DropdownMenu.Item onClick={handleDelete}>Delete</DropdownMenu.Item>
41
53
  </DropdownMenu.Viewport>
42
54
  <DropdownMenu.Arrow />
43
55
  </DropdownMenu.Content>
@@ -48,8 +60,8 @@ const DefaultStory = ({ text }: StoryProps) => {
48
60
 
49
61
  const meta: Meta<StoryProps> = {
50
62
  title: 'ui/react-ui-editor/Outliner',
51
- decorators: [withTheme, withLayout({ fullscreen: true })],
52
63
  render: DefaultStory,
64
+ decorators: [withAttention, withTheme, withLayout({ fullscreen: true })],
53
65
  parameters: { layout: 'fullscreen' },
54
66
  };
55
67
 
@@ -7,12 +7,13 @@ import '@dxos-theme';
7
7
  import React, { useState, useEffect, type FC } from 'react';
8
8
 
9
9
  import { faker } from '@dxos/random';
10
- import { IconButton, Popover } from '@dxos/react-ui';
11
- import { hoverableHidden } from '@dxos/react-ui-theme';
10
+ import { Popover } from '@dxos/react-ui';
11
+ import { Card } from '@dxos/react-ui-stack';
12
+ import { hoverableControlItem, hoverableControlItemTransition, hoverableControls } from '@dxos/react-ui-theme';
12
13
  import { withLayout, withTheme, type Meta } from '@dxos/storybook-utils';
13
14
 
14
- import { EditorStory } from './util';
15
- import { RefPopover, useRefPopover } from '../components';
15
+ import { EditorStory } from './components';
16
+ import { PreviewProvider, useRefPopover } from '../components';
16
17
  import {
17
18
  preview,
18
19
  image,
@@ -49,10 +50,12 @@ const PreviewCard = () => {
49
50
  const { target } = useRefPopover('PreviewCard');
50
51
  return (
51
52
  <Popover.Portal>
52
- <Popover.Content classNames='popover-card-width p-2' onOpenAutoFocus={(event) => event.preventDefault()}>
53
+ <Popover.Content onOpenAutoFocus={(event) => event.preventDefault()}>
53
54
  <Popover.Viewport>
54
- <h2 className='grow truncate'>{target?.label}</h2>
55
- {target && <div className='line-clamp-3'>{target.text}</div>}
55
+ <Card.Container role='popover'>
56
+ <Card.Heading>{target?.label}</Card.Heading>
57
+ {target && <Card.Text classNames='line-clamp-3'>{target.text}</Card.Text>}
58
+ </Card.Container>
56
59
  </Popover.Viewport>
57
60
  <Popover.Arrow />
58
61
  </Popover.Content>
@@ -64,52 +67,51 @@ const PreviewCard = () => {
64
67
  const PreviewBlock: FC<PreviewRenderProps> = ({ readonly, link, onAction, onLookup }) => {
65
68
  const target = useRefTarget(link, onLookup);
66
69
  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>
70
+ <Card.Content classNames={hoverableControls}>
71
+ <div className='flex items-start'>
73
72
  {!readonly && (
74
- <div className='flex gap-1'>
73
+ <Card.Toolbar classNames='is-min p-[--dx-card-spacing-inline]'>
75
74
  {(link.suggest && (
76
75
  <>
76
+ <Card.ToolbarIconButton
77
+ label='Discard'
78
+ icon={'ph--x--regular'}
79
+ onClick={() => onAction({ type: 'delete', link })}
80
+ />
77
81
  {target && (
78
- <IconButton
79
- classNames='text-green-500'
82
+ <Card.ToolbarIconButton
83
+ classNames='bg-successSurface text-successSurfaceText'
80
84
  label='Apply'
81
- icon={'ph--check--regular'}
85
+ icon='ph--check--regular'
82
86
  onClick={() => onAction({ type: 'insert', link, target })}
83
87
  />
84
88
  )}
85
- <IconButton
86
- classNames='text-red-500'
87
- label='Cancel'
88
- icon={'ph--x--regular'}
89
- onClick={() => onAction({ type: 'delete', link })}
90
- />
91
89
  </>
92
90
  )) || (
93
- <IconButton
91
+ <Card.ToolbarIconButton
94
92
  iconOnly
95
93
  label='Delete'
96
- icon={'ph--x--regular'}
97
- classNames={hoverableHidden}
94
+ icon='ph--x--regular'
95
+ classNames={[hoverableControlItem, hoverableControlItemTransition]}
98
96
  onClick={() => onAction({ type: 'delete', link })}
99
97
  />
100
98
  )}
101
- </div>
99
+ </Card.Toolbar>
102
100
  )}
101
+ <Card.Heading classNames='grow order-first mie-0'>
102
+ {/* <span className='text-xs text-subdued mie-2'>Prompt</span> */}
103
+ {link.label}
104
+ </Card.Heading>
103
105
  </div>
104
- {target && <div className='line-clamp-3'>{target.text}</div>}
105
- </div>
106
+ {target && <Card.Text classNames='line-clamp-3 mbs-0'>{target.text}</Card.Text>}
107
+ </Card.Content>
106
108
  );
107
109
  };
108
110
 
109
111
  const meta: Meta<typeof EditorStory> = {
110
112
  title: 'ui/react-ui-editor/Preview',
113
+ component: EditorStory,
111
114
  decorators: [withTheme, withLayout({ fullscreen: true })],
112
- render: EditorStory,
113
115
  parameters: { layout: 'fullscreen' },
114
116
  };
115
117
 
@@ -117,7 +119,7 @@ export default meta;
117
119
 
118
120
  export const Default = {
119
121
  render: () => (
120
- <RefPopover.Provider onLookup={handlePreviewLookup}>
122
+ <PreviewProvider onLookup={handlePreviewLookup}>
121
123
  <EditorStory
122
124
  text={str(
123
125
  '# Preview',
@@ -144,6 +146,6 @@ export const Default = {
144
146
  ]}
145
147
  />
146
148
  <PreviewCard />
147
- </RefPopover.Provider>
149
+ </PreviewProvider>
148
150
  ),
149
151
  };
@@ -23,7 +23,7 @@ import {
23
23
  names,
24
24
  renderLinkButton,
25
25
  text,
26
- } from './util';
26
+ } from './components';
27
27
  import { editorMonospace } from '../defaults';
28
28
  import {
29
29
  InputModeExtensions,
@@ -41,9 +41,9 @@ import { str } from '../testing';
41
41
 
42
42
  const meta: Meta<typeof EditorStory> = {
43
43
  title: 'ui/react-ui-editor/TextEditor',
44
- render: EditorStory,
44
+ component: EditorStory,
45
45
  decorators: [withTheme, withLayout({ fullscreen: true })],
46
- parameters: { layout: 'fullscreen' },
46
+ parameters: { layout: 'fullscreen', controls: { disable: true } },
47
47
  };
48
48
 
49
49
  export default meta;
@@ -0,0 +1,135 @@
1
+ //
2
+ // Copyright 2023 DXOS.org
3
+ //
4
+
5
+ import { type EditorView } from '@codemirror/view';
6
+ import React, { type ReactNode, forwardRef, useEffect, useState, useImperativeHandle, useMemo } from 'react';
7
+
8
+ import { Expando } from '@dxos/echo-schema';
9
+ import { invariant } from '@dxos/invariant';
10
+ import { PublicKey } from '@dxos/keys';
11
+ import { live } from '@dxos/live-object';
12
+ import { createDocAccessor, createObject } from '@dxos/react-client/echo';
13
+ import { useForwardedRef, useThemeContext } from '@dxos/react-ui';
14
+ import { useAttentionAttributes } from '@dxos/react-ui-attention';
15
+ import { JsonFilter } from '@dxos/react-ui-syntax-highlighter';
16
+ import { mx } from '@dxos/react-ui-theme';
17
+ import { isNonNullable } from '@dxos/util';
18
+
19
+ import { editorSlots, editorGutter } from '../../defaults';
20
+ import {
21
+ type DebugNode,
22
+ type ThemeExtensionsOptions,
23
+ createDataExtensions,
24
+ createBasicExtensions,
25
+ createMarkdownExtensions,
26
+ createThemeExtensions,
27
+ debugTree,
28
+ } from '../../extensions';
29
+ import { useTextEditor, type UseTextEditorProps } from '../../hooks';
30
+
31
+ // Type definitions.
32
+ export type DebugMode = 'raw' | 'tree' | 'raw+tree';
33
+
34
+ const defaultId = 'editor-' + PublicKey.random().toHex().slice(0, 8);
35
+
36
+ export type StoryProps = Pick<UseTextEditorProps, 'scrollTo' | 'selection' | 'extensions'> &
37
+ Pick<ThemeExtensionsOptions, 'slots'> & {
38
+ id?: string;
39
+ debug?: DebugMode;
40
+ debugCustom?: (view: EditorView) => ReactNode;
41
+ text?: string;
42
+ object?: Expando;
43
+ readOnly?: boolean;
44
+ placeholder?: string;
45
+ lineNumbers?: boolean;
46
+ onReady?: (view: EditorView) => void;
47
+ };
48
+
49
+ export const EditorStory = forwardRef<EditorView | undefined, StoryProps>(
50
+ ({ debug, debugCustom, text, extensions: _extensions, ...props }, forwardedRef) => {
51
+ const attentionAttrs = useAttentionAttributes('testing');
52
+ const [tree, setTree] = useState<DebugNode>();
53
+ const [object] = useState(createObject(live(Expando, { content: text ?? '' })));
54
+ const viewRef = useForwardedRef(forwardedRef);
55
+ const view = viewRef.current;
56
+ const extensions = useMemo(
57
+ () => (debug ? [_extensions, debugTree(setTree)].filter(isNonNullable) : _extensions),
58
+ [debug, _extensions],
59
+ );
60
+
61
+ return (
62
+ <div className={mx('w-full h-full grid overflow-hidden', debug && 'grid-cols-2 lg:grid-cols-[1fr_600px]')}>
63
+ <EditorComponent ref={viewRef} object={object} text={text} extensions={extensions} {...props} />
64
+
65
+ {debug && (
66
+ <div
67
+ className='grid h-full auto-rows-fr border-l border-separator divide-y divide-separator overflow-hidden'
68
+ {...attentionAttrs}
69
+ >
70
+ {view && debugCustom?.(view)}
71
+ {(debug === 'raw' || debug === 'raw+tree') && (
72
+ <pre className='p-1 text-xs text-green-800 dark:text-green-200 overflow-auto'>
73
+ {view?.state.doc.toString()}
74
+ </pre>
75
+ )}
76
+ {(debug === 'tree' || debug === 'raw+tree') && <JsonFilter data={tree} classNames='p-1 text-xs' />}
77
+ </div>
78
+ )}
79
+ </div>
80
+ );
81
+ },
82
+ );
83
+
84
+ /**
85
+ * Default story component.
86
+ */
87
+ export const EditorComponent = forwardRef<EditorView | undefined, StoryProps>(
88
+ (
89
+ {
90
+ id = defaultId,
91
+ text,
92
+ object,
93
+ readOnly,
94
+ placeholder = 'New document.',
95
+ lineNumbers,
96
+ scrollTo,
97
+ selection,
98
+ extensions,
99
+ slots = editorSlots,
100
+ onReady,
101
+ },
102
+ forwardedRef,
103
+ ) => {
104
+ invariant(object);
105
+ const { themeMode } = useThemeContext();
106
+ const attentionAttrs = useAttentionAttributes(id);
107
+ const { parentRef, focusAttributes, view } = useTextEditor(
108
+ () => ({
109
+ id,
110
+ scrollTo,
111
+ selection,
112
+ initialValue: text,
113
+ extensions: [
114
+ createDataExtensions({ id, text: createDocAccessor(object, ['content']) }),
115
+ createBasicExtensions({ readOnly, placeholder, lineNumbers, scrollPastEnd: true }),
116
+ createMarkdownExtensions({ themeMode }),
117
+ createThemeExtensions({ themeMode, syntaxHighlighting: true, slots }),
118
+ editorGutter,
119
+ extensions || [],
120
+ ],
121
+ }),
122
+ [id, object, extensions, themeMode],
123
+ );
124
+
125
+ useImperativeHandle(forwardedRef, () => view, [view]);
126
+
127
+ useEffect(() => {
128
+ if (view) {
129
+ onReady?.(view);
130
+ }
131
+ }, [view]);
132
+
133
+ return <div ref={parentRef} className='flex overflow-hidden' {...attentionAttrs} {...focusAttributes} />;
134
+ },
135
+ );
@@ -0,0 +1,6 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ export * from './EditorStory';
6
+ export * from './util';