@dxos/react-ui-editor 0.3.9-main.501fe30 → 0.3.9-main.52d137d

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 (64) hide show
  1. package/dist/lib/browser/index.mjs +88 -183
  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/index.d.ts +2 -2
  9. package/dist/types/src/automerge/automerge-plugin/index.d.ts.map +1 -1
  10. package/dist/types/src/automerge/automerge-plugin/plugin.d.ts +5 -6
  11. package/dist/types/src/automerge/automerge-plugin/plugin.d.ts.map +1 -1
  12. package/dist/types/src/automerge/automerge.stories.d.ts +0 -1
  13. package/dist/types/src/automerge/automerge.stories.d.ts.map +1 -1
  14. package/dist/types/src/automerge/demo.d.ts +25 -0
  15. package/dist/types/src/automerge/demo.d.ts.map +1 -0
  16. package/dist/types/src/components/Editor/Editor.d.ts +1 -0
  17. package/dist/types/src/components/Editor/Editor.d.ts.map +1 -1
  18. package/dist/types/src/components/Editor/Editor.stories.d.ts +8 -2
  19. package/dist/types/src/components/Editor/Editor.stories.d.ts.map +1 -1
  20. package/dist/types/src/components/Markdown/Markdown.d.ts +1 -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 -0
  23. package/dist/types/src/components/Markdown/Markdown.stories.d.ts.map +1 -1
  24. package/dist/types/src/components/Markdown/markdownTheme.d.ts +1 -8
  25. package/dist/types/src/components/Markdown/markdownTheme.d.ts.map +1 -1
  26. package/dist/types/src/components/RichText/RichText.stories.d.ts +3 -1
  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 +15 -0
  30. package/dist/types/src/components/TextEditor/TextEditor.stories.d.ts.map +1 -1
  31. package/dist/types/src/components/TextEditor/extensions/{hyperlinkWidget.d.ts → hyperlink.d.ts} +3 -3
  32. package/dist/types/src/components/TextEditor/extensions/hyperlink.d.ts.map +1 -0
  33. package/dist/types/src/components/TextEditor/extensions/hyperlinkTooltip.d.ts.map +1 -1
  34. package/dist/types/src/components/TextEditor/extensions/index.d.ts +1 -2
  35. package/dist/types/src/components/TextEditor/extensions/index.d.ts.map +1 -1
  36. package/dist/types/src/components/TextEditor/theme.d.ts +0 -1
  37. package/dist/types/src/components/TextEditor/theme.d.ts.map +1 -1
  38. package/dist/types/stories/html/code-mirror.stories.d.ts.map +1 -1
  39. package/package.json +27 -26
  40. package/src/automerge/automerge-plugin/PatchSemaphore.ts +5 -5
  41. package/src/automerge/automerge-plugin/codeMirrorToAm.ts +7 -10
  42. package/src/automerge/automerge-plugin/index.ts +2 -2
  43. package/src/automerge/automerge-plugin/plugin.ts +10 -39
  44. package/src/automerge/automerge.stories.tsx +46 -31
  45. package/src/automerge/demo.ts +106 -0
  46. package/src/components/Editor/Editor.stories.tsx +5 -3
  47. package/src/components/Editor/Editor.tsx +1 -4
  48. package/src/components/Markdown/Markdown.stories.tsx +25 -36
  49. package/src/components/Markdown/Markdown.tsx +22 -47
  50. package/src/components/Markdown/markdownTheme.ts +1 -12
  51. package/src/components/RichText/RichText.stories.tsx +33 -35
  52. package/src/components/TextEditor/TextEditor.stories.tsx +64 -39
  53. package/src/components/TextEditor/TextEditor.tsx +0 -1
  54. package/src/components/TextEditor/extensions/{hyperlinkWidget.tsx → hyperlink.tsx} +10 -21
  55. package/src/components/TextEditor/extensions/hyperlinkTooltip.tsx +0 -1
  56. package/src/components/TextEditor/extensions/index.ts +1 -2
  57. package/src/components/TextEditor/theme.ts +4 -6
  58. package/dist/types/src/automerge/automerge-plugin/handle.d.ts +0 -9
  59. package/dist/types/src/automerge/automerge-plugin/handle.d.ts.map +0 -1
  60. package/dist/types/src/components/TextEditor/extensions/hyperlinkDecoration.d.ts +0 -11
  61. package/dist/types/src/components/TextEditor/extensions/hyperlinkDecoration.d.ts.map +0 -1
  62. package/dist/types/src/components/TextEditor/extensions/hyperlinkWidget.d.ts.map +0 -1
  63. package/src/automerge/automerge-plugin/handle.ts +0 -14
  64. package/src/components/TextEditor/extensions/hyperlinkDecoration.ts +0 -99
