@dxos/ui-editor 0.8.4-main.bc674ce → 0.8.4-main.bcb3aa67d6

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 (76) hide show
  1. package/dist/lib/browser/index.mjs +711 -521
  2. package/dist/lib/browser/index.mjs.map +4 -4
  3. package/dist/lib/browser/meta.json +1 -1
  4. package/dist/lib/node-esm/index.mjs +711 -521
  5. package/dist/lib/node-esm/index.mjs.map +4 -4
  6. package/dist/lib/node-esm/meta.json +1 -1
  7. package/dist/types/src/defaults.d.ts +3 -10
  8. package/dist/types/src/defaults.d.ts.map +1 -1
  9. package/dist/types/src/extensions/auto-scroll.d.ts +6 -0
  10. package/dist/types/src/extensions/auto-scroll.d.ts.map +1 -0
  11. package/dist/types/src/extensions/automerge/automerge.d.ts.map +1 -1
  12. package/dist/types/src/extensions/factories.d.ts.map +1 -1
  13. package/dist/types/src/extensions/folding.d.ts.map +1 -1
  14. package/dist/types/src/extensions/index.d.ts +3 -2
  15. package/dist/types/src/extensions/index.d.ts.map +1 -1
  16. package/dist/types/src/extensions/markdown/bundle.d.ts +3 -0
  17. package/dist/types/src/extensions/markdown/bundle.d.ts.map +1 -1
  18. package/dist/types/src/extensions/markdown/decorate.d.ts.map +1 -1
  19. package/dist/types/src/extensions/markdown/highlight.d.ts.map +1 -1
  20. package/dist/types/src/extensions/markdown/link.d.ts.map +1 -1
  21. package/dist/types/src/extensions/markdown/styles.d.ts.map +1 -1
  22. package/dist/types/src/extensions/preview/preview.d.ts +3 -1
  23. package/dist/types/src/extensions/preview/preview.d.ts.map +1 -1
  24. package/dist/types/src/extensions/scroll-past-end.d.ts +3 -0
  25. package/dist/types/src/extensions/scroll-past-end.d.ts.map +1 -0
  26. package/dist/types/src/extensions/scroller.d.ts +66 -0
  27. package/dist/types/src/extensions/scroller.d.ts.map +1 -0
  28. package/dist/types/src/extensions/tags/streamer.d.ts +1 -1
  29. package/dist/types/src/styles/index.d.ts +0 -2
  30. package/dist/types/src/styles/index.d.ts.map +1 -1
  31. package/dist/types/src/styles/theme.d.ts +15 -0
  32. package/dist/types/src/styles/theme.d.ts.map +1 -1
  33. package/dist/types/src/util/cursor.d.ts +1 -1
  34. package/dist/types/src/util/cursor.d.ts.map +1 -1
  35. package/dist/types/tsconfig.tsbuildinfo +1 -1
  36. package/package.json +32 -32
  37. package/src/defaults.ts +19 -21
  38. package/src/extensions/annotations.ts +1 -1
  39. package/src/extensions/auto-scroll.ts +129 -0
  40. package/src/extensions/automerge/automerge.test.tsx +2 -2
  41. package/src/extensions/automerge/automerge.ts +6 -5
  42. package/src/extensions/blocks.ts +5 -5
  43. package/src/extensions/comments.ts +5 -5
  44. package/src/extensions/dnd.ts +2 -2
  45. package/src/extensions/factories.ts +7 -8
  46. package/src/extensions/folding.ts +3 -20
  47. package/src/extensions/index.ts +3 -2
  48. package/src/extensions/markdown/bundle.ts +23 -9
  49. package/src/extensions/markdown/decorate.ts +15 -11
  50. package/src/extensions/markdown/highlight.ts +15 -7
  51. package/src/extensions/markdown/link.ts +27 -33
  52. package/src/extensions/markdown/parser.test.ts +0 -1
  53. package/src/extensions/markdown/styles.ts +36 -9
  54. package/src/extensions/markdown/table.ts +24 -2
  55. package/src/extensions/outliner/outliner.ts +3 -3
  56. package/src/extensions/preview/preview.ts +62 -15
  57. package/src/extensions/scroll-past-end.ts +32 -0
  58. package/src/extensions/scroller.ts +233 -0
  59. package/src/extensions/selection.ts +1 -1
  60. package/src/extensions/tags/streamer.ts +2 -2
  61. package/src/extensions/tags/xml-tags.ts +7 -4
  62. package/src/styles/index.ts +0 -2
  63. package/src/styles/theme.ts +116 -34
  64. package/src/util/cursor.ts +1 -1
  65. package/dist/types/src/extensions/autoscroll.d.ts +0 -20
  66. package/dist/types/src/extensions/autoscroll.d.ts.map +0 -1
  67. package/dist/types/src/extensions/scrolling.d.ts +0 -78
  68. package/dist/types/src/extensions/scrolling.d.ts.map +0 -1
  69. package/dist/types/src/styles/markdown.d.ts +0 -8
  70. package/dist/types/src/styles/markdown.d.ts.map +0 -1
  71. package/dist/types/src/styles/tokens.d.ts +0 -3
  72. package/dist/types/src/styles/tokens.d.ts.map +0 -1
  73. package/src/extensions/autoscroll.ts +0 -165
  74. package/src/extensions/scrolling.ts +0 -189
  75. package/src/styles/markdown.ts +0 -26
  76. package/src/styles/tokens.ts +0 -17
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dxos/ui-editor",
3
- "version": "0.8.4-main.bc674ce",
3
+ "version": "0.8.4-main.bcb3aa67d6",
4
4
  "description": "Text editor components.",
