@dxos/ui-editor 0.9.0 → 0.9.1-staging.ee54ba693a

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 (115) hide show
  1. package/dist/lib/browser/index.mjs +2533 -1744
  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 +2533 -1744
  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.map +1 -1
  8. package/dist/types/src/extensions/autocomplete/placeholder.d.ts.map +1 -1
  9. package/dist/types/src/extensions/automerge/automerge.d.ts +6 -2
  10. package/dist/types/src/extensions/automerge/automerge.d.ts.map +1 -1
  11. package/dist/types/src/extensions/automerge/cursor.d.ts +6 -2
  12. package/dist/types/src/extensions/automerge/cursor.d.ts.map +1 -1
  13. package/dist/types/src/extensions/automerge/sync.d.ts +2 -2
  14. package/dist/types/src/extensions/automerge/sync.d.ts.map +1 -1
  15. package/dist/types/src/extensions/automerge/update-automerge.d.ts +2 -2
  16. package/dist/types/src/extensions/automerge/update-automerge.d.ts.map +1 -1
  17. package/dist/types/src/extensions/blocks.d.ts.map +1 -1
  18. package/dist/types/src/extensions/busy.d.ts +13 -0
  19. package/dist/types/src/extensions/busy.d.ts.map +1 -0
  20. package/dist/types/src/extensions/comments.d.ts.map +1 -1
  21. package/dist/types/src/extensions/dnd.d.ts +5 -1
  22. package/dist/types/src/extensions/dnd.d.ts.map +1 -1
  23. package/dist/types/src/extensions/factories.d.ts +2 -2
  24. package/dist/types/src/extensions/factories.d.ts.map +1 -1
  25. package/dist/types/src/extensions/fader.d.ts.map +1 -0
  26. package/dist/types/src/extensions/index.d.ts +6 -1
  27. package/dist/types/src/extensions/index.d.ts.map +1 -1
  28. package/dist/types/src/extensions/markdown/styles.d.ts.map +1 -1
  29. package/dist/types/src/extensions/marker.d.ts +40 -0
  30. package/dist/types/src/extensions/marker.d.ts.map +1 -0
  31. package/dist/types/src/extensions/pending/index.d.ts +3 -0
  32. package/dist/types/src/extensions/pending/index.d.ts.map +1 -0
  33. package/dist/types/src/extensions/pending/pending-text-stream.d.ts +75 -0
  34. package/dist/types/src/extensions/pending/pending-text-stream.d.ts.map +1 -0
  35. package/dist/types/src/extensions/pending/pending-text-stream.test.d.ts +2 -0
  36. package/dist/types/src/extensions/pending/pending-text-stream.test.d.ts.map +1 -0
  37. package/dist/types/src/extensions/pending/pending-text.d.ts +46 -0
  38. package/dist/types/src/extensions/pending/pending-text.d.ts.map +1 -0
  39. package/dist/types/src/extensions/pending/pending-text.test.d.ts +2 -0
  40. package/dist/types/src/extensions/pending/pending-text.test.d.ts.map +1 -0
  41. package/dist/types/src/extensions/pos.d.ts +49 -0
  42. package/dist/types/src/extensions/pos.d.ts.map +1 -0
  43. package/dist/types/src/extensions/pos.test.d.ts +2 -0
  44. package/dist/types/src/extensions/pos.test.d.ts.map +1 -0
  45. package/dist/types/src/extensions/scrolling/auto-scroll.d.ts.map +1 -1
  46. package/dist/types/src/extensions/selection.d.ts +12 -1
  47. package/dist/types/src/extensions/selection.d.ts.map +1 -1
  48. package/dist/types/src/extensions/selection.test.d.ts +2 -0
  49. package/dist/types/src/extensions/selection.test.d.ts.map +1 -0
  50. package/dist/types/src/extensions/tags/index.d.ts +2 -2
  51. package/dist/types/src/extensions/tags/index.d.ts.map +1 -1
  52. package/dist/types/src/extensions/tags/stub.d.ts +35 -0
  53. package/dist/types/src/extensions/tags/stub.d.ts.map +1 -0
  54. package/dist/types/src/extensions/tags/widgets/anchor.d.ts +13 -0
  55. package/dist/types/src/extensions/tags/widgets/anchor.d.ts.map +1 -0
  56. package/dist/types/src/extensions/tags/widgets/index.d.ts +2 -0
  57. package/dist/types/src/extensions/tags/widgets/index.d.ts.map +1 -0
  58. package/dist/types/src/extensions/tags/xml-tags.d.ts +9 -5
  59. package/dist/types/src/extensions/tags/xml-tags.d.ts.map +1 -1
  60. package/dist/types/src/extensions/typewriter.d.ts.map +1 -0
  61. package/dist/types/src/extensions/typewriter.test.d.ts.map +1 -0
  62. package/dist/types/src/index.d.ts +1 -1
  63. package/dist/types/src/index.d.ts.map +1 -1
  64. package/dist/types/src/styles/theme.d.ts.map +1 -1
  65. package/dist/types/tsconfig.tsbuildinfo +1 -1
  66. package/package.json +37 -35
  67. package/src/defaults.ts +3 -1
  68. package/src/extensions/autocomplete/placeholder.ts +11 -1
  69. package/src/extensions/automerge/automerge.test.tsx +25 -53
  70. package/src/extensions/automerge/automerge.ts +7 -3
  71. package/src/extensions/automerge/cursor.ts +7 -2
  72. package/src/extensions/automerge/sync.ts +2 -2
  73. package/src/extensions/automerge/update-automerge.ts +2 -2
  74. package/src/extensions/blocks.ts +36 -36
  75. package/src/extensions/busy.ts +32 -0
  76. package/src/extensions/comments.ts +5 -9
  77. package/src/extensions/dnd.ts +127 -3
  78. package/src/extensions/factories.test.ts +1 -1
  79. package/src/extensions/factories.ts +2 -2
  80. package/src/extensions/index.ts +6 -1
  81. package/src/extensions/markdown/styles.ts +7 -6
  82. package/src/extensions/marker.ts +143 -0
  83. package/src/extensions/pending/index.ts +6 -0
  84. package/src/extensions/pending/pending-text-stream.test.ts +133 -0
  85. package/src/extensions/pending/pending-text-stream.ts +269 -0
  86. package/src/extensions/pending/pending-text.test.ts +134 -0
  87. package/src/extensions/pending/pending-text.ts +264 -0
  88. package/src/extensions/pos.test.ts +97 -0
  89. package/src/extensions/pos.ts +206 -0
  90. package/src/extensions/scrolling/auto-scroll.ts +12 -8
  91. package/src/extensions/selection.test.ts +26 -0
  92. package/src/extensions/selection.ts +14 -15
  93. package/src/extensions/tags/DESIGN.md +58 -0
  94. package/src/extensions/tags/index.ts +2 -2
  95. package/src/extensions/tags/stub.ts +80 -0
  96. package/src/extensions/tags/widgets/anchor.ts +30 -0
  97. package/src/extensions/{preview → tags/widgets}/index.ts +1 -1
  98. package/src/extensions/tags/xml-tags.ts +116 -74
  99. package/src/extensions/tags/xml-util.test.ts +1 -1
  100. package/src/index.ts +1 -1
  101. package/src/styles/theme.ts +0 -1
  102. package/dist/types/src/extensions/preview/index.d.ts +0 -2
  103. package/dist/types/src/extensions/preview/index.d.ts.map +0 -1
  104. package/dist/types/src/extensions/preview/preview.d.ts +0 -34
  105. package/dist/types/src/extensions/preview/preview.d.ts.map +0 -1
  106. package/dist/types/src/extensions/tags/fader.d.ts.map +0 -1
  107. package/dist/types/src/extensions/tags/typewriter.d.ts.map +0 -1
  108. package/dist/types/src/extensions/tags/typewriter.test.d.ts.map +0 -1
  109. package/src/extensions/preview/preview.ts +0 -242
  110. /package/dist/types/src/extensions/{tags/fader.d.ts → fader.d.ts} +0 -0
  111. /package/dist/types/src/extensions/{tags/typewriter.d.ts → typewriter.d.ts} +0 -0
  112. /package/dist/types/src/extensions/{tags/typewriter.test.d.ts → typewriter.test.d.ts} +0 -0
  113. /package/src/extensions/{tags/fader.ts → fader.ts} +0 -0
  114. /package/src/extensions/{tags/typewriter.test.ts → typewriter.test.ts} +0 -0
  115. /package/src/extensions/{tags/typewriter.ts → typewriter.ts} +0 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dxos/ui-editor",