@@ -2,20 +2,17 @@
2
2
  // Copyright 2023 DXOS.org
3
3
  //
4
4
 
5
- import '@preact/signals-react'; // Register react integration
6
- import { BroadcastChannelNetworkAdapter } from '@automerge/automerge-repo-network-broadcastchannel';
5
+ import { basicSetup } from '@codemirror/basic-setup';
7
6
  import { EditorView } from '@codemirror/view';
8
- import { basicSetup } from 'codemirror';
9
- import get from 'lodash.get';
10
7
  import React, { useEffect, useRef, useState } from 'react';
11
8
 
12
- import { type Prop } from '@dxos/automerge/automerge';
13
- import { type DocHandle, Repo } from '@dxos/automerge/automerge-repo';
9
+ import { type Prop, next as automerge } from '@dxos/automerge/automerge';
14
10
 
15
- import { type IDocHandle, automergePlugin } from './automerge-plugin';
11
+ import { plugin as amgPlugin, reconcile } from './automerge-plugin';
12
+ import { Peer } from './demo';
16
13
 
17
14
  type EditorProps = {
18
- handle: IDocHandle;
15
+ handle: Peer;
19
16
  path: Prop[];
20
17
  };
21
18
 
@@ -24,13 +21,27 @@ const Editor = ({ handle, path }: EditorProps) => {
24
21
  const editorRoot = useRef<EditorView>();
25
22
 
26
23
  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: get(handle.docSync()!, path),
29
- extensions: [basicSetup, automergePlugin(handle, path)],
28
+ doc: source,
29
+ extensions: [basicSetup, plugin],
30
+ dispatch: (transaction) => {
31
+ view.update([transaction]);
32
+ reconcile(handle, view);
33
+ },
30
34
  parent: containerRef.current as any,
31
35
  }));
32
36
 
37
+ const handleChange = () => {
38
+ reconcile(handle, view);
39
+ };
40
+
41
+ handle.changeEvent.on(handleChange);
42
+
33
43
  return () => {
44
+ handle.changeEvent.off(handleChange);
34
45
  view.destroy();
35
46
  };
36
47
  }, []);
@@ -39,29 +50,31 @@ const Editor = ({ handle, path }: EditorProps) => {
39
50
  };
40
51
 
41
52
  const Story = () => {
42
- const [object1, setObject1] = useState<DocHandle<any> | null>(null);
43
- const [object2, setObject2] = useState<DocHandle<any> | null>(null);
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>({});
44
57
 
45
58
  useEffect(() => {
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
- });
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);
65
78
  }, []);
66
79
 