5
5
  "homepage": "https://dxos.org",
6
6
  "bugs": "https://github.com/dxos/dxos/issues",
@@ -58,35 +58,35 @@
58
58
  "@replit/codemirror-vim": "^6.2.1",
59
59
  "@replit/codemirror-vscode-keymap": "^6.0.2",
60
60
  "@uiw/codemirror-theme-vscode": "^4.25.2",
61
- "ajv": "^8.17.1",
61
+ "ajv": "^8.18.0",
62
62
  "codemirror": "^6.0.1",
63
63
  "lib0": "^0.2.65",
64
64
  "lodash.defaultsdeep": "^4.6.1",
65
65
  "lodash.merge": "^4.6.2",
66
66
  "lodash.sortby": "^4.7.0",
67
67
  "style-mod": "^4.1.0",
68
- "@dxos/async": "0.8.4-main.bc674ce",
69
- "@dxos/client": "0.8.4-main.bc674ce",
70
- "@dxos/app-graph": "0.8.4-main.bc674ce",
71
- "@dxos/context": "0.8.4-main.bc674ce",
72
- "@dxos/display-name": "0.8.4-main.bc674ce",
73
- "@dxos/echo": "0.8.4-main.bc674ce",
74
- "@dxos/invariant": "0.8.4-main.bc674ce",
75
- "@dxos/echo-db": "0.8.4-main.bc674ce",
76
- "@dxos/debug": "0.8.4-main.bc674ce",
77
- "@dxos/lit-ui": "0.8.4-main.bc674ce",
78
- "@dxos/ui": "0.8.4-main.bc674ce",
79
- "@dxos/protocols": "0.8.4-main.bc674ce",
80
- "@dxos/log": "0.8.4-main.bc674ce",
81
- "@dxos/ui-theme": "0.8.4-main.bc674ce",
82
- "@dxos/ui-types": "0.8.4-main.bc674ce",
83
- "@dxos/util": "0.8.4-main.bc674ce"
68
+ "@dxos/app-graph": "0.8.4-main.bcb3aa67d6",
69
+ "@dxos/context": "0.8.4-main.bcb3aa67d6",
70
+ "@dxos/debug": "0.8.4-main.bcb3aa67d6",
71
+ "@dxos/display-name": "0.8.4-main.bcb3aa67d6",
72
+ "@dxos/client": "0.8.4-main.bcb3aa67d6",
73
+ "@dxos/echo": "0.8.4-main.bcb3aa67d6",
74
+ "@dxos/invariant": "0.8.4-main.bcb3aa67d6",
75
+ "@dxos/lit-ui": "0.8.4-main.bcb3aa67d6",
76
+ "@dxos/log": "0.8.4-main.bcb3aa67d6",
77
+ "@dxos/protocols": "0.8.4-main.bcb3aa67d6",
78
+ "@dxos/ui": "0.8.4-main.bcb3aa67d6",
79
+ "@dxos/echo-db": "0.8.4-main.bcb3aa67d6",
80
+ "@dxos/util": "0.8.4-main.bcb3aa67d6",
81
+ "@dxos/ui-theme": "0.8.4-main.bcb3aa67d6",
82
+ "@dxos/ui-types": "0.8.4-main.bcb3aa67d6",
83
+ "@dxos/async": "0.8.4-main.bcb3aa67d6"
84
84
  },