3
- "version": "0.9.0",
3
+ "version": "0.9.1-staging.ee54ba693a",
4
4
  "description": "Text editor components.",
5
5
  "homepage": "https://dxos.org",
6
6
  "bugs": "https://github.com/dxos/dxos/issues",
@@ -32,7 +32,7 @@
32
32
  "src"
33
33
  ],
34
34
  "dependencies": {
35
- "@automerge/automerge": "3.3.0-fragments.1",
35
+ "@automerge/automerge": "3.3.0-fragments.2",
36
36
  "@codemirror/autocomplete": "^6.20.2",
37
37
  "@codemirror/commands": "^6.10.3",
38
38
  "@codemirror/lang-html": "^6.4.11",
@@ -43,7 +43,7 @@
43
43
  "@codemirror/lang-yaml": "^6.1.3",
44
44
  "@codemirror/language": "^6.12.3",
45
45
  "@codemirror/language-data": "^6.5.2",
46
- "@codemirror/lint": "6.9.6",
46
+ "@codemirror/lint": "6.9.7",
47
47
  "@codemirror/search": "^6.7.0",
48
48
  "@codemirror/state": "^6.6.0",
49
49
  "@codemirror/theme-one-dark": "^6.1.3",
@@ -62,29 +62,31 @@
62
62
  "lodash.merge": "^4.6.2",
63
63
  "lodash.sortby": "^4.7.0",
64
64
  "style-mod": "^4.1.0",
65
- "@dxos/async": "0.9.0",
66
- "@dxos/client": "0.9.0",
67
- "@dxos/app-graph": "0.9.0",
68
- "@dxos/display-name": "0.9.0",
69
- "@dxos/context": "0.9.0",
70
- "@dxos/echo": "0.9.0",
71
- "@dxos/echo-client": "0.9.0",
72
- "@dxos/debug": "0.9.0",
73
- "@dxos/keys": "0.9.0",
74
- "@dxos/invariant": "0.9.0",
75
- "@dxos/lit-ui": "0.9.0",
76
- "@dxos/log": "0.9.0",
77
- "@dxos/ui": "0.9.0",
78
- "@dxos/protocols": "0.9.0",
79
- "@dxos/ui-theme": "0.9.0",
80
- "@dxos/ui-types": "0.9.0",
81
- "@dxos/util": "0.9.0"
65
+ "@dxos/app-graph": "0.9.1-staging.ee54ba693a",
66
+ "@dxos/async": "0.9.1-staging.ee54ba693a",
67
+ "@dxos/client": "0.9.1-staging.ee54ba693a",
68
+ "@dxos/context": "0.9.1-staging.ee54ba693a",
69
+ "@dxos/debug": "0.9.1-staging.ee54ba693a",
70
+ "@dxos/display-name": "0.9.1-staging.ee54ba693a",
71
+ "@dxos/echo-client": "0.9.1-staging.ee54ba693a",
72
+ "@dxos/echo": "0.9.1-staging.ee54ba693a",
73
+ "@dxos/echo-doc": "0.9.1-staging.ee54ba693a",
74
+ "@dxos/keys": "0.9.1-staging.ee54ba693a",
75
+ "@dxos/invariant": "0.9.1-staging.ee54ba693a",
76
+ "@dxos/lit-ui": "0.9.1-staging.ee54ba693a",
77
+ "@dxos/nlp": "0.9.1-staging.ee54ba693a",
78
+ "@dxos/ui": "0.9.1-staging.ee54ba693a",
79
+ "@dxos/ui-theme": "0.9.1-staging.ee54ba693a",
80
+ "@dxos/protocols": "0.9.1-staging.ee54ba693a",
81
+ "@dxos/ui-types": "0.9.1-staging.ee54ba693a",
82
+ "@dxos/util": "0.9.1-staging.ee54ba693a",
83
+ "@dxos/log": "0.9.1-staging.ee54ba693a"
82
84
  },