67
80
  if (!object1 || !object2) {
@@ -72,9 +85,11 @@ const Story = () => {
72
85
  <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', width: '100vw' }}>
73
86
  <div>
74
87
  <Editor handle={object1} path={['text']} />
88
+ <div style={{ whiteSpace: 'pre' }}>{JSON.stringify(stats1, null, 2)}</div>
75
89
  </div>
76
90
  <div>
77
91
  <Editor handle={object2} path={['text']} />
92
+ <div style={{ whiteSpace: 'pre' }}>{JSON.stringify(stats2, null, 2)}</div>
78
93
  </div>
79
94
  </div>
80
95
  );
@@ -0,0 +1,106 @@
1
+ //
2
+ // Copyright 2023 DXOS.org
3
+ //
4
+
5
+ import { Event } from '@dxos/async';
6
+ import {
7
+ type Doc,
8
+ next as automerge,
9
+ type ChangeOptions,
10
+ type ChangeFn,
11
+ type Heads,
12
+ type SyncState,
13
+ } from '@dxos/automerge/automerge';
14
+
15
+ export class Peer {
16
+ changeEvent = new Event();
17
+
18
+ storage = new Map<string, Uint8Array>();
19
+ stats = {
20
+ messagesSent: 0,
21
+ messagesReceived: 0,
22
+ bytesSent: 0,
23
+ bytesReceived: 0,
24
+ snapshotBytes: 0,
25
+ incrementalBytes: 0,
26
+ };
27
+
28
+ doc!: Doc<{ text: string }>;
29
+
30
+ change(options: string | ChangeOptions<any> | ChangeFn<any>, callback?: ChangeFn<any>) {
31
+ this.doc = automerge.change(this.doc, options, callback);
32
+ this._handleChange();
33
+ }
34
+
35
+ changeAt(scope: Heads, options: string | ChangeOptions<any> | ChangeFn<any>, callback?: ChangeFn<any>) {
36
+ const { newDoc, newHeads } = automerge.changeAt(this.doc, scope, options, callback);
37
+ this.doc = newDoc;
38
+ this._handleChange();
39
+ return newHeads;
40
+ }
41
+
42
+ replicate(): { readable: ReadableStream<Uint8Array>; writable: WritableStream<Uint8Array> } {
43
+ let syncState: SyncState = automerge.initSyncState();
44
+
45
+ let unsub: (() => void) | undefined;
46
+ const readable = new ReadableStream({
47
+ start: (controller) => {
48
+ unsub = this.changeEvent.on(() => {
49
+ const [newState, msg] = automerge.generateSyncMessage(this.doc, syncState);
50
+ syncState = newState;
51
+
52
+ if (msg) {
53
+ this.stats.messagesSent++;
54
+ this.stats.bytesSent += msg.byteLength;
55
+ controller.enqueue(msg);
56
+ }
57
+ });
58
+ },
59
+ pull: (controller) => {
60
+ const [newState, msg] = automerge.generateSyncMessage(this.doc, syncState);
61
+ syncState = newState;
62
+
63
+ if (msg) {
64
+ this.stats.messagesSent++;
65
+ this.stats.bytesSent += msg.byteLength;
66
+ controller.enqueue(msg);
67
+ }
68
+ },
69
+ cancel: () => {
70
+ unsub?.();
71
+ },
72
+ });
73
+
74
+ const writable = new WritableStream({
75
+ write: (chunk) => {
76
+ this.stats.messagesReceived++;
77
+ this.stats.bytesReceived += chunk.byteLength;
78
+
79
+ const [newDoc, newState] = automerge.receiveSyncMessage(this.doc, syncState, chunk);
80
+ this.doc = newDoc;
81
+ syncState = newState;
82
+
83
+ this._handleChange();
84
+ },
85
+ });
86
+
87
+ return { readable, writable };
88
+ }
89
+
90
+ private _handleChange() {
91
+ // TODO(dmaretskyi): Save since + debounce.
92
+
93
+ if (this.stats.incrementalBytes > 1024 && this.stats.incrementalBytes > this.stats.snapshotBytes) {
94
+ const snapshot = automerge.save(this.doc);
95
+ this.stats.snapshotBytes = snapshot.byteLength;
96
+ this.stats.incrementalBytes = 0;
97
+ console.log('snapshot save', snapshot.byteLength);
98
+ } else {
99
+ const chunk = automerge.saveIncremental(this.doc);
100
+ this.stats.incrementalBytes += chunk.length;
101
+ console.log('incremental save', chunk.length);
102
+ }
103
+
104
+ this.changeEvent.emit();
105
+ }
106
+ }
@@ -9,7 +9,7 @@ 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 { setupPeersInSpace, textGenerator, useDataGenerator, ClientRepeater } from '@dxos/react-client/testing';
12
+ import { ClientDecorator, setupPeersInSpace, textGenerator, useDataGenerator } from '@dxos/react-client/testing';
13
13
  import { useId } from '@dxos/react-ui';
14
14
  import { withTheme } from '@dxos/storybook-utils';
15
15
 
@@ -56,7 +56,8 @@ const { spaceKey: markdownSpaceKey, clients: markdownClients } = await setupPeer
56
56
  });