85
85
  "devDependencies": {
86
86
  "@automerge/automerge": "3.2.3",
87
87
  "@automerge/automerge-repo": "2.5.1",
88
88
  "@automerge/automerge-repo-network-broadcastchannel": "2.5.1",
89
- "@effect/platform": "0.93.6",
89
+ "@effect/platform": "0.94.4",
90
90
  "@types/chai": "^4.2.15",
91
91
  "@types/chai-dom": "^1.11.0",
92
92
  "@types/lodash.defaultsdeep": "^4.6.6",
@@ -94,25 +94,25 @@
94
94
  "@types/lodash.sortby": "^4.7.7",
95
95
  "chai": "^4.4.1",
96
96
  "chai-dom": "^1.11.0",
97
- "effect": "3.19.11",
98
- "happy-dom": "^13.3.1",
97
+ "effect": "3.20.0",
98
+ "happy-dom": "^20.0.0",
99
99
  "jsdom": "^27.0.0",
100
100
  "mocha": "^10.6.0",
101
- "vite": "7.1.9",
101
+ "vite": "^7.1.11",
102
102
  "vite-plugin-top-level-await": "^1.6.0",
103
103
  "vite-plugin-wasm": "^3.5.0",
104
- "@dxos/config": "0.8.4-main.bc674ce",
105
- "@dxos/echo": "0.8.4-main.bc674ce",
106
- "@dxos/keyboard": "0.8.4-main.bc674ce",
107
- "@dxos/random": "0.8.4-main.bc674ce",
108
- "@dxos/schema": "0.8.4-main.bc674ce",
109
- "@dxos/ui-theme": "0.8.4-main.bc674ce",
110
- "@dxos/storybook-utils": "0.8.4-main.bc674ce"
104
+ "@dxos/echo": "0.8.4-main.bcb3aa67d6",
105
+ "@dxos/schema": "0.8.4-main.bcb3aa67d6",
106
+ "@dxos/random": "0.8.4-main.bcb3aa67d6",
107
+ "@dxos/storybook-utils": "0.8.4-main.bcb3aa67d6",
108
+ "@dxos/ui-theme": "0.8.4-main.bcb3aa67d6",
109
+ "@dxos/keyboard": "0.8.4-main.bcb3aa67d6",
110
+ "@dxos/config": "0.8.4-main.bcb3aa67d6"
111
111
  },
112
112
  "peerDependencies": {
113
- "@effect/platform": "0.93.6",
114
- "effect": "3.19.11",
115
- "@dxos/ui-theme": "0.8.4-main.bc674ce"
113
+ "@effect/platform": "0.94.4",
114
+ "effect": "3.20.0",
115
+ "@dxos/ui-theme": "0.8.4-main.bcb3aa67d6"
116
116
  },
