@dxos/react-ui-editor 0.3.10-main.c5193cc → 0.3.10-main.c9b5e9b

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 (55) hide show
  1. package/dist/lib/browser/index.mjs +615 -380
  2. package/dist/lib/browser/index.mjs.map +4 -4
  3. package/dist/lib/browser/meta.json +1 -1
  4. package/dist/types/src/components/TextEditor/MarkdownEditor.stories.d.ts +6 -3
  5. package/dist/types/src/components/TextEditor/MarkdownEditor.stories.d.ts.map +1 -1
  6. package/dist/types/src/components/TextEditor/TextEditor.d.ts.map +1 -1
  7. package/dist/types/src/components/TextEditor/TextEditor.stories.d.ts +3 -0
  8. package/dist/types/src/components/TextEditor/TextEditor.stories.d.ts.map +1 -1
  9. package/dist/types/src/components/TextEditor/extensions/basic.d.ts.map +1 -1
  10. package/dist/types/src/components/TextEditor/extensions/blast.d.ts +1 -1
  11. package/dist/types/src/components/TextEditor/extensions/blast.d.ts.map +1 -1
  12. package/dist/types/src/components/TextEditor/extensions/code.d.ts +2 -0
  13. package/dist/types/src/components/TextEditor/extensions/code.d.ts.map +1 -0
  14. package/dist/types/src/components/TextEditor/extensions/comments.d.ts +1 -1
  15. package/dist/types/src/components/TextEditor/extensions/comments.d.ts.map +1 -1
  16. package/dist/types/src/components/TextEditor/extensions/experimental.d.ts +1 -1
  17. package/dist/types/src/components/TextEditor/extensions/experimental.d.ts.map +1 -1
  18. package/dist/types/src/components/TextEditor/extensions/highlight.d.ts +24 -0
  19. package/dist/types/src/components/TextEditor/extensions/highlight.d.ts.map +1 -0
  20. package/dist/types/src/components/TextEditor/extensions/index.d.ts +2 -0
  21. package/dist/types/src/components/TextEditor/extensions/index.d.ts.map +1 -1
  22. package/dist/types/src/components/TextEditor/extensions/markdown/bundle.d.ts +8 -0
  23. package/dist/types/src/components/TextEditor/extensions/markdown/bundle.d.ts.map +1 -1
  24. package/dist/types/src/components/TextEditor/extensions/markdown/highlight.d.ts +3 -1
  25. package/dist/types/src/components/TextEditor/extensions/markdown/highlight.d.ts.map +1 -1
  26. package/dist/types/src/components/TextEditor/themes/default.d.ts.map +1 -1
  27. package/dist/types/src/hooks/automerge/automerge.stories.d.ts.map +1 -1
  28. package/dist/types/src/hooks/useTextModel.d.ts +6 -0
  29. package/dist/types/src/hooks/useTextModel.d.ts.map +1 -1
  30. package/dist/types/src/hooks/yjs/yjs.stories.d.ts +1 -1
  31. package/dist/types/src/hooks/yjs/yjs.stories.d.ts.map +1 -1
  32. package/dist/types/src/styles/markdown.d.ts.map +1 -1
  33. package/dist/types/src/testing/replicator.d.ts.map +1 -1
  34. package/package.json +22 -20
  35. package/src/components/TextEditor/MarkdownEditor.stories.tsx +48 -32
  36. package/src/components/TextEditor/TextEditor.stories.tsx +12 -1
  37. package/src/components/TextEditor/TextEditor.tsx +18 -1
  38. package/src/components/TextEditor/extensions/basic.ts +4 -3
  39. package/src/components/TextEditor/extensions/blast.ts +14 -28
  40. package/src/components/TextEditor/extensions/code.ts +99 -0
  41. package/src/components/TextEditor/extensions/comments.ts +22 -40
  42. package/src/components/TextEditor/extensions/experimental.tsx +15 -36
  43. package/src/components/TextEditor/extensions/highlight.ts +128 -0
  44. package/src/components/TextEditor/extensions/index.ts +2 -0
  45. package/src/components/TextEditor/extensions/link.ts +1 -2
  46. package/src/components/TextEditor/extensions/markdown/bundle.ts +19 -19
  47. package/src/components/TextEditor/extensions/markdown/highlight.ts +37 -11
  48. package/src/components/TextEditor/extensions/table.ts +44 -48
  49. package/src/components/TextEditor/extensions/tasklist.ts +9 -9
  50. package/src/components/TextEditor/themes/default.ts +43 -2
  51. package/src/hooks/automerge/automerge.stories.tsx +1 -0
  52. package/src/hooks/useTextModel.ts +9 -0
  53. package/src/hooks/yjs/yjs.stories.tsx +1 -1
  54. package/src/styles/markdown.ts +5 -4
  55. package/src/testing/replicator.ts +1 -0