57
57
 
58
58
  export const Markdown = {
59
- render: () => <ClientRepeater Component={Story} clients={markdownClients} args={{ spaceKey: markdownSpaceKey }} />,
59
+ render: (args: { id: number }) => <Story {...args} spaceKey={markdownSpaceKey} />,
60
+ decorators: [ClientDecorator({ clients: markdownClients })],
60
61
  };
61
62
 
62
63
  const { spaceKey: richSpaceKey, clients: richClients } = await setupPeersInSpace({
@@ -69,7 +70,7 @@ const { spaceKey: richSpaceKey, clients: richClients } = await setupPeersInSpace
69
70
  });
70
71
 
71
72
  export const Rich = {
72
- render: () => <ClientRepeater Component={Story} clients={richClients} args={{ spaceKey: richSpaceKey }} />,
73
+ render: (args: { id: number }) => <Story {...args} spaceKey={richSpaceKey} />,
73
74
  args: {
74
75
  slots: {
75
76
  editor: {
@@ -77,4 +78,5 @@ export const Rich = {
77
78
  },
78
79
  },
79
80
  },
81
+ decorators: [ClientDecorator({ clients: richClients })],
80
82
  };
@@ -16,6 +16,7 @@ export type EditorProps = UseTextModelOptions & {
16
16
 
17
17
  /**
18
18
  * Memoized editor which depends on DXOS platform.
19
+ *
19
20
  * Determines which editor to render based on the kind of text.
20
21
  */
21
22
  // NOTE: Without `memo`, if parent component uses `observer` the editor re-renders excessively.
@@ -23,10 +24,6 @@ export type EditorProps = UseTextModelOptions & {
23
24
  export const Editor = memo(
24
25
  forwardRef<TipTapEditor | MarkdownEditorRef, EditorProps>(({ slots, ...params }, forwardedRef) => {
25
26
  const model = useTextModel(params);
26
- if (!model) {
27
- return null;
28
- }
29
-
30
27
  if (model?.content instanceof YXmlFragment) {
31
28
  return <RichTextEditor ref={forwardedRef as Ref<TipTapEditor>} model={model} slots={slots} />;
32
29
  } else {
@@ -8,7 +8,7 @@ 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 { ClientRepeater, textGenerator, useDataGenerator } from '@dxos/react-client/testing';
11
+ import { ClientDecorator, textGenerator, useDataGenerator } from '@dxos/react-client/testing';
12
12
  import { useId } from '@dxos/react-ui';
13
13
  import { withTheme } from '@dxos/storybook-utils';
14
14
 
@@ -20,51 +20,40 @@ export default {
20
20
  decorators: [withTheme],
21
21
  };
22
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
-
33
23
  export const Default = {
34
24
  args: {
35
25
  model: {
36
26
  id: 'editor',
37
- content,
27
+ content: 'Hello, Storybook!',
38
28
  },
39
29
  },
40
30
  };
41
31
 
42
32
  const replicator = new Replicator(TextKind.PLAIN);
43
33
 
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
-
67
34
  export const WithYjs = {
35
+ render: () => {
36
+ const [generate, setGenerate] = useState(false);
37
+ const generateId = useId('generate');
38
+
39
+ const [id] = useState(PublicKey.random().toHex());
40
+ const model = useYjsModel({ id, replicator });
41
+
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
+ <MarkdownEditor model={model} />
54
+ </main>
55
+ );
56
+ },
68
57
  // TODO(wittjosiah): Decorator for doing this without clients being initialized?
69
- render: () => <ClientRepeater count={2} Component={Story} />,
58
+ decorators: [ClientDecorator({ count: 2 })],
70
59
  };
@@ -30,7 +30,6 @@ import {
30
30
  EditorView,
31
31
  } from '@codemirror/view';
32
32
  import { useFocusableGroup } from '@fluentui/react-tabster';
33
- // import { GFM } from '@lezer/markdown';
34
33
  import { vim } from '@replit/codemirror-vim';
35
34
  import React, {
36
35
  type KeyboardEvent,
@@ -49,17 +48,16 @@ import { getColorForValue } from '@dxos/react-ui-theme';
49
48
  import { YText } from '@dxos/text-model';
50
49
 
51
50
  import { markdownTagsExtension } from './markdownTags';
52
- import { markdownHighlightStyle, markdownTheme } from './markdownTheme';
51
+ import { markdownDarkHighlighting, markdownTheme } from './markdownTheme';
53
52
  import { type EditorModel, type EditorSlots } from '../../model';
54
53
 
55
54
  export const EditorModes = ['default', 'vim'] as const;
56
55
  export type EditorMode = (typeof EditorModes)[number];
57
56
 
58
57
  export type MarkdownEditorProps = {
59
- model: EditorModel;
58
+ model?: EditorModel;
60
59
  slots?: EditorSlots;
61
60
  editorMode?: EditorMode;
62
- showWidgets?: boolean;
63
61
  extensions?: Extension[];
64
62
  onChange?: (content: string | Text) => void;
65
63
  };
@@ -118,73 +116,50 @@ export const MarkdownEditor = forwardRef<MarkdownEditorRef, MarkdownEditorProps>
118
116
  const state = EditorState.create({
119
117
  doc: content?.toString(),
120
118
  extensions: [
121
- // Based on https://github.com/codemirror/dev/issues/44#issuecomment-789093799
119
+ // Based on https://github.com/codemirror/dev/issues/44#issuecomment-789093799.
122
120
  listenChangesExtension,
123
121
 
124
- // TODO(burdon): Create custom bundle to make this class more reusable and retire simpler TextEditor component.
122
+ ...(editorMode === 'vim' ? [vim()] : []),
123
+
125
124
  // All of https://github.com/codemirror/basic-setup minus line numbers and fold gutter.
126
- // https://codemirror.net/docs/ref/#codemirror.basicSetup
127
- autocompletion(),
125
+ highlightActiveLineGutter(),
126
+ highlightSpecialChars(),
127
+ history(),
128
+ drawSelection(),
129
+ dropCursor(),
130
+ EditorState.allowMultipleSelections.of(true),
131
+ indentOnInput(),
132
+ syntaxHighlighting(defaultHighlightStyle, { fallback: true }),
128
133
  bracketMatching(),
129
134
  closeBrackets(),
135
+ autocompletion(),
136
+ rectangularSelection(),
130
137
  crosshairCursor(),
131
- dropCursor(),
132
- drawSelection(),
133
138
  highlightActiveLine(),
134
- highlightActiveLineGutter(),
135
139
  highlightSelectionMatches(),
136
- highlightSpecialChars(),
137
- history(),
138
- indentOnInput(),
139
140
  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
-
145
141
  keymap.of([
146
142
  ...closeBracketsKeymap,
147
- ...completionKeymap,
148
143
  ...defaultKeymap,
149
- ...foldKeymap,
144
+ ...searchKeymap,
150
145
  ...historyKeymap,
146
+ ...foldKeymap,
147
+ ...completionKeymap,
151
148
  ...lintKeymap,
152
- ...searchKeymap,
153
149
  indentWithTab,
154
150
  ]),
151
+ EditorView.lineWrapping,
155
152
 
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.
153
+ // Themes.
154
+ markdown({ base: markdownLanguage, codeLanguages: languages, extensions: [markdownTagsExtension] }),
174
155
  EditorView.theme({ ...markdownTheme, ...slots.editor?.markdownTheme }),
175
156
  ...(themeMode === 'dark'
176
157
  ? [syntaxHighlighting(oneDarkHighlightStyle)]
177
158
  : [syntaxHighlighting(defaultHighlightStyle)]),
178
-
179
159
  // TODO(thure): All but one rule here apply to both themes; rename or refactor.
180
- syntaxHighlighting(markdownHighlightStyle),
181
-
182
- // Settings.
183
- ...(editorMode === 'vim' ? [vim()] : []),
160
+ syntaxHighlighting(markdownDarkHighlighting),
184
161
 
185
- // TODO(burdon): Move to extensions.
186
162
  // Replication and awareness (incl. remote selection).
187
- // https://codemirror.net/docs/ref/#collab
188
163
  ...(content instanceof YText ? [yCollab(content, provider?.awareness)] : []),
189
164
 
190
165
  // Custom.
@@ -32,10 +32,6 @@ 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
-
39
35
  export const markdownTheme = {
40
36
  // TODO(thure): consider whether these commented-out rules from one-dark-theme should be integrated.
41
37
  // '&': {
@@ -168,13 +164,6 @@ export const markdownTheme = {
168
164
  '& .cm-yLineSelection': {
169
165
  margin: '0',
170
166
  },
171
- '.cm-link': {
172
- color: 'rgb(20 89 208)',
173
- textDecorationLine: 'underline',
174
- textDecorationThickness: '1px',
175
- textUnderlineOffset: '2px',
176
- borderRadius: '.125rem',
177
- },
178
167
  ...Object.keys(get(tokens, 'extend.fontSize', {})).reduce((acc: Record<string, any>, fontSize) => {
179
168
  const height = get(tokens, ['extend', 'fontSize', fontSize, 1, 'lineHeight']);
180
169
  // 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.
@@ -185,7 +174,7 @@ export const markdownTheme = {
185
174
  }, {}),
186
175
  };
187
176
 
188
- export const markdownHighlightStyle = HighlightStyle.define(
177
+ export const markdownDarkHighlighting = HighlightStyle.define(
189
178
  [
190
179
  {
191
180
  tag: [
@@ -8,7 +8,7 @@ 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 { ClientRepeater, textGenerator, useDataGenerator } from '@dxos/react-client/testing';
11
+ import { ClientDecorator, 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
14
  import { withTheme } from '@dxos/storybook-utils';
@@ -31,42 +31,40 @@ export const Default = {
31
31
  };
32
32
 
33
33
  const replicator = new Replicator(TextKind.RICH);
34
+ export const WithYjs = {
35
+ render: (args: Omit<RichTextEditorProps, 'model'>) => {
36
+ const [generate, setGenerate] = useState(false);
37
+ const generateId = useId('generate');
34
38
 
35
- const Story = (args: Omit<RichTextEditorProps, 'model'>) => {
36
- const [generate, setGenerate] = useState(false);
37
- const generateId = useId('generate');
38
-
39
- const [id] = useState(PublicKey.random().toHex());
40
- const model = useYjsModel({ id, replicator });
41
-
42
- useDataGenerator({
43
- generator: generate ? textGenerator : undefined,
44
- options: { text: typeof model?.content !== 'string' ? model?.content : undefined },
45
- });
39
+ const [id] = useState(PublicKey.random().toHex());
40
+ const model = useYjsModel({ id, replicator });
46
41
 
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
- };
42
+ useDataGenerator({
43
+ generator: generate ? textGenerator : undefined,
44
+ options: { text: typeof model?.content !== 'string' ? model?.content : undefined },
45
+ });
68
46
 
69
- export const WithYjs = {
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
+ },
70
68
  // TODO(wittjosiah): Decorator for doing this without clients being initialized?
71
- render: () => <ClientRepeater count={2} Component={Story} />,
69
+ decorators: [ClientDecorator({ count: 2 })],
72
70
  };