@dxos/ui-editor 0.8.4-main.fcfe5033a5 → 0.9.0

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 (169) hide show
  1. package/LICENSE +102 -5
  2. package/README.md +1 -1
  3. package/dist/lib/browser/index.mjs +1258 -1004
  4. package/dist/lib/browser/index.mjs.map +4 -4
  5. package/dist/lib/browser/meta.json +1 -1
  6. package/dist/lib/browser/types/index.mjs +26 -6
  7. package/dist/lib/browser/types/index.mjs.map +4 -4
  8. package/dist/lib/node-esm/index.mjs +1258 -1003
  9. package/dist/lib/node-esm/index.mjs.map +4 -4
  10. package/dist/lib/node-esm/meta.json +1 -1
  11. package/dist/lib/node-esm/types/index.mjs +27 -6
  12. package/dist/lib/node-esm/types/index.mjs.map +4 -4
  13. package/dist/types/src/defaults.d.ts.map +1 -1
  14. package/dist/types/src/extensions/annotations.d.ts.map +1 -1
  15. package/dist/types/src/extensions/autocomplete/autocomplete.d.ts.map +1 -1
  16. package/dist/types/src/extensions/autocomplete/match.d.ts.map +1 -1
  17. package/dist/types/src/extensions/autocomplete/placeholder.d.ts +5 -2
  18. package/dist/types/src/extensions/autocomplete/placeholder.d.ts.map +1 -1
  19. package/dist/types/src/extensions/autocomplete/typeahead.d.ts.map +1 -1
  20. package/dist/types/src/extensions/automerge/automerge.d.ts +1 -1
  21. package/dist/types/src/extensions/automerge/automerge.d.ts.map +1 -1
  22. package/dist/types/src/extensions/automerge/cursor.d.ts +1 -1
  23. package/dist/types/src/extensions/automerge/cursor.d.ts.map +1 -1
  24. package/dist/types/src/extensions/automerge/defs.d.ts.map +1 -1
  25. package/dist/types/src/extensions/automerge/sync.d.ts +1 -1
  26. package/dist/types/src/extensions/automerge/sync.d.ts.map +1 -1
  27. package/dist/types/src/extensions/automerge/update-automerge.d.ts +1 -1
  28. package/dist/types/src/extensions/automerge/update-automerge.d.ts.map +1 -1
  29. package/dist/types/src/extensions/automerge/update-codemirror.d.ts.map +1 -1
  30. package/dist/types/src/extensions/awareness/awareness-provider.d.ts.map +1 -1
  31. package/dist/types/src/extensions/awareness/awareness.d.ts.map +1 -1
  32. package/dist/types/src/extensions/blast.d.ts.map +1 -1
  33. package/dist/types/src/extensions/comments.d.ts +19 -1
  34. package/dist/types/src/extensions/comments.d.ts.map +1 -1
  35. package/dist/types/src/extensions/debug.d.ts.map +1 -1
  36. package/dist/types/src/extensions/dnd.d.ts.map +1 -1
  37. package/dist/types/src/extensions/factories.d.ts +3 -2
  38. package/dist/types/src/extensions/factories.d.ts.map +1 -1
  39. package/dist/types/src/extensions/factories.test.d.ts +2 -0
  40. package/dist/types/src/extensions/factories.test.d.ts.map +1 -0
  41. package/dist/types/src/extensions/focus.d.ts +1 -1
  42. package/dist/types/src/extensions/index.d.ts +3 -4
  43. package/dist/types/src/extensions/index.d.ts.map +1 -1
  44. package/dist/types/src/extensions/json.d.ts +1 -1
  45. package/dist/types/src/extensions/json.d.ts.map +1 -1
  46. package/dist/types/src/extensions/listener.d.ts.map +1 -1
  47. package/dist/types/src/extensions/markdown/bundle.d.ts.map +1 -1
  48. package/dist/types/src/extensions/markdown/changes.d.ts.map +1 -1
  49. package/dist/types/src/extensions/markdown/debug.d.ts.map +1 -1
  50. package/dist/types/src/extensions/markdown/decorate.d.ts.map +1 -1
  51. package/dist/types/src/extensions/markdown/formatting.d.ts.map +1 -1
  52. package/dist/types/src/extensions/markdown/highlight.d.ts.map +1 -1
  53. package/dist/types/src/extensions/markdown/image.d.ts +13 -2
  54. package/dist/types/src/extensions/markdown/image.d.ts.map +1 -1
  55. package/dist/types/src/extensions/markdown/image.test.d.ts +2 -0
  56. package/dist/types/src/extensions/markdown/image.test.d.ts.map +1 -0
  57. package/dist/types/src/extensions/markdown/link.d.ts.map +1 -1
  58. package/dist/types/src/extensions/markdown/table.d.ts.map +1 -1
  59. package/dist/types/src/extensions/mention.d.ts.map +1 -1
  60. package/dist/types/src/extensions/outliner/menu.d.ts.map +1 -1
  61. package/dist/types/src/extensions/outliner/outliner.d.ts.map +1 -1
  62. package/dist/types/src/extensions/outliner/selection.d.ts.map +1 -1
  63. package/dist/types/src/extensions/outliner/tree.d.ts.map +1 -1
  64. package/dist/types/src/extensions/preview/preview.d.ts +2 -2
  65. package/dist/types/src/extensions/preview/preview.d.ts.map +1 -1
  66. package/dist/types/src/extensions/replacer.d.ts.map +1 -1
  67. package/dist/types/src/extensions/scrolling/auto-scroll.d.ts +18 -0
  68. package/dist/types/src/extensions/scrolling/auto-scroll.d.ts.map +1 -0
  69. package/dist/types/src/extensions/scrolling/crawler.d.ts +83 -0
  70. package/dist/types/src/extensions/scrolling/crawler.d.ts.map +1 -0
  71. package/dist/types/src/extensions/scrolling/index.d.ts +6 -0
  72. package/dist/types/src/extensions/scrolling/index.d.ts.map +1 -0
  73. package/dist/types/src/extensions/scrolling/scroll-past-end.d.ts.map +1 -0
  74. package/dist/types/src/extensions/scrolling/scrollbar-autohide.d.ts +15 -0
  75. package/dist/types/src/extensions/scrolling/scrollbar-autohide.d.ts.map +1 -0
  76. package/dist/types/src/extensions/scrolling/scroller.d.ts +16 -0
  77. package/dist/types/src/extensions/scrolling/scroller.d.ts.map +1 -0
  78. package/dist/types/src/extensions/selection.d.ts.map +1 -1
  79. package/dist/types/src/extensions/snippets.d.ts +10 -0
  80. package/dist/types/src/extensions/snippets.d.ts.map +1 -0
  81. package/dist/types/src/extensions/spacing.d.ts +3 -0
  82. package/dist/types/src/extensions/spacing.d.ts.map +1 -0
  83. package/dist/types/src/extensions/submit.d.ts.map +1 -1
  84. package/dist/types/src/extensions/tags/extended-markdown.d.ts.map +1 -1
  85. package/dist/types/src/extensions/tags/fader.d.ts.map +1 -1
  86. package/dist/types/src/extensions/tags/index.d.ts +3 -1
  87. package/dist/types/src/extensions/tags/index.d.ts.map +1 -1
  88. package/dist/types/src/extensions/tags/typewriter.d.ts +43 -0
  89. package/dist/types/src/extensions/tags/typewriter.d.ts.map +1 -0
  90. package/dist/types/src/extensions/tags/typewriter.test.d.ts +2 -0
  91. package/dist/types/src/extensions/tags/typewriter.test.d.ts.map +1 -0
  92. package/dist/types/src/extensions/tags/xml-block-decoration.d.ts +31 -0
  93. package/dist/types/src/extensions/tags/xml-block-decoration.d.ts.map +1 -0
  94. package/dist/types/src/extensions/tags/xml-formatting.d.ts +24 -0
  95. package/dist/types/src/extensions/tags/xml-formatting.d.ts.map +1 -0
  96. package/dist/types/src/extensions/tags/xml-tags.d.ts +1 -8
  97. package/dist/types/src/extensions/tags/xml-tags.d.ts.map +1 -1
  98. package/dist/types/src/extensions/tags/xml-util.d.ts.map +1 -1
  99. package/dist/types/src/index.d.ts +0 -1
  100. package/dist/types/src/index.d.ts.map +1 -1
  101. package/dist/types/src/styles/theme.d.ts.map +1 -1
  102. package/dist/types/src/types/types.d.ts +2 -2
  103. package/dist/types/src/types/types.d.ts.map +1 -1
  104. package/dist/types/src/util/cursor.d.ts.map +1 -1
  105. package/dist/types/src/util/debug.d.ts.map +1 -1
  106. package/dist/types/src/util/decorations.d.ts.map +1 -1
  107. package/dist/types/src/util/dom.d.ts.map +1 -1
  108. package/dist/types/src/util/facet.d.ts.map +1 -1
  109. package/dist/types/src/util/util.d.ts.map +1 -1
  110. package/dist/types/tsconfig.tsbuildinfo +1 -1
  111. package/package.json +55 -57
  112. package/src/defaults.ts +6 -4
  113. package/src/extensions/autocomplete/placeholder.ts +37 -18
  114. package/src/extensions/automerge/automerge.test.tsx +35 -9
  115. package/src/extensions/automerge/automerge.ts +1 -1
  116. package/src/extensions/automerge/cursor.ts +1 -1
  117. package/src/extensions/automerge/sync.ts +1 -1
  118. package/src/extensions/automerge/update-automerge.ts +1 -1
  119. package/src/extensions/comments.ts +54 -31
  120. package/src/extensions/factories.test.ts +88 -0
  121. package/src/extensions/factories.ts +22 -4
  122. package/src/extensions/index.ts +3 -4
  123. package/src/extensions/json.ts +1 -1
  124. package/src/extensions/markdown/decorate.ts +1 -1
  125. package/src/extensions/markdown/image.test.ts +54 -0
  126. package/src/extensions/markdown/image.ts +70 -9
  127. package/src/extensions/markdown/link.ts +7 -2
  128. package/src/extensions/outliner/outliner.ts +1 -1
  129. package/src/extensions/preview/preview.ts +14 -12
  130. package/src/extensions/scrolling/auto-scroll.ts +261 -0
  131. package/src/extensions/{scroller.ts → scrolling/crawler.ts} +89 -48
  132. package/src/extensions/scrolling/index.ts +9 -0
  133. package/src/extensions/{scroll-past-end.ts → scrolling/scroll-past-end.ts} +6 -6
  134. package/src/extensions/scrolling/scrollbar-autohide.ts +61 -0
  135. package/src/extensions/scrolling/scroller.ts +27 -0
  136. package/src/extensions/snippets.ts +67 -0
  137. package/src/extensions/spacing.ts +15 -0
  138. package/src/extensions/tags/index.ts +3 -1
  139. package/src/extensions/tags/testing/text.md +36 -0
  140. package/src/extensions/tags/testing/text.txt +35 -0
  141. package/src/extensions/tags/{wire.test.ts → typewriter.test.ts} +2 -2
  142. package/src/extensions/tags/typewriter.ts +594 -0
  143. package/src/extensions/tags/xml-block-decoration.ts +123 -0
  144. package/src/extensions/tags/xml-formatting.ts +125 -0
  145. package/src/extensions/tags/xml-tags.ts +6 -32
  146. package/src/extensions/tags/xml-util.test.ts +90 -3
  147. package/src/extensions/tags/xml-util.ts +62 -5
  148. package/src/index.ts +0 -1
  149. package/src/styles/theme.ts +23 -13
  150. package/src/typings.d.ts +8 -0
  151. package/dist/lib/browser/chunk-D724USEC.mjs +0 -34
  152. package/dist/lib/browser/chunk-D724USEC.mjs.map +0 -7
  153. package/dist/lib/node-esm/chunk-JRVJWKQF.mjs +0 -36
  154. package/dist/lib/node-esm/chunk-JRVJWKQF.mjs.map +0 -7
  155. package/dist/types/src/extensions/auto-scroll.d.ts +0 -8
  156. package/dist/types/src/extensions/auto-scroll.d.ts.map +0 -1
  157. package/dist/types/src/extensions/scroll-past-end.d.ts.map +0 -1
  158. package/dist/types/src/extensions/scroller.d.ts +0 -63
  159. package/dist/types/src/extensions/scroller.d.ts.map +0 -1
  160. package/dist/types/src/extensions/tags/wire.d.ts +0 -23
  161. package/dist/types/src/extensions/tags/wire.d.ts.map +0 -1
  162. package/dist/types/src/extensions/tags/wire.test.d.ts +0 -2
  163. package/dist/types/src/extensions/tags/wire.test.d.ts.map +0 -1
  164. package/dist/types/src/extensions/typewriter.d.ts +0 -10
  165. package/dist/types/src/extensions/typewriter.d.ts.map +0 -1
  166. package/src/extensions/auto-scroll.ts +0 -179
  167. package/src/extensions/tags/wire.ts +0 -459
  168. package/src/extensions/typewriter.ts +0 -68
  169. /package/dist/types/src/extensions/{scroll-past-end.d.ts → scrolling/scroll-past-end.d.ts} +0 -0