83
85
  "devDependencies": {
84
- "@automerge/automerge": "3.3.0-fragments.1",
85
- "@automerge/automerge-repo": "2.6.0-subduction.23",
86
- "@automerge/automerge-repo-network-broadcastchannel": "2.6.0-subduction.23",
87
- "@effect/platform": "0.96.1",
86
+ "@automerge/automerge": "3.3.0-fragments.2",
87
+ "@automerge/automerge-repo": "2.6.0-subduction.37",
88
+ "@automerge/automerge-repo-network-broadcastchannel": "2.6.0-subduction.37",
89
+ "@effect/platform": "0.96.2",
88
90
  "@types/chai": "^4.2.15",
89
91
  "@types/chai-dom": "^1.11.0",
90
92
  "@types/lodash.defaultsdeep": "^4.6.6",
@@ -92,25 +94,25 @@
92
94
  "@types/lodash.sortby": "^4.7.7",
93
95
  "chai": "^4.4.1",
94
96
  "chai-dom": "^1.11.0",
95
- "effect": "3.21.3",
97
+ "effect": "3.21.4",
96
98
  "happy-dom": "^20.0.0",
97
99
  "jsdom": "^29.1.1",
98
100
  "mocha": "^10.6.0",
99
101
  "vite": "^8.0.16",
100
102
  "vite-plugin-top-level-await": "^1.6.0",
101
103
  "vite-plugin-wasm": "^3.6.0",
102
- "@dxos/config": "0.9.0",
103
- "@dxos/echo": "0.9.0",
104
- "@dxos/keyboard": "0.9.0",
105
- "@dxos/random": "0.9.0",
106
- "@dxos/schema": "0.9.0",
107
- "@dxos/storybook-utils": "0.9.0",
108
- "@dxos/ui-theme": "0.9.0"
104
+ "@dxos/echo": "0.9.1-staging.ee54ba693a",
105
+ "@dxos/config": "0.9.1-staging.ee54ba693a",
106
+ "@dxos/keyboard": "0.9.1-staging.ee54ba693a",
107
+ "@dxos/random": "0.9.1-staging.ee54ba693a",
108
+ "@dxos/schema": "0.9.1-staging.ee54ba693a",
109
+ "@dxos/ui-theme": "0.9.1-staging.ee54ba693a",
110
+ "@dxos/storybook-utils": "0.9.1-staging.ee54ba693a"
109
111
  },
