@dxos/react-ui-editor 0.3.10-main.560f82b → 0.3.10-main.5e11230

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 (28) hide show
  1. package/dist/lib/browser/index.mjs +328 -260
  2. package/dist/lib/browser/index.mjs.map +3 -3
  3. package/dist/lib/browser/meta.json +1 -1
  4. package/dist/types/src/components/TextEditor/MarkdownEditor.stories.d.ts.map +1 -1
  5. package/dist/types/src/components/TextEditor/TextEditor.d.ts.map +1 -1
  6. package/dist/types/src/components/TextEditor/extensions/blast.d.ts.map +1 -1
  7. package/dist/types/src/components/TextEditor/extensions/comments.d.ts +1 -1
  8. package/dist/types/src/components/TextEditor/extensions/comments.d.ts.map +1 -1
  9. package/dist/types/src/components/TextEditor/extensions/experimental.d.ts +1 -1
  10. package/dist/types/src/components/TextEditor/extensions/experimental.d.ts.map +1 -1
  11. package/dist/types/src/components/TextEditor/extensions/markdown/bundle.d.ts +7 -0
  12. package/dist/types/src/components/TextEditor/extensions/markdown/bundle.d.ts.map +1 -1
  13. package/dist/types/src/components/TextEditor/extensions/markdown/highlight.d.ts +3 -1
  14. package/dist/types/src/components/TextEditor/extensions/markdown/highlight.d.ts.map +1 -1
  15. package/dist/types/src/components/TextEditor/themes/default.d.ts.map +1 -1
  16. package/package.json +20 -20
  17. package/src/components/TextEditor/MarkdownEditor.stories.tsx +14 -12
  18. package/src/components/TextEditor/TextEditor.tsx +4 -22
  19. package/src/components/TextEditor/extensions/blast.ts +13 -1
  20. package/src/components/TextEditor/extensions/comments.ts +21 -39
  21. package/src/components/TextEditor/extensions/experimental.tsx +14 -35
  22. package/src/components/TextEditor/extensions/image.ts +4 -5
  23. package/src/components/TextEditor/extensions/link.ts +2 -2
  24. package/src/components/TextEditor/extensions/markdown/bundle.ts +35 -42
  25. package/src/components/TextEditor/extensions/markdown/highlight.ts +37 -11
  26. package/src/components/TextEditor/extensions/table.ts +47 -48
  27. package/src/components/TextEditor/extensions/tasklist.ts +8 -8
  28. package/src/components/TextEditor/themes/default.ts +37 -0