117
117
  "publishConfig": {
118
118
  "access": "public"
package/src/defaults.ts CHANGED
@@ -6,29 +6,27 @@ import { mx } from '@dxos/ui-theme';
6
6
 
7
7
  import { type ThemeExtensionsOptions } from './extensions';
8
8
 
9
- /**
10
- * CodeMirror content width.
11
- * 40rem = 640px. Corresponds to initial plank width (Google docs, Stashpad, etc.)
12
- * 50rem = 800px. Maximum content width for solo mode.
13
- * NOTE: Max width - 4rem = 2rem left/right margin (or 2rem gutter plus 1rem left/right margin).
14
- */
15
- export const editorWidth = '!mli-auto is-full max-is-[min(50rem,100%-4rem)]';
9
+ // NOTE: Padding is added to the editor to account for the focus ring (since otherwise the CM gutter will clip it)
10
+ export const editorClassNames = (role?: string) =>
11
+ mx(
12
+ 'dx-attention-surface p-0.5 data-[toolbar=disabled]:pt-2 dx-focus-ring-inset',
13
+ role === 'section' ? '[&_.cm-scroller]:overflow-hidden [&_.cm-scroller]:min-h-24' : 'dx-container overflow-hidden',
14
+ );
16
15
 
17
- export const editorSlots: ThemeExtensionsOptions['slots'] = {
18
- scroll: {
19
- className: 'pbs-2',
20
- },
16
+ export const documentSlots: ThemeExtensionsOptions['slots'] = {
21
17
  content: {
22
- className: editorWidth,
18
+ /**
19
+ * CodeMirror content width.
20
+ * 40rem = 640px. Corresponds to initial plank width (Google docs, Stashpad, etc.)
21
+ * 50rem = 800px. Maximum content width for solo mode.
22
+ * NOTE: Max width - 4rem = 2rem left/right margin (or 2rem gutter plus 1rem left/right margin).
23
+ */
24
+ className: 'mx-auto! w-full pointer-fine:max-w-[min(50rem,100%-4rem)] pointer-coarse:max-w-[min(50rem,100%-2rem)]',
23
25
  },
24
26
  };
25
27
 
26
- export const editorWithToolbarLayout =
27
- 'grid grid-cols-1 grid-rows-[min-content_1fr] data-[toolbar=disabled]:grid-rows-[1fr] justify-center content-start overflow-hidden';
28
-
29
- // NOTE: Padding is added to the editor to account for the focus ring (since otherwise the CM gutter will clip it)
30
- export const stackItemContentEditorClassNames = (role?: string) =>
31
- mx(
32
- 'p-0.5 dx-focus-ring-inset attention-surface data-[toolbar=disabled]:pbs-2',
33
- role === 'section' ? '[&_.cm-scroller]:overflow-hidden [&_.cm-scroller]:min-bs-24' : 'min-bs-0',
34
- );
28
+ export const compactSlots: ThemeExtensionsOptions['slots'] = {
29
+ content: {
30
+ className: 'mx-2! w-full',
31
+ },
32
+ };
@@ -48,7 +48,7 @@ export const annotations = ({ match }: AnnotationOptions = {}): Extension => {
48
48
  '.cm-annotation': {
49
49
  textDecoration: 'underline',
50
50
  textDecorationStyle: 'wavy',
51
- textDecorationColor: 'var(--dx-errorText)',
51
+ textDecorationColor: 'var(--color-error-text)',
52
52
  },
53
53
  }),
54
54
  ];