@@ -22,7 +22,7 @@ import { vscodeDarkStyle, vscodeLightStyle } from '@uiw/codemirror-theme-vscode'
22
22
  import defaultsDeep from 'lodash.defaultsdeep';
23
23
 
24
24
  import { generateName } from '@dxos/display-name';
25
- import { type DocAccessor } from '@dxos/echo-db';
25
+ import { type DocAccessor } from '@dxos/echo-client';
26
26
  import { log } from '@dxos/log';
27
27
  import { type Messenger } from '@dxos/protocols';
28
28
  import { type Identity } from '@dxos/protocols/proto/dxos/client/services';
@@ -33,7 +33,7 @@ import { baseTheme, createFontTheme, editorGutter } from '../styles';
33
33
  import { automerge } from './automerge';
34
34
  import { SpaceAwarenessProvider, awareness } from './awareness';
35
35
  import { focus } from './focus';
36
- import { scrollPastEnd } from './scroll-past-end';
36
+ import { scrollPastEnd } from './scrolling';
37
37
 
38
38
  //
39
39
  // Basic
@@ -143,6 +143,19 @@ export const createBasicExtensions = (propsProp?: BasicExtensionsOptions): Exten
143
143
  props.lineWrapping && EditorView.lineWrapping,
144
144
  props.placeholder && placeholder(props.placeholder),
145
145
  props.readOnly !== undefined && EditorState.readOnly.of(props.readOnly),
146
+ // `EditorState.readOnly` is advisory — CodeMirror doesn't auto-reject doc-changing
147
+ // transactions. Some extensions (e.g. `@codemirror/lang-markdown`'s Enter handler that
148
+ // continues a list) dispatch programmatic edits regardless. Drop user-initiated edits
149
+ // (`input` / `delete` keymap dispatches plus `undo` / `redo` from the history extension)
150
+ // but pass programmatic dispatches — streaming `MarkdownStream` and similar consumers
151
+ // depend on being able to populate the doc themselves.
152
+ props.readOnly &&
153
+ EditorState.transactionFilter.of((tr) =>
154
+ tr.docChanged &&
155
+ (tr.isUserEvent('input') || tr.isUserEvent('delete') || tr.isUserEvent('undo') || tr.isUserEvent('redo'))
156
+ ? []
157
+ : tr,
158
+ ),
146
159
  props.scrollPastEnd && scrollPastEnd(),
147
160
  props.tabbable && tabbable,
148
161
  props.tabSize && EditorState.tabSize.of(props.tabSize),
@@ -179,6 +192,7 @@ export const createBasicExtensions = (propsProp?: BasicExtensionsOptions): Exten
179
192
  export type ThemeExtensionsOptions = {
180
193
  monospace?: boolean;
181
194
  themeMode?: ThemeMode;
195
+ scrollbarThin?: boolean;
182
196
  slots?: {
183
197
  editor?: {
184
198
  className?: string;
@@ -217,9 +231,10 @@ export const defaultStyles = {
217
231
  */
218
232
  export const createThemeExtensions = ({
219
233
  monospace,
220
- themeMode,
234
+ scrollbarThin,
221
235
  slots: slotsProp,
222
236
  syntaxHighlighting: syntaxHighlightingProp,
237
+ themeMode,
223
238
  }: ThemeExtensionsOptions = {}): Extension => {
224
239
  const slots: NonNullable<ThemeExtensionsOptions['slots']> = defaultsDeep({}, slotsProp, defaultThemeSlots);
225
240
  return [
@@ -230,13 +245,16 @@ export const createThemeExtensions = ({
230
245
  syntaxHighlighting(HighlightStyle.define(themeMode === 'dark' ? defaultStyles.dark : defaultStyles.light)),
231
246
  slots.editor?.className && EditorView.editorAttributes.of({ class: slots.editor.className }),
232
247
  slots.content?.className && EditorView.contentAttributes.of({ class: slots.content.className }),
233
- slots.scroller?.className &&
248
+ (slots.scroller?.className || scrollbarThin) &&
234
249
  ViewPlugin.fromClass(
235
250
  class {
236
251
  constructor(view: EditorView) {
237
252
  if (slots.scroller?.className) {
238
253
  view.scrollDOM.classList.add(...slots.scroller.className.split(/\s+/));
239
254
  }
255
+ if (scrollbarThin) {
256
+ view.scrollDOM.style.setProperty('--scrollbar-size', '4px');
257
+ }
240
258
  }
241
259
  },
242
260
  ),
@@ -4,7 +4,6 @@
4
4
 
5
5
  export * from './annotations';
6
6
  export * from './autocomplete';
7
- export * from './auto-scroll';
8
7
  export * from './automerge';
9
8
  export * from './awareness';
10
9
  export * from './blast';
@@ -26,10 +25,10 @@ export * from './modes';
26
25
  export * from './outliner';
27
26
  export * from './preview';
28
27
  export * from './replacer';
29
- export * from './scroll-past-end';
30
- export * from './scroller';
28
+ export * from './scrolling';
31
29
  export * from './selection';
30
+ export * from './spacing';
31
+ export * from './snippets';
32
32
  export * from './state';
33
33
  export * from './submit';
34
34
  export * from './tags';
35
- export * from './typewriter';
@@ -7,7 +7,7 @@ import { type LintSource, linter } from '@codemirror/lint';
7
7
  import { type Extension } from '@codemirror/state';
8
8
  import Ajv, { type ValidateFunction } from 'ajv';
9
9
 
10
- import { type JsonSchemaType } from '@dxos/echo/internal';
10
+ import { type JsonSchema as JsonSchemaType } from '@dxos/echo/JsonSchema';
11
11
 
12
12
  export type JsonExtensionsOptions = {
13
13
  schema?: JsonSchemaType;
@@ -617,7 +617,7 @@ export const decorateMarkdown = (options: DecorateOptions = {}) => {
617
617
  ],
618
618
  },
619
619
  ),
620
- image(),
620
+ image({ skip: options.skip ? (node) => !!options.skip?.(node) : undefined }),
621
621
  table(),
622
622
  adjustChanges(),
623
623
  formattingStyles,
@@ -0,0 +1,54 @@
1
+ //
2
+ // Copyright 2026 DXOS.org
3
+ //
4
+
5
+ import { markdown, markdownLanguage } from '@codemirror/lang-markdown';
6
+ import { EditorState } from '@codemirror/state';
7
+ import { EditorView } from '@codemirror/view';
8
+ import { describe, test } from 'vitest';
9
+
10
+ import { focus } from '../focus';
11
+ import { image } from './image';
12
+
13
+ const createView = (doc: string, extensions: any[]) => {
14
+ const parent = document.createElement('div');
15
+ return new EditorView({
16
+ state: EditorState.create({
17
+ doc,
18
+ extensions: [markdown({ base: markdownLanguage }), focus, ...extensions],
19
+ }),
20
+ parent,
21
+ });
22
+ };
23
+
24
+ const countImageElements = (view: EditorView): number => view.dom.querySelectorAll('img.cm-image').length;
25
+
26
+ describe('image extension', () => {
27
+ test('renders <img> for an http image link by default', ({ expect }) => {
28
+ const view = createView('![](http://example.com/x.png)', [image(), EditorView.editable.of(false)]);
29
+ expect(countImageElements(view)).toBeGreaterThan(0);
30
+ view.destroy();
31
+ });
32
+
33
+ test('honors skip callback to suppress remote image rendering', ({ expect }) => {
34
+ const skip = ({ url }: { name: 'Image'; url: string }) => /^https?:\/\//.test(url);
35
+ const view = createView('![alt](http://example.com/x.png)', [image({ skip }), EditorView.editable.of(false)]);
36
+ expect(countImageElements(view)).toBe(0);
37
+ view.destroy();
38
+ });
39
+
40
+ test('skip can be selective: blocks http(s) while still rendering file: URLs', ({ expect }) => {
41
+ const skip = ({ url }: { name: 'Image'; url: string }) => /^https?:\/\//.test(url);
42
+
43
+ const blockedView = createView('![alt](https://other.example.com/y.png)', [
44
+ image({ skip }),
45
+ EditorView.editable.of(false),
46
+ ]);
47
+ expect(countImageElements(blockedView)).toBe(0);
48
+ blockedView.destroy();
49
+
50
+ const allowedView = createView('![alt](file:///tmp/z.png)', [image({ skip }), EditorView.editable.of(false)]);
51
+ expect(countImageElements(allowedView)).toBeGreaterThan(0);
52
+ allowedView.destroy();
53
+ });
54
+ });
@@ -3,23 +3,48 @@
3
3
  //
4
4
 
5
5
  import { syntaxTree } from '@codemirror/language';
6
- import { type EditorState, type Extension, type Range, StateField, type Transaction } from '@codemirror/state';
7
- import { Decoration, type DecorationSet, EditorView, WidgetType } from '@codemirror/view';
6
+ import {
7
+ type EditorState,
8
+ type Extension,
9
+ type Range,
10
+ StateEffect,
11
+ StateField,
12
+ type Transaction,
13
+ } from '@codemirror/state';
14
+ import { Decoration, type DecorationSet, EditorView, ViewPlugin, WidgetType } from '@codemirror/view';
8
15
 
9
16
  import { focusField } from '../focus';
10
17
 
11
- export type ImageOptions = {};
18
+ // Effect dispatched when the viewport extends (e.g., scrolling reveals new content). The state
19
+ // field listens for this and rebuilds decorations across the full doc so images outside the
20
+ // initial parse range get widgetized.
21
+ const rebuildEffect = StateEffect.define<void>();
22
+
23
+ export type ImageNodeData = { name: 'Image'; url: string };
24
+
25
+ export type ImageOptions = {
26
+ /**
27
+ * Predicate that returns true to suppress rendering of an image node.
28
+ * When skipped, the markdown link source is left visible to the user instead of being
29
+ * replaced by an `<img>` widget.
30
+ */
31
+ skip?: (node: ImageNodeData) => boolean;
32
+ };
12
33
 
13
34
  /**
14
35
  * Create image decorations.
15
36
  */
16
- export const image = (_options: ImageOptions = {}): Extension => {
37
+ export const image = (options: ImageOptions = {}): Extension => {
17
38
  return [
18
39
  StateField.define<DecorationSet>({
19
40
  create: (state) => {
20
- return Decoration.set(buildDecorations(state, 0, state.doc.length));
41
+ return Decoration.set(buildDecorations(state, 0, state.doc.length, options));
21
42
  },
22
43
  update: (value: DecorationSet, tr: Transaction) => {
44
+ // Full rebuild when the viewport extended (lazy parse now covers more of the doc).
45
+ if (tr.effects.some((effect) => effect.is(rebuildEffect))) {
46
+ return Decoration.set(buildDecorations(tr.state, 0, tr.state.doc.length, options));
47
+ }
23
48
  if (!tr.docChanged && !tr.selection) {
24
49
  return value;
25
50
  }
@@ -42,15 +67,25 @@ export const image = (_options: ImageOptions = {}): Extension => {
42
67
  filterFrom: from,
43
68
  filterTo: to,
44
69
  filter: () => false,
45
- add: buildDecorations(tr.state, from, to),
70
+ add: buildDecorations(tr.state, from, to, options),
46
71
  });
47
72
  },
48
73
  provide: (field) => EditorView.decorations.from(field),
49
74
  }),
75
+ // Block-replace decorations have to live in a state field, but viewport changes are only
76
+ // observable from a view plugin. Bridge the two by dispatching a rebuild effect whenever
77
+ // the viewport extends so newly-parsed image nodes get widgetized without requiring focus.
78
+ ViewPlugin.define((view) => ({
79
+ update: (update) => {
80
+ if (update.viewportChanged) {
81
+ queueMicrotask(() => view.dispatch({ effects: rebuildEffect.of(undefined) }));
82
+ }
83
+ },
84
+ })),
50
85
  ];
51
86
  };
52
87
 
53
- const buildDecorations = (state: EditorState, from: number, to: number) => {
88
+ const buildDecorations = (state: EditorState, from: number, to: number, options: ImageOptions = {}) => {
54
89
  const decorations: Range<Decoration>[] = [];
55
90
  const cursor = state.selection.main.head;
56
91
  syntaxTree(state).iterate({
@@ -66,6 +101,11 @@ const buildDecorations = (state: EditorState, from: number, to: number) => {
66
101
  return;
67
102
  }
68
103
 
104
+ // Consumer-supplied filter (e.g., disable remote-image rendering by setting).
105
+ if (options.skip?.({ name: 'Image', url })) {
106
+ return;
107
+ }
108
+
69
109
  preloadImage(url);
70
110
  decorations.push(
71
111
  Decoration.replace({
@@ -106,13 +146,34 @@ class ImageWidget extends WidgetType {
106
146
  const img = document.createElement('img');
107
147
  img.setAttribute('src', this._url);
108
148
  img.setAttribute('class', 'cm-image');
149
+ const focused = view.state.field(focusField);
109
150
  // If focused, hide image until successfully loaded to avoid flickering effects.
110
- if (view.state.field(focusField)) {
111
- img.onload = () => img.classList.add('cm-loaded-image');
151
+ if (focused) {
152
+ img.onload = () => {
153
+ img.classList.add('cm-loaded-image');
154
+ collapseIfTrackingPixel(img);
155
+ };
112
156
  } else {
113
157
  img.classList.add('cm-loaded-image');
158
+ img.onload = () => collapseIfTrackingPixel(img);
114
159
  }
160
+ // Error (e.g., blocked tracker URL): also collapse so we don't leave a hole.
161
+ img.onerror = () => collapseLine(img);
115
162
 
116
163
  return img;
117
164
  }
118
165
  }
166
+
167
+ /**
168
+ * Tracking pixels are commonly 1×1 (or 0×0) transparent images embedded by mail senders.
169
+ * They add no visual value, so hide them once their natural dimensions are known.
170
+ */
171
+ const collapseIfTrackingPixel = (img: HTMLImageElement) => {
172
+ if (img.naturalWidth <= 1 && img.naturalHeight <= 1) {
173
+ collapseLine(img);
174
+ }
175
+ };
176
+
177
+ const collapseLine = (img: HTMLImageElement) => {
178
+ img.style.display = 'none';
179
+ };
@@ -7,10 +7,15 @@ import { syntaxTree } from '@codemirror/language';
7
7
  import { hoverTooltip } from '@codemirror/view';
8
8
  import { type SyntaxNode } from '@lezer/common';
9
9
 
10
- import { tooltipContent } from '@dxos/ui-theme';
10
+ import { mx, surfaceShadow } from '@dxos/ui-theme';
11
11
 
12
12
  import { type RenderCallback } from '../../types';
13
13
 
14
+ const tooltipClassName = mx(
15
+ 'inline-flex items-center p-1 max-w-64 text-sm bg-inverse-surface text-inverse-fg rounded-sm',
16
+ surfaceShadow({ elevation: 'positioned' }),
17
+ );
18
+
14
19
  export const linkTooltip = (renderTooltip: RenderCallback<{ url: string }>) => {
15
20
  return hoverTooltip((view, pos, side) => {
16
21
  const syntax = syntaxTree(view.state).resolveInner(pos, side);
@@ -35,7 +40,7 @@ export const linkTooltip = (renderTooltip: RenderCallback<{ url: string }>) => {
35
40
  above: true,
36
41
  create: () => {
37
42
  const el = document.createElement('div');
38
- el.className = tooltipContent({});
43
+ el.className = tooltipClassName;
39
44
  renderTooltip(el, { url: urlText }, view);
40
45
  return { dom: el, offset: { x: 0, y: 4 } };
41
46
  },
@@ -156,7 +156,7 @@ const decorations = () => [
156
156
  },
157
157
 
158
158
  '.cm-list-item-focused': {
159
- borderColor: 'var(--color-neutral-focus-indicator)',
159
+ borderColor: 'var(--color-focus-ring-subtle)',
160
160
  },
161
161
  '&:focus-within .cm-list-item-selected': {
162
162
  borderColor: 'var(--color-separator)',
@@ -7,7 +7,8 @@ import { type EditorState, type Extension, RangeSetBuilder, StateEffect, StateFi
7
7
  import { Decoration, type DecorationSet, EditorView, ViewPlugin, WidgetType } from '@codemirror/view';
8
8
  import { type SyntaxNode } from '@lezer/common';
9
9
 
10
- import { type Database, DXN, Entity } from '@dxos/echo';
10
+ import { type Database, Entity } from '@dxos/echo';
11
+ import { EID, URI } from '@dxos/keys';
11
12
 
12
13
  export type PreviewBlock = {
13
14
  link: PreviewLinkRef;
@@ -78,12 +79,13 @@ const resolveLabel = (
78
79
  dxnStr: string,
79
80
  viewRef: { current: EditorView | undefined },
80
81
  ): string | undefined => {
81
- const dxn = DXN.tryParse(dxnStr);
82
- if (!dxn) {
82
+ const echoUri = EID.tryParse(dxnStr);
83
+ const dxnRef = echoUri ?? (dxnStr.startsWith('dxn:') ? URI.make(dxnStr) : undefined);
84
+ if (!dxnRef) {
83
85
  return;
84
86
  }
85
87
 
86
- const ref = db.makeRef(dxn);
88
+ const ref = db.makeRef(dxnRef);
87
89
  const target = ref.target;
88
90
  if (target) {
89
91
  return Entity.getLabel(target);
@@ -111,7 +113,7 @@ const buildDecorations = (
111
113
  switch (node.name) {
112
114
  //
113
115
  // Inline widget.
114
- // [Label](dxn:echo:123)
116
+ // [Label](echo:/123)
115
117
  //
116
118
  case 'Link': {
117
119
  const link = getLinkRef(state, node.node);
@@ -132,7 +134,7 @@ const buildDecorations = (
132
134
 
133
135
  //
134
136
  // Block widget (transclusion).
135
- // ![Label](dxn:echo:123)
137
+ // ![Label](echo:/123)
136
138
  //
137
139
  case 'Image': {
138
140
  if (options.addBlockContainer && options.removeBlockContainer) {
@@ -159,15 +161,15 @@ const buildDecorations = (
159
161
 
160
162
  /**
161
163
  * Link references.
162
- * [Label](dxn:echo:123) Inline reference
163
- * ![Label](dxn:echo:123) Block reference
164
+ * [Label](echo:/123) Inline reference
165
+ * ![Label](echo:/123) Block reference
164
166
  */
165
167
  export const getLinkRef = (state: EditorState, node: SyntaxNode): PreviewLinkRef | undefined => {
166
168
  const mark = node.getChildren('LinkMark');
167
169
  const urlNode = node.getChild('URL');
168
170
  if (mark && urlNode) {
169
171
  const dxn = state.sliceDoc(urlNode.from, urlNode.to);
170
- if (dxn.startsWith('dxn:')) {
172
+ if (dxn.startsWith('dxn:') || dxn.startsWith('echo:')) {
171
173
  const label = state.sliceDoc(mark[0].to, mark[1].from);
172
174
  return {
173
175
  block: state.sliceDoc(mark[0].from, mark[0].from + 1) === '!',
@@ -180,7 +182,7 @@ export const getLinkRef = (state: EditorState, node: SyntaxNode): PreviewLinkRef
180
182
 
181
183
  /**
182
184
  * Inline widget.
183
- * [Label](dxn:echo:123)
185
+ * [Label](echo:/123)
184
186
  */
185
187
  class PreviewInlineWidget extends WidgetType {
186
188
  constructor(
@@ -209,7 +211,7 @@ class PreviewInlineWidget extends WidgetType {
209
211
 
210
212
  /**
211
213
  * Block widget (e.g., for surfaces).
212
- * ![Label][dxn:echo:123]
214
+ * ![Label][echo:/123]
213
215
  */
214
216
  class PreviewBlockWidget extends WidgetType {
215
217
  constructor(
@@ -229,7 +231,7 @@ class PreviewBlockWidget extends WidgetType {
229
231
 
230
232
  override toDOM(_view: EditorView) {
231
233
  const root = document.createElement('div');
232
- root.classList.add('cm-preview-block', 'dx-density-fine');
234
+ root.classList.add('cm-preview-block', 'dx-density-md');
233
235
  this._options.addBlockContainer?.({ link: this._link, el: root });
234
236
  return root;
235
237
  }