@@ -18,25 +18,21 @@ import {
18
18
  import { debounce } from '@dxos/async';
19
19
  import { invariant } from '@dxos/invariant';
20
20
 
21
- // 1. TODO(burdon): Make atomic (for tasklist also).
22
- // - https://discuss.codemirror.net/t/easily-track-remove-content-with-decorations/4606
23
- // - https://discuss.codemirror.net/t/creating-atomic-replace-decorations/2961
24
- // - https://codemirror.net/docs/ref/#state.EditorState%5EtransactionFilter (transaction filter: move, delete).
25
- // 2. TODO(burdon): Create/track threads in composer (change global state). Select/follow. Scroll with page.
26
- // - Separate from chat/search.
27
- // 3. TODO(burdon): Support multiple threads in sidebar?
28
- // 4. TODO(burdon): Anchor AI thread when creating section.
29
- // 5. TODO(burdon): Button to resolve thread to close comments.
30
-
31
- // 6. TODO(burdon): Perf: Update existing rangeset.
32
- // https://discuss.codemirror.net/t/rangeset-with-metadata-and-different-decorations/3874
33
-
34
- // TODO(burdon): Import note (performance):
35
- // Easily track & remove content with decorations
36
- // https://discuss.codemirror.net/t/easily-track-remove-content-with-decorations/4606
21
+ // TODO(burdon): Reconcile with theme.
22
+ const styles = EditorView.baseTheme({
23
+ '& .cm-bookmark': {
24
+ cursor: 'pointer',
25
+ margin: '4px',
26
+ padding: '4px',
27
+ backgroundColor: 'yellow',
28
+ },
29
+ '& .cm-bookmark-selected': {
30
+ backgroundColor: 'orange',
31
+ },
32
+ });
37
33
 
38
34
  class BookmarkWidget extends WidgetType {
39
- constructor(private readonly _pos: number, private readonly _id: string) {
35
+ constructor(private readonly _pos: number, private readonly _id: string, private readonly _handleClick: () => void) {
40
36
  super();
41
37
  invariant(this._id);
42
38
  }
@@ -48,29 +44,15 @@ class BookmarkWidget extends WidgetType {
48
44
  override toDOM() {
49
45
  const span = document.createElement('span');
50
46
  span.className = 'cm-bookmark';
51
- // TODO(burdon): Call out to react?
52
- // https://emojifinder.com/comment
53
- span.textContent = '💬';
47
+ span.textContent = '§';
48
+ span.onclick = () => this._handleClick();
54
49
  return span;
55
50
  }
56
51
  }
57
52
 
58
- // TODO(burdon): Reconcile with theme.
59
- const styles = EditorView.baseTheme({
60
- '& .cm-bookmark': {
61
- cursor: 'pointer',
62
- margin: '4px',
63
- padding: '4px',
64
- backgroundColor: 'yellow',
65
- },
66
- '& .cm-bookmark-selected': {
67
- backgroundColor: 'orange',
68
- },
69
- });
70
-
71
53
  type CommentsInfo = {
72
54
  active?: string;
73
- items: { id: string; pos: number; location: Rect | null }[];
55
+ items?: { id: string; pos: number; location: Rect | null }[];
74
56
  };
75
57
 
76
58
  export type CommentsOptions = {
@@ -91,7 +73,7 @@ export const comments = (options: CommentsOptions = {}): Extension => {
91
73
  const id = match[1];
92
74
  return Decoration.replace({
93
75
  id,
94
- widget: new BookmarkWidget(pos, id),
76
+ widget: new BookmarkWidget(pos, id, () => handleUpdate({ active: id })),
95
77
  });
96
78
  },
97
79
  });
@@ -116,7 +98,7 @@ export const comments = (options: CommentsOptions = {}): Extension => {
116
98
  },
117
99
  );
118
100
 
119
- const doUpdate = debounce((data: CommentsInfo) => {
101
+ const handleUpdate = debounce((data: CommentsInfo) => {
120
102
  options.onUpdate?.(data);
121
103
  }, 200);
122
104
 
@@ -158,8 +140,8 @@ export const comments = (options: CommentsOptions = {}): Extension => {
158
140
  const decorations: { from: number; to: number; value: Decoration }[] = [];
159
141
  const rangeSet = view.plugin(bookmarks)?.bookmarks;
160
142
  let closest = Infinity;
161
- // TODO(burdon): Always shows entire document?
162
- const { from, to } = view.visibleRanges?.[0] ?? { from: 0, to: view.state.doc.length };
143
+ // TODO(burdon): Handle multiple visible ranges in large documents.
144
+ const { from, to } = /* view.visibleRanges?.[0] ?? */ { from: 0, to: view.state.doc.length };
163
145
  rangeSet?.between(from, to, (from, to, value) => {
164
146
  decorations.push({ from, to, value });
165
147
  const d = Math.min(Math.abs(pos - from), Math.abs(pos - to));
@@ -171,7 +153,7 @@ export const comments = (options: CommentsOptions = {}): Extension => {
171
153
  });
172
154
 
173
155
  if (decorations.length) {
174
- doUpdate({
156
+ handleUpdate({
175
157
  active,
176
158
  items: decorations.map(
177
159
  ({
@@ -13,28 +13,21 @@ import {
13
13
  type ViewUpdate,
14
14
  } from '@codemirror/view';
15
15
 
16
- // TODO(burdon): Modes: parallel parsing and decoration (e.g., associated with language).
17
- // TODO(burdon): Add-on: runmode: run lexer over content (with rendering codemirror).
18
- // https://codemirror.net/5/doc/manual.html#addon_runmode
19
- // TODO(burdon): Add-on: dialog.
20
- // TODO(burdon): Comments: https://codemirror.net/5/doc/manual.html#setBookmark
21
- // TODO(burdon): Split view: https://codemirror.net/examples/split
22
- // TODO(burdon): https://codemirror.net/5/demo/simplemode.html
23
-
24
- // https://codemirror.net/examples/autocompletion
25
- // https://codemirror.net/docs/ref/#autocomplete.autocompletion
26
- // https://codemirror.net/docs/ref/#autocomplete.Completion
27
-
28
- // TODO(burdon): Hint to customize?
29
- // https://codemirror.net/examples/autocompletion
30
-
31
- // Notes: Can't edit widget content (or cursor nav through them).
32
- // - https://discuss.codemirror.net/t/focusing-inputs-within-widgets/5178/7
33
- // Widgets intentionally always get set to contenteditable=false,
34
- // or they would become part of CodeMirror’s editable content element.
35
- // You should be able to introduce new contenteditable=true child elements inside of them.
16
+ // TODO(burdon): Reconcile with theme.
17
+ export const styles = EditorView.baseTheme({
18
+ '.cm-hyperlink-label': {
19
+ cursor: 'pointer',
20
+ textDecoration: 'underline',
21
+ },
22
+ '.cm-hyperlink-bracket': {
23
+ opacity: 0.2,
24
+ },
25
+ '.cm-hyperlink-url': {
26
+ opacity: 0.5,
27
+ fontFamily: 'monospace',
28
+ },
29
+ });
36
30
 
37
- // TODO(burdon): URL match?
38
31
  const markdownLinkRegexp = /\[([^\]]+)]\(([^)]+)\)(!?)/gi;
39
32
 
40
33
  /**
@@ -118,18 +111,4 @@ export function hyperLinkExtension() {
118
111
  );
119
112
  }
120
113
 
121
- export const styles = EditorView.baseTheme({
122
- '.cm-hyperlink-label': {
123
- cursor: 'pointer',
124
- textDecoration: 'underline',
125
- },
126
- '.cm-hyperlink-bracket': {
127
- opacity: 0.2,
128
- },
129
- '.cm-hyperlink-url': {
130
- opacity: 0.5,
131
- fontFamily: 'monospace',
132
- },
133
- });
134
-
135
114
  export const hyperlinkWidget: Extension = [hyperLinkExtension(), styles];
@@ -25,19 +25,19 @@ export const image = (options: ImageOptions = {}): Extension => {
25
25
 
26
26
  const update = (state: EditorState, options: ImageOptions) => {
27
27
  const builder = new RangeSetBuilder();
28
+ const cursor = state.selection.main.head;
28
29
 
29
- // TODO(burdon): If not-readonly then show text.
30
- // if (state.readOnly) {
31
30
  syntaxTree(state).iterate({
32
31
  enter: (node) => {
33
32
  if (node.name === 'Image') {
34
33
  const urlNode = node.node.getChild('URL');
35
34
  if (urlNode) {
35
+ const hide = state.readOnly || cursor < node.from || cursor > node.to;
36
36
  const url = state.sliceDoc(urlNode.from, urlNode.to);
37
37
  builder.add(
38
- state.readOnly ? node.from : node.to,
38
+ hide ? node.from : node.to,
39
39
  node.to,
40
- Decoration.widget({
40
+ Decoration.replace({
41
41
  widget: new ImageWidget(url),
42
42
  }),
43
43
  );
@@ -45,7 +45,6 @@ const update = (state: EditorState, options: ImageOptions) => {
45
45
  }
46
46
  },
47
47
  });
48
- // }
49
48
 
50
49
  return builder.finish();
51
50
  };
@@ -94,20 +94,20 @@ class LinkText extends WidgetType {
94
94
  const update = (state: EditorState, options: LinkOptions) => {
95
95
  const builder = new RangeSetBuilder();
96
96
  const cursor = state.selection.main.head;
97
+
97
98
  syntaxTree(state).iterate({
98
99
  enter: (node) => {
99
100
  // Check if cursor is inside text.
100
101
  if (node.name === 'Link') {
101
102
  const marks = node.node.getChildren('LinkMark');
102
103
  const text = marks.length >= 2 ? state.sliceDoc(marks[0].to, marks[1].from) : '';
103
-
104
104
  const urlNode = node.node.getChild('URL');
105
105
  const url = urlNode ? state.sliceDoc(urlNode.from, urlNode.to) : '';
106
106
  if (!url) {
107
107
  return false;
108
108
  }
109
109
 
110
- if (cursor < node.from || cursor > node.to) {
110
+ if (state.readOnly || cursor < node.from || cursor > node.to) {
111
111
  builder.add(
112
112
  node.from,
113
113
  node.to,
@@ -3,29 +3,18 @@
3
3
  //
4
4
 
5
5
  import { closeBrackets, closeBracketsKeymap } from '@codemirror/autocomplete';
6
- import { defaultKeymap, history, historyKeymap, indentWithTab } from '@codemirror/commands';
6
+ import { history, historyKeymap, indentWithTab, standardKeymap } from '@codemirror/commands';
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
- import {
14
- crosshairCursor,
15
- drawSelection,
16
- dropCursor,
17
- EditorView,
18
- highlightActiveLine,
19
- highlightActiveLineGutter,
20
- highlightSpecialChars,
21
- keymap,
22
- placeholder,
23
- rectangularSelection,
24
- } from '@codemirror/view';
13
+ import { crosshairCursor, drawSelection, dropCursor, EditorView, keymap, placeholder } from '@codemirror/view';
25
14
 
26
15
  import type { ThemeMode } from '@dxos/react-ui';
27
16
 
28
- import { markdownHighlightStyle, markdownTagsExtension } from './highlight';
17
+ import { markdownHighlightStyle, markdownTagsExtensions } from './highlight';
29
18
 
30
19
  export type MarkdownBundleOptions = {
31
20
  readonly?: boolean;
@@ -33,45 +22,34 @@ export type MarkdownBundleOptions = {
33
22
  placeholder?: string;
34
23
  };
35
24
 
25
+ /**
26
+ * Markdown bundle.
27
+ * Refs:
28
+ * https://github.com/codemirror/basic-setup
29
+ * https://codemirror.net/docs/community
30
+ * https://codemirror.net/docs/ref/#codemirror.basicSetup
31
+ */
32
+ // TODO(burdon): Add Composer here: https://codemirror.net/docs/community
36
33
  export const markdownBundle = ({
37
34
  readonly,
38
35
  themeMode,
39
36
  placeholder: _placeholder,
40
37
  }: 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
38
  return [
44
- bracketMatching(),
45
- closeBrackets(),
46
- _placeholder && placeholder(_placeholder),
47
-
48
39
  EditorState.allowMultipleSelections.of(true),
40
+ EditorState.tabSize.of(2),
49
41
  EditorView.lineWrapping,
50
42
 
43
+ customKeymap(),
44
+
45
+ bracketMatching(),
46
+ closeBrackets(),
51
47
  crosshairCursor(),
52
48
  dropCursor(),
53
49
  drawSelection(),
54
- highlightActiveLine(),
55
- highlightActiveLineGutter(),
56
- highlightSelectionMatches(),
57
- highlightSpecialChars(),
58
50
  history(),
59
51
  indentOnInput(),
60
- rectangularSelection(),
61
-
62
- // https://codemirror.net/docs/ref/#view.keymap
63
- keymap.of([
64
- // https://codemirror.net/docs/ref/#commands.defaultKeymap
65
- ...defaultKeymap,
66
- // https://codemirror.net/docs/ref/#commands.historyKeymap
67
- ...historyKeymap,
68
- // https://codemirror.net/docs/ref/#autocomplete.closeBracketsKeymap
69
- ...closeBracketsKeymap,
70
- // https://codemirror.net/docs/ref/#search.searchKeymap
71
- ...searchKeymap,
72
- // https://codemirror.net/docs/ref/#commands.indentWithTab
73
- indentWithTab,
74
- ]),
52
+ _placeholder && placeholder(_placeholder),
75
53
 
76
54
  // Main extension.
77
55
  // https://github.com/codemirror/lang-markdown
@@ -89,8 +67,8 @@ export const markdownBundle = ({
89
67
 
90
68
  // Parser extensions.
91
69
  extensions: [
92
- // GFM, // TODO(burdon): Provided by default?
93
- markdownTagsExtension,
70
+ // GFM provided by default.
71
+ markdownTagsExtensions,
94
72
  ],
95
73
  }),
96
74
 
@@ -101,3 +79,18 @@ export const markdownBundle = ({
101
79
  syntaxHighlighting(markdownHighlightStyle(readonly)),
102
80
  ].filter(Boolean) as Extension[];
103
81
  };
82
+
83
+ // https://codemirror.net/docs/ref/#view.keymap
84
+ const customKeymap = () =>
85
+ keymap.of([
86
+ // https://codemirror.net/docs/ref/#commands.indentWithTab
87
+ indentWithTab,
88
+ // https://codemirror.net/docs/ref/#commands.standardKeymap
89
+ ...standardKeymap,
90
+ // https://codemirror.net/docs/ref/#commands.historyKeymap
91
+ ...historyKeymap,
92
+ // https://codemirror.net/docs/ref/#autocomplete.closeBracketsKeymap
93
+ ...closeBracketsKeymap,
94
+ // https://codemirror.net/docs/ref/#search.searchKeymap
95
+ ...searchKeymap,
96
+ ]);
@@ -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,60 @@ 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
+ // TODO(burdon): Use remark?: https://www.npmjs.com/package/remark-gfm
46
+ // TODO(burdon): Style in monospace:
47
+ // https://discuss.codemirror.net/t/markdown-table-highlighting/658
48
+ // https://github.com/lezer-parser/markdown/blob/main/src/extension.ts
49
+ syntaxTree(state).iterate({
50
+ enter: (node) => {
51
+ // Check if cursor is inside text.
52
+ switch (node.name) {
53
+ case 'Table': {
54
+ tables.push({ from: node.from, to: node.to });
55
+ break;
56
+ }
57
+ case 'TableHeader': {
58
+ getTable().header = [];
59
+ break;
60
+ }
61
+ case 'TableRow': {
62
+ (getTable().rows ??= []).push([]);
63
+ break;
64
+ }
65
+ case 'TableCell': {
66
+ const row = getRow();
67
+ if (row) {
68
+ row.push(state.sliceDoc(node.from, node.to));
69
+ } else {
70
+ getTable().header?.push(state.sliceDoc(node.from, node.to));
74
71
  }
72
+ break;
75
73
  }
76
- },
77
- });
74
+ }
75
+ },
76
+ });
78
77
 
79
- tables.forEach((table) =>
80
- builder.add(table.from, table.to!, Decoration.replace({ widget: new TableWidget(table) })),
81
- );
82
- }
78
+ tables.forEach((table) => {
79
+ const hide = state.readOnly || cursor < table.from || cursor > table.to;
80
+ hide && builder.add(table.from, table.to!, Decoration.replace({ widget: new TableWidget(table) }));
81
+ });
83
82
 
84
83
  return builder.finish();
85
84
  };
@@ -98,11 +97,11 @@ class TableWidget extends WidgetType {
98
97
 
99
98
  {
100
99
  const header = table.appendChild(document.createElement('thead'));
101
- const row = header.appendChild(document.createElement('tr'));
100
+ const tr = header.appendChild(document.createElement('tr'));
102
101
  this._table.header?.forEach((cell) => {
103
102
  const th = document.createElement('th');
104
103
  th.setAttribute('class', 'cm-table-head');
105
- row.appendChild(th).textContent = cell;
104
+ tr.appendChild(th).textContent = cell;
106
105
  });
107
106
  }
108
107
 
@@ -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 = {}) => {
@@ -203,6 +203,43 @@ export const defaultTheme: {
203
203
  acc[`& .text-${fontSize} + .cm-widgetBuffer + .cm-ySelectionCaret`] = { height };
204
204
  return acc;
205
205
  }, {}),
206
+
207
+ /**
208
+ * Panels
209
+ * https://github.com/codemirror/search/blob/main/src/search.ts#L745
210
+ *
211
+ * Find/replace panel.
212
+ * <div class="cm-announced">...</div>
213
+ * <div class="cm-scroller">...</div>
214
+ * <div class="cm-panels cm-panels-bottom">
215
+ * <div class="cm-search cm-panel">
216
+ * <input class="cm-textfield" />
217
+ * <button class="cm-button">...</button>
218
+ * <label><input type="checkbox" />...</label>
219
+ * </div>
220
+ * </div
221
+ */
222
+ '& .cm-panels': {
223
+ border: `1px solid ${get(tokens, 'extend.colors.neutral.200')}`,
224
+ borderBottom: 'none',
225
+ },
226
+ '& .cm-panel': {
227
+ background: get(tokens, 'extend.colors.neutral.50'),
228
+ fontFamily: get(tokens, 'fontFamily.body', []).join(','),
229
+ },
230
+ '& .cm-button': {
231
+ margin: '4px',
232
+ fontFamily: get(tokens, 'fontFamily.body', []).join(','),
233
+ background: get(tokens, 'extend.colors.neutral.100'),
234
+ border: 'none',
235
+ },
236
+
237
+ '& .cm-searchMatch': {
238
+ backgroundColor: get(tokens, 'extend.colors.yellow.100'),
239
+ },
240
+ '& .cm-searchMatch.cm-searchMatch-selected': {
241
+ backgroundColor: get(tokens, 'extend.colors.primary.100'),
242
+ },
206
243
  };
207
244
 
208
245
  export const textTheme: {