110
112
  "peerDependencies": {
111
- "@effect/platform": "0.96.1",
112
- "effect": "3.21.3",
113
- "@dxos/ui-theme": "0.9.0"
113
+ "@effect/platform": "0.96.2",
114
+ "effect": "3.21.4",
115
+ "@dxos/ui-theme": "0.9.1-staging.ee54ba693a"
114
116
  },
115
117
  "publishConfig": {
116
118
  "access": "public"
package/src/defaults.ts CHANGED
@@ -9,7 +9,9 @@ import { type ThemeExtensionsOptions } from './extensions';
9
9
  export const editorClassNames = (role?: string) =>
10
10
  mx(
11
11
  'dx-attention-surface data-[toolbar=disabled]:pt-2 dx-focus-ring-inset',
12
- role === 'section' ? '[&_.cm-scroller]:overflow-hidden [&_.cm-scroller]:min-h-24' : 'dx-container overflow-hidden',
12
+ role === 'org.dxos.role.section'
13
+ ? '[&_.cm-scroller]:overflow-hidden [&_.cm-scroller]:min-h-24'
14
+ : 'dx-container overflow-hidden',
13
15
  );
14
16
 
15
17
  export const documentSlots: ThemeExtensionsOptions['slots'] = {
@@ -7,6 +7,7 @@ import { type Extension } from '@codemirror/state';
7
7
  import { Decoration, EditorView, ViewPlugin, type ViewUpdate, WidgetType } from '@codemirror/view';
8
8
 
9
9
  import { clientRectsFor, flattenRect } from '../../util';
10
+ import { isBusy } from '../busy';
10
11
 
11
12
  type Content = string | HTMLElement | ((view: EditorView) => HTMLElement);
12
13
 
@@ -28,6 +29,10 @@ export const placeholder = ({ content, delay = 3_000, focusOnly = false }: Place
28
29
  _decorations = Decoration.none;
29
30
 
30
31
  update(update: ViewUpdate) {
32
+ // A busy-state toggle (e.g. transcription starting) carries no doc/selection/focus change,
33
+ // so it must be detected explicitly to clear an already-scheduled placeholder.
34
+ const busyChanged = isBusy(update.startState) !== isBusy(update.view.state);
35
+
31
36
  // React to actual user activity only. The empty `view.update([])`
32
37
  // dispatched from the timeout below carries no doc/selection/focus
33
38
  // change, so it falls through here as a no-op — that's how the
@@ -35,7 +40,7 @@ export const placeholder = ({ content, delay = 3_000, focusOnly = false }: Place
35
40
  // provider to read it. Without this gate, the unconditional reset
36
41
  // would clobber the decoration in the same tick and the placeholder
37
42
  // would never visibly render.
38
- if (!update.docChanged && !update.selectionSet && !update.focusChanged) {
43
+ if (!update.docChanged && !update.selectionSet && !update.focusChanged && !busyChanged) {
39
44
  return;
40
45
  }
41
46
 
@@ -52,6 +57,11 @@ export const placeholder = ({ content, delay = 3_000, focusOnly = false }: Place
52
57
  return;
53
58
  }
54
59
 
60
+ // Suppress the hint while another extension owns the caret region (e.g. transcription).
61
+ if (isBusy(update.view.state)) {
62
+ return;
63
+ }
64
+
55
65
  const activeLine = update.view.state.doc.lineAt(update.view.state.selection.main.head);
56
66
  if (activeLine.text.trim() !== '') {
57
67
  return;
@@ -2,62 +2,43 @@
2
2
  // Copyright 2024 DXOS.org
3
3
  //
4
4
 
5
- import { type ChangeFn, type ChangeOptions, type Doc, type Heads } from '@automerge/automerge';
6
- import { type DocHandle, Repo, decodeHeads, encodeHeads, initSubduction } from '@automerge/automerge-repo';
5
+ import { next as A } from '@automerge/automerge';
7
6
  import { EditorState } from '@codemirror/state';
8
7
  import { EditorView } from '@codemirror/view';
9
8
  import { render, screen } from '@testing-library/react';
10
- import React, { type FC, useEffect, useRef, useState } from 'react';
11
- import { beforeAll, describe, test } from 'vitest';
9
+ import * as Schema from 'effect/Schema';
10
+ import React, { useEffect, useRef } from 'react';
11
+ import { describe, test } from 'vitest';
12
12
 
13
- import { type IDocHandle } from '@dxos/echo-client';
14
- import { getDeep } from '@dxos/util';
13
+ import { DXN, Obj, Type } from '@dxos/echo';
14
+ import { Doc } from '@dxos/echo-doc';
15
15
 
16
16
  import { automerge } from './automerge';
17
17
 
18
- // Adapter: `IDocHandle` (used by `DocHandleProxy` in production) takes raw `Heads` (hex)
19
- // at `changeAt`, but automerge-repo's `DocHandle.changeAt` expects bs58check-encoded
20
- // `UrlHeads`. Encode on the way in, decode on the way out so the test's repo-backed
21
- // handle satisfies the extension's `IDocHandle` contract.
22
- const adaptRepoHandle = <T,>(handle: DocHandle<T>): IDocHandle<T> => ({
23
- doc: () => handle.doc() as Doc<T> | undefined,
24
- change: (callback: ChangeFn<T>, options?: ChangeOptions<T>) =>
25
- options ? handle.change(callback, options) : handle.change(callback),
26
- changeAt: (heads: Heads, callback: ChangeFn<T>, options?: ChangeOptions<T>): Heads | undefined => {
27
- const encoded = options
28
- ? handle.changeAt(encodeHeads(heads), callback, options)
29
- : handle.changeAt(encodeHeads(heads), callback);
30
- return encoded ? decodeHeads(encoded) : undefined;
31
- },
32
- addListener: (event, listener) => handle.on(event, listener),
33
- removeListener: (event, listener) => handle.off(event, listener),
34
- });
35
-
36
- const path = ['text'];
37
-
38
- type TestObject = {
39
- text: string;
40
- };
18
+ const TestType = Schema.Struct({ text: Schema.String }).pipe(
19
+ Type.makeObject(DXN.make('com.example.test.editor', '0.1.0')),
20
+ );
21
+ type TestType = Type.InstanceType<typeof TestType>;
41
22
 
42
23
  class Generator {
43
- constructor(private readonly _handle: DocHandle<TestObject>) {}
24
+ constructor(private readonly _accessor: Doc.Accessor<TestType>) {}
25
+
44
26
  update(text: string): void {
45
- this._handle.change((doc: TestObject) => {
46
- doc.text = text;
27
+ this._accessor.handle.change((doc) => {
28
+ A.splice(doc, [...this._accessor.path], 0, 0, text);
47
29
  });
48
30
  }
49
31
  }
50
32
 
51
- const Test: FC<{ handle: DocHandle<TestObject>; generator: Generator }> = ({ handle, generator }) => {
33
+ const Test = ({ accessor }: { accessor: Doc.Accessor<TestType> }) => {
52
34
  const ref = useRef<HTMLDivElement>(null);
53
- const [view, setView] = useState<EditorView>();
54
35
  useEffect(() => {
55
36
  const extensions = [
56
- automerge({ handle: adaptRepoHandle(handle), path }),
57
- EditorView.updateListener.of((update) => {
37
+ automerge(accessor),
38
+ EditorView.updateListener.of(() => {
58
39
  if (view.state.doc.toString() === 'hello!') {
59
40
  // Update editor.
60
- update.view.dispatch({
41
+ view.dispatch({
61
42
  changes: { from: view.state.doc.length - 1, insert: ' world' },
62
43
  });
63
44
  }
@@ -65,33 +46,24 @@ const Test: FC<{ handle: DocHandle<TestObject>; generator: Generator }> = ({ han
65
46
  ];
66
47
 
67
48
  const view = new EditorView({
68
- state: EditorState.create({ doc: getDeep(handle.doc()!, path), extensions }),
49
+ state: EditorState.create({ doc: Doc.getValue<string>(accessor) ?? '', extensions }),
69
50
  parent: ref.current!,
70
51
  });
71
52
 
72
- setView(view);
73
- }, []);
74
-
75
- useEffect(() => {}, [view]);
53
+ return () => view.destroy();
54
+ }, [accessor]);
76
55
 
77
56
  return <div ref={ref} data-testid='editor' />;
78
57
  };
79
58
 
80
59
  // TODO(burdon): Test history/undo.
81
- // TODO(burdon): https://testing-library.com/docs/react-testing-library/example-intro/
82
60
 
83
61
  describe('Automerge', () => {
84
- // Subduction-fork `Repo` constructs a `MemorySigner` internally; WASM must be
85
- // initialized first or the constructor throws `'set_subduction_logger' of undefined`.
86
- beforeAll(async () => {
87
- await initSubduction();
88
- });
89
-
90
62
  test('basic sync', ({ expect }) => {
91
- const repo = new Repo({ network: [] });
92
- const handle = repo.create<TestObject>();
93
- const generator = new Generator(handle);
94
- render(<Test handle={handle} generator={generator} />);
63
+ const obj = Obj.make(TestType, { text: '' });
64
+ const accessor = Doc.createAccessor(obj, ['text']);
65
+ const generator = new Generator(accessor);
66
+ render(<Test accessor={accessor} />);
95
67
 
96
68
  const editor = screen.getByTestId('editor');
97
69
  expect(editor.textContent).toBe('');
@@ -8,7 +8,7 @@ import { next as A } from '@automerge/automerge';
8
8
  import { type Extension, StateField, Transaction } from '@codemirror/state';
9
9
  import { EditorView, ViewPlugin } from '@codemirror/view';
10
10
 
11
- import { DocAccessor } from '@dxos/echo-client';
11
+ import { Doc } from '@dxos/echo-doc';
12
12
 
13
13
  import { Cursor } from '../../util';
14
14
  import { initialSync } from '../state';
@@ -16,7 +16,11 @@ import { cursorConverter } from './cursor';
16
16
  import { type State, isReconcile, reconcileAnnotation, updateHeadsEffect } from './defs';
17
17
  import { Syncer } from './sync';
18
18
 
19
- export const automerge = (accessor: DocAccessor): Extension => {
19
+ /**
20
+ * CodeMirror extension that two-way syncs the editor with the string the {@link Doc.Accessor} points
21
+ * at, reconciling local edits and remote document mutations via Automerge.
22
+ */
23
+ export const automerge = (accessor: Doc.Accessor): Extension => {
20
24
  const syncState = StateField.define<State>({
21
25
  create: () => {
22
26
  return {
@@ -68,7 +72,7 @@ export const automerge = (accessor: DocAccessor): Extension => {
68
72
  accessor.handle.addListener('change', this._handleChange);
69
73
 
70
74
  requestAnimationFrame(() => {
71
- const value = DocAccessor.getValue<string>(accessor);
75
+ const value = Doc.getValue<string>(accessor);
72
76
  const current = this._view.state.doc.toString();
73
77
  if (value !== current) {
74
78
  this._view.dispatch({
@@ -2,12 +2,17 @@
2
2
  // Copyright 2024 DXOS.org
3
3
  //
4
4
 
5
- import { type DocAccessor, fromCursor, toCursor } from '@dxos/echo-client';
5
+ import { fromCursor, toCursor } from '@dxos/echo-client';
6
+ import { Doc } from '@dxos/echo-doc';
6
7
  import { log } from '@dxos/log';
7
8
 
8
9
  import { type CursorConverter } from '../../util';
9
10
 
10
- export const cursorConverter = (accessor: DocAccessor): CursorConverter => ({
11
+ /**
12
+ * Maps between editor offsets and stable Automerge cursors for the text the {@link Doc.Accessor}
13
+ * points at; falls back to empty/zero positions when the document is unavailable.
14
+ */
15
+ export const cursorConverter = (accessor: Doc.Accessor): CursorConverter => ({
11
16
  toCursor: (pos, assoc) => {
12
17
  try {
13
18
  return toCursor(accessor, pos, assoc);
@@ -8,7 +8,7 @@ import { next as A } from '@automerge/automerge';
8
8
  import { type StateField } from '@codemirror/state';
9
9
  import { type EditorView } from '@codemirror/view';
10
10
 
11
- import { type IDocHandle } from '@dxos/echo-client';
11
+ import { Doc } from '@dxos/echo-doc';
12
12
  import { log } from '@dxos/log';
13
13
 
14
14
  import { type State, getLastHeads, getPath, isReconcile, reconcileAnnotation, updateHeads } from './defs';
@@ -23,7 +23,7 @@ export class Syncer {
23
23
 
24
24
  // prettier-ignore
25
25
  constructor(
26
- private readonly _handle: IDocHandle,
26
+ private readonly _handle: Doc.Handle,
27
27
  private readonly _state: StateField<State>
28
28
  ) {}
29
29
 
@@ -7,13 +7,13 @@
7
7
  import { next as A, type Heads } from '@automerge/automerge';
8
8
  import { type EditorState, type StateField, type Text, type Transaction } from '@codemirror/state';
9
9
 
10
- import { type IDocHandle } from '@dxos/echo-client';
10
+ import { Doc } from '@dxos/echo-doc';
11
11
 
12
12
  import { type State } from './defs';
13
13
 
14
14
  export const updateAutomerge = (
15
15
  field: StateField<State>,
16
- handle: IDocHandle,
16
+ handle: Doc.Handle,
17
17
  transactions: Transaction[],
18
18
  state: EditorState, // TODO(burdon): Just pass in the state field value?
19
19
  ): Heads | undefined => {
@@ -7,7 +7,42 @@ import { Decoration, type DecorationSet, EditorView, ViewPlugin, type ViewUpdate
7
7
 
8
8
  import { mx } from '@dxos/ui-theme';
9
9
 
10
- const paragraphBlockPlugin = ViewPlugin.fromClass(
10
+ export const blocks = () => [
11
+ paragraphBlock,
12
+ EditorView.baseTheme({
13
+ '.cm-line.block-line': {
14
+ paddingLeft: '0.75rem',
15
+ paddingRight: '0.75rem',
16
+ borderLeft: '1px solid var(--color-subdued-separator)',
17
+ borderRight: '1px solid var(--color-subdued-separator)',
18
+ },
19
+ '.cm-line.block-single': {
20
+ border: '1px solid var(--color-subdued-separator)',
21
+ borderRadius: '6px',
22
+ paddingTop: '0.5rem',
23
+ paddingBottom: '0.5rem',
24
+ marginTop: '0.5rem',
25
+ marginBottom: '0.5rem',
26
+ },
27
+ '.cm-line.block-first': {
28
+ borderTop: '1px solid var(--color-subdued-separator)',
29
+ borderTopLeftRadius: '6px',
30
+ borderTopRightRadius: '6px',
31
+ paddingTop: '0.5rem',
32
+ marginTop: '0.5rem',
33
+ },
34
+ '.cm-line.block-middle': {},
35
+ '.cm-line.block-last': {
36
+ borderBottom: '1px solid var(--color-subdued-separator)',
37
+ borderBottomLeftRadius: '6px',
38
+ borderBottomRightRadius: '6px',
39
+ paddingBottom: '0.5rem',
40
+ marginBottom: '0.5rem',
41
+ },
42
+ }),
43
+ ];
44
+
45
+ const paragraphBlock = ViewPlugin.fromClass(
11
46
  class {
12
47
  decorations: DecorationSet;
13
48
 
@@ -94,38 +129,3 @@ const paragraphBlockPlugin = ViewPlugin.fromClass(
94
129
  decorations: (v) => v.decorations,
95
130
  },
96
131
  );
97
-
98
- export const blocks = () => [
99
- paragraphBlockPlugin,
100
- EditorView.baseTheme({
101
- '.cm-line.block-line': {
102
- paddingLeft: '0.75rem',
103
- paddingRight: '0.75rem',
104
- borderLeft: '1px solid var(--color-subdued-separator)',
105
- borderRight: '1px solid var(--color-subdued-separator)',
106
- },
107
- '.cm-line.block-single': {
108
- border: '1px solid var(--color-subdued-separator)',
109
- borderRadius: '6px',
110
- paddingTop: '0.5rem',
111
- paddingBottom: '0.5rem',
112
- marginTop: '0.5rem',
113
- marginBottom: '0.5rem',
114
- },
115
- '.cm-line.block-first': {
116
- borderTop: '1px solid var(--color-subdued-separator)',
117
- borderTopLeftRadius: '6px',
118
- borderTopRightRadius: '6px',
119
- paddingTop: '0.5rem',
120
- marginTop: '0.5rem',
121
- },
122
- '.cm-line.block-middle': {},
123
- '.cm-line.block-last': {
124
- borderBottom: '1px solid var(--color-subdued-separator)',
125
- borderBottomLeftRadius: '6px',
126
- borderBottomRightRadius: '6px',
127
- paddingBottom: '0.5rem',
128
- marginBottom: '0.5rem',
129
- },
130
- }),
131
- ];
@@ -0,0 +1,32 @@
1
+ //
2
+ // Copyright 2026 DXOS.org
3
+ //
4
+
5
+ import { type EditorState, type Extension, StateEffect, StateField } from '@codemirror/state';
6
+
7
+ /**
8
+ * Generic shared "busy" flag that extensions use to coordinate exclusive use of the cursor region.
9
+ * One extension sets it (e.g. transcription while a pending block is active); others read it (e.g. a
10
+ * placeholder hint) to suppress themselves while something else owns the caret.
11
+ */
12
+ export const setBusy = StateEffect.define<boolean>();
13
+
14
+ export const busyState = StateField.define<boolean>({
15
+ create: () => false,
16
+ update: (value, tr) => {
17
+ let next = value;
18
+ for (const effect of tr.effects) {
19
+ if (effect.is(setBusy)) {
20
+ next = effect.value;
21
+ }
22
+ }
23
+
24
+ return next;
25
+ },
26
+ });
27
+
28
+ /** Whether any extension has flagged the editor busy. Safe when the field is absent (returns false). */
29
+ export const isBusy = (state: EditorState): boolean => state.field(busyState, false) ?? false;
30
+
31
+ /** Contributes the shared busy flag. Include once; setters/readers reference the same field. */
32
+ export const busy = (): Extension => busyState;
@@ -22,6 +22,7 @@ import { isNonNullable } from '@dxos/util';
22
22
 
23
23
  import { type Comment, type Range, type RenderCallback } from '../types';
24
24
  import { Cursor, singleValueFacet, wrapWithCatch } from '../util';
25
+ import { markerMark, markerTheme } from './marker';
25
26
  import { documentId } from './selection';
26
27
 
27
28
  //
@@ -99,22 +100,16 @@ export const commentsState = StateField.define<CommentsState>({
99
100
  /**
100
101
  * NOTE: Matches search.
101
102
  */
103
+ // Surface/box-shadow come from the shared `markerTheme`, tinted by the mark's `data-hue`; the comment
104
+ // just adds the pointer affordance.
102
105
  const styles = EditorView.theme({
103
106
  '.cm-comment > span': {
104
- boxDecorationBreak: 'clone',
105
- boxShadow: '0 0 0 3px var(--color-cm-comment-surface)',
106
- backgroundColor: 'var(--color-cm-comment-surface)',
107
- color: 'var(--color-cm-comment-text) !important',
108
107
  cursor: 'pointer',
109
108
  },
110
- '.cm-comment[data-current="1"] > span': {
111
- boxShadow: '0 0 0 3px var(--color-cm-comment-current-surface)',
112
- backgroundColor: 'var(--color-cm-comment-current-surface)',
113
- },
114
109
  });
115
110
 
116
111
  const createCommentMark = (id: string, isCurrent: boolean) =>
117
- Decoration.mark({
112
+ markerMark(isCurrent ? 'orange' : 'teal', {
118
113
  class: 'cm-comment',
119
114
  attributes: {
120
115
  'data-testid': 'cm-comment',
@@ -386,6 +381,7 @@ export const comments = (options: CommentsOptions = {}): Extension => {
386
381
  commentsState,
387
382
  commentsDecorations,
388
383
  handleCommentClick,
384
+ markerTheme(),
389
385
  styles,
390
386
 
391
387
  //