@@ -0,0 +1,129 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ import { EditorView, ViewPlugin } from '@codemirror/view';
6
+
7
+ import { addEventListener, combine, throttle } from '@dxos/async';
8
+ import { Domino } from '@dxos/ui';
9
+
10
+ import { scrollerCrawlEffect, scrollerLineEffect } from './scroller';
11
+ import { getSize } from '@dxos/ui-theme';
12
+
13
+ export type AutoScrollToProps = {};
14
+
15
+ /**
16
+ * Extension that supports pinning the scroll position and automatically scrolls to the bottom when content is added.
17
+ */
18
+ export const autoScroll = (_: AutoScrollToProps = {}) => {
19
+ let buttonContainer: HTMLDivElement | undefined;
20
+ let isPinned = true;
21
+
22
+ const setPinned = (pinned: boolean) => {
23
+ buttonContainer?.classList.toggle('opacity-0', pinned);
24
+ isPinned = pinned;
25
+ };
26
+
27
+ return [
28
+ // Update listener for logging when scrolling is needed.
29
+ EditorView.updateListener.of(({ view, heightChanged, state }) => {
30
+ // Maybe scroll if doc changed and pinned.
31
+ // NOTE: Geometry changed is triggered when widgets change height (e.g., toggle tool block).
32
+ if (heightChanged) {
33
+ if (isPinned) {
34
+ // NOTE: Use scroll geometry instead of coordsAtPos to avoid forced layout/scroll side-effects.
35
+ const { scrollTop, scrollHeight, clientHeight } = view.scrollDOM;
36
+ const delta = scrollHeight - scrollTop - clientHeight;
37
+ if (delta > 0 && scrollTop > 0) {
38
+ setPinned(true);
39
+ view.dispatch({
40
+ effects: scrollerCrawlEffect.of(true),
41
+ });
42
+ } else if (delta < 0) {
43
+ setPinned(false);
44
+ }
45
+ } else {
46
+ // TODO(burdon): Should re-pin if content shrinks.
47
+ if (state.doc.length === 0) {
48
+ setPinned(true);
49
+ }
50
+ }
51
+ }
52
+ }),
53
+
54
+ // Detect user scroll and unpin (or re-pin if scrolled to the bottom).
55
+ ViewPlugin.fromClass(
56
+ class {
57
+ private readonly cleanup: () => void;
58
+ constructor(view: EditorView) {
59
+ this.cleanup = createUserScrollDetector(
60
+ view.scrollDOM,
61
+ throttle(() => {
62
+ requestAnimationFrame(() => {
63
+ const { scrollTop, scrollHeight, clientHeight } = view.scrollDOM;
64
+ const delta = scrollHeight - scrollTop - clientHeight;
65
+ const pinned = delta === 0;
66
+ setPinned(pinned);
67
+ if (!pinned) {
68
+ view.dispatch({ effects: scrollerCrawlEffect.of(false) });
69
+ }
70
+ });
71
+ }, 500),
72
+ );
73
+ }
74
+ destroy() {
75
+ this.cleanup();
76
+ }
77
+ },
78
+ ),
79
+
80
+ // Scroll button.
81
+ ViewPlugin.fromClass(
82
+ class {
83
+ constructor(view: EditorView) {
84
+ const icon = Domino.of('dx-icon' as any)
85
+ .classNames(getSize(4))
86
+ .attributes({ icon: 'ph--arrow-down--regular' });
87
+ const button = Domino.of('button')
88
+ .classNames('dx-button bg-accent-surface')
89
+ .attributes({ 'data-density': 'fine' })
90
+ .children(icon)
91
+ .on('click', () => {
92
+ setPinned(true);
93
+ view.dispatch({
94
+ effects: scrollerLineEffect.of({ line: -1, position: 'end', behavior: 'smooth' }),
95
+ });
96
+ });
97
+
98
+ buttonContainer = Domino.of('div')
99
+ .classNames('cm-scroll-button transition-opacity duration-300 opacity-0')
100
+ .children(button).root as HTMLDivElement;
101
+
102
+ view.scrollDOM.parentElement!.appendChild(buttonContainer);
103
+ }
104
+ },
105
+ ),
106
+ ];
107
+ };
108
+
109
+ /**
110
+ * Attaches listeners to detect genuine user-initiated scrolling on an element.
111
+ * Two sources are covered:
112
+ * - `wheel`: fires only from physical mouse wheel / trackpad gestures.
113
+ * - `pointerdown` on the scrollbar gutter: detected by comparing clientX to
114
+ * the element's clientWidth (the content area, excluding the scrollbar).
115
+ * Returns a cleanup function that removes the listeners.
116
+ */
117
+ // TODO(burdon): Still jumps when widgets are rendered.
118
+ // - Track position of specific element/line in document and scroll relative to that.
119
+ function createUserScrollDetector(element: HTMLElement, onUserScroll: () => void): () => void {
120
+ return combine(
121
+ addEventListener(element, 'wheel', () => onUserScroll(), { passive: true }),
122
+ addEventListener(element, 'pointerdown', (event) => {
123
+ // If the pointer lands beyond the content width it hit the scrollbar gutter.
124
+ if (event.clientX > element.getBoundingClientRect().right - (element.offsetWidth - element.clientWidth)) {
125
+ onUserScroll();
126
+ }
127
+ }),
128
+ );
129
+ }
@@ -9,7 +9,7 @@ import { render, screen } from '@testing-library/react';
9
9
  import React, { type FC, useEffect, useRef, useState } from 'react';
10
10
  import { describe, test } from 'vitest';
11
11
 
12
- import { get } from '@dxos/util';
12
+ import { getDeep } from '@dxos/util';
13
13
 
14
14
  import { automerge } from './automerge';
15
15
 
@@ -46,7 +46,7 @@ const Test: FC<{ handle: DocHandle<TestObject>; generator: Generator }> = ({ han
46
46
  ];
47
47
 
48
48
  const view = new EditorView({
49
- state: EditorState.create({ doc: get(handle.doc()!, path), extensions }),
49
+ state: EditorState.create({ doc: getDeep(handle.doc()!, path), extensions }),
50
50
  parent: ref.current!,
51
51
  });
52
52
 
@@ -72,11 +72,12 @@ export const automerge = (accessor: DocAccessor): Extension => {
72
72
  const value = DocAccessor.getValue<string>(accessor);
73
73
  const current = this._view.state.doc.toString();
74
74
  if (value !== current) {
75
- // TODO(burdon): This attempts to set the initial state, but creates problems.
76
- // this._view.dispatch({
77
- // changes: { from: 0, to: this._view.state.doc.length, insert: value },
78
- // annotations: initialSync,
79
- // });
75
+ // TODO(burdon): This attempts to set the initial state.
76
+ console.warn('ENABLING INITIAL SYNC -- THIS MAY BE A REGRESSION');
77
+ this._view.dispatch({
78
+ changes: { from: 0, to: this._view.state.doc.length, insert: value },
79
+ annotations: initialSync,
80
+ });
80
81
  }
81
82
  });
82
83
  }
@@ -101,11 +101,11 @@ export const blocks = () => [
101
101
  '.cm-line.block-line': {
102
102
  paddingLeft: '0.75rem',
103
103
  paddingRight: '0.75rem',
104
- borderLeft: '1px solid var(--dx-subduedSeparator)',
105
- borderRight: '1px solid var(--dx-subduedSeparator)',
104
+ borderLeft: '1px solid var(--color-subdued-separator)',
105
+ borderRight: '1px solid var(--color-subdued-separator)',
106
106
  },
107
107
  '.cm-line.block-single': {
108
- border: '1px solid var(--dx-subduedSeparator)',
108
+ border: '1px solid var(--color-subdued-separator)',
109
109
  borderRadius: '6px',
110
110
  paddingTop: '0.5rem',
111
111
  paddingBottom: '0.5rem',
@@ -113,7 +113,7 @@ export const blocks = () => [
113
113
  marginBottom: '0.5rem',
114
114
  },
115
115
  '.cm-line.block-first': {
116
- borderTop: '1px solid var(--dx-subduedSeparator)',
116
+ borderTop: '1px solid var(--color-subdued-separator)',
117
117
  borderTopLeftRadius: '6px',
118
118
  borderTopRightRadius: '6px',
119
119
  paddingTop: '0.5rem',
@@ -121,7 +121,7 @@ export const blocks = () => [
121
121
  },
122
122
  '.cm-line.block-middle': {},
123
123
  '.cm-line.block-last': {
124
- borderBottom: '1px solid var(--dx-subduedSeparator)',
124
+ borderBottom: '1px solid var(--color-subdued-separator)',
125
125
  borderBottomLeftRadius: '6px',
126
126
  borderBottomRightRadius: '6px',
127
127
  paddingBottom: '0.5rem',
@@ -103,14 +103,14 @@ export const commentsState = StateField.define<CommentsState>({
103
103
  const styles = EditorView.theme({
104
104
  '.cm-comment, .cm-comment-current': {
105
105
  padding: '3px 0',
106
- color: 'var(--dx-cmCommentText)',
107
- backgroundColor: 'var(--dx-cmCommentSurface)',
106
+ color: 'var(--color-cm-comment-text)',
107
+ backgroundColor: 'var(--color-cm-comment-surface)',
108
108
  },
109
109
  '.cm-comment > span, .cm-comment-current > span': {
110
110
  boxDecorationBreak: 'clone',
111
- boxShadow: '0 0 1px 3px var(--dx-cmCommentSurface)',
112
- backgroundColor: 'var(--dx-cmCommentSurface)',
113
- color: 'var(--dx-cmCommentText)',
111
+ boxShadow: '0 0 1px 3px var(--color-cm-comment-surface)',
112
+ backgroundColor: 'var(--color-cm-comment-surface)',
113
+ color: 'var(--color-cm-comment-text)',
114
114
  cursor: 'pointer',
115
115
  },
116
116
  });
@@ -29,8 +29,8 @@ export const dropFile = (options: DropOptions = {}): Extension => {
29
29
 
30
30
  const styles = EditorView.theme({
31
31
  '.cm-dropCursor': {
32
- borderLeft: '2px solid var(--dx-accentText)',
33
- color: 'var(--dx-accentText)',
32
+ borderLeft: '2px solid var(--color-accent-text)',
33
+ color: 'var(--color-accent-text)',
34
34
  padding: '0 4px',
35
35
  },
36
36
  '.cm-dropCursor:after': {
@@ -17,7 +17,6 @@ import {
17
17
  keymap,
18
18
  lineNumbers,
19
19
  placeholder,
20
- scrollPastEnd,
21
20
  } from '@codemirror/view';
22
21
  import { vscodeDarkStyle, vscodeLightStyle } from '@uiw/codemirror-theme-vscode';
23
22
  import defaultsDeep from 'lodash.defaultsdeep';
@@ -27,8 +26,7 @@ import { type DocAccessor } from '@dxos/echo-db';
27
26
  import { log } from '@dxos/log';
28
27
  import { type Messenger } from '@dxos/protocols';
29
28
  import { type Identity } from '@dxos/protocols/proto/dxos/client/services';
30
- import { type HuePalette } from '@dxos/ui-theme';
31
- import { type ThemeMode } from '@dxos/ui-types';
29
+ import { type ChromaticPalette, type ThemeMode } from '@dxos/ui-types';
32
30
  import { hexToHue, isTruthy } from '@dxos/util';
33
31
 
34
32
  import { baseTheme, createFontTheme, editorGutter } from '../styles';
@@ -36,6 +34,7 @@ import { baseTheme, createFontTheme, editorGutter } from '../styles';
36
34
  import { automerge } from './automerge';
37
35
  import { SpaceAwarenessProvider, awareness } from './awareness';
38
36
  import { focus } from './focus';
37
+ import { scrollPastEnd } from './scroll-past-end';
39
38
 
40
39
  //
41
40
  // Basic
@@ -198,13 +197,13 @@ export type ThemeExtensionsOptions = {
198
197
 
199
198
  export const grow: ThemeExtensionsOptions['slots'] = {
200
199
  editor: {
201
- className: 'bs-full is-full',
200
+ className: 'h-full w-full',
202
201
  },
203
202
  } as const;
204
203
 
205
204
  export const fullWidth: ThemeExtensionsOptions['slots'] = {
206
205
  editor: {
207
- className: 'is-full',
206
+ className: 'w-full',
208
207
  },
209
208
  } as const;
210
209
 
@@ -263,7 +262,7 @@ export const createDataExtensions = <T>({ id, text, messenger, identity }: DataE
263
262
 
264
263
  if (messenger && identity) {
265
264
  const peerId = identity?.identityKey.toHex();
266
- const hue = (identity?.profile?.data?.hue as HuePalette | undefined) ?? hexToHue(peerId ?? '0');
265
+ const hue = (identity?.profile?.data?.hue as ChromaticPalette | undefined) ?? hexToHue(peerId ?? '0');
267
266
  extensions.push(
268
267
  awareness(
269
268
  new SpaceAwarenessProvider({
@@ -271,8 +270,8 @@ export const createDataExtensions = <T>({ id, text, messenger, identity }: DataE
271
270
  channel: `awareness.${id}`,
272
271
  peerId: identity.identityKey.toHex(),
273
272
  info: {
274
- darkColor: `var(--dx-${hue}Cursor)`,
275
- lightColor: `var(--dx-${hue}Cursor)`,
273
+ darkColor: `var(--color-${hue}-border)`,
274
+ lightColor: `var(--color-${hue}-border)`,
276
275
  displayName: identity.profile?.displayName ?? generateName(identity.identityKey.toHex()),
277
276
  },
278
277
  }),
@@ -17,12 +17,13 @@ export const folding = (): Extension => {
17
17
  placeholderDOM: () => Domino.of('span').root,
18
18
  }),
19
19
  foldGutter({
20
+ // NOTE: We can't animate since the element is remounted on state change.
20
21
  markerDOM: (open) => {
21
22
  return Domino.of('div')
22
- .classNames('flex bs-full justify-center items-center')
23
+ .classNames('flex h-full justify-center items-center')
23
24
  .children(
24
25
  Domino.of('svg', Domino.SVG)
25
- .classNames(mx('is-4 bs-4 cursor-pointer', open && 'rotate-90'))
26
+ .classNames(mx('w-4 h-4 cursor-pointer', open && 'rotate-90'))
26
27
  .children(
27
28
  Domino.of('use', Domino.SVG).attributes({
28
29
  href: Domino.icon('ph--caret-right--regular'),
@@ -30,24 +31,6 @@ export const folding = (): Extension => {
30
31
  ),
31
32
  ).root;
32
33
  },
33
- // TODO(burdon): markerDOM is called either way, defeating the animation: transition-transform duration-200
34
- // domEventHandlers: {
35
- // click: (view, line: BlockInfo, event) => {
36
- // event.preventDefault();
37
- // event.stopPropagation();
38
- // const range = foldable(view.state, line.from, line.to);
39
- // if (range) {
40
- // view.dispatch({ effects: foldEffect.of(range) });
41
- // (event.target as HTMLElement)?.classList.add('rotate-90');
42
- // } else {
43
- // foldedRanges(view.state).between(line.from, line.to, (from, to) => {
44
- // view.dispatch({ effects: unfoldEffect.of({ from, to }) });
45
- // (event.target as HTMLElement)?.classList.remove('rotate-90');
46
- // });
47
- // }
48
- // return true;
49
- // },
50
- // },
51
34
  }),
52
35
  EditorView.theme({
53
36
  '.cm-foldGutter': {
@@ -4,7 +4,7 @@
4
4
 
5
5
  export * from './annotations';
6
6
  export * from './autocomplete';
7
- export * from './autoscroll';
7
+ export * from './auto-scroll';
8
8
  export * from './automerge';
9
9
  export * from './awareness';
10
10
  export * from './blast';
@@ -26,8 +26,9 @@ export * from './modes';
26
26
  export * from './outliner';
27
27
  export * from './preview';
28
28
  export * from './replacer';
29
+ export * from './scroll-past-end';
30
+ export * from './scroller';
29
31
  export * from './selection';
30
- export * from './scrolling';
31
32
  export * from './state';
32
33
  export * from './submit';
33
34
  export * from './tags';
@@ -6,8 +6,7 @@ import { completionKeymap } from '@codemirror/autocomplete';
6
6
  import { defaultKeymap, indentWithTab } from '@codemirror/commands';
7
7
  import { jsonLanguage } from '@codemirror/lang-json';
8
8
  import { markdown, markdownLanguage } from '@codemirror/lang-markdown';
9
- import { xml } from '@codemirror/lang-xml';
10
- import { LanguageDescription, syntaxHighlighting } from '@codemirror/language';
9
+ import { type LanguageDescription, foldNodeProp, syntaxHighlighting } from '@codemirror/language';
11
10
  import { languages } from '@codemirror/language-data';
12
11
  import { type Extension } from '@codemirror/state';
13
12
  import { keymap } from '@codemirror/view';
@@ -18,6 +17,8 @@ import { isTruthy } from '@dxos/util';
18
17
  import { markdownHighlightStyle, markdownTagsExtensions } from './highlight';
19
18
 
20
19
  export type MarkdownBundleOptions = {
20
+ /** Additional fenced-code languages prepended to the standard language-data list. */
21
+ codeLanguages?: LanguageDescription[];
21
22
  extensions?: MarkdownConfig[];
22
23
  indentWithTab?: boolean;
23
24
  setextHeading?: boolean;
@@ -45,8 +46,9 @@ export const createMarkdownExtensions = (options: MarkdownBundleOptions = {}): E
45
46
  base: markdownLanguage,
46
47
 
47
48
  // Languages for syntax highlighting fenced code blocks.
49
+ // Caller-supplied languages are checked first so they can override defaults.
48
50
  defaultCodeLanguage: jsonLanguage,
49
- codeLanguages: languages,
51
+ codeLanguages: [...(options.codeLanguages ?? []), ...languages],
50
52
 
51
53
  // Don't complete HTML tags.
52
54
  completeHTMLTags: false,
@@ -56,6 +58,10 @@ export const createMarkdownExtensions = (options: MarkdownBundleOptions = {}): E
56
58
  // GFM provided by default.
57
59
  markdownTagsExtensions,
58
60
  ...(options.extensions ?? defaultExtensions()),
61
+ // Disable folding for fenced code blocks by overriding foldNodeProp.
62
+ // Note: returning null from foldService does not prevent syntaxFolding fallback,
63
+ // so we must override the node prop directly on the FencedCode node type.
64
+ noFencedCodeFolding,
59
65
  ],
60
66
  }),
61
67
 
@@ -77,12 +83,20 @@ export const createMarkdownExtensions = (options: MarkdownBundleOptions = {}): E
77
83
  ];
78
84
  };
79
85
 
80
- const xmlLanguageDesc = LanguageDescription.of({
81
- name: 'xml',
82
- alias: ['html', 'xhtml'],
83
- extensions: ['xml', 'xhtml'],
84
- load: async () => xml(),
85
- });
86
+ /**
87
+ * Disables folding for fenced code blocks.
88
+ *
89
+ * foldService cannot block folding because returning null just defers to the next service,
90
+ * and CodeMirror always falls back to syntaxFolding (which reads foldNodeProp).
91
+ * The only reliable fix is to override foldNodeProp on FencedCode to return null.
92
+ */
93
+ const noFencedCodeFolding: MarkdownConfig = {
94
+ props: [
95
+ foldNodeProp.add({
96
+ FencedCode: () => null,
97
+ }),
98
+ ],
99
+ };
86
100
 
87
101
  /**
88
102
  * Default customizations.