@@ -0,0 +1,128 @@
1
+ //
2
+ // Copyright 2023 DXOS.org
3
+ //
4
+
5
+ import { type Extension, StateEffect, StateField } from '@codemirror/state';
6
+ import { Decoration, EditorView } from '@codemirror/view';
7
+ import sortBy from 'lodash.sortby';
8
+ import { useEffect } from 'react';
9
+
10
+ import { type DocumentRange } from '../../../hooks';
11
+
12
+ // TODO(burdon): Detect click, or cursor proximity.
13
+
14
+ // TODO(burdon): Reconcile with theme.
15
+ const styles = EditorView.baseTheme({
16
+ '& .cm-highlight': {
17
+ backgroundColor: 'yellow', // TODO(burdon): Reconcile with tailwind theme.
18
+ },
19
+ '& .cm-highlight::before': {
20
+ content: ']',
21
+ },
22
+ '& .cm-highlight-active': {
23
+ backgroundColor: 'lime',
24
+ },
25
+ });
26
+
27
+ const marks = {
28
+ highlight: Decoration.mark({ class: 'cm-highlight' }),
29
+ HighlightActive: Decoration.mark({ class: 'cm-highlight-active' }),
30
+ };
31
+
32
+ type HighlightRange = DocumentRange & { active?: boolean };
33
+
34
+ // TODO(burdon): Make configurable to support multiple layers.
35
+ const setHighlights = StateEffect.define<HighlightRange[]>();
36
+
37
+ /**
38
+ * Update field set.
39
+ */
40
+ // TODO(burdon): Reconcile range from data model, vs. in-markdown characters.
41
+ export const useHighlights = (view: EditorView | undefined, ranges: HighlightRange[] = []) => {
42
+ useEffect(() => {
43
+ view?.dispatch({
44
+ effects: setHighlights.of(ranges),
45
+ });
46
+ }, [view, ranges]);
47
+ };
48
+
49
+ /**
50
+ * State field that tracks highlight ranges.
51
+ */
52
+ export const highlightStateField = StateField.define<HighlightRange[]>({
53
+ create: () => [],
54
+ update: (threads, tr) => {
55
+ for (const effect of tr.effects) {
56
+ if (effect.is(setHighlights)) {
57
+ return effect.value;
58
+ }
59
+ }
60
+
61
+ return threads;
62
+ },
63
+ });
64
+
65
+ /**
66
+ * Decorate ranges.
67
+ */
68
+ // TODO(burdon): Pass in field to make reusable (e.g., comments, search).
69
+ export const highlightDecorations = EditorView.decorations.compute([highlightStateField], (state) => {
70
+ const selectionRanges = state.field(highlightStateField);
71
+ const decorations =
72
+ sortBy(selectionRanges ?? [], (selection) => selection.from)?.flatMap((selection) => {
73
+ const range = { from: selection.from, to: selection.to + 1 };
74
+ if (selection.to > selection.from) {
75
+ if (selection.active) {
76
+ return marks.HighlightActive.range(range.from, range.to);
77
+ } else {
78
+ return marks.highlight.range(range.from, range.to);
79
+ }
80
+ } else {
81
+ return [];
82
+ }
83
+ }) ?? [];
84
+
85
+ return Decoration.set(decorations);
86
+ });
87
+
88
+ // TODO(burdon): Use current range to create comment.
89
+ // TODO(burdon): Allow/prevent overlapping?
90
+
91
+ /**
92
+ * Track cursor entering and existing ranges.
93
+ */
94
+ const listener = (options: HighlightOptions) => {
95
+ return EditorView.updateListener.of((update) => {
96
+ const { view, state } = update;
97
+ const { /* from, to, */ head } = state.selection.main;
98
+ const ranges = state.field(highlightStateField);
99
+
100
+ // TODO(burdon): Modify range if editing.
101
+ // console.log('::::', update);
102
+
103
+ // Set active.
104
+ let mod = false;
105
+ const newRanges = ranges.map((range) => {
106
+ const active = head >= range.from && head <= range.to;
107
+ if (active !== range.active) {
108
+ mod = true;
109
+ return { ...range, active };
110
+ } else {
111
+ return range;
112
+ }
113
+ });
114
+
115
+ if (mod) {
116
+ view.dispatch({ effects: setHighlights.of(newRanges) });
117
+ options?.onChange?.(newRanges.find((range) => range.active)?.id);
118
+ }
119
+ });
120
+ };
121
+
122
+ export type HighlightOptions = {
123
+ onChange?: (active?: string) => {};
124
+ };
125
+
126
+ export const highlight = (options: HighlightOptions = {}): Extension => {
127
+ return [highlightStateField, highlightDecorations, listener(options), styles];
128
+ };
@@ -5,8 +5,10 @@
5
5
  export * from './autocomplete';
6
6
  export * from './basic';
7
7
  export * from './blast';
8
+ export * from './code';
8
9
  export * from './demo';
9
10
  export * from './comments';
11
+ export * from './highlight';
10
12
  export * from './image';
11
13
  export * from './link';
12
14
  export * from './listener';
@@ -101,14 +101,13 @@ const update = (state: EditorState, options: LinkOptions) => {
101
101
  if (node.name === 'Link') {
102
102
  const marks = node.node.getChildren('LinkMark');
103
103
  const text = marks.length >= 2 ? state.sliceDoc(marks[0].to, marks[1].from) : '';
104
-
105
104
  const urlNode = node.node.getChild('URL');
106
105
  const url = urlNode ? state.sliceDoc(urlNode.from, urlNode.to) : '';
107
106
  if (!url) {
108
107
  return false;
109
108
  }
110
109
 
111
- if (cursor < node.from || cursor > node.to) {
110
+ if (state.readOnly || cursor < node.from || cursor > node.to) {
112
111
  builder.add(
113
112
  node.from,
114
113
  node.to,
@@ -7,7 +7,7 @@ import { history, historyKeymap, indentWithTab, standardKeymap } from '@codemirr
7
7
  import { markdownLanguage, markdown } from '@codemirror/lang-markdown';
8
8
  import { bracketMatching, defaultHighlightStyle, indentOnInput, syntaxHighlighting } from '@codemirror/language';
9
9
  import { languages } from '@codemirror/language-data';
10
- import { highlightSelectionMatches, searchKeymap } from '@codemirror/search';
10
+ import { searchKeymap } from '@codemirror/search';
11
11
  import { EditorState, type Extension } from '@codemirror/state';
12
12
  import { oneDarkHighlightStyle } from '@codemirror/theme-one-dark';
13
13
  import {
@@ -16,16 +16,13 @@ import {
16
16
  dropCursor,
17
17
  EditorView,
18
18
  highlightActiveLine,
19
- highlightActiveLineGutter,
20
- highlightSpecialChars,
21
19
  keymap,
22
20
  placeholder,
23
- rectangularSelection,
24
21
  } from '@codemirror/view';
25
22
 
26
23
  import type { ThemeMode } from '@dxos/react-ui';
27
24
 
28
- import { markdownHighlightStyle, markdownTagsExtension } from './highlight';
25
+ import { markdownHighlightStyle, markdownTagsExtensions } from './highlight';
29
26
 
30
27
  export type MarkdownBundleOptions = {
31
28
  readonly?: boolean;
@@ -33,33 +30,36 @@ export type MarkdownBundleOptions = {
33
30
  placeholder?: string;
34
31
  };
35
32
 
33
+ /**
34
+ * Markdown bundle.
35
+ * Refs:
36
+ * https://github.com/codemirror/basic-setup
37
+ * https://github.com/codemirror/basic-setup/blob/main/src/codemirror.ts#L50
38
+ * https://codemirror.net/docs/community
39
+ * https://codemirror.net/docs/ref/#codemirror.basicSetup
40
+ */
41
+ // TODO(burdon): Add Composer here: https://codemirror.net/docs/community
36
42
  export const markdownBundle = ({
37
43
  readonly,
38
44
  themeMode,
39
45
  placeholder: _placeholder,
40
46
  }: MarkdownBundleOptions): Extension[] => {
41
- // All of https://github.com/codemirror/basic-setup minus line numbers and fold gutter.
42
- // https://codemirror.net/docs/ref/#codemirror.basicSetup
43
47
  return [
44
- bracketMatching(),
45
- closeBrackets(),
46
- _placeholder && placeholder(_placeholder),
47
-
48
48
  EditorState.allowMultipleSelections.of(true),
49
+ EditorState.tabSize.of(2),
49
50
  EditorView.lineWrapping,
50
51
 
52
+ customKeymap(),
53
+
54
+ bracketMatching(),
55
+ closeBrackets(),
51
56
  crosshairCursor(),
52
57
  dropCursor(),
53
58
  drawSelection(),
54
59
  highlightActiveLine(),
55
- highlightActiveLineGutter(),
56
- highlightSelectionMatches(),
57
- highlightSpecialChars(),
58
60
  history(),
59
61
  indentOnInput(),
60
- rectangularSelection(),
61
-
62
- customkeymap(),
62
+ _placeholder && placeholder(_placeholder),
63
63
 
64
64
  // Main extension.
65
65
  // https://github.com/codemirror/lang-markdown
@@ -78,7 +78,7 @@ export const markdownBundle = ({
78
78
  // Parser extensions.
79
79
  extensions: [
80
80
  // GFM provided by default.
81
- markdownTagsExtension,
81
+ markdownTagsExtensions,
82
82
  ],
83
83
  }),
84
84
 
@@ -91,7 +91,7 @@ export const markdownBundle = ({
91
91
  };
92
92
 
93
93
  // https://codemirror.net/docs/ref/#view.keymap
94
- const customkeymap = () =>
94
+ const customKeymap = () =>
95
95
  keymap.of([
96
96
  // https://codemirror.net/docs/ref/#commands.indentWithTab
97
97
  indentWithTab,
@@ -5,7 +5,7 @@
5
5
  import { markdownLanguage } from '@codemirror/lang-markdown';
6
6
  import { HighlightStyle } from '@codemirror/language';
7
7
  import { tags, styleTags, Tag } from '@lezer/highlight';
8
- import { type MarkdownConfig } from '@lezer/markdown';
8
+ import { type MarkdownConfig, Table } from '@lezer/markdown';
9
9
  import get from 'lodash.get';
10
10
 
11
11
  import { mx } from '@dxos/react-ui-theme';
@@ -24,11 +24,6 @@ import {
24
24
  tokens,
25
25
  } from '../../../../styles';
26
26
 
27
- // TODO(burdon): Style table in monospace.
28
- // https://discuss.codemirror.net/t/markdown-table-highlighting/658
29
-
30
- // TODO(burdon): Readonly: use remark rather than hiding markup chars (which cursor can still move into).
31
-
32
27
  /**
33
28
  * Custom tags defined and processed by the GFM lezer extension.
34
29
  * https://github.com/lezer-parser/markdown
@@ -47,24 +42,42 @@ export const markdownTags = {
47
42
  ListMark: Tag.define(),
48
43
  QuoteMark: Tag.define(),
49
44
  URL: Tag.define(),
50
- };
51
45
 
52
- export const markdownTagsExtension: MarkdownConfig = {
53
- props: [styleTags(markdownTags)],
46
+ // Custom.
47
+ TableCell: Tag.define(),
54
48
  };
55
49
 
50
+ // TODO(burdon): Customize table parser (make all content monospace).
51
+ // https://github.com/lezer-parser/markdown/blob/main/src/extension.ts
52
+ Table.defineNodes?.forEach((node: any) => {
53
+ switch (node?.name) {
54
+ case 'TableCell': {
55
+ node.style = markdownTags.TableCell;
56
+ break;
57
+ }
58
+ }
59
+ });
60
+
61
+ export const markdownTagsExtensions: MarkdownConfig[] = [
62
+ Table,
63
+ {
64
+ props: [styleTags(markdownTags)],
65
+ },
66
+ ];
67
+
56
68
  /**
57
69
  * Styling based on `lezer` parser tags.
58
70
  * https://codemirror.net/examples/styling
59
71
  * https://github.com/lezer-parser/highlight
72
+ * https://github.com/lezer-parser/highlight/blob/main/src/highlight.ts#L427
60
73
  * https://lezer.codemirror.net/docs/ref/#highlight.tags (list of tags)
61
74
  *
62
75
  * Examples:
63
76
  * - https://github.com/codemirror/language/blob/main/src/highlight.ts#L194
64
77
  * - https://github.com/codemirror/theme-one-dark/blob/main/src/one-dark.ts#L115
65
78
  */
66
- export const markdownHighlightStyle = (readonly?: boolean) =>
67
- HighlightStyle.define(
79
+ export const markdownHighlightStyle = (readonly?: boolean) => {
80
+ return HighlightStyle.define(
68
81
  [
69
82
  {
70
83
  tag: [
@@ -129,6 +142,11 @@ export const markdownHighlightStyle = (readonly?: boolean) =>
129
142
  class: readonly ? 'hidden' : codeMark,
130
143
  },
131
144
 
145
+ {
146
+ tag: [tags.monospace],
147
+ class: 'font-mono',
148
+ },
149
+
132
150
  // Headings.
133
151
  { tag: tags.heading1, class: heading(1, readonly) },
134
152
  { tag: tags.heading2, class: heading(2, readonly) },
@@ -163,11 +181,18 @@ export const markdownHighlightStyle = (readonly?: boolean) =>
163
181
  class: blockquote,
164
182
  },
165
183
 
184
+ // TODO(burdon): Replace with extension.
166
185
  {
167
186
  tag: [markdownTags.HorizontalRule],
168
187
  class: mx(horizontalRule, readonly && '-indent-[100rem]'),
169
188
  },
170
189
 
190
+ // TODO(burdon): Only if being edited.
191
+ {
192
+ tag: [markdownTags.TableCell],
193
+ class: 'font-mono',
194
+ },
195
+
171
196
  // Main content, paragraphs, etc.
172
197
  // {
173
198
  // tag: [tags.content],
@@ -179,3 +204,4 @@ export const markdownHighlightStyle = (readonly?: boolean) =>
179
204
  all: { fontFamily: get(tokens, 'fontFamily.body', []).join(',') },
180
205
  },
181
206
  );
207
+ };
@@ -25,61 +25,57 @@ export const table = (options: TableOptions = {}) => {
25
25
 
26
26
  type Table = {
27
27
  from: number;
28
- to?: number;
28
+ to: number;
29
29
  header?: string[];
30
30
  rows?: string[][];
31
31
  };
32
32
 
33
33
  const update = (state: EditorState, options: TableOptions) => {
34
34
  const builder = new RangeSetBuilder();
35
- if (state.readOnly) {
36
- const tables: Table[] = [];
37
- const getTable = () => tables[tables.length - 1];
38
- const getRow = () => {
39
- const table = getTable();
40
- return table.rows?.[table.rows.length - 1];
41
- };
42
-
43
- // Parse table.
44
- // TODO(burdon): Use remark?: https://www.npmjs.com/package/remark-gfm
45
- syntaxTree(state).iterate({
46
- enter: (node) => {
47
- // console.log(node.name);
48
- // Check if cursor is inside text.
49
- switch (node.name) {
50
- case 'Table': {
51
- tables.push({ from: node.from });
52
- break;
53
- }
54
- case 'TableHeader': {
55
- getTable().header = [];
56
- break;
57
- }
58
- case 'TableRow': {
59
- (getTable().rows ??= []).push([]);
60
- break;
61
- }
62
- case 'TableCell': {
63
- const row = getRow();
64
- if (row) {
65
- row.push(state.sliceDoc(node.from, node.to));
66
- } else {
67
- getTable().header?.push(state.sliceDoc(node.from, node.to));
68
- }
69
- break;
70
- }
71
- case 'TableDelimiter': {
72
- getTable().to = node.to;
73
- break;
35
+ const cursor = state.selection.main.head;
36
+
37
+ const tables: Table[] = [];
38
+ const getTable = () => tables[tables.length - 1];
39
+ const getRow = () => {
40
+ const table = getTable();
41
+ return table.rows?.[table.rows.length - 1];
42
+ };
43
+
44
+ // Parse table.
45
+ syntaxTree(state).iterate({
46
+ enter: (node) => {
47
+ // Check if cursor is inside text.
48
+ switch (node.name) {
49
+ case 'Table': {
50
+ tables.push({ from: node.from, to: node.to });
51
+ break;
52
+ }
53
+ case 'TableHeader': {
54
+ getTable().header = [];
55
+ break;
56
+ }
57
+ case 'TableRow': {
58
+ (getTable().rows ??= []).push([]);
59
+ break;
60
+ }
61
+ case 'TableCell': {
62
+ const row = getRow();
63
+ if (row) {
64
+ row.push(state.sliceDoc(node.from, node.to));
65
+ } else {
66
+ getTable().header?.push(state.sliceDoc(node.from, node.to));
74
67
  }
68
+ break;
75
69
  }
76
- },
77
- });
70
+ }
71
+ },
72
+ });
78
73
 
79
- tables.forEach((table) =>
80
- builder.add(table.from, table.to!, Decoration.replace({ widget: new TableWidget(table) })),
81
- );
82
- }
74
+ tables.forEach((table) => {
75
+ const hide = state.readOnly || cursor < table.from || cursor > table.to;
76
+ hide && builder.add(table.from, table.to, Decoration.replace({ widget: new TableWidget(table) }));
77
+ builder.add(table.from, table.to, Decoration.mark({ class: 'cm-table' }));
78
+ });
83
79
 
84
80
  return builder.finish();
85
81
  };
@@ -98,11 +94,11 @@ class TableWidget extends WidgetType {
98
94
 
99
95
  {
100
96
  const header = table.appendChild(document.createElement('thead'));
101
- const row = header.appendChild(document.createElement('tr'));
97
+ const tr = header.appendChild(document.createElement('tr'));
102
98
  this._table.header?.forEach((cell) => {
103
99
  const th = document.createElement('th');
104
100
  th.setAttribute('class', 'cm-table-head');
105
- row.appendChild(th).textContent = cell;
101
+ tr.appendChild(th).textContent = cell;
106
102
  });
107
103
  }
108
104
 
@@ -12,6 +12,14 @@ import {
12
12
  type ViewUpdate,
13
13
  } from '@codemirror/view';
14
14
 
15
+ // TODO(burdon): Reconcile with theme.
16
+ const styles = EditorView.baseTheme({
17
+ '& .cm-task-item': {
18
+ paddingLeft: '4px',
19
+ paddingRight: '8px',
20
+ },
21
+ });
22
+
15
23
  class CheckboxWidget extends WidgetType {
16
24
  constructor(
17
25
  private readonly _pos: number,
@@ -53,14 +61,6 @@ class CheckboxWidget extends WidgetType {
53
61
  }
54
62
  }
55
63
 
56
- // TODO(burdon): Reconcile with theme.
57
- const styles = EditorView.baseTheme({
58
- '& .cm-task-item': {
59
- paddingLeft: '4px',
60
- paddingRight: '8px',
61
- },
62
- });
63
-
64
64
  export type TasklistOptions = {};
65
65
 
66
66
  export const tasklist = (options: TasklistOptions = {}) => {
@@ -109,7 +109,7 @@ export const tasklist = (options: TasklistOptions = {}) => {
109
109
  }
110
110
  },
111
111
  {
112
- decorations: (instance) => instance.tasks,
112
+ decorations: (value) => value.tasks,
113
113
  provide: (plugin) =>
114
114
  EditorView.atomicRanges.of((view) => {
115
115
  return view.plugin(plugin)?.tasks || Decoration.none;
@@ -55,10 +55,10 @@ export const defaultTheme: {
55
55
  //
56
56
  // Cursor
57
57
  //
58
- '&light .cm-cursor': {
58
+ '&light .cm-cursor, &light .cm-dropCursor': {
59
59
  borderLeft: '2px solid black',
60
60
  },
61
- '&dark .cm-cursor': {
61
+ '&dark .cm-cursor, &dark .cm-dropCursor': {
62
62
  borderLeft: '2px solid white',
63
63
  },
64
64
  '& .cm-placeholder': {
@@ -173,6 +173,10 @@ export const defaultTheme: {
173
173
  //
174
174
  // table
175
175
  //
176
+ '& .cm-table *': {
177
+ fontFamily: `${get(tokens, 'fontFamily.mono', []).join(',')} !important`,
178
+ textDecoration: 'none !important',
179
+ },
176
180
  '& .cm-table-head': {
177
181
  padding: '2px 16px 2px 0px',
178
182
  borderBottom: `1px solid ${get(tokens, 'extend.colors.neutral.500')}`,
@@ -203,6 +207,43 @@ export const defaultTheme: {
203
207
  acc[`& .text-${fontSize} + .cm-widgetBuffer + .cm-ySelectionCaret`] = { height };
204
208
  return acc;
205
209
  }, {}),
210
+
211
+ /**
212
+ * Panels
213
+ * https://github.com/codemirror/search/blob/main/src/search.ts#L745
214
+ *
215
+ * Find/replace panel.
216
+ * <div class="cm-announced">...</div>
217
+ * <div class="cm-scroller">...</div>
218
+ * <div class="cm-panels cm-panels-bottom">
219
+ * <div class="cm-search cm-panel">
220
+ * <input class="cm-textfield" />
221
+ * <button class="cm-button">...</button>
222
+ * <label><input type="checkbox" />...</label>
223
+ * </div>
224
+ * </div
225
+ */
226
+ '& .cm-panels': {
227
+ border: `1px solid ${get(tokens, 'extend.colors.neutral.200')}`,
228
+ borderBottom: 'none',
229
+ },
230
+ '& .cm-panel': {
231
+ background: get(tokens, 'extend.colors.neutral.50'),
232
+ fontFamily: get(tokens, 'fontFamily.body', []).join(','),
233
+ },
234
+ '& .cm-button': {
235
+ margin: '4px',
236
+ fontFamily: get(tokens, 'fontFamily.body', []).join(','),
237
+ background: get(tokens, 'extend.colors.neutral.100'),
238
+ border: 'none',
239
+ },
240
+
241
+ '& .cm-searchMatch': {
242
+ backgroundColor: get(tokens, 'extend.colors.yellow.100'),
243
+ },
244
+ '& .cm-searchMatch.cm-searchMatch-selected': {
245
+ backgroundColor: get(tokens, 'extend.colors.primary.100'),
246
+ },
206
247
  };
207
248
 
208
249
  export const textTheme: {
@@ -9,6 +9,7 @@ import { basicSetup } from 'codemirror';
9
9
  import get from 'lodash.get';
10
10
  import React, { useEffect, useRef, useState } from 'react';
11
11
 
12
+ // TODO(burdon): Why separate imports?
12
13
  import { type Prop } from '@dxos/automerge/automerge';
13
14
  import { type DocHandle, Repo } from '@dxos/automerge/automerge-repo';
14
15
 
@@ -26,12 +26,19 @@ import { SpaceAwarenessProvider } from './yjs';
26
26
  // TODO(burdon): Move.
27
27
  type Awareness = awarenessProtocol.Awareness;
28
28
 
29
+ export type DocumentRange = {
30
+ id: string;
31
+ from: number;
32
+ to: number;
33
+ };
34
+
29
35
  // TODO(wittjosiah): Factor out to common package? @dxos/react-client?
30
36
  export type EditorModel = {
31
37
  id: string;
32
38
  // TODO(burdon): Remove.
33
39
  content: string | YText | YXmlFragment | DocAccessor;
34
40
  text: () => string;
41
+ ranges: DocumentRange[];
35
42
  extension?: Extension;
36
43
  awareness?: Awareness;
37
44
  peer?: {
@@ -80,6 +87,7 @@ const createYjsModel = ({ identity, space, text }: UseTextModelOptions): EditorM
80
87
  id: text.doc.guid,
81
88
  content: text.content,
82
89
  text: () => text.content!.toString(),
90
+ ranges: [],
83
91
  extension: yCollab(text.content as YText, provider?.awareness),
84
92
  awareness: provider?.awareness,
85
93
  peer: identity
@@ -99,6 +107,7 @@ const createAutomergeModel = ({ identity, text }: UseTextModelOptions): EditorMo
99
107
  id: obj.id,
100
108
  content: doc,
101
109
  text: () => get(doc.handle.docSync(), doc.path),
110
+ ranges: [],
102
111
  extension: automergePlugin(doc.handle, doc.path),
103
112
  peer: identity
104
113
  ? {
@@ -16,6 +16,7 @@ import { MarkdownEditor } from '../../components';
16
16
  import { Replicator, useYjsModel } from '../../testing';
17
17
 
18
18
  export default {
19
+ title: 'react-ui-editor/YJS',
19
20
  component: MarkdownEditor,
20
21
  decorators: [withTheme],
21
22
  };
@@ -50,7 +51,6 @@ const Story = () => {
50
51
  };
51
52
 
52
53
  export const Default = {
53
- title: 'react-ui-editor/YJS',
54
54
  // TODO(wittjosiah): Decorator for doing this without clients being initialized?
55
55
  render: () => <ClientRepeater count={2} Component={Story} />,
56
56
  };
@@ -6,12 +6,13 @@ import { mx } from '@dxos/react-ui-theme';
6
6
 
7
7
  export type HeadingLevel = 1 | 2 | 3 | 4 | 5 | 6;
8
8
 
9
+ // TODO(burdon): Better way to align vertically than negative margin? Font-specific?
9
10
  // https://tailwindcss.com/docs/font-weight
10
11
  export const headings: Record<HeadingLevel, string[]> = {
11
- 1: ['mbs-4 mbe-2 font-medium text-inherit no-underline text-4xl', '-ml-1.5'],
12
- 2: ['mbs-4 mbe-2 font-medium text-inherit no-underline text-3xl', '-ml-1.5'],
13
- 3: ['mbs-4 mbe-2 font-medium text-inherit no-underline text-2xl', '-ml-1.5'],
14
- 4: ['mbs-4 mbe-2 font-medium text-inherit no-underline text-xl', '-ml-[5px]'],
12
+ 1: ['mbs-4 mbe-2 font-medium text-inherit no-underline text-4xl', '-ml-[10px]'],
13
+ 2: ['mbs-4 mbe-2 font-medium text-inherit no-underline text-3xl', '-ml-[8px]'],
14
+ 3: ['mbs-4 mbe-2 font-medium text-inherit no-underline text-2xl', '-ml-[7px]'],
15
+ 4: ['mbs-4 mbe-2 font-medium text-inherit no-underline text-xl', '-ml-[6px]'],
15
16
  5: ['mbs-4 mbe-2 font-medium text-inherit no-underline text-lg', '-ml-[5px]'],
16
17
  6: ['mbs-4 mbe-2 font-medium text-inherit no-underline', '-ml-[4px]'],
17
18
  };
@@ -169,6 +169,7 @@ export class Replicator {
169
169
  id: doc.guid,
170
170
  text: () => content.toString(),
171
171
  content,
172
+ ranges: [],
172
173
  awareness: provider.awareness,
173
174
  peer: { id },
174
175
  };