@dxos/react-ui-editor 0.3.9-main.4e39f3a → 0.3.9-main.501fe30

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 (67) hide show
  1. package/dist/lib/browser/index.mjs +284 -48
  2. package/dist/lib/browser/index.mjs.map +4 -4
  3. package/dist/lib/browser/meta.json +1 -1
  4. package/dist/types/src/automerge/automerge-plugin/PatchSemaphore.d.ts +2 -2
  5. package/dist/types/src/automerge/automerge-plugin/PatchSemaphore.d.ts.map +1 -1
  6. package/dist/types/src/automerge/automerge-plugin/codeMirrorToAm.d.ts +2 -2
  7. package/dist/types/src/automerge/automerge-plugin/codeMirrorToAm.d.ts.map +1 -1
  8. package/dist/types/src/automerge/automerge-plugin/handle.d.ts +9 -0
  9. package/dist/types/src/automerge/automerge-plugin/handle.d.ts.map +1 -0
  10. package/dist/types/src/automerge/automerge-plugin/index.d.ts +2 -2
  11. package/dist/types/src/automerge/automerge-plugin/index.d.ts.map +1 -1
  12. package/dist/types/src/automerge/automerge-plugin/plugin.d.ts +6 -5
  13. package/dist/types/src/automerge/automerge-plugin/plugin.d.ts.map +1 -1
  14. package/dist/types/src/automerge/automerge.stories.d.ts +1 -0
  15. package/dist/types/src/automerge/automerge.stories.d.ts.map +1 -1
  16. package/dist/types/src/components/Editor/Editor.d.ts +0 -1
  17. package/dist/types/src/components/Editor/Editor.d.ts.map +1 -1
  18. package/dist/types/src/components/Editor/Editor.stories.d.ts +3 -8
  19. package/dist/types/src/components/Editor/Editor.stories.d.ts.map +1 -1
  20. package/dist/types/src/components/Markdown/Markdown.d.ts +4 -2
  21. package/dist/types/src/components/Markdown/Markdown.d.ts.map +1 -1
  22. package/dist/types/src/components/Markdown/Markdown.stories.d.ts +1 -1
  23. package/dist/types/src/components/Markdown/Markdown.stories.d.ts.map +1 -1
  24. package/dist/types/src/components/Markdown/markdownTheme.d.ts +10 -17
  25. package/dist/types/src/components/Markdown/markdownTheme.d.ts.map +1 -1
  26. package/dist/types/src/components/RichText/RichText.stories.d.ts +2 -3
  27. package/dist/types/src/components/RichText/RichText.stories.d.ts.map +1 -1
  28. package/dist/types/src/components/TextEditor/TextEditor.d.ts.map +1 -1
  29. package/dist/types/src/components/TextEditor/TextEditor.stories.d.ts +1 -0
  30. package/dist/types/src/components/TextEditor/TextEditor.stories.d.ts.map +1 -1
  31. package/dist/types/src/components/TextEditor/extensions/hyperlinkDecoration.d.ts +11 -0
  32. package/dist/types/src/components/TextEditor/extensions/hyperlinkDecoration.d.ts.map +1 -0
  33. package/dist/types/src/components/TextEditor/extensions/hyperlinkTooltip.d.ts +8 -0
  34. package/dist/types/src/components/TextEditor/extensions/hyperlinkTooltip.d.ts.map +1 -0
  35. package/dist/types/src/components/TextEditor/extensions/hyperlinkWidget.d.ts +10 -0
  36. package/dist/types/src/components/TextEditor/extensions/hyperlinkWidget.d.ts.map +1 -0
  37. package/dist/types/src/components/TextEditor/extensions/index.d.ts +4 -0
  38. package/dist/types/src/components/TextEditor/extensions/index.d.ts.map +1 -0
  39. package/dist/types/src/components/TextEditor/index.d.ts +1 -0
  40. package/dist/types/src/components/TextEditor/index.d.ts.map +1 -1
  41. package/dist/types/src/components/TextEditor/theme.d.ts +1 -0
  42. package/dist/types/src/components/TextEditor/theme.d.ts.map +1 -1
  43. package/dist/types/stories/html/code-mirror.stories.d.ts.map +1 -1
  44. package/package.json +28 -26
  45. package/src/automerge/automerge-plugin/PatchSemaphore.ts +5 -5
  46. package/src/automerge/automerge-plugin/codeMirrorToAm.ts +10 -7
  47. package/src/automerge/automerge-plugin/handle.ts +14 -0
  48. package/src/automerge/automerge-plugin/index.ts +2 -2
  49. package/src/automerge/automerge-plugin/plugin.ts +39 -10
  50. package/src/automerge/automerge.stories.tsx +31 -46
  51. package/src/components/Editor/Editor.stories.tsx +5 -5
  52. package/src/components/Editor/Editor.tsx +4 -1
  53. package/src/components/Markdown/Markdown.stories.tsx +38 -25
  54. package/src/components/Markdown/Markdown.tsx +53 -24
  55. package/src/components/Markdown/markdownTheme.ts +32 -17
  56. package/src/components/RichText/RichText.stories.tsx +37 -33
  57. package/src/components/TextEditor/TextEditor.stories.tsx +47 -6
  58. package/src/components/TextEditor/TextEditor.tsx +1 -0
  59. package/src/components/TextEditor/extensions/hyperlinkDecoration.ts +99 -0
  60. package/src/components/TextEditor/extensions/hyperlinkTooltip.tsx +61 -0
  61. package/src/components/TextEditor/extensions/hyperlinkWidget.tsx +119 -0
  62. package/src/components/TextEditor/extensions/index.ts +7 -0
  63. package/src/components/TextEditor/index.ts +2 -0
  64. package/src/components/TextEditor/theme.ts +10 -4
  65. package/dist/types/src/automerge/demo.d.ts +0 -25
  66. package/dist/types/src/automerge/demo.d.ts.map +0 -1
  67. package/src/automerge/demo.ts +0 -106
@@ -2,17 +2,20 @@
2
2
  // Copyright 2023 DXOS.org
3
3
  //
4
4
 
5
- import { basicSetup } from '@codemirror/basic-setup';
5
+ import '@preact/signals-react'; // Register react integration
6
+ import { BroadcastChannelNetworkAdapter } from '@automerge/automerge-repo-network-broadcastchannel';
6
7
  import { EditorView } from '@codemirror/view';
8
+ import { basicSetup } from 'codemirror';
9
+ import get from 'lodash.get';
7
10
  import React, { useEffect, useRef, useState } from 'react';
8
11
 
9
- import { type Prop, next as automerge } from '@dxos/automerge/automerge';
12
+ import { type Prop } from '@dxos/automerge/automerge';
13
+ import { type DocHandle, Repo } from '@dxos/automerge/automerge-repo';
10
14
 
11
- import { plugin as amgPlugin, reconcile } from './automerge-plugin';
12
- import { Peer } from './demo';
15
+ import { type IDocHandle, automergePlugin } from './automerge-plugin';
13
16
 
14
17
  type EditorProps = {
15
- handle: Peer;
18
+ handle: IDocHandle;
16
19
  path: Prop[];
17
20
  };
18
21
 
@@ -21,27 +24,13 @@ const Editor = ({ handle, path }: EditorProps) => {
21
24
  const editorRoot = useRef<EditorView>();
22
25
 
23
26
  useEffect(() => {
24
- const doc = handle.doc;
25
- const source = doc.text; // this should use path
26
- const plugin = amgPlugin(doc, path);
27
27
  const view = (editorRoot.current = new EditorView({
28
- doc: source,
29
- extensions: [basicSetup, plugin],
30
- dispatch: (transaction) => {
31
- view.update([transaction]);
32
- reconcile(handle, view);
33
- },
28
+ doc: get(handle.docSync()!, path),
29
+ extensions: [basicSetup, automergePlugin(handle, path)],
34
30
  parent: containerRef.current as any,
35
31
  }));
36
32
 
37
- const handleChange = () => {
38
- reconcile(handle, view);
39
- };
40
-
41
- handle.changeEvent.on(handleChange);
42
-
43
33
  return () => {
44
- handle.changeEvent.off(handleChange);
45
34
  view.destroy();
46
35
  };
47
36
  }, []);
@@ -50,31 +39,29 @@ const Editor = ({ handle, path }: EditorProps) => {
50
39
  };
51
40
 
52
41
  const Story = () => {
53
- const [object1, setObject1] = useState<Peer | null>(null);
54
- const [object2, setObject2] = useState<Peer | null>(null);
55
- const [stats1, setStats1] = useState<any>({});
56
- const [stats2, setStats2] = useState<any>({});
42
+ const [object1, setObject1] = useState<DocHandle<any> | null>(null);
43
+ const [object2, setObject2] = useState<DocHandle<any> | null>(null);
57
44
 
58
45
  useEffect(() => {
59
- const object1 = new Peer();
60
- object1.doc = automerge.from({ text: 'Hello world!' });
61
-
62
- const object2 = new Peer();
63
- object2.doc = automerge.init();
64
-
65
- const r1 = object1.replicate();
66
- const r2 = object2.replicate();
67
-
68
- void r1.readable.pipeTo(r2.writable);
69
- void r2.readable.pipeTo(r1.writable);
70
-
71
- setObject1(object1);
72
- setObject2(object2);
73
-
74
- setInterval(() => {
75
- setStats1({ ...object1.stats });
76
- setStats2({ ...object2.stats });
77
- }, 500);
46
+ queueMicrotask(async () => {
47
+ const repo1 = new Repo({
48
+ network: [new BroadcastChannelNetworkAdapter()],
49
+ });
50
+ const repo2 = new Repo({
51
+ network: [new BroadcastChannelNetworkAdapter()],
52
+ });
53
+
54
+ const object1 = repo1.create();
55
+ object1.change((doc: any) => {
56
+ doc.text = 'Hello world!';
57
+ });
58
+
59
+ const object2 = repo2.find(object1.url);
60
+ await object2.whenReady();
61
+
62
+ setObject1(object1);
63
+ setObject2(object2);
64
+ });
78
65
  }, []);
79
66
 
80
67
  if (!object1 || !object2) {
@@ -85,11 +72,9 @@ const Story = () => {
85
72
  <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', width: '100vw' }}>
86
73
  <div>
87
74
  <Editor handle={object1} path={['text']} />
88
- <div style={{ whiteSpace: 'pre' }}>{JSON.stringify(stats1, null, 2)}</div>
89
75
  </div>
90
76
  <div>
91
77
  <Editor handle={object2} path={['text']} />
92
- <div style={{ whiteSpace: 'pre' }}>{JSON.stringify(stats2, null, 2)}</div>
93
78
  </div>
94
79
  </div>
95
80
  );
@@ -9,14 +9,16 @@ import React, { useState } from 'react';
9
9
  import { type PublicKey } from '@dxos/react-client';
10
10
  import { TextObject, TextKind, useQuery, useSpace } from '@dxos/react-client/echo';
11
11
  import { useIdentity } from '@dxos/react-client/halo';
12
- import { ClientDecorator, setupPeersInSpace, textGenerator, useDataGenerator } from '@dxos/react-client/testing';
12
+ import { setupPeersInSpace, textGenerator, useDataGenerator, ClientRepeater } from '@dxos/react-client/testing';
13
13
  import { useId } from '@dxos/react-ui';
14
+ import { withTheme } from '@dxos/storybook-utils';
14
15
 
15
16
  import { Editor, type EditorProps } from './Editor';
16
17
  import { EditorDocument, types as schema } from '../../testing';
17
18
 
18
19
  export default {
19
20
  component: Editor,
21
+ decorators: [withTheme],
20
22
  };
21
23
 
22
24
  const Story = ({ spaceKey, ...params }: Pick<EditorProps, 'slots'> & { spaceKey: PublicKey }) => {
@@ -54,8 +56,7 @@ const { spaceKey: markdownSpaceKey, clients: markdownClients } = await setupPeer
54
56
  });
55
57
 
56
58
  export const Markdown = {
57
- render: (args: { id: number }) => <Story {...args} spaceKey={markdownSpaceKey} />,
58
- decorators: [ClientDecorator({ clients: markdownClients })],
59
+ render: () => <ClientRepeater Component={Story} clients={markdownClients} args={{ spaceKey: markdownSpaceKey }} />,
59
60
  };
60
61
 
61
62
  const { spaceKey: richSpaceKey, clients: richClients } = await setupPeersInSpace({
@@ -68,7 +69,7 @@ const { spaceKey: richSpaceKey, clients: richClients } = await setupPeersInSpace
68
69
  });
69
70
 
70
71
  export const Rich = {
71
- render: (args: { id: number }) => <Story {...args} spaceKey={richSpaceKey} />,
72
+ render: () => <ClientRepeater Component={Story} clients={richClients} args={{ spaceKey: richSpaceKey }} />,
72
73
  args: {
73
74
  slots: {
74
75
  editor: {
@@ -76,5 +77,4 @@ export const Rich = {
76
77
  },
77
78
  },
78
79
  },
79
- decorators: [ClientDecorator({ clients: richClients })],
80
80
  };
@@ -16,7 +16,6 @@ export type EditorProps = UseTextModelOptions & {
16
16
 
17
17
  /**
18
18
  * Memoized editor which depends on DXOS platform.
19
- *
20
19
  * Determines which editor to render based on the kind of text.
21
20
  */
22
21
  // NOTE: Without `memo`, if parent component uses `observer` the editor re-renders excessively.
@@ -24,6 +23,10 @@ export type EditorProps = UseTextModelOptions & {
24
23
  export const Editor = memo(
25
24
  forwardRef<TipTapEditor | MarkdownEditorRef, EditorProps>(({ slots, ...params }, forwardedRef) => {
26
25
  const model = useTextModel(params);
26
+ if (!model) {
27
+ return null;
28
+ }
29
+
27
30
  if (model?.content instanceof YXmlFragment) {
28
31
  return <RichTextEditor ref={forwardedRef as Ref<TipTapEditor>} model={model} slots={slots} />;
29
32
  } else {
@@ -8,50 +8,63 @@ import React, { useState } from 'react';
8
8
 
9
9
  import { PublicKey } from '@dxos/react-client';
10
10
  import { TextKind } from '@dxos/react-client/echo';
11
- import { ClientDecorator, textGenerator, useDataGenerator } from '@dxos/react-client/testing';
11
+ import { ClientRepeater, textGenerator, useDataGenerator } from '@dxos/react-client/testing';
12
12
  import { useId } from '@dxos/react-ui';
13
+ import { withTheme } from '@dxos/storybook-utils';
13
14
 
14
15
  import { MarkdownEditor } from './Markdown';
15
16
  import { Replicator, useYjsModel } from '../../testing';
16
17
 
17
18
  export default {
18
19
  component: MarkdownEditor,
20
+ decorators: [withTheme],
19
21
  };
20
22
 
23
+ const content = [
24
+ '',
25
+ '',
26
+ 'This is all about [DXOS](https://dxos.org); read more [here](https://docs.dxos.org/guide/getting-started.html).',
27
+ '',
28
+ 'This is a link: https://www.dxos.org',
29
+ '',
30
+ '',
31
+ ].join('\n');
32
+
21
33
  export const Default = {
22
34
  args: {
23
35
  model: {
24
36
  id: 'editor',
25
- content: 'Hello, Storybook!',
37
+ content,
26
38
  },
27
39
  },
28
40
  };
29
41
 
30
42
  const replicator = new Replicator(TextKind.PLAIN);
31
43
 
44
+ const Story = () => {
45
+ const [generate, setGenerate] = useState(false);
46
+ const generateId = useId('generate');
47
+
48
+ const [id] = useState(PublicKey.random().toHex());
49
+ const model = useYjsModel({ id, replicator });
50
+
51
+ useDataGenerator({
52
+ generator: generate ? textGenerator : undefined,
53
+ options: { text: typeof model?.content !== 'string' ? model?.content : undefined },
54
+ });
55
+
56
+ return (
57
+ <main className='flex-1 min-w-0 p-4'>
58
+ <div id={generateId} className='flex'>
59
+ <input type='checkbox' onChange={(event) => setGenerate(event.target.checked)} />
60
+ Generate Data
61
+ </div>
62
+ <MarkdownEditor model={model} />
63
+ </main>
64
+ );
65
+ };
66
+
32
67
  export const WithYjs = {
33
- render: () => {
34
- const [generate, setGenerate] = useState(false);
35
- const generateId = useId('generate');
36
-
37
- const [id] = useState(PublicKey.random().toHex());
38
- const model = useYjsModel({ id, replicator });
39
-
40
- useDataGenerator({
41
- generator: generate ? textGenerator : undefined,
42
- options: { text: typeof model?.content !== 'string' ? model?.content : undefined },
43
- });
44
-
45
- return (
46
- <main className='flex-1 min-w-0 p-4'>
47
- <div id={generateId} className='flex'>
48
- <input type='checkbox' onChange={(event) => setGenerate(event.target.checked)} />
49
- Generate Data
50
- </div>
51
- <MarkdownEditor model={model} />
52
- </main>
53
- );
54
- },
55
68
  // TODO(wittjosiah): Decorator for doing this without clients being initialized?
56
- decorators: [ClientDecorator({ count: 2 })],
69
+ render: () => <ClientRepeater count={2} Component={Story} />,
57
70
  };
@@ -15,7 +15,7 @@ import {
15
15
  import { languages } from '@codemirror/language-data';
16
16
  import { lintKeymap } from '@codemirror/lint';
17
17
  import { searchKeymap, highlightSelectionMatches } from '@codemirror/search';
18
- import { EditorState, StateField, type Text } from '@codemirror/state';
18
+ import { EditorState, type Extension, StateField, type Text } from '@codemirror/state';
19
19
  import { oneDarkHighlightStyle } from '@codemirror/theme-one-dark';
20
20
  import {
21
21
  keymap,
@@ -30,6 +30,7 @@ import {
30
30
  EditorView,
31
31
  } from '@codemirror/view';
32
32
  import { useFocusableGroup } from '@fluentui/react-tabster';
33
+ // import { GFM } from '@lezer/markdown';
33
34
  import { vim } from '@replit/codemirror-vim';
34
35
  import React, {
35
36
  type KeyboardEvent,
@@ -48,16 +49,18 @@ import { getColorForValue } from '@dxos/react-ui-theme';
48
49
  import { YText } from '@dxos/text-model';
49
50
 
50
51
  import { markdownTagsExtension } from './markdownTags';
51
- import { markdownDarkHighlighting, markdownTheme } from './markdownTheme';
52
+ import { markdownHighlightStyle, markdownTheme } from './markdownTheme';
52
53
  import { type EditorModel, type EditorSlots } from '../../model';
53
54
 
54
55
  export const EditorModes = ['default', 'vim'] as const;
55
56
  export type EditorMode = (typeof EditorModes)[number];
56
57
 
57
58
  export type MarkdownEditorProps = {
58
- model?: EditorModel;
59
+ model: EditorModel;
59
60
  slots?: EditorSlots;
60
61
  editorMode?: EditorMode;
62
+ showWidgets?: boolean;
63
+ extensions?: Extension[];
61
64
  onChange?: (content: string | Text) => void;
62
65
  };
63
66
 
@@ -68,7 +71,7 @@ export type MarkdownEditorRef = {
68
71
  };
69
72
 
70
73
  export const MarkdownEditor = forwardRef<MarkdownEditorRef, MarkdownEditorProps>(
71
- ({ model, slots = {}, editorMode, onChange }, forwardedRef) => {
74
+ ({ model, slots = {}, editorMode, extensions = [], onChange }, forwardedRef) => {
72
75
  const { id, content, provider, peer } = model ?? {};
73
76
  const { themeMode } = useThemeContext();
74
77
  const tabsterDOMAttribute = useFocusableGroup({ tabBehavior: 'limited' });
@@ -115,51 +118,77 @@ export const MarkdownEditor = forwardRef<MarkdownEditorRef, MarkdownEditorProps>
115
118
  const state = EditorState.create({
116
119
  doc: content?.toString(),
117
120
  extensions: [
118
- // Based on https://github.com/codemirror/dev/issues/44#issuecomment-789093799.
121
+ // Based on https://github.com/codemirror/dev/issues/44#issuecomment-789093799
119
122
  listenChangesExtension,
120
123
 
121
- ...(editorMode === 'vim' ? [vim()] : []),
122
-
124
+ // TODO(burdon): Create custom bundle to make this class more reusable and retire simpler TextEditor component.
123
125
  // All of https://github.com/codemirror/basic-setup minus line numbers and fold gutter.
124
- highlightActiveLineGutter(),
125
- highlightSpecialChars(),
126
- history(),
127
- drawSelection(),
128
- dropCursor(),
129
- EditorState.allowMultipleSelections.of(true),
130
- indentOnInput(),
131
- syntaxHighlighting(defaultHighlightStyle, { fallback: true }),
126
+ // https://codemirror.net/docs/ref/#codemirror.basicSetup
127
+ autocompletion(),
132
128
  bracketMatching(),
133
129
  closeBrackets(),
134
- autocompletion(),
135
- rectangularSelection(),
136
130
  crosshairCursor(),
131
+ dropCursor(),
132
+ drawSelection(),
137
133
  highlightActiveLine(),
134
+ highlightActiveLineGutter(),
138
135
  highlightSelectionMatches(),
136
+ highlightSpecialChars(),
137
+ history(),
138
+ indentOnInput(),
139
139
  placeholder(slots.editor?.placeholder ?? ''), // TODO(burdon): Needs consistent styling.
140
+ rectangularSelection(),
141
+ syntaxHighlighting(defaultHighlightStyle, { fallback: true }),
142
+ EditorState.allowMultipleSelections.of(true),
143
+ EditorView.lineWrapping,
144
+
140
145
  keymap.of([
141
146
  ...closeBracketsKeymap,
147
+ ...completionKeymap,
142
148
  ...defaultKeymap,
143
- ...searchKeymap,
144
- ...historyKeymap,
145
149
  ...foldKeymap,
146
- ...completionKeymap,
150
+ ...historyKeymap,
147
151
  ...lintKeymap,
152
+ ...searchKeymap,
148
153
  indentWithTab,
149
154
  ]),
150
- EditorView.lineWrapping,
151
155
 
152
- // Themes.
153
- markdown({ base: markdownLanguage, codeLanguages: languages, extensions: [markdownTagsExtension] }),
156
+ // Main extension.
157
+ // https://github.com/codemirror/lang-markdown
158
+ markdown({
159
+ base: markdownLanguage,
160
+ codeLanguages: languages,
161
+ extensions: [
162
+ // TODO(burdon): This seems to upgrade the parser.
163
+ // GitHub flavored markdown bundle: Table, TaskList, Strikethrough, and Autolink.
164
+ // https://github.com/lezer-parser/markdown?tab=readme-ov-file#github-flavored-markdown
165
+ // https://github.github.com/gfm
166
+ // GFM,
167
+
168
+ // Custom styling.
169
+ markdownTagsExtension,
170
+ ],
171
+ }),
172
+
173
+ // Theme.
154
174
  EditorView.theme({ ...markdownTheme, ...slots.editor?.markdownTheme }),
155
175
  ...(themeMode === 'dark'
156
176
  ? [syntaxHighlighting(oneDarkHighlightStyle)]
157
177
  : [syntaxHighlighting(defaultHighlightStyle)]),
178
+
158
179
  // TODO(thure): All but one rule here apply to both themes; rename or refactor.
159
- syntaxHighlighting(markdownDarkHighlighting),
180
+ syntaxHighlighting(markdownHighlightStyle),
181
+
182
+ // Settings.
183
+ ...(editorMode === 'vim' ? [vim()] : []),
160
184
 
185
+ // TODO(burdon): Move to extensions.
161
186
  // Replication and awareness (incl. remote selection).
187
+ // https://codemirror.net/docs/ref/#collab
162
188
  ...(content instanceof YText ? [yCollab(content, provider?.awareness)] : []),
189
+
190
+ // Custom.
191
+ ...extensions,
163
192
  ],
164
193
  });
165
194
 
@@ -32,6 +32,10 @@ export const cursor = '#ffffff';
32
32
 
33
33
  const monospace = get(tokens, 'fontFamily.mono', ['monospace']).join(',');
34
34
 
35
+ // TODO(burdon): Subtle difference in layout for documents and stacks.
36
+ // TODO(burdon): On first character, height of editor changes by a few pixels.
37
+ // TODO(burdon): Is the '&' prefix required? If so, document.
38
+
35
39
  export const markdownTheme = {
36
40
  // TODO(thure): consider whether these commented-out rules from one-dark-theme should be integrated.
37
41
  // '&': {
@@ -71,24 +75,28 @@ export const markdownTheme = {
71
75
  // color: '#ddd'
72
76
  // },
73
77
 
74
- '.dark & .cm-tooltip': {
78
+ '.cm-tooltip': {
79
+ backgroundColor: 'transparent',
75
80
  border: 'none',
76
- backgroundColor: tooltipBackground,
77
- },
78
- '.dark & .cm-tooltip .cm-tooltip-arrow:before': {
79
- borderTopColor: 'transparent',
80
- borderBottomColor: 'transparent',
81
- },
82
- '.dark & .cm-tooltip .cm-tooltip-arrow:after': {
83
- borderTopColor: tooltipBackground,
84
- borderBottomColor: tooltipBackground,
85
- },
86
- '.dark & .cm-tooltip-autocomplete': {
87
- '& > ul > li[aria-selected]': {
88
- backgroundColor: highlightBackground,
89
- color: ivory,
90
- },
91
81
  },
82
+ // '.dark & .cm-tooltip': {
83
+ // border: 'none',
84
+ // backgroundColor: tooltipBackground,
85
+ // },
86
+ // '.dark & .cm-tooltip .cm-tooltip-arrow:before': {
87
+ // borderTopColor: 'transparent',
88
+ // borderBottomColor: 'transparent',
89
+ // },
90
+ // '.dark & .cm-tooltip .cm-tooltip-arrow:after': {
91
+ // borderTopColor: tooltipBackground,
92
+ // borderBottomColor: tooltipBackground,
93
+ // },
94
+ // '.dark & .cm-tooltip-autocomplete': {
95
+ // '& > ul > li[aria-selected]': {
96
+ // backgroundColor: highlightBackground,
97
+ // color: ivory,
98
+ // },
99
+ // },
92
100
  '&.cm-focused': {
93
101
  outline: 'none',
94
102
  },
@@ -160,6 +168,13 @@ export const markdownTheme = {
160
168
  '& .cm-yLineSelection': {
161
169
  margin: '0',
162
170
  },
171
+ '.cm-link': {
172
+ color: 'rgb(20 89 208)',
173
+ textDecorationLine: 'underline',
174
+ textDecorationThickness: '1px',
175
+ textUnderlineOffset: '2px',
176
+ borderRadius: '.125rem',
177
+ },
163
178
  ...Object.keys(get(tokens, 'extend.fontSize', {})).reduce((acc: Record<string, any>, fontSize) => {
164
179
  const height = get(tokens, ['extend', 'fontSize', fontSize, 1, 'lineHeight']);
165
180
  // TODO(thure): This appears to be the best or only way to set selection caret heights, but it's far more verbose than it needs to be.
@@ -170,7 +185,7 @@ export const markdownTheme = {
170
185
  }, {}),
171
186
  };
172
187
 
173
- export const markdownDarkHighlighting = HighlightStyle.define(
188
+ export const markdownHighlightStyle = HighlightStyle.define(
174
189
  [
175
190
  {
176
191
  tag: [
@@ -8,15 +8,17 @@ import React, { useState } from 'react';
8
8
 
9
9
  import { PublicKey } from '@dxos/react-client';
10
10
  import { TextKind } from '@dxos/react-client/echo';
11
- import { ClientDecorator, textGenerator, useDataGenerator } from '@dxos/react-client/testing';
11
+ import { ClientRepeater, textGenerator, useDataGenerator } from '@dxos/react-client/testing';
12
12
  import { useId } from '@dxos/react-ui';
13
13
  import { mx } from '@dxos/react-ui-theme';
14
+ import { withTheme } from '@dxos/storybook-utils';
14
15
 
15
16
  import { RichTextEditor, type RichTextEditorProps } from './RichText';
16
17
  import { Replicator, useYjsModel } from '../../testing';
17
18
 
18
19
  export default {
19
20
  component: RichTextEditor,
21
+ decorators: [withTheme],
20
22
  };
21
23
 
22
24
  export const Default = {
@@ -29,40 +31,42 @@ export const Default = {
29
31
  };
30
32
 
31
33
  const replicator = new Replicator(TextKind.RICH);
32
- export const WithYjs = {
33
- render: (args: Omit<RichTextEditorProps, 'model'>) => {
34
- const [generate, setGenerate] = useState(false);
35
- const generateId = useId('generate');
36
34
 
37
- const [id] = useState(PublicKey.random().toHex());
38
- const model = useYjsModel({ id, replicator });
35
+ const Story = (args: Omit<RichTextEditorProps, 'model'>) => {
36
+ const [generate, setGenerate] = useState(false);
37
+ const generateId = useId('generate');
39
38
 
40
- useDataGenerator({
41
- generator: generate ? textGenerator : undefined,
42
- options: { text: typeof model?.content !== 'string' ? model?.content : undefined },
43
- });
39
+ const [id] = useState(PublicKey.random().toHex());
40
+ const model = useYjsModel({ id, replicator });
44
41
 
45
- return (
46
- <main className='flex-1 min-w-0 p-4'>
47
- <div id={generateId} className='flex'>
48
- <input type='checkbox' onChange={(event) => setGenerate(event.target.checked)} />
49
- Generate Data
50
- </div>
51
- <RichTextEditor
52
- {...args}
53
- model={model}
54
- slots={{
55
- editor: {
56
- className: mx(
57
- 'z-0 rounded bg-white text-neutral-900 w-full p-4 dark:bg-neutral-850 dark:text-white min-bs-[12em]',
58
- args.slots?.editor?.className,
59
- ),
60
- },
61
- }}
62
- />
63
- </main>
64
- );
65
- },
42
+ useDataGenerator({
43
+ generator: generate ? textGenerator : undefined,
44
+ options: { text: typeof model?.content !== 'string' ? model?.content : undefined },
45
+ });
46
+
47
+ return (
48
+ <main className='flex-1 min-w-0 p-4'>
49
+ <div id={generateId} className='flex'>
50
+ <input type='checkbox' onChange={(event) => setGenerate(event.target.checked)} />
51
+ Generate Data
52
+ </div>
53
+ <RichTextEditor
54
+ {...args}
55
+ model={model}
56
+ slots={{
57
+ editor: {
58
+ className: mx(
59
+ 'z-0 rounded bg-white text-neutral-900 w-full p-4 dark:bg-neutral-850 dark:text-white min-bs-[12em]',
60
+ args.slots?.editor?.className,
61
+ ),
62
+ },
63
+ }}
64
+ />
65
+ </main>
66
+ );
67
+ };
68
+
69
+ export const WithYjs = {
66
70
  // TODO(wittjosiah): Decorator for doing this without clients being initialized?
67
- decorators: [ClientDecorator({ count: 2 })],
71
+ render: () => <ClientRepeater count={2} Component={Story} />,
68
72
  };
@@ -3,28 +3,69 @@
3
3
  //
4
4
 
5
5
  import '@dxosTheme';
6
- import React, { useState } from 'react';
6
+ import { markdown } from '@codemirror/lang-markdown';
7
+ import { ArrowCircleUp } from '@phosphor-icons/react';
8
+ import React, { StrictMode, useState } from 'react';
9
+ import { createRoot } from 'react-dom/client';
7
10
 
8
11
  import { TextObject } from '@dxos/echo-schema';
9
- import { fixedInsetFlexLayout, groupSurface, inputSurface, mx } from '@dxos/react-ui-theme';
12
+ import { fixedInsetFlexLayout, getSize, groupSurface, inputSurface, mx } from '@dxos/react-ui-theme';
13
+ import { withTheme } from '@dxos/storybook-utils';
10
14
 
11
15
  import { TextEditor } from './TextEditor';
16
+ import { createHyperlinkTooltip, hyperlinkDecoration } from './extensions';
12
17
  import { useTextModel } from '../../model';
13
18
 
19
+ const text = [
20
+ '',
21
+ '',
22
+ 'This is all about [DXOS](https://dxos.org); read more [here](https://docs.dxos.org/guide/getting-started.html).',
23
+ '',
24
+ '',
25
+ ].join('\n');
26
+
27
+ const hyperLinkTooltip = () =>
28
+ createHyperlinkTooltip((el, url) => {
29
+ const web = new URL(url);
30
+ createRoot(el).render(
31
+ <StrictMode>
32
+ <div className='flex gap-1 items-center'>
33
+ <ArrowCircleUp className={mx(getSize(6), 'text-blue-500')} />
34
+ <p className='pr-1'>{web.origin}</p>
35
+ </div>
36
+ </StrictMode>,
37
+ );
38
+ });
39
+
14
40
  const Story = () => {
15
- const [item] = useState({ text: new TextObject('Hello world!') });
41
+ const [item] = useState({ text: new TextObject(text) });
16
42
  const model = useTextModel({ text: item.text });
17
43
 
18
44
  return (
19
- <div className={mx(fixedInsetFlexLayout, groupSurface, 'p-4 gap-4')}>
20
- <TextEditor model={model} slots={{ root: { className: mx(inputSurface, 'px-2') } }} />
21
- <pre>{JSON.stringify(model?.content?.toString(), null, 2)}</pre>
45
+ <div className={mx(fixedInsetFlexLayout, groupSurface)}>
46
+ <div className='flex justify-center p-8'>
47
+ <div className='w-[800px]'>
48
+ <TextEditor
49
+ model={model}
50
+ extensions={[
51
+ markdown(),
52
+ hyperlinkDecoration({ link: false }),
53
+ hyperLinkTooltip(),
54
+ // EditorView.domEventHandlers({
55
+ // mousedown: (e, view) => {},
56
+ // }),
57
+ ]}
58
+ slots={{ root: { className: mx(inputSurface, 'p-2') } }}
59
+ />
60
+ </div>
61
+ </div>
22
62
  </div>
23
63
  );
24
64
  };
25
65
 
26
66
  export default {
27
67
  component: TextEditor,
68
+ decorators: [withTheme],
28
69
  render: Story,
29
70
  };
30
71
 
@@ -96,6 +96,7 @@ export const TextEditor = forwardRef<TextEditorRef, TextEditorProps>(
96
96
  : [syntaxHighlighting(defaultHighlightStyle)]),
97
97
 
98
98
  // Replication.
99
+ // TODO(burdon): Move to default extensions.
99
100
  ...(content instanceof YText ? [yCollab(content, undefined)] : []),
100
101
 
101
102
  // Custom.