@dxos/react-ui-editor 0.4.8-main.ec0899a → 0.4.8-main.f253994

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 (49) hide show
  1. package/dist/lib/browser/index.mjs +454 -277
  2. package/dist/lib/browser/index.mjs.map +4 -4
  3. package/dist/lib/browser/meta.json +1 -1
  4. package/dist/types/src/components/TextEditor/TextEditor.d.ts +2 -2
  5. package/dist/types/src/components/TextEditor/TextEditor.d.ts.map +1 -1
  6. package/dist/types/src/components/TextEditor/TextEditor.stories.d.ts +2 -1
  7. package/dist/types/src/components/TextEditor/TextEditor.stories.d.ts.map +1 -1
  8. package/dist/types/src/components/Toolbar/Toolbar.d.ts +11 -5
  9. package/dist/types/src/components/Toolbar/Toolbar.d.ts.map +1 -1
  10. package/dist/types/src/components/Toolbar/Toolbar.stories.d.ts +4 -2
  11. package/dist/types/src/components/Toolbar/Toolbar.stories.d.ts.map +1 -1
  12. package/dist/types/src/extensions/automerge/automerge.stories.d.ts +1 -0
  13. package/dist/types/src/extensions/automerge/automerge.stories.d.ts.map +1 -1
  14. package/dist/types/src/extensions/comments.d.ts.map +1 -1
  15. package/dist/types/src/extensions/factories.d.ts.map +1 -1
  16. package/dist/types/src/extensions/index.d.ts +1 -0
  17. package/dist/types/src/extensions/index.d.ts.map +1 -1
  18. package/dist/types/src/extensions/markdown/formatting.d.ts +7 -4
  19. package/dist/types/src/extensions/markdown/formatting.d.ts.map +1 -1
  20. package/dist/types/src/extensions/markdown/image.d.ts +6 -0
  21. package/dist/types/src/extensions/markdown/image.d.ts.map +1 -1
  22. package/dist/types/src/extensions/modes.d.ts +1 -0
  23. package/dist/types/src/extensions/modes.d.ts.map +1 -1
  24. package/dist/types/src/extensions/state.d.ts +20 -0
  25. package/dist/types/src/extensions/state.d.ts.map +1 -0
  26. package/dist/types/src/hooks/useActionHandler.d.ts.map +1 -1
  27. package/dist/types/src/hooks/useTextEditor.d.ts.map +1 -1
  28. package/dist/types/src/hooks/useTextEditor.stories.d.ts +1 -0
  29. package/dist/types/src/hooks/useTextEditor.stories.d.ts.map +1 -1
  30. package/dist/types/src/translations.d.ts +1 -0
  31. package/dist/types/src/translations.d.ts.map +1 -1
  32. package/package.json +25 -27
  33. package/src/components/TextEditor/TextEditor.stories.tsx +16 -4
  34. package/src/components/TextEditor/TextEditor.tsx +13 -12
  35. package/src/components/Toolbar/Toolbar.stories.tsx +23 -24
  36. package/src/components/Toolbar/Toolbar.tsx +101 -35
  37. package/src/extensions/automerge/automerge.ts +4 -4
  38. package/src/extensions/comments.ts +2 -2
  39. package/src/extensions/factories.ts +0 -3
  40. package/src/extensions/index.ts +1 -0
  41. package/src/extensions/markdown/formatting.test.ts +13 -13
  42. package/src/extensions/markdown/formatting.ts +91 -83
  43. package/src/extensions/markdown/image.ts +6 -0
  44. package/src/extensions/modes.ts +3 -1
  45. package/src/extensions/state.ts +90 -0
  46. package/src/hooks/useActionHandler.ts +5 -1
  47. package/src/hooks/useTextEditor.stories.tsx +3 -3
  48. package/src/hooks/useTextEditor.ts +3 -0
  49. package/src/translations.ts +1 -0
@@ -15,6 +15,7 @@ var translations_default = [
15
15
  "blockquote label": "Block quote",
16
16
  "codeblock label": "Code block",
17
17
  "comment label": "Create comment",
18
+ "image label": "Insert image",
18
19
  "heading label": "Heading level",
19
20
  "table label": "Create table"
20
21
  }
@@ -23,18 +24,18 @@ var translations_default = [
23
24
  ];
24
25
 
25
26
  // packages/ui/react-ui-editor/src/index.ts
26
- import { keymap as keymap9 } from "@codemirror/view";
27
+ import { keymap as keymap10 } from "@codemirror/view";
27
28
  import { tags as tags2 } from "@lezer/highlight";
28
29
  import { TextKind } from "@dxos/protocols/proto/dxos/echo/model/text";
29
30
  import { Doc, YText, YXmlFragment } from "@dxos/text-model";
30
31
 
31
32
  // packages/ui/react-ui-editor/src/components/TextEditor/TextEditor.tsx
32
33
  import { EditorState as EditorState2 } from "@codemirror/state";
33
- import { EditorView as EditorView16 } from "@codemirror/view";
34
+ import { EditorView as EditorView17 } from "@codemirror/view";
34
35
  import { useFocusableGroup } from "@fluentui/react-tabster";
35
- import React, { forwardRef, useCallback, useEffect as useEffect2, useImperativeHandle, useRef, useState as useState2 } from "react";
36
+ import React, { forwardRef, useCallback, useEffect as useEffect2, useImperativeHandle, useRef, useState } from "react";
36
37
  import { log as log9 } from "@dxos/log";
37
- import { isNotFalsy as isNotFalsy3 } from "@dxos/util";
38
+ import { isNotFalsy as isNotFalsy4 } from "@dxos/util";
38
39
 
39
40
  // packages/ui/react-ui-editor/src/extensions/annotations.ts
40
41
  import { StateField } from "@codemirror/state";
@@ -56,8 +57,8 @@ var Cursor = class _Cursor {
56
57
  });
57
58
  }
58
59
  static {
59
- this.getCursorFromRange = (state, range) => {
60
- const cursorConverter2 = state.facet(_Cursor.converter);
60
+ this.getCursorFromRange = (state2, range) => {
61
+ const cursorConverter2 = state2.facet(_Cursor.converter);
61
62
  const from = cursorConverter2.toCursor(range.from);
62
63
  const to = cursorConverter2.toCursor(range.to, -1);
63
64
  return [
@@ -67,8 +68,8 @@ var Cursor = class _Cursor {
67
68
  };
68
69
  }
69
70
  static {
70
- this.getRangeFromCursor = (state, cursor) => {
71
- const cursorConverter2 = state.facet(_Cursor.converter);
71
+ this.getRangeFromCursor = (state2, cursor) => {
72
+ const cursorConverter2 = state2.facet(_Cursor.converter);
72
73
  const parts = cursor.split(":");
73
74
  const from = cursorConverter2.fromCursor(parts[0]);
74
75
  const to = cursorConverter2.fromCursor(parts[1]);
@@ -85,15 +86,15 @@ var annotationMark = Decoration.mark({
85
86
  class: "cm-annotation"
86
87
  });
87
88
  var annotations = (options = {}) => {
88
- const match = (state) => {
89
+ const match = (state2) => {
89
90
  const annotations2 = [];
90
- const text = state.doc.toString();
91
+ const text = state2.doc.toString();
91
92
  if (options.match) {
92
93
  const matches = text.matchAll(options.match);
93
94
  for (const match2 of matches) {
94
95
  const from = match2.index;
95
96
  const to = from + match2[0].length;
96
- const cursor = Cursor.getCursorFromRange(state, {
97
+ const cursor = Cursor.getCursorFromRange(state2, {
97
98
  from,
98
99
  to
99
100
  });
@@ -105,8 +106,8 @@ var annotations = (options = {}) => {
105
106
  return annotations2;
106
107
  };
107
108
  const annotationsState = StateField.define({
108
- create: (state) => {
109
- return match(state);
109
+ create: (state2) => {
110
+ return match(state2);
110
111
  },
111
112
  update: (value, tr) => {
112
113
  if (!tr.changes.empty) {
@@ -119,10 +120,10 @@ var annotations = (options = {}) => {
119
120
  annotationsState,
120
121
  EditorView.decorations.compute([
121
122
  annotationsState
122
- ], (state) => {
123
- const annotations2 = state.field(annotationsState);
123
+ ], (state2) => {
124
+ const annotations2 = state2.field(annotationsState);
124
125
  const decorations = annotations2.map((annotation2) => {
125
- const range = Cursor.getRangeFromCursor(state, annotation2.cursor);
126
+ const range = Cursor.getRangeFromCursor(state2, annotation2.cursor);
126
127
  return range && annotationMark.range(range.from, range.to);
127
128
  }).filter(isNotFalsy);
128
129
  return Decoration.set(decorations);
@@ -239,8 +240,8 @@ var cursorConverter = ({ handle, path }) => ({
239
240
 
240
241
  // packages/ui/react-ui-editor/src/extensions/automerge/defs.ts
241
242
  import { Annotation, StateEffect } from "@codemirror/state";
242
- var getPath = (state, field) => state.field(field).path;
243
- var getLastHeads = (state, field) => state.field(field).lastHeads;
243
+ var getPath = (state2, field) => state2.field(field).path;
244
+ var getLastHeads = (state2, field) => state2.field(field).lastHeads;
244
245
  var updateHeadsEffect = StateEffect.define({});
245
246
  var updateHeads = (newHeads) => updateHeadsEffect.of({
246
247
  newHeads
@@ -255,8 +256,8 @@ import { next as A3 } from "@dxos/automerge/automerge";
255
256
 
256
257
  // packages/ui/react-ui-editor/src/extensions/automerge/update-automerge.ts
257
258
  import { next as A2 } from "@dxos/automerge/automerge";
258
- var updateAutomerge = (field, handle, transactions, state) => {
259
- const { lastHeads, path } = state.field(field);
259
+ var updateAutomerge = (field, handle, transactions, state2) => {
260
+ const { lastHeads, path } = state2.field(field);
260
261
  let hasChanges = false;
261
262
  for (const tr of transactions) {
262
263
  tr.changes.iterChanges(() => {
@@ -303,7 +304,7 @@ var updateCodeMirror = (view, selection, target, patches) => {
303
304
  annotations: reconcileAnnotation.of(false)
304
305
  });
305
306
  };
306
- var handlePatch = (patch, target, state) => {
307
+ var handlePatch = (patch, target, state2) => {
307
308
  if (patch.action === "insert") {
308
309
  return handleInsert(target, patch);
309
310
  } else if (patch.action === "splice") {
@@ -311,7 +312,7 @@ var handlePatch = (patch, target, state) => {
311
312
  } else if (patch.action === "del") {
312
313
  return handleDel(target, patch);
313
314
  } else if (patch.action === "put") {
314
- return handlePut(target, patch, state);
315
+ return handlePut(target, patch, state2);
315
316
  } else {
316
317
  return null;
317
318
  }
@@ -355,7 +356,7 @@ var handleDel = (target, patch) => {
355
356
  }
356
357
  ];
357
358
  };
358
- var handlePut = (target, patch, state) => {
359
+ var handlePut = (target, patch, state2) => {
359
360
  const index = charPath(target, [
360
361
  ...patch.path,
361
362
  0
@@ -363,7 +364,7 @@ var handlePut = (target, patch, state) => {
363
364
  if (index == null) {
364
365
  return [];
365
366
  }
366
- const length = state.doc.length;
367
+ const length = state2.doc.length;
367
368
  if (typeof patch.value !== "string") {
368
369
  return [];
369
370
  }
@@ -474,13 +475,13 @@ var automerge = (accessor) => {
474
475
  ViewPlugin.fromClass(class {
475
476
  constructor(_view) {
476
477
  this._view = _view;
477
- accessor.handle.addListener("change", this._handleChange.bind(this));
478
+ this._handleChange = () => {
479
+ syncer.reconcile(this._view, false);
480
+ };
481
+ accessor.handle.addListener("change", this._handleChange);
478
482
  }
479
483
  destroy() {
480
- accessor.handle.removeListener("change", this._handleChange.bind(this));
481
- }
482
- _handleChange() {
483
- syncer.reconcile(this._view, false);
484
+ accessor.handle.removeListener("change", this._handleChange);
484
485
  }
485
486
  }),
486
487
  // Reconcile local updates.
@@ -561,9 +562,9 @@ var RemoteSelectionsDecorator = class {
561
562
  _updateRemoteSelections(update2) {
562
563
  const decorations = [];
563
564
  const awarenessStates = this._provider.getRemoteStates();
564
- for (const state of awarenessStates) {
565
- const anchor = state.position?.anchor ? this._cursorConverter.fromCursor(state.position.anchor) : null;
566
- const head = state.position?.head ? this._cursorConverter.fromCursor(state.position.head) : null;
565
+ for (const state2 of awarenessStates) {
566
+ const anchor = state2.position?.anchor ? this._cursorConverter.fromCursor(state2.position.anchor) : null;
567
+ const head = state2.position?.head ? this._cursorConverter.fromCursor(state2.position.head) : null;
567
568
  if (anchor == null || head == null) {
568
569
  continue;
569
570
  }
@@ -571,8 +572,8 @@ var RemoteSelectionsDecorator = class {
571
572
  const end = Math.min(Math.max(anchor, head), update2.view.state.doc.length);
572
573
  const startLine = update2.view.state.doc.lineAt(start);
573
574
  const endLine = update2.view.state.doc.lineAt(end);
574
- const color = state.info.color ?? "#30bced";
575
- const lightColor = state.info.lightColor ?? color + "33";
575
+ const color = state2.info.color ?? "#30bced";
576
+ const lightColor = state2.info.lightColor ?? color + "33";
576
577
  if (startLine.number === endLine.number) {
577
578
  decorations.push({
578
579
  from: start,
@@ -625,7 +626,7 @@ var RemoteSelectionsDecorator = class {
625
626
  value: Decoration2.widget({
626
627
  side: head - anchor > 0 ? -1 : 1,
627
628
  block: false,
628
- widget: new RemoteCaretWidget(state.info.displayName ?? "Anonymous", color)
629
+ widget: new RemoteCaretWidget(state2.info.displayName ?? "Anonymous", color)
629
630
  })
630
631
  });
631
632
  }
@@ -1156,7 +1157,7 @@ var commandConfig = Facet3.define({
1156
1157
  });
1157
1158
  var commandState = StateField3.define({
1158
1159
  create: () => ({}),
1159
- update: (state, tr) => {
1160
+ update: (state2, tr) => {
1160
1161
  for (const effect of tr.effects) {
1161
1162
  if (effect.is(closeEffect)) {
1162
1163
  return {};
@@ -1207,7 +1208,7 @@ var commandState = StateField3.define({
1207
1208
  };
1208
1209
  }
1209
1210
  }
1210
- return state;
1211
+ return state2;
1211
1212
  },
1212
1213
  provide: (field) => [
1213
1214
  showTooltip.from(field, (value) => value.tooltip ?? null)
@@ -1453,8 +1454,8 @@ var setComments = StateEffect3.define();
1453
1454
  var setSelection = StateEffect3.define();
1454
1455
  var setCommentState = StateEffect3.define();
1455
1456
  var commentsState = StateField4.define({
1456
- create: (state) => ({
1457
- id: state.facet(documentId),
1457
+ create: (state2) => ({
1458
+ id: state2.facet(documentId),
1458
1459
  comments: [],
1459
1460
  selection: {}
1460
1461
  }),
@@ -1526,8 +1527,8 @@ var commentCurrentMark = Decoration4.mark({
1526
1527
  });
1527
1528
  var commentsDecorations = EditorView7.decorations.compute([
1528
1529
  commentsState
1529
- ], (state) => {
1530
- const { selection: { current }, comments: comments2 } = state.field(commentsState);
1530
+ ], (state2) => {
1531
+ const { selection: { current }, comments: comments2 } = state2.field(commentsState);
1531
1532
  const decorations = sortBy(comments2 ?? [], (range) => range.range.from)?.flatMap((comment) => {
1532
1533
  const range = comment.range;
1533
1534
  if (!range || range.from === range.to) {
@@ -1691,7 +1692,7 @@ var optionsFacet = Facet4.define({
1691
1692
  });
1692
1693
  var comments = (options = {}) => {
1693
1694
  const { key: shortcut = "meta-'" } = options;
1694
- const handleSelect = debounce((state) => options.onSelect?.(state), 200);
1695
+ const handleSelect = debounce((state2) => options.onSelect?.(state2), 200);
1695
1696
  return [
1696
1697
  optionsFacet.of(options),
1697
1698
  documentId.of(options.id),
@@ -1740,9 +1741,9 @@ var comments = (options = {}) => {
1740
1741
  //
1741
1742
  // Track deleted ranges and update ranges for decorations.
1742
1743
  //
1743
- EditorView7.updateListener.of(({ view, state, changes }) => {
1744
+ EditorView7.updateListener.of(({ view, state: state2, changes }) => {
1744
1745
  let mod = false;
1745
- const { comments: comments2, ...value } = state.field(commentsState);
1746
+ const { comments: comments2, ...value } = state2.field(commentsState);
1746
1747
  changes.iterChanges((from, to, from2, to2) => {
1747
1748
  comments2.forEach(({ comment, range }) => {
1748
1749
  if (from2 === to2) {
@@ -1772,10 +1773,10 @@ var comments = (options = {}) => {
1772
1773
  //
1773
1774
  // Track selection/proximity.
1774
1775
  //
1775
- EditorView7.updateListener.of(({ view, state }) => {
1776
+ EditorView7.updateListener.of(({ view, state: state2 }) => {
1776
1777
  let min = Infinity;
1777
- const { selection: { current, closest }, comments: comments2 } = state.field(commentsState);
1778
- const { head } = state.selection.main;
1778
+ const { selection: { current, closest }, comments: comments2 } = state2.field(commentsState);
1779
+ const { head } = state2.selection.main;
1779
1780
  const selection = {};
1780
1781
  comments2.forEach(({ comment, range }) => {
1781
1782
  if (head >= range.from && head <= range.to) {
@@ -1796,7 +1797,7 @@ var comments = (options = {}) => {
1796
1797
  });
1797
1798
  handleSelect({
1798
1799
  selection,
1799
- id: state.facet(documentId),
1800
+ id: state2.facet(documentId),
1800
1801
  comments: comments2.map(({ comment, range }) => ({
1801
1802
  comment,
1802
1803
  range,
@@ -1831,14 +1832,14 @@ var focusComment = (view, id, center = true) => {
1831
1832
  });
1832
1833
  }
1833
1834
  };
1834
- var useComments = (view, id, comments2 = []) => {
1835
+ var useComments = (view, id, comments2) => {
1835
1836
  useEffect(() => {
1836
1837
  if (view) {
1837
1838
  if (id === view.state.facet(documentId)) {
1838
1839
  view.dispatch({
1839
1840
  effects: setComments.of({
1840
1841
  id,
1841
- comments: comments2
1842
+ comments: comments2 ?? []
1842
1843
  })
1843
1844
  });
1844
1845
  }
@@ -1864,11 +1865,11 @@ var cursorLineMargin = (options = {
1864
1865
  if (!lines || lines <= 0) {
1865
1866
  return [];
1866
1867
  }
1867
- return EditorView8.updateListener.of(({ transactions, state, selectionSet, startState, view }) => {
1868
+ return EditorView8.updateListener.of(({ transactions, state: state2, selectionSet, startState, view }) => {
1868
1869
  if (transactions.length < 1 || transactions.some((tr) => tr.isUserEvent(annotation))) {
1869
1870
  return;
1870
1871
  }
1871
- const s = state;
1872
+ const s = state2;
1872
1873
  const { main } = s.selection;
1873
1874
  const rect = view.dom.getBoundingClientRect();
1874
1875
  const viewportTop = rect.top - view.documentTop;
@@ -2233,9 +2234,6 @@ var createBasicExtensions = (_props) => {
2233
2234
  var defaultSlots = {
2234
2235
  editor: {
2235
2236
  className: "w-full bs-full"
2236
- },
2237
- content: {
2238
- className: "!p-2"
2239
2237
  }
2240
2238
  };
2241
2239
  var createThemeExtensions = ({ theme, themeMode, slots: _slots } = {}) => {
@@ -2609,8 +2607,8 @@ var checkedTask = Decoration5.replace({
2609
2607
  var uncheckedTask = Decoration5.replace({
2610
2608
  widget: new CheckboxWidget(false)
2611
2609
  });
2612
- var editingRange = (state, range, focus) => {
2613
- const { readOnly, selection: { main: { head } } } = state;
2610
+ var editingRange = (state2, range, focus) => {
2611
+ const { readOnly, selection: { main: { head } } } = state2;
2614
2612
  return focus && !readOnly && head >= range.from && head <= range.to;
2615
2613
  };
2616
2614
  var MarksByParent = /* @__PURE__ */ new Set([
@@ -2623,14 +2621,14 @@ var MarksByParent = /* @__PURE__ */ new Set([
2623
2621
  var buildDecorations = (view, options, focus) => {
2624
2622
  const deco = new RangeSetBuilder2();
2625
2623
  const atomicDeco = new RangeSetBuilder2();
2626
- const { state } = view;
2624
+ const { state: state2 } = view;
2627
2625
  for (const { from, to } of view.visibleRanges) {
2628
- syntaxTree(state).iterate({
2626
+ syntaxTree(state2).iterate({
2629
2627
  from,
2630
2628
  to,
2631
2629
  enter: (node) => {
2632
2630
  if (node.name === "FencedCode") {
2633
- const editing = editingRange(state, node, focus);
2631
+ const editing = editingRange(state2, node, focus);
2634
2632
  for (const block of view.viewportLineBlocks) {
2635
2633
  if (block.to < node.from) {
2636
2634
  continue;
@@ -2639,7 +2637,7 @@ var buildDecorations = (view, options, focus) => {
2639
2637
  break;
2640
2638
  }
2641
2639
  const first = block.from <= node.from;
2642
- const last = block.to >= node.to && /^(\s>)*```$/.test(state.doc.sliceString(block.from, block.to));
2640
+ const last = block.to >= node.to && /^(\s>)*```$/.test(state2.doc.sliceString(block.from, block.to));
2643
2641
  deco.add(block.from, block.from, first ? fencedCodeLineFirst : last ? fencedCodeLineLast : fencedCodeLine);
2644
2642
  if (!editing && (first || last)) {
2645
2643
  atomicDeco.add(block.from, block.to, hide);
@@ -2649,9 +2647,9 @@ var buildDecorations = (view, options, focus) => {
2649
2647
  } else if (node.name === "Link") {
2650
2648
  const marks = node.node.getChildren("LinkMark");
2651
2649
  const urlNode = node.node.getChild("URL");
2652
- const editing = editingRange(state, node, focus);
2650
+ const editing = editingRange(state2, node, focus);
2653
2651
  if (urlNode && marks.length >= 2) {
2654
- const url = state.sliceDoc(urlNode.from, urlNode.to);
2652
+ const url = state2.sliceDoc(urlNode.from, urlNode.to);
2655
2653
  if (!editing) {
2656
2654
  atomicDeco.add(node.from, marks[0].to, hide);
2657
2655
  }
@@ -2672,24 +2670,24 @@ var buildDecorations = (view, options, focus) => {
2672
2670
  }
2673
2671
  } else if (node.name === "HeaderMark") {
2674
2672
  const parent = node.node.parent;
2675
- if (/^ATX/.test(parent.name) && !editingRange(state, state.doc.lineAt(node.from), focus)) {
2676
- const next = state.doc.sliceString(node.to, node.to + 1);
2673
+ if (/^ATX/.test(parent.name) && !editingRange(state2, state2.doc.lineAt(node.from), focus)) {
2674
+ const next = state2.doc.sliceString(node.to, node.to + 1);
2677
2675
  atomicDeco.add(node.from, node.to + (next === " " ? 1 : 0), hide);
2678
2676
  }
2679
2677
  } else if (node.name === "HorizontalRule") {
2680
- if (!editingRange(state, node, focus)) {
2678
+ if (!editingRange(state2, node, focus)) {
2681
2679
  deco.add(node.from, node.to, horizontalRule);
2682
2680
  }
2683
2681
  } else if (node.name === "TaskMarker") {
2684
- if (!editingRange(state, node, focus)) {
2685
- const checked = state.doc.sliceString(node.from + 1, node.to - 1) === "x";
2682
+ if (!editingRange(state2, node, focus)) {
2683
+ const checked = state2.doc.sliceString(node.from + 1, node.to - 1) === "x";
2686
2684
  atomicDeco.add(node.from - 2, node.from - 1, Decoration5.mark({
2687
2685
  class: "cm-task"
2688
2686
  }));
2689
2687
  atomicDeco.add(node.from, node.to, checked ? checkedTask : uncheckedTask);
2690
2688
  }
2691
2689
  } else if (MarksByParent.has(node.name)) {
2692
- if (!editingRange(state, node.node.parent, focus)) {
2690
+ if (!editingRange(state2, node.node.parent, focus)) {
2693
2691
  atomicDeco.add(node.from, node.to, hide);
2694
2692
  }
2695
2693
  }
@@ -2799,8 +2797,9 @@ import { snippet } from "@codemirror/autocomplete";
2799
2797
  import { syntaxTree as syntaxTree2 } from "@codemirror/language";
2800
2798
  import { EditorSelection } from "@codemirror/state";
2801
2799
  import { EditorView as EditorView13, keymap as keymap6 } from "@codemirror/view";
2802
- import { useState, useMemo } from "react";
2803
- var compareFormatting = (a, b) => a.blockType === b.blockType && a.strong === b.strong && a.emphasis === b.emphasis && a.strikethrough === b.strikethrough && a.code === b.code && a.link === b.link && a.listStyle === b.listStyle && a.blockQuote === b.blockQuote;
2800
+ import { useMemo } from "react";
2801
+ import { useStateRef } from "@dxos/react-async";
2802
+ var formattingEquals = (a, b) => a.blockType === b.blockType && a.strong === b.strong && a.emphasis === b.emphasis && a.strikethrough === b.strikethrough && a.code === b.code && a.link === b.link && a.listStyle === b.listStyle && a.blockQuote === b.blockQuote;
2804
2803
  var Inline;
2805
2804
  (function(Inline2) {
2806
2805
  Inline2[Inline2["Strong"] = 0] = "Strong";
@@ -2814,13 +2813,13 @@ var List;
2814
2813
  List2[List2["Bullet"] = 1] = "Bullet";
2815
2814
  List2[List2["Task"] = 2] = "Task";
2816
2815
  })(List || (List = {}));
2817
- var setHeading = (level) => ({ state, dispatch }) => {
2818
- const { selection: { ranges }, doc } = state;
2816
+ var setHeading = (level) => ({ state: state2, dispatch }) => {
2817
+ const { selection: { ranges }, doc } = state2;
2819
2818
  const changes = [];
2820
2819
  let prevBlock = -1;
2821
2820
  for (const range of ranges) {
2822
2821
  let sawBlock = false;
2823
- syntaxTree2(state).iterate({
2822
+ syntaxTree2(state2).iterate({
2824
2823
  from: range.from,
2825
2824
  to: range.to,
2826
2825
  enter: (node) => {
@@ -2873,7 +2872,7 @@ var setHeading = (level) => ({ state, dispatch }) => {
2873
2872
  }
2874
2873
  });
2875
2874
  let line;
2876
- if (!sawBlock && range.empty && level > 0 && !/\S/.test((line = state.doc.lineAt(range.from)).text)) {
2875
+ if (!sawBlock && range.empty && level > 0 && !/\S/.test((line = state2.doc.lineAt(range.from)).text)) {
2877
2876
  changes.push({
2878
2877
  from: line.from,
2879
2878
  to: line.to,
@@ -2884,23 +2883,23 @@ var setHeading = (level) => ({ state, dispatch }) => {
2884
2883
  if (!changes.length) {
2885
2884
  return false;
2886
2885
  }
2887
- const changeSet = state.changes(changes);
2888
- dispatch(state.update({
2886
+ const changeSet = state2.changes(changes);
2887
+ dispatch(state2.update({
2889
2888
  changes: changeSet,
2890
- selection: state.selection.map(changeSet, 1),
2889
+ selection: state2.selection.map(changeSet, 1),
2891
2890
  userEvent: "format.setHeading",
2892
2891
  scrollIntoView: true
2893
2892
  }));
2894
2893
  return true;
2895
2894
  };
2896
- var setStyle = (type, enable) => ({ state, dispatch }) => {
2895
+ var setStyle = (type, enable) => ({ state: state2, dispatch }) => {
2897
2896
  const marker = inlineMarkerText(type);
2898
- const changes = state.changeByRange((range) => {
2897
+ const changes = state2.changeByRange((range) => {
2899
2898
  if (!enable && range.empty) {
2900
- const after = state.doc.sliceString(range.head, range.head + 6);
2899
+ const after = state2.doc.sliceString(range.head, range.head + 6);
2901
2900
  const found = after.indexOf(marker);
2902
2901
  if (found >= 0 && /^[*~`]*$/.test(after.slice(0, found))) {
2903
- const before = state.doc.sliceString(range.head - 6, range.head);
2902
+ const before = state2.doc.sliceString(range.head - 6, range.head);
2904
2903
  if (before.slice(before.length - found - marker.length, before.length - found) === marker && [
2905
2904
  ...before.slice(before.length - found)
2906
2905
  ].reverse().join("") === after.slice(0, found)) {
@@ -2927,14 +2926,14 @@ var setStyle = (type, enable) => ({ state, dispatch }) => {
2927
2926
  let startCovered = false;
2928
2927
  let endCovered = false;
2929
2928
  let { from, to } = range;
2930
- syntaxTree2(state).iterate({
2929
+ syntaxTree2(state2).iterate({
2931
2930
  from,
2932
2931
  to,
2933
2932
  enter: (node) => {
2934
2933
  const { name } = node;
2935
2934
  if (Object.hasOwn(Textblocks, name) && Textblocks[name] !== "codeblock") {
2936
2935
  blockStart = blockContentStart(node);
2937
- blockEnd = blockContentEnd(node, state.doc);
2936
+ blockEnd = blockContentEnd(node, state2.doc);
2938
2937
  startCovered = endCovered = false;
2939
2938
  } else if (name === "Link" || name === "Image" && enable) {
2940
2939
  if (from < node.from && to > node.from && to <= node.to) {
@@ -2979,7 +2978,7 @@ var setStyle = (type, enable) => ({ state, dispatch }) => {
2979
2978
  });
2980
2979
  if (markType !== type && closeStart >= to) {
2981
2980
  changesAtEnd.push({
2982
- from: skipSpaces(Math.min(to, blockEnd), state.doc, 1, blockEnd),
2981
+ from: skipSpaces(Math.min(to, blockEnd), state2.doc, 1, blockEnd),
2983
2982
  insert: inlineMarkerText(markType)
2984
2983
  });
2985
2984
  }
@@ -2991,7 +2990,7 @@ var setStyle = (type, enable) => ({ state, dispatch }) => {
2991
2990
  });
2992
2991
  if (markType !== type && openEnd <= from) {
2993
2992
  changes2.push({
2994
- from: skipSpaces(Math.max(from, blockStart), state.doc, -1, blockStart),
2993
+ from: skipSpaces(Math.max(from, blockStart), state2.doc, -1, blockStart),
2995
2994
  insert: inlineMarkerText(markType)
2996
2995
  });
2997
2996
  }
@@ -3021,7 +3020,7 @@ var setStyle = (type, enable) => ({ state, dispatch }) => {
3021
3020
  changes2.push(startCovered);
3022
3021
  } else if (startCovered) {
3023
3022
  changes2.push({
3024
- from: skipSpaces(rangeStart, state.doc, -1, blockStart),
3023
+ from: skipSpaces(rangeStart, state2.doc, -1, blockStart),
3025
3024
  insert: marker
3026
3025
  });
3027
3026
  }
@@ -3029,7 +3028,7 @@ var setStyle = (type, enable) => ({ state, dispatch }) => {
3029
3028
  changes2.push(endCovered);
3030
3029
  } else if (endCovered) {
3031
3030
  changes2.push({
3032
- from: skipSpaces(rangeEnd, state.doc, 1, blockEnd),
3031
+ from: skipSpaces(rangeEnd, state2.doc, 1, blockEnd),
3033
3032
  insert: marker
3034
3033
  });
3035
3034
  }
@@ -3037,7 +3036,7 @@ var setStyle = (type, enable) => ({ state, dispatch }) => {
3037
3036
  }
3038
3037
  }
3039
3038
  });
3040
- if (blockStart < 0 && range.empty && enable && !/\S/.test(state.doc.lineAt(range.from).text)) {
3039
+ if (blockStart < 0 && range.empty && enable && !/\S/.test(state2.doc.lineAt(range.from).text)) {
3041
3040
  return {
3042
3041
  changes: {
3043
3042
  from: range.head,
@@ -3046,13 +3045,13 @@ var setStyle = (type, enable) => ({ state, dispatch }) => {
3046
3045
  range: EditorSelection.cursor(range.head + marker.length)
3047
3046
  };
3048
3047
  }
3049
- const changeSet = state.changes(changes2.concat(changesAtEnd));
3048
+ const changeSet = state2.changes(changes2.concat(changesAtEnd));
3050
3049
  return {
3051
3050
  changes: changeSet,
3052
3051
  range: range.empty && !changeSet.empty ? EditorSelection.cursor(range.head + marker.length) : EditorSelection.range(changeSet.mapPos(range.from, 1), changeSet.mapPos(range.to, -1))
3053
3052
  };
3054
3053
  });
3055
- dispatch(state.update(changes, {
3054
+ dispatch(state2.update(changes, {
3056
3055
  userEvent: enable ? "format.style.add" : "format.style.remove",
3057
3056
  scrollIntoView: true
3058
3057
  }));
@@ -3126,8 +3125,8 @@ var insertTable = (view) => {
3126
3125
  const { from } = doc.line(number);
3127
3126
  snippets.table(view, null, from, from);
3128
3127
  };
3129
- var removeLinkInner = (from, to, changes, state) => {
3130
- syntaxTree2(state).iterate({
3128
+ var removeLinkInner = (from, to, changes, state2) => {
3129
+ syntaxTree2(state2).iterate({
3131
3130
  from,
3132
3131
  to,
3133
3132
  enter: (node) => {
@@ -3141,8 +3140,8 @@ var removeLinkInner = (from, to, changes, state) => {
3141
3140
  });
3142
3141
  } else if (name === "LinkTitle" || name === "URL") {
3143
3142
  changes.push({
3144
- from: skipSpaces(node2.from, state.doc, -1),
3145
- to: skipSpaces(node2.to, state.doc, 1)
3143
+ from: skipSpaces(node2.from, state2.doc, -1),
3144
+ to: skipSpaces(node2.to, state2.doc, 1)
3146
3145
  });
3147
3146
  }
3148
3147
  });
@@ -3151,32 +3150,32 @@ var removeLinkInner = (from, to, changes, state) => {
3151
3150
  }
3152
3151
  });
3153
3152
  };
3154
- var removeLink = ({ state, dispatch }) => {
3153
+ var removeLink = ({ state: state2, dispatch }) => {
3155
3154
  const changes = [];
3156
- for (const { from, to } of state.selection.ranges) {
3157
- removeLinkInner(from, to, changes, state);
3155
+ for (const { from, to } of state2.selection.ranges) {
3156
+ removeLinkInner(from, to, changes, state2);
3158
3157
  }
3159
3158
  if (!changes) {
3160
3159
  return false;
3161
3160
  }
3162
- dispatch(state.update({
3161
+ dispatch(state2.update({
3163
3162
  changes,
3164
3163
  userEvent: "format.link.remove",
3165
3164
  scrollIntoView: true
3166
3165
  }));
3167
3166
  return true;
3168
3167
  };
3169
- var addLink = ({ state, dispatch }) => {
3170
- const changes = state.changeByRange((range) => {
3168
+ var addLink = ({ url, image: image2 } = {}) => ({ state: state2, dispatch }) => {
3169
+ const changes = state2.changeByRange((range) => {
3171
3170
  let { from, to } = range;
3172
3171
  const cutStyles = [];
3173
3172
  let okay = null;
3174
- syntaxTree2(state).iterate({
3173
+ syntaxTree2(state2).iterate({
3175
3174
  from,
3176
3175
  to,
3177
3176
  enter: (node) => {
3178
3177
  if (Object.hasOwn(Textblocks, node.name)) {
3179
- okay = Textblocks[node.name] !== "codeblock" && from >= blockContentStart(node) && to <= blockContentEnd(node, state.doc);
3178
+ okay = Textblocks[node.name] !== "codeblock" && from >= blockContentStart(node) && to <= blockContentEnd(node, state2.doc);
3180
3179
  } else if (Object.hasOwn(InlineMarker, node.name)) {
3181
3180
  const sNode = node.node;
3182
3181
  if (node.from < from && node.to <= to) {
@@ -3196,7 +3195,7 @@ var addLink = ({ state, dispatch }) => {
3196
3195
  }
3197
3196
  });
3198
3197
  if (okay === null) {
3199
- const line = state.doc.lineAt(from);
3198
+ const line = state2.doc.lineAt(from);
3200
3199
  okay = to <= line.to && !/\S/.test(line.text.slice(from - line.from));
3201
3200
  }
3202
3201
  if (!okay) {
@@ -3206,26 +3205,26 @@ var addLink = ({ state, dispatch }) => {
3206
3205
  }
3207
3206
  const changes2 = [];
3208
3207
  const changesAfter = [];
3209
- removeLinkInner(from, to, changesAfter, state);
3208
+ removeLinkInner(from, to, changesAfter, state2);
3210
3209
  let cursorOffset = 1;
3211
3210
  for (const style of cutStyles) {
3212
3211
  const type = InlineMarker[style.name];
3213
3212
  const mark2 = inlineMarkerText(type);
3214
3213
  if (style.from < from) {
3215
3214
  changes2.push({
3216
- from: skipSpaces(from, state.doc, -1),
3215
+ from: skipSpaces(from, state2.doc, -1),
3217
3216
  insert: mark2
3218
3217
  });
3219
3218
  changesAfter.push({
3220
- from: skipSpaces(from, state.doc, 1, to),
3219
+ from: skipSpaces(from, state2.doc, 1, to),
3221
3220
  insert: mark2
3222
3221
  });
3223
3222
  } else {
3224
3223
  changes2.push({
3225
- from: skipSpaces(to, state.doc, -1, from),
3224
+ from: skipSpaces(to, state2.doc, -1, from),
3226
3225
  insert: mark2
3227
3226
  });
3228
- const after = skipSpaces(to, state.doc, 1);
3227
+ const after = skipSpaces(to, state2.doc, 1);
3229
3228
  if (after === to) {
3230
3229
  cursorOffset += mark2.length;
3231
3230
  }
@@ -3237,34 +3236,34 @@ var addLink = ({ state, dispatch }) => {
3237
3236
  }
3238
3237
  changes2.push({
3239
3238
  from,
3240
- insert: "["
3239
+ insert: image2 ? "![" : "["
3241
3240
  }, {
3242
3241
  from: to,
3243
- insert: "]()"
3242
+ insert: `](${url ?? ""})`
3244
3243
  });
3245
- const changeSet = state.changes(changes2.concat(changesAfter));
3244
+ const changeSet = state2.changes(changes2.concat(changesAfter));
3246
3245
  return {
3247
3246
  changes: changeSet,
3248
- range: EditorSelection.cursor(changeSet.mapPos(to, 1) - cursorOffset)
3247
+ range: EditorSelection.cursor(changeSet.mapPos(to, 1) - cursorOffset - (url ? url.length + 2 : 0))
3249
3248
  };
3250
3249
  });
3251
3250
  if (changes.changes.empty) {
3252
3251
  return false;
3253
3252
  }
3254
- dispatch(state.update(changes, {
3253
+ dispatch(state2.update(changes, {
3255
3254
  userEvent: "format.link.add",
3256
3255
  scrollIntoView: true
3257
3256
  }));
3258
3257
  return true;
3259
3258
  };
3260
- var addList = (type) => ({ state, dispatch }) => {
3259
+ var addList = (type) => ({ state: state2, dispatch }) => {
3261
3260
  let lastBlock = -1;
3262
3261
  let counter = 1;
3263
3262
  let first = true;
3264
3263
  let parentColumn = null;
3265
3264
  const blocks = [];
3266
- for (const { from, to } of state.selection.ranges) {
3267
- syntaxTree2(state).iterate({
3265
+ for (const { from, to } of state2.selection.ranges) {
3266
+ syntaxTree2(state2).iterate({
3268
3267
  from,
3269
3268
  to,
3270
3269
  enter: (node) => {
@@ -3276,7 +3275,7 @@ var addList = (type) => ({ state, dispatch }) => {
3276
3275
  }
3277
3276
  if (before?.name === (type === 0 ? "OrderedList" : "BulletList")) {
3278
3277
  const item = before.lastChild;
3279
- const itemLine = state.doc.lineAt(item.from);
3278
+ const itemLine = state2.doc.lineAt(item.from);
3280
3279
  const itemText = itemLine.text.slice(item.from - itemLine.from);
3281
3280
  parentColumn = item.from - itemLine.from + /^\s*/.exec(itemText)[0].length;
3282
3281
  if (type === 0) {
@@ -3311,10 +3310,10 @@ var addList = (type) => ({ state, dispatch }) => {
3311
3310
  });
3312
3311
  }
3313
3312
  if (!blocks.length) {
3314
- const { from, to } = state.doc.lineAt(state.selection.main.anchor);
3313
+ const { from, to } = state2.doc.lineAt(state2.selection.main.anchor);
3315
3314
  if (from === to) {
3316
3315
  const insert = type === 1 ? "- " : type === 0 ? "1. " : "- [ ] ";
3317
- dispatch(state.update({
3316
+ dispatch(state2.update({
3318
3317
  changes: [
3319
3318
  {
3320
3319
  from,
@@ -3335,11 +3334,11 @@ var addList = (type) => ({ state, dispatch }) => {
3335
3334
  for (let i = 0; i < blocks.length; i++) {
3336
3335
  const { node, counter: counter2, parentColumn: parentColumn2 } = blocks[i];
3337
3336
  const nodeFrom = node.name === "CodeBlock" ? node.from - 4 : node.from;
3338
- let padding = nodeFrom > 0 && !/\s/.test(state.doc.sliceString(nodeFrom - 1, nodeFrom)) ? 1 : 0;
3337
+ let padding = nodeFrom > 0 && !/\s/.test(state2.doc.sliceString(nodeFrom - 1, nodeFrom)) ? 1 : 0;
3339
3338
  if (type === 0) {
3340
3339
  padding += String(counter2).length;
3341
3340
  }
3342
- let line = state.doc.lineAt(nodeFrom);
3341
+ let line = state2.doc.lineAt(nodeFrom);
3343
3342
  const column = nodeFrom - line.from;
3344
3343
  if (parentColumn2 !== null && parentColumn2 > column) {
3345
3344
  padding = Math.max(padding, parentColumn2 - column);
@@ -3364,7 +3363,7 @@ var addList = (type) => ({ state, dispatch }) => {
3364
3363
  insert: mark2
3365
3364
  });
3366
3365
  while (line.to < node.to) {
3367
- line = state.doc.lineAt(line.to + 1);
3366
+ line = state2.doc.lineAt(line.to + 1);
3368
3367
  const open = /^[\s>]*/.exec(line.text)[0].length;
3369
3368
  changes.push({
3370
3369
  from: line.from + Math.min(open, column),
@@ -3379,25 +3378,25 @@ var addList = (type) => ({ state, dispatch }) => {
3379
3378
  next = next.nextSibling;
3380
3379
  }
3381
3380
  if (next?.name === "OrderedList") {
3382
- renumberListItems(next.firstChild, last.counter + 1, changes, state.doc);
3381
+ renumberListItems(next.firstChild, last.counter + 1, changes, state2.doc);
3383
3382
  }
3384
3383
  }
3385
- const changeSet = state.changes(changes);
3386
- dispatch(state.update({
3384
+ const changeSet = state2.changes(changes);
3385
+ dispatch(state2.update({
3387
3386
  changes: changeSet,
3388
- selection: state.selection.map(changeSet, 1),
3387
+ selection: state2.selection.map(changeSet, 1),
3389
3388
  userEvent: "format.list.add",
3390
3389
  scrollIntoView: true
3391
3390
  }));
3392
3391
  return true;
3393
3392
  };
3394
- var removeList = (type) => ({ state, dispatch }) => {
3393
+ var removeList = (type) => ({ state: state2, dispatch }) => {
3395
3394
  let lastBlock = -1;
3396
3395
  const changes = [];
3397
3396
  const stack = [];
3398
3397
  const targetNodeType = type === 0 ? "OrderedList" : type === 1 ? "BulletList" : "TaskList";
3399
- for (const { from, to } of state.selection.ranges) {
3400
- syntaxTree2(state).iterate({
3398
+ for (const { from, to } of state2.selection.ranges) {
3399
+ syntaxTree2(state2).iterate({
3401
3400
  from,
3402
3401
  to,
3403
3402
  enter: (node) => {
@@ -3414,7 +3413,7 @@ var removeList = (type) => ({ state, dispatch }) => {
3414
3413
  stack.pop();
3415
3414
  } else if (name === "ListItem" && stack[stack.length - 1] === targetNodeType && node.from !== lastBlock) {
3416
3415
  lastBlock = node.from;
3417
- let line = state.doc.lineAt(node.from);
3416
+ let line = state2.doc.lineAt(node.from);
3418
3417
  const mark2 = /^\s*(\d+[.)] |[-*+] (\[[ x]\] )?)/.exec(line.text.slice(node.from - line.from));
3419
3418
  if (!mark2) {
3420
3419
  return false;
@@ -3425,7 +3424,7 @@ var removeList = (type) => ({ state, dispatch }) => {
3425
3424
  to: node.from + mark2[0].length
3426
3425
  });
3427
3426
  while (line.to < node.to) {
3428
- line = state.doc.lineAt(line.to + 1);
3427
+ line = state2.doc.lineAt(line.to + 1);
3429
3428
  const open = /^[\s>]*/.exec(line.text)[0].length;
3430
3429
  if (open > column) {
3431
3430
  changes.push({
@@ -3435,7 +3434,7 @@ var removeList = (type) => ({ state, dispatch }) => {
3435
3434
  }
3436
3435
  }
3437
3436
  if (node.to >= to) {
3438
- renumberListItems(node.node.nextSibling, 1, changes, state.doc);
3437
+ renumberListItems(node.node.nextSibling, 1, changes, state2.doc);
3439
3438
  }
3440
3439
  return false;
3441
3440
  }
@@ -3445,7 +3444,7 @@ var removeList = (type) => ({ state, dispatch }) => {
3445
3444
  if (!changes.length) {
3446
3445
  return false;
3447
3446
  }
3448
- dispatch(state.update({
3447
+ dispatch(state2.update({
3449
3448
  changes,
3450
3449
  userEvent: "format.list.remove",
3451
3450
  scrollIntoView: true
@@ -3475,12 +3474,12 @@ var renumberListItems = (item, counter, changes, doc) => {
3475
3474
  }
3476
3475
  }
3477
3476
  };
3478
- var setBlockquote = (enable) => ({ state, dispatch }) => {
3477
+ var setBlockquote = (enable) => ({ state: state2, dispatch }) => {
3479
3478
  const lines = [];
3480
3479
  let lastBlock = -1;
3481
- for (const { from, to } of state.selection.ranges) {
3480
+ for (const { from, to } of state2.selection.ranges) {
3482
3481
  const sawBlock = false;
3483
- syntaxTree2(state).iterate({
3482
+ syntaxTree2(state2).iterate({
3484
3483
  from,
3485
3484
  to,
3486
3485
  enter: (node) => {
@@ -3489,9 +3488,9 @@ var setBlockquote = (enable) => ({ state, dispatch }) => {
3489
3488
  return false;
3490
3489
  }
3491
3490
  lastBlock = node.from;
3492
- let line2 = state.doc.lineAt(node.from);
3491
+ let line2 = state2.doc.lineAt(node.from);
3493
3492
  if (line2.number > 1) {
3494
- const prevLine = state.doc.line(line2.number - 1);
3493
+ const prevLine = state2.doc.line(line2.number - 1);
3495
3494
  if (/^[>\s]*$/.test(prevLine.text)) {
3496
3495
  if (!enable || lines.length && lines[lines.length - 1].number === prevLine.number - 1) {
3497
3496
  lines.push(prevLine);
@@ -3503,10 +3502,10 @@ var setBlockquote = (enable) => ({ state, dispatch }) => {
3503
3502
  if (line2.to >= node.to) {
3504
3503
  break;
3505
3504
  }
3506
- line2 = state.doc.line(line2.number + 1);
3505
+ line2 = state2.doc.line(line2.number + 1);
3507
3506
  }
3508
- if (!enable && line2.number < state.doc.lines) {
3509
- const nextLine = state.doc.line(line2.number + 1);
3507
+ if (!enable && line2.number < state2.doc.lines) {
3508
+ const nextLine = state2.doc.line(line2.number + 1);
3510
3509
  if (/^[>\s]*$/.test(nextLine.text)) {
3511
3510
  lines.push(nextLine);
3512
3511
  }
@@ -3516,7 +3515,7 @@ var setBlockquote = (enable) => ({ state, dispatch }) => {
3516
3515
  }
3517
3516
  });
3518
3517
  let line;
3519
- if (!sawBlock && enable && from === to && !/\S/.test((line = state.doc.lineAt(from)).text)) {
3518
+ if (!sawBlock && enable && from === to && !/\S/.test((line = state2.doc.lineAt(from)).text)) {
3520
3519
  lines.push(line);
3521
3520
  }
3522
3521
  }
@@ -3540,10 +3539,10 @@ var setBlockquote = (enable) => ({ state, dispatch }) => {
3540
3539
  if (!changes.length) {
3541
3540
  return false;
3542
3541
  }
3543
- const changeSet = state.changes(changes);
3544
- dispatch(state.update({
3542
+ const changeSet = state2.changes(changes);
3543
+ dispatch(state2.update({
3545
3544
  changes: changeSet,
3546
- selection: state.selection.map(changeSet, 1),
3545
+ selection: state2.selection.map(changeSet, 1),
3547
3546
  userEvent: enable ? "format.blockquote.add" : "format.blockquote.remove",
3548
3547
  scrollIntoView: true
3549
3548
  }));
@@ -3555,11 +3554,11 @@ var toggleBlockquote = (target) => {
3555
3554
  return (getFormatting(target.state).blockQuote ? removeBlockquote : addBlockquote)(target);
3556
3555
  };
3557
3556
  var addCodeblock = (target) => {
3558
- const { state, dispatch } = target;
3559
- const { selection } = state;
3557
+ const { state: state2, dispatch } = target;
3558
+ const { selection } = state2;
3560
3559
  if (selection.ranges.length === 1 && selection.main.empty) {
3561
3560
  const { head } = selection.main;
3562
- const line = state.doc.lineAt(head);
3561
+ const line = state2.doc.lineAt(head);
3563
3562
  if (!/\S/.test(line.text) && head === line.from) {
3564
3563
  snippets.codeblock(target, null, line.from, line.to);
3565
3564
  return true;
@@ -3569,7 +3568,7 @@ var addCodeblock = (target) => {
3569
3568
  for (const { from, to } of selection.ranges) {
3570
3569
  let blockFrom = from;
3571
3570
  let blockTo = to;
3572
- syntaxTree2(state).iterate({
3571
+ syntaxTree2(state2).iterate({
3573
3572
  from,
3574
3573
  to,
3575
3574
  enter: (node) => {
@@ -3578,8 +3577,8 @@ var addCodeblock = (target) => {
3578
3577
  blockFrom = node.from;
3579
3578
  blockTo = node.to;
3580
3579
  } else {
3581
- blockFrom = Math.min(blockFrom, state.doc.lineAt(node.from).from);
3582
- blockTo = Math.max(blockTo, state.doc.lineAt(node.to).to);
3580
+ blockFrom = Math.min(blockFrom, state2.doc.lineAt(node.from).from);
3581
+ blockTo = Math.max(blockTo, state2.doc.lineAt(node.to).to);
3583
3582
  }
3584
3583
  }
3585
3584
  }
@@ -3597,7 +3596,7 @@ var addCodeblock = (target) => {
3597
3596
  return false;
3598
3597
  }
3599
3598
  const changes = ranges.map(({ from, to }) => {
3600
- const column = from - state.doc.lineAt(from).from;
3599
+ const column = from - state2.doc.lineAt(from).from;
3601
3600
  return [
3602
3601
  {
3603
3602
  from,
@@ -3609,30 +3608,30 @@ var addCodeblock = (target) => {
3609
3608
  }
3610
3609
  ];
3611
3610
  });
3612
- dispatch(state.update({
3611
+ dispatch(state2.update({
3613
3612
  changes,
3614
3613
  userEvent: "format.codeblock.add",
3615
3614
  scrollIntoView: true
3616
3615
  }));
3617
3616
  return true;
3618
3617
  };
3619
- var removeCodeblock = ({ state, dispatch }) => {
3618
+ var removeCodeblock = ({ state: state2, dispatch }) => {
3620
3619
  const changes = [];
3621
3620
  let lastBlock = -1;
3622
- for (const { from, to } of state.selection.ranges) {
3623
- syntaxTree2(state).iterate({
3621
+ for (const { from, to } of state2.selection.ranges) {
3622
+ syntaxTree2(state2).iterate({
3624
3623
  from,
3625
3624
  to,
3626
3625
  enter: (node) => {
3627
3626
  if (Textblocks[node.name] === "codeblock" && lastBlock !== node.from) {
3628
3627
  lastBlock = node.from;
3629
- const firstLine = state.doc.lineAt(node.from);
3628
+ const firstLine = state2.doc.lineAt(node.from);
3630
3629
  if (node.name === "FencedCode") {
3631
3630
  changes.push({
3632
3631
  from: node.from,
3633
3632
  to: firstLine.to + 1 + node.from - firstLine.from
3634
3633
  });
3635
- const lastLine = state.doc.lineAt(node.to);
3634
+ const lastLine = state2.doc.lineAt(node.to);
3636
3635
  if (/^([\s>]|[-*+] |\d+[).])*`+$/.test(lastLine.text)) {
3637
3636
  changes.push({
3638
3637
  from: lastLine.from - (lastLine.number === firstLine.number + 1 ? 0 : 1),
@@ -3641,7 +3640,7 @@ var removeCodeblock = ({ state, dispatch }) => {
3641
3640
  }
3642
3641
  } else {
3643
3642
  const column = node.from - firstLine.from;
3644
- for (let line = firstLine; ; line = state.doc.line(line.number + 1)) {
3643
+ for (let line = firstLine; ; line = state2.doc.line(line.number + 1)) {
3645
3644
  changes.push({
3646
3645
  from: line.from + column - 4,
3647
3646
  to: line.from + column
@@ -3658,7 +3657,7 @@ var removeCodeblock = ({ state, dispatch }) => {
3658
3657
  if (!changes.length) {
3659
3658
  return false;
3660
3659
  }
3661
- dispatch(state.update({
3660
+ dispatch(state2.update({
3662
3661
  changes,
3663
3662
  userEvent: "format.codeblock.remove",
3664
3663
  scrollIntoView: true
@@ -3717,7 +3716,7 @@ var Textblocks = {
3717
3716
  TableCell: "tablecell",
3718
3717
  Task: "paragraph"
3719
3718
  };
3720
- var getFormatting = (state) => {
3719
+ var getFormatting = (state2) => {
3721
3720
  let blockType = null;
3722
3721
  const inline = [
3723
3722
  null,
@@ -3743,7 +3742,7 @@ var getFormatting = (state) => {
3743
3742
  continue;
3744
3743
  } else if (currentBlock.active[i]) {
3745
3744
  inline[i] = true;
3746
- } else if (/\S/.test(state.doc.sliceString(currentBlock.pos, upto))) {
3745
+ } else if (/\S/.test(state2.doc.sliceString(currentBlock.pos, upto))) {
3747
3746
  inline[i] = false;
3748
3747
  }
3749
3748
  }
@@ -3754,12 +3753,12 @@ var getFormatting = (state) => {
3754
3753
  currentBlock.pos = Math.min(upto, currentBlock.end);
3755
3754
  }
3756
3755
  };
3757
- const { selection } = state;
3756
+ const { selection } = state2;
3758
3757
  for (const range of selection.ranges) {
3759
3758
  if (range.empty && inline.some((v) => v === null)) {
3760
3759
  const contextSize = Math.min(range.head, 6);
3761
- const contextBefore = state.doc.sliceString(range.head - contextSize, range.head);
3762
- let contextAfter = state.doc.sliceString(range.head, range.head + contextSize);
3760
+ const contextBefore = state2.doc.sliceString(range.head - contextSize, range.head);
3761
+ let contextAfter = state2.doc.sliceString(range.head, range.head + contextSize);
3763
3762
  for (let i = 0; i < contextSize; i++) {
3764
3763
  const ch = contextAfter[i];
3765
3764
  if (ch !== contextBefore[contextBefore.length - 1 - i] || !/[~`*]/.test(ch)) {
@@ -3778,7 +3777,7 @@ var getFormatting = (state) => {
3778
3777
  }
3779
3778
  }
3780
3779
  }
3781
- syntaxTree2(state).iterate({
3780
+ syntaxTree2(state2).iterate({
3782
3781
  from: range.from,
3783
3782
  to: range.to,
3784
3783
  enter: (node) => {
@@ -3852,7 +3851,7 @@ var getFormatting = (state) => {
3852
3851
  }
3853
3852
  });
3854
3853
  }
3855
- const { from, to } = state.doc.lineAt(selection.main.anchor);
3854
+ const { from, to } = state2.doc.lineAt(selection.main.anchor);
3856
3855
  const blankLine = from === to;
3857
3856
  return {
3858
3857
  blankLine,
@@ -3867,17 +3866,17 @@ var getFormatting = (state) => {
3867
3866
  };
3868
3867
  };
3869
3868
  var useFormattingState = () => {
3870
- const [state, setState] = useState(null);
3869
+ const [state2, setState, stateRef] = useStateRef();
3871
3870
  const observer = useMemo(() => EditorView13.updateListener.of((update2) => {
3872
3871
  if (update2.docChanged || update2.selectionSet) {
3873
3872
  const newState = getFormatting(update2.state);
3874
- if (!state || !compareFormatting(state, newState)) {
3873
+ if (!stateRef.current || !formattingEquals(stateRef.current, newState)) {
3875
3874
  setState(newState);
3876
3875
  }
3877
3876
  }
3878
3877
  }), []);
3879
3878
  return [
3880
- state,
3879
+ state2,
3881
3880
  observer
3882
3881
  ];
3883
3882
  };
@@ -3888,8 +3887,8 @@ import { StateField as StateField5 } from "@codemirror/state";
3888
3887
  import { Decoration as Decoration6, EditorView as EditorView14, WidgetType as WidgetType4 } from "@codemirror/view";
3889
3888
  var image = (options = {}) => {
3890
3889
  return StateField5.define({
3891
- create: (state) => {
3892
- return Decoration6.set(buildDecorations2(0, state.doc.length, state));
3890
+ create: (state2) => {
3891
+ return Decoration6.set(buildDecorations2(0, state2.doc.length, state2));
3893
3892
  },
3894
3893
  update: (value, tr) => {
3895
3894
  if (!tr.docChanged && !tr.selection) {
@@ -3923,16 +3922,16 @@ var preloadImage = (url) => {
3923
3922
  preloaded.add(url);
3924
3923
  }
3925
3924
  };
3926
- var buildDecorations2 = (from, to, state) => {
3925
+ var buildDecorations2 = (from, to, state2) => {
3927
3926
  const decorations = [];
3928
- const cursor = state.selection.main.head;
3929
- syntaxTree3(state).iterate({
3927
+ const cursor = state2.selection.main.head;
3928
+ syntaxTree3(state2).iterate({
3930
3929
  enter: (node) => {
3931
3930
  if (node.name === "Image") {
3932
3931
  const urlNode = node.node.getChild("URL");
3933
3932
  if (urlNode) {
3934
- const hide2 = state.readOnly || cursor < node.from || cursor > node.to;
3935
- const url = state.sliceDoc(urlNode.from, urlNode.to);
3933
+ const hide2 = state2.readOnly || cursor < node.from || cursor > node.to;
3934
+ const url = state2.sliceDoc(urlNode.from, urlNode.to);
3936
3935
  preloadImage(url);
3937
3936
  decorations.push(Decoration6.replace({
3938
3937
  block: true,
@@ -3962,6 +3961,8 @@ var ImageWidget = class extends WidgetType4 {
3962
3961
  return img;
3963
3962
  }
3964
3963
  };
3964
+ var imageUpload = (options = {}) => {
3965
+ };
3965
3966
 
3966
3967
  // packages/ui/react-ui-editor/src/extensions/markdown/link.ts
3967
3968
  import { syntaxTree as syntaxTree4 } from "@codemirror/language";
@@ -4003,21 +4004,21 @@ import { RangeSetBuilder as RangeSetBuilder3, StateField as StateField6 } from "
4003
4004
  import { Decoration as Decoration7, EditorView as EditorView15, WidgetType as WidgetType5 } from "@codemirror/view";
4004
4005
  var table = (options = {}) => {
4005
4006
  return StateField6.define({
4006
- create: (state) => update(state, options),
4007
+ create: (state2) => update(state2, options),
4007
4008
  update: (_, tr) => update(tr.state, options),
4008
4009
  provide: (field) => EditorView15.decorations.from(field)
4009
4010
  });
4010
4011
  };
4011
- var update = (state, options) => {
4012
+ var update = (state2, options) => {
4012
4013
  const builder = new RangeSetBuilder3();
4013
- const cursor = state.selection.main.head;
4014
+ const cursor = state2.selection.main.head;
4014
4015
  const tables = [];
4015
4016
  const getTable = () => tables[tables.length - 1];
4016
4017
  const getRow = () => {
4017
4018
  const table2 = getTable();
4018
4019
  return table2.rows?.[table2.rows.length - 1];
4019
4020
  };
4020
- syntaxTree5(state).iterate({
4021
+ syntaxTree5(state2).iterate({
4021
4022
  enter: (node) => {
4022
4023
  switch (node.name) {
4023
4024
  case "Table": {
@@ -4038,9 +4039,9 @@ var update = (state, options) => {
4038
4039
  case "TableCell": {
4039
4040
  const row = getRow();
4040
4041
  if (row) {
4041
- row.push(state.sliceDoc(node.from, node.to));
4042
+ row.push(state2.sliceDoc(node.from, node.to));
4042
4043
  } else {
4043
- getTable().header?.push(state.sliceDoc(node.from, node.to));
4044
+ getTable().header?.push(state2.sliceDoc(node.from, node.to));
4044
4045
  }
4045
4046
  break;
4046
4047
  }
@@ -4048,7 +4049,7 @@ var update = (state, options) => {
4048
4049
  }
4049
4050
  });
4050
4051
  tables.forEach((table2) => {
4051
- const hide2 = state.readOnly || cursor < table2.from || cursor > table2.to;
4052
+ const hide2 = state2.readOnly || cursor < table2.from || cursor > table2.to;
4052
4053
  if (hide2) {
4053
4054
  builder.add(table2.from, table2.to, Decoration7.replace({
4054
4055
  widget: new TableWidget(table2)
@@ -4139,6 +4140,7 @@ var mention = ({ onSearch }) => {
4139
4140
  import { Facet as Facet6 } from "@codemirror/state";
4140
4141
  import { keymap as keymap7 } from "@codemirror/view";
4141
4142
  import { vim } from "@replit/codemirror-vim";
4143
+ var focusEvent = "focus.container";
4142
4144
  var editorMode = Facet6.define({
4143
4145
  combine: (modes) => modes[0] ?? {}
4144
4146
  });
@@ -4155,7 +4157,7 @@ var EditorModes = {
4155
4157
  key: "Alt-Escape",
4156
4158
  run: (view) => {
4157
4159
  view.dispatch({
4158
- userEvent: "focus.container"
4160
+ userEvent: focusEvent
4159
4161
  });
4160
4162
  return true;
4161
4163
  }
@@ -4164,8 +4166,95 @@ var EditorModes = {
4164
4166
  ]
4165
4167
  };
4166
4168
 
4169
+ // packages/ui/react-ui-editor/src/extensions/state.ts
4170
+ import { Transaction as Transaction2 } from "@codemirror/state";
4171
+ import { EditorView as EditorView16, keymap as keymap8 } from "@codemirror/view";
4172
+ import { debounce as debounce2 } from "@dxos/async";
4173
+ import { invariant as invariant4 } from "@dxos/invariant";
4174
+ import { isNotFalsy as isNotFalsy3 } from "@dxos/util";
4175
+ var __dxlog_file11 = "/home/runner/work/dxos/dxos/packages/ui/react-ui-editor/src/extensions/state.ts";
4176
+ var scrollAnnotation = "dxos.org/cm/scrolling";
4177
+ var keyPrefix = "dxos.org/react-ui-editor/state";
4178
+ var localStorageStateStoreAdapter = {
4179
+ setState: (id, state2) => {
4180
+ invariant4(id, void 0, {
4181
+ F: __dxlog_file11,
4182
+ L: 35,
4183
+ S: void 0,
4184
+ A: [
4185
+ "id",
4186
+ ""
4187
+ ]
4188
+ });
4189
+ localStorage.setItem(`${keyPrefix}/${id}`, JSON.stringify(state2));
4190
+ },
4191
+ getState: (id) => {
4192
+ invariant4(id, void 0, {
4193
+ F: __dxlog_file11,
4194
+ L: 39,
4195
+ S: void 0,
4196
+ A: [
4197
+ "id",
4198
+ ""
4199
+ ]
4200
+ });
4201
+ const state2 = localStorage.getItem(`${keyPrefix}/${id}`);
4202
+ return state2 ? JSON.parse(state2) : void 0;
4203
+ }
4204
+ };
4205
+ var state = ({ getState, setState } = {}) => {
4206
+ const setStateDebounced = debounce2(setState, 1e3);
4207
+ return [
4208
+ // TODO(burdon): Track scrolling (currently only updates when cursor moves).
4209
+ EditorView16.updateListener.of(({ view, changes, transactions }) => {
4210
+ const id = view.state.facet(documentId2);
4211
+ if (!id || transactions.some((tr) => tr.isUserEvent(scrollAnnotation))) {
4212
+ return;
4213
+ }
4214
+ if (setState) {
4215
+ const { top } = view.dom.getBoundingClientRect();
4216
+ const pos = view.posAtCoords({
4217
+ x: 0,
4218
+ y: top
4219
+ });
4220
+ if (pos !== null) {
4221
+ const { anchor, head } = view.state.selection.main;
4222
+ setStateDebounced(id, {
4223
+ scrollTo: {
4224
+ from: pos,
4225
+ yMargin: 0
4226
+ },
4227
+ selection: {
4228
+ anchor,
4229
+ head
4230
+ }
4231
+ });
4232
+ }
4233
+ }
4234
+ }),
4235
+ getState && keymap8.of([
4236
+ {
4237
+ key: "ctrl-r",
4238
+ run: (view) => {
4239
+ const state2 = getState(view.state.facet(documentId2));
4240
+ if (state2) {
4241
+ view.dispatch({
4242
+ effects: EditorView16.scrollIntoView(state2.scrollTo.from, {
4243
+ yMargin: 0
4244
+ }),
4245
+ selection: state2.selection,
4246
+ annotations: Transaction2.userEvent.of(scrollAnnotation)
4247
+ });
4248
+ }
4249
+ return true;
4250
+ }
4251
+ }
4252
+ ])
4253
+ ].filter(isNotFalsy3);
4254
+ };
4255
+
4167
4256
  // packages/ui/react-ui-editor/src/extensions/typewriter.ts
4168
- import { keymap as keymap8 } from "@codemirror/view";
4257
+ import { keymap as keymap9 } from "@codemirror/view";
4169
4258
  var defaultItems = [
4170
4259
  "hello world!",
4171
4260
  "this is a test.",
@@ -4175,7 +4264,7 @@ var typewriter = ({ delay = 75, items = defaultItems } = {}) => {
4175
4264
  let t;
4176
4265
  let idx = 0;
4177
4266
  return [
4178
- keymap8.of([
4267
+ keymap9.of([
4179
4268
  {
4180
4269
  // Reset.
4181
4270
  key: "alt-meta-'",
@@ -4222,15 +4311,17 @@ var typewriter = ({ delay = 75, items = defaultItems } = {}) => {
4222
4311
  };
4223
4312
 
4224
4313
  // packages/ui/react-ui-editor/src/components/TextEditor/TextEditor.tsx
4225
- var __dxlog_file11 = "/home/runner/work/dxos/dxos/packages/ui/react-ui-editor/src/components/TextEditor/TextEditor.tsx";
4314
+ var __dxlog_file12 = "/home/runner/work/dxos/dxos/packages/ui/react-ui-editor/src/components/TextEditor/TextEditor.tsx";
4226
4315
  var instanceCount = 0;
4227
- var TextEditor = /* @__PURE__ */ forwardRef(({ id, doc, selection, extensions, className, autoFocus, scrollTo = EditorView16.scrollIntoView(0), moveToEndOfLine, debug, dataTestId }, forwardedRef) => {
4228
- const [instanceId] = useState2(() => `text-editor-${++instanceCount}`);
4316
+ var TextEditor = /* @__PURE__ */ forwardRef(({ id, doc, selection, extensions, className, autoFocus, scrollTo = EditorView17.scrollIntoView(0, {
4317
+ yMargin: 0
4318
+ }), moveToEndOfLine, debug, dataTestId }, forwardedRef) => {
4319
+ const [instanceId] = useState(() => `text-editor-${++instanceCount}`);
4229
4320
  const tabsterDOMAttribute = useFocusableGroup({
4230
4321
  tabBehavior: "limited"
4231
4322
  });
4232
4323
  const rootRef = useRef(null);
4233
- const [view, setView] = useState2(null);
4324
+ const [view, setView] = useState(null);
4234
4325
  useImperativeHandle(forwardedRef, () => view, [
4235
4326
  view
4236
4327
  ]);
@@ -4243,42 +4334,42 @@ var TextEditor = /* @__PURE__ */ forwardRef(({ id, doc, selection, extensions, c
4243
4334
  autoFocus
4244
4335
  ]);
4245
4336
  useEffect2(() => {
4246
- log9("updating", {
4337
+ log9("create", {
4247
4338
  id,
4248
4339
  instanceId
4249
4340
  }, {
4250
- F: __dxlog_file11,
4341
+ F: __dxlog_file12,
4251
4342
  L: 88,
4252
4343
  S: void 0,
4253
4344
  C: (f, a) => f(...a)
4254
4345
  });
4255
- const state = EditorState2.create({
4346
+ const state2 = EditorState2.create({
4256
4347
  doc,
4257
4348
  selection,
4258
4349
  extensions: [
4259
4350
  id && documentId2.of(id),
4260
4351
  // TODO(burdon): NOTE: Doesn't catch errors in keymap functions.
4261
- EditorView16.exceptionSink.of((err) => {
4352
+ EditorView17.exceptionSink.of((err) => {
4262
4353
  log9.catch(err, void 0, {
4263
- F: __dxlog_file11,
4354
+ F: __dxlog_file12,
4264
4355
  L: 101,
4265
4356
  S: void 0,
4266
4357
  C: (f, a) => f(...a)
4267
4358
  });
4268
4359
  }),
4269
4360
  // Focus.
4270
- EditorView16.updateListener.of((update2) => {
4361
+ EditorView17.updateListener.of((update2) => {
4271
4362
  update2.transactions.forEach((transaction) => {
4272
- if (transaction.isUserEvent("focus.container")) {
4363
+ if (transaction.isUserEvent(focusEvent)) {
4273
4364
  rootRef.current?.focus();
4274
4365
  }
4275
4366
  });
4276
4367
  }),
4277
4368
  extensions
4278
- ].filter(isNotFalsy3)
4369
+ ].filter(isNotFalsy4)
4279
4370
  });
4280
- const view2 = new EditorView16({
4281
- state,
4371
+ const view2 = new EditorView17({
4372
+ state: state2,
4282
4373
  parent: rootRef.current,
4283
4374
  scrollTo,
4284
4375
  // NOTE: Uncomment to debug/monitor all transactions.
@@ -4290,21 +4381,29 @@ var TextEditor = /* @__PURE__ */ forwardRef(({ id, doc, selection, extensions, c
4290
4381
  view3.update(trs);
4291
4382
  }
4292
4383
  });
4293
- setView(view2);
4294
- if (moveToEndOfLine) {
4384
+ if (moveToEndOfLine && !(scrollTo || selection)) {
4295
4385
  const { to } = view2.state.doc.lineAt(0);
4296
- view2?.dispatch({
4386
+ view2.dispatch({
4297
4387
  selection: {
4298
4388
  anchor: to
4299
4389
  }
4300
4390
  });
4301
4391
  }
4302
- if (state.facet(editorMode).noTabster) {
4392
+ if (state2.facet(editorMode).noTabster) {
4303
4393
  rootRef.current?.removeAttribute("data-tabster");
4304
4394
  }
4395
+ setView(view2);
4305
4396
  return () => {
4397
+ log9("destroy", {
4398
+ id,
4399
+ instanceId
4400
+ }, {
4401
+ F: __dxlog_file12,
4402
+ L: 150,
4403
+ S: void 0,
4404
+ C: (f, a) => f(...a)
4405
+ });
4306
4406
  view2?.destroy();
4307
- setView(null);
4308
4407
  };
4309
4408
  }, [
4310
4409
  doc,
@@ -4334,9 +4433,10 @@ var TextEditor = /* @__PURE__ */ forwardRef(({ id, doc, selection, extensions, c
4334
4433
  });
4335
4434
 
4336
4435
  // packages/ui/react-ui-editor/src/components/Toolbar/Toolbar.tsx
4337
- import { ChatText, Code, CodeBlock, Link, ListBullets, ListChecks, ListNumbers, Paragraph, TextStrikethrough, Table as Table2, TextB, TextHOne, TextHTwo, TextHThree, TextHFour, TextHFive, TextHSix, TextItalic, Quotes } from "@phosphor-icons/react";
4436
+ import { ChatText, Code, CodeBlock, Image, Link, ListBullets, ListChecks, ListNumbers, Paragraph, Quotes, TextStrikethrough, Table as Table2, TextB, TextHOne, TextHTwo, TextHThree, TextHFour, TextHFive, TextHSix, TextItalic } from "@phosphor-icons/react";
4338
4437
  import { createContext } from "@radix-ui/react-context";
4339
- import React2, { useRef as useRef2, useState as useState3 } from "react";
4438
+ import React2, { useEffect as useEffect3, useRef as useRef2, useState as useState2 } from "react";
4439
+ import { useDropzone } from "react-dropzone";
4340
4440
  import { DensityProvider, ElevationProvider, Select, Toolbar as NaturalToolbar, Tooltip, useTranslation } from "@dxos/react-ui";
4341
4441
  import { getSize } from "@dxos/react-ui-theme";
4342
4442
  var tooltipProps = {
@@ -4353,10 +4453,10 @@ var HeadingIcons = {
4353
4453
  "6": TextHSix
4354
4454
  };
4355
4455
  var [ToolbarContextProvider, useToolbarContext] = createContext("Toolbar");
4356
- var ToolbarRoot = ({ children, onAction, classNames, state }) => {
4456
+ var ToolbarRoot = ({ children, onAction, classNames, state: state2 }) => {
4357
4457
  return /* @__PURE__ */ React2.createElement(ToolbarContextProvider, {
4358
4458
  onAction,
4359
- state
4459
+ state: state2
4360
4460
  }, /* @__PURE__ */ React2.createElement(DensityProvider, {
4361
4461
  density: "fine"
4362
4462
  }, /* @__PURE__ */ React2.createElement(ElevationProvider, {
@@ -4370,7 +4470,7 @@ var ToolbarRoot = ({ children, onAction, classNames, state }) => {
4370
4470
  };
4371
4471
  var buttonStyles = "min-bs-0 p-2";
4372
4472
  var iconStyles = getSize(5);
4373
- var ToolbarButton = ({ Icon, children, ...props }) => {
4473
+ var ToolbarToggleButton = ({ Icon, children, ...props }) => {
4374
4474
  return /* @__PURE__ */ React2.createElement(Tooltip.Root, null, /* @__PURE__ */ React2.createElement(Tooltip.Trigger, {
4375
4475
  asChild: true
4376
4476
  }, /* @__PURE__ */ React2.createElement(NaturalToolbar.ToggleGroupItem, {
@@ -4383,20 +4483,33 @@ var ToolbarButton = ({ Icon, children, ...props }) => {
4383
4483
  className: "sr-only"
4384
4484
  }, children))), /* @__PURE__ */ React2.createElement(Tooltip.Portal, null, /* @__PURE__ */ React2.createElement(Tooltip.Content, tooltipProps, children, /* @__PURE__ */ React2.createElement(Tooltip.Arrow, null))));
4385
4485
  };
4486
+ var ToolbarButton = ({ Icon, children, ...props }) => {
4487
+ return /* @__PURE__ */ React2.createElement(Tooltip.Root, null, /* @__PURE__ */ React2.createElement(Tooltip.Trigger, {
4488
+ asChild: true
4489
+ }, /* @__PURE__ */ React2.createElement(NaturalToolbar.Button, {
4490
+ variant: "ghost",
4491
+ ...props,
4492
+ classNames: buttonStyles
4493
+ }, /* @__PURE__ */ React2.createElement(Icon, {
4494
+ className: iconStyles
4495
+ }), /* @__PURE__ */ React2.createElement("span", {
4496
+ className: "sr-only"
4497
+ }, children))), /* @__PURE__ */ React2.createElement(Tooltip.Portal, null, /* @__PURE__ */ React2.createElement(Tooltip.Content, tooltipProps, children, /* @__PURE__ */ React2.createElement(Tooltip.Arrow, null))));
4498
+ };
4386
4499
  var ToolbarSeparator = () => /* @__PURE__ */ React2.createElement("div", {
4387
4500
  role: "separator",
4388
4501
  className: "grow"
4389
4502
  });
4390
4503
  var MarkdownHeading = () => {
4391
4504
  const { t } = useTranslation(translationKey);
4392
- const { onAction, state } = useToolbarContext("MarkdownFormatting");
4393
- const blockType = state ? state.blockType : "paragraph";
4505
+ const { onAction, state: state2 } = useToolbarContext("MarkdownFormatting");
4506
+ const blockType = state2 ? state2.blockType : "paragraph";
4394
4507
  const header = blockType && /heading(\d)/.exec(blockType);
4395
4508
  const value = header ? header[1] : blockType === "paragraph" || !blockType ? "0" : void 0;
4396
4509
  const HeadingIcon = HeadingIcons[value ?? "0"];
4397
4510
  const suppressNextTooltip = useRef2(false);
4398
- const [tooltipOpen, setTooltipOpen] = useState3(false);
4399
- const [selectOpen, setSelectOpen] = useState3(false);
4511
+ const [tooltipOpen, setTooltipOpen] = useState2(false);
4512
+ const [selectOpen, setSelectOpen] = useState2(false);
4400
4513
  return /* @__PURE__ */ React2.createElement(Tooltip.Root, {
4401
4514
  open: tooltipOpen,
4402
4515
  onOpenChange: (nextOpen) => {
@@ -4464,43 +4577,43 @@ var markdownStyles = [
4464
4577
  {
4465
4578
  type: "strong",
4466
4579
  Icon: TextB,
4467
- getState: (state) => state.strong
4580
+ getState: (state2) => state2.strong
4468
4581
  },
4469
4582
  {
4470
4583
  type: "emphasis",
4471
4584
  Icon: TextItalic,
4472
- getState: (state) => state.emphasis
4585
+ getState: (state2) => state2.emphasis
4473
4586
  },
4474
4587
  {
4475
4588
  type: "strikethrough",
4476
4589
  Icon: TextStrikethrough,
4477
- getState: (state) => state.strikethrough
4590
+ getState: (state2) => state2.strikethrough
4478
4591
  },
4479
4592
  {
4480
4593
  type: "code",
4481
4594
  Icon: Code,
4482
- getState: (state) => state.code
4595
+ getState: (state2) => state2.code
4483
4596
  },
4484
4597
  {
4485
4598
  type: "link",
4486
4599
  Icon: Link,
4487
- getState: (state) => state.link
4600
+ getState: (state2) => state2.link
4488
4601
  }
4489
4602
  ];
4490
4603
  var MarkdownStyles = () => {
4491
- const { onAction, state } = useToolbarContext("MarkdownStyles");
4604
+ const { onAction, state: state2 } = useToolbarContext("MarkdownStyles");
4492
4605
  const { t } = useTranslation(translationKey);
4493
4606
  return /* @__PURE__ */ React2.createElement(NaturalToolbar.ToggleGroup, {
4494
4607
  type: "multiple",
4495
- value: markdownStyles.filter(({ getState }) => state && getState(state)).map(({ type }) => type)
4496
- }, markdownStyles.map(({ type, getState, Icon }) => /* @__PURE__ */ React2.createElement(ToolbarButton, {
4608
+ value: markdownStyles.filter(({ getState }) => state2 && getState(state2)).map(({ type }) => type)
4609
+ }, markdownStyles.map(({ type, getState, Icon }) => /* @__PURE__ */ React2.createElement(ToolbarToggleButton, {
4497
4610
  key: type,
4498
4611
  value: type,
4499
4612
  Icon,
4500
- disabled: state?.blockType === "codeblock",
4501
- onClick: state ? () => onAction?.({
4613
+ disabled: state2?.blockType === "codeblock",
4614
+ onClick: state2 ? () => onAction?.({
4502
4615
  type,
4503
- data: !getState(state)
4616
+ data: !getState(state2)
4504
4617
  }) : void 0
4505
4618
  }, t(`${type} label`))));
4506
4619
  };
@@ -4508,32 +4621,32 @@ var markdownLists = [
4508
4621
  {
4509
4622
  type: "list-bullet",
4510
4623
  Icon: ListBullets,
4511
- getState: (state) => state.listStyle === "bullet"
4624
+ getState: (state2) => state2.listStyle === "bullet"
4512
4625
  },
4513
4626
  {
4514
4627
  type: "list-ordered",
4515
4628
  Icon: ListNumbers,
4516
- getState: (state) => state.listStyle === "ordered"
4629
+ getState: (state2) => state2.listStyle === "ordered"
4517
4630
  },
4518
4631
  {
4519
4632
  type: "list-task",
4520
4633
  Icon: ListChecks,
4521
- getState: (state) => state.listStyle === "task"
4634
+ getState: (state2) => state2.listStyle === "task"
4522
4635
  }
4523
4636
  ];
4524
4637
  var MarkdownLists = () => {
4525
- const { onAction, state } = useToolbarContext("MarkdownStyles");
4638
+ const { onAction, state: state2 } = useToolbarContext("MarkdownStyles");
4526
4639
  const { t } = useTranslation(translationKey);
4527
4640
  return /* @__PURE__ */ React2.createElement(NaturalToolbar.ToggleGroup, {
4528
4641
  type: "single",
4529
- value: state?.listStyle ? `list-${state.listStyle}` : ""
4530
- }, markdownLists.map(({ type, getState, Icon }) => /* @__PURE__ */ React2.createElement(ToolbarButton, {
4642
+ value: state2?.listStyle ? `list-${state2.listStyle}` : ""
4643
+ }, markdownLists.map(({ type, getState, Icon }) => /* @__PURE__ */ React2.createElement(ToolbarToggleButton, {
4531
4644
  key: type,
4532
4645
  value: type,
4533
4646
  Icon,
4534
- onClick: state ? () => onAction?.({
4647
+ onClick: state2 ? () => onAction?.({
4535
4648
  type,
4536
- data: !getState(state)
4649
+ data: !getState(state2)
4537
4650
  }) : void 0
4538
4651
  }, t(`${type} label`))));
4539
4652
  };
@@ -4541,63 +4654,101 @@ var markdownBlocks = [
4541
4654
  {
4542
4655
  type: "blockquote",
4543
4656
  Icon: Quotes,
4544
- getState: (state) => state.blockQuote
4657
+ getState: (state2) => state2.blockQuote
4545
4658
  },
4546
4659
  {
4547
4660
  type: "codeblock",
4548
4661
  Icon: CodeBlock,
4549
- getState: (state) => state.blockType === "codeblock"
4662
+ getState: (state2) => state2.blockType === "codeblock"
4550
4663
  },
4551
4664
  {
4552
4665
  type: "table",
4553
4666
  Icon: Table2,
4554
- getState: (state) => state.blockType === "tablecell",
4555
- disabled: (state) => !state.blankLine
4667
+ getState: (state2) => state2.blockType === "tablecell",
4668
+ disabled: (state2) => !state2.blankLine
4556
4669
  }
4557
4670
  ];
4558
4671
  var MarkdownBlocks = () => {
4559
- const { onAction, state } = useToolbarContext("MarkdownStyles");
4672
+ const { onAction, state: state2 } = useToolbarContext("MarkdownStyles");
4560
4673
  const { t } = useTranslation(translationKey);
4561
- const value = markdownBlocks.find(({ getState }) => state && getState(state));
4674
+ const value = markdownBlocks.find(({ getState }) => state2 && getState(state2));
4562
4675
  return /* @__PURE__ */ React2.createElement(NaturalToolbar.ToggleGroup, {
4563
4676
  type: "single",
4564
4677
  value: value?.type ?? ""
4565
- }, markdownBlocks.map(({ type, disabled, getState, Icon }) => /* @__PURE__ */ React2.createElement(ToolbarButton, {
4678
+ }, markdownBlocks.map(({ type, disabled, getState, Icon }) => /* @__PURE__ */ React2.createElement(ToolbarToggleButton, {
4566
4679
  key: type,
4567
4680
  value: type,
4568
4681
  Icon,
4569
- disabled: !state || disabled?.(state),
4570
- onClick: state ? () => onAction?.({
4682
+ disabled: !state2 || disabled?.(state2),
4683
+ onClick: state2 ? () => onAction?.({
4571
4684
  type,
4572
- data: !getState(state)
4685
+ data: !getState(state2)
4573
4686
  }) : void 0
4574
4687
  }, t(`${type} label`))));
4575
4688
  };
4576
4689
  var MarkdownStandard = () => /* @__PURE__ */ React2.createElement(React2.Fragment, null, /* @__PURE__ */ React2.createElement(MarkdownHeading, null), /* @__PURE__ */ React2.createElement(MarkdownStyles, null), /* @__PURE__ */ React2.createElement(MarkdownLists, null), /* @__PURE__ */ React2.createElement(MarkdownBlocks, null));
4577
- var MarkdownExtended = () => {
4690
+ var MarkdownCustom = ({ onUpload } = {}) => {
4578
4691
  const { onAction } = useToolbarContext("MarkdownStyles");
4579
4692
  const { t } = useTranslation(translationKey);
4580
- return /* @__PURE__ */ React2.createElement(Tooltip.Root, null, /* @__PURE__ */ React2.createElement(Tooltip.Trigger, {
4581
- asChild: true
4582
- }, /* @__PURE__ */ React2.createElement(NaturalToolbar.Button, {
4583
- variant: "ghost",
4693
+ const { acceptedFiles, getInputProps, open } = useDropzone({
4694
+ multiple: false,
4695
+ noDrag: true,
4696
+ accept: {
4697
+ "image/*": [
4698
+ ".jpg",
4699
+ ".jpeg",
4700
+ ".png",
4701
+ ".gif"
4702
+ ]
4703
+ }
4704
+ });
4705
+ useEffect3(() => {
4706
+ if (onUpload && acceptedFiles.length) {
4707
+ setTimeout(async () => {
4708
+ const f = acceptedFiles[0];
4709
+ const file = new File([
4710
+ f
4711
+ ], f.name, {
4712
+ type: f.type,
4713
+ lastModified: f.lastModified
4714
+ });
4715
+ const { url } = await onUpload(file);
4716
+ if (url) {
4717
+ onAction?.({
4718
+ type: "image",
4719
+ data: url
4720
+ });
4721
+ }
4722
+ });
4723
+ }
4724
+ }, [
4725
+ acceptedFiles
4726
+ ]);
4727
+ return /* @__PURE__ */ React2.createElement(React2.Fragment, null, /* @__PURE__ */ React2.createElement("input", getInputProps()), /* @__PURE__ */ React2.createElement(ToolbarButton, {
4728
+ value: "image",
4729
+ Icon: Image,
4730
+ onClick: () => open()
4731
+ }, t("image label")));
4732
+ };
4733
+ var MarkdownActions = () => {
4734
+ const { onAction } = useToolbarContext("MarkdownStyles");
4735
+ const { t } = useTranslation(translationKey);
4736
+ return /* @__PURE__ */ React2.createElement(React2.Fragment, null, /* @__PURE__ */ React2.createElement(ToolbarButton, {
4737
+ value: "comment",
4738
+ Icon: ChatText,
4584
4739
  "data-testid": "editor.toolbar.comment",
4585
4740
  onClick: () => onAction?.({
4586
4741
  type: "comment"
4587
- }),
4588
- classNames: buttonStyles
4589
- }, /* @__PURE__ */ React2.createElement(ChatText, {
4590
- className: iconStyles
4591
- }), /* @__PURE__ */ React2.createElement("span", {
4592
- className: "sr-only"
4593
- }, t("comment label")))), /* @__PURE__ */ React2.createElement(Tooltip.Portal, null, /* @__PURE__ */ React2.createElement(Tooltip.Content, tooltipProps, t("comment label"), /* @__PURE__ */ React2.createElement(Tooltip.Arrow, null))));
4742
+ })
4743
+ }, t("comment label")));
4594
4744
  };
4595
4745
  var Toolbar = {
4596
4746
  Root: ToolbarRoot,
4597
- Button: ToolbarButton,
4747
+ Button: ToolbarToggleButton,
4598
4748
  Separator: ToolbarSeparator,
4599
4749
  Markdown: MarkdownStandard,
4600
- Extended: MarkdownExtended
4750
+ Custom: MarkdownCustom,
4751
+ Actions: MarkdownActions
4601
4752
  };
4602
4753
 
4603
4754
  // packages/ui/react-ui-editor/src/hooks/useActionHandler.ts
@@ -4634,7 +4785,13 @@ var useActionHandler = (view) => {
4634
4785
  insertTable(view);
4635
4786
  break;
4636
4787
  case "link":
4637
- (action.data === false ? removeLink : addLink)(view);
4788
+ (action.data === false ? removeLink : addLink())(view);
4789
+ break;
4790
+ case "image":
4791
+ addLink({
4792
+ url: action.data,
4793
+ image: true
4794
+ })(view);
4638
4795
  break;
4639
4796
  case "comment":
4640
4797
  createComment(view);
@@ -4663,43 +4820,51 @@ var useDocAccessor = (text) => {
4663
4820
 
4664
4821
  // packages/ui/react-ui-editor/src/hooks/useTextEditor.ts
4665
4822
  import { EditorSelection as EditorSelection2, EditorState as EditorState3 } from "@codemirror/state";
4666
- import { EditorView as EditorView17 } from "@codemirror/view";
4667
- import { useEffect as useEffect3, useMemo as useMemo3, useRef as useRef3, useState as useState4 } from "react";
4823
+ import { EditorView as EditorView18 } from "@codemirror/view";
4824
+ import { useEffect as useEffect4, useMemo as useMemo3, useRef as useRef3, useState as useState3 } from "react";
4668
4825
  import { log as log10 } from "@dxos/log";
4669
- import { isNotFalsy as isNotFalsy4 } from "@dxos/util";
4670
- var __dxlog_file12 = "/home/runner/work/dxos/dxos/packages/ui/react-ui-editor/src/hooks/useTextEditor.ts";
4826
+ import { isNotFalsy as isNotFalsy5 } from "@dxos/util";
4827
+ var __dxlog_file13 = "/home/runner/work/dxos/dxos/packages/ui/react-ui-editor/src/hooks/useTextEditor.ts";
4671
4828
  var useTextEditor = (cb = () => ({}), deps = []) => {
4672
4829
  let { id, doc, selection, extensions, autoFocus, scrollTo, debug } = useMemo3(cb, deps ?? []);
4673
4830
  const onUpdate = useRef3();
4674
- const [view, setView] = useState4();
4831
+ const [view, setView] = useState3();
4675
4832
  const parentRef = useRef3(null);
4676
- useEffect3(() => {
4833
+ useEffect4(() => {
4677
4834
  let view2;
4678
4835
  if (parentRef.current) {
4679
- const state = EditorState3.create({
4836
+ log10("create", {
4837
+ id
4838
+ }, {
4839
+ F: __dxlog_file13,
4840
+ L: 36,
4841
+ S: void 0,
4842
+ C: (f, a) => f(...a)
4843
+ });
4844
+ const state2 = EditorState3.create({
4680
4845
  doc,
4681
4846
  selection,
4682
4847
  extensions: [
4683
4848
  id && documentId2.of(id),
4684
4849
  // TODO(burdon): Doesn't catch errors in keymap functions.
4685
- EditorView17.exceptionSink.of((err) => {
4850
+ EditorView18.exceptionSink.of((err) => {
4686
4851
  log10.catch(err, void 0, {
4687
- F: __dxlog_file12,
4688
- L: 44,
4852
+ F: __dxlog_file13,
4853
+ L: 46,
4689
4854
  S: void 0,
4690
4855
  C: (f, a) => f(...a)
4691
4856
  });
4692
4857
  }),
4693
4858
  extensions,
4694
- EditorView17.updateListener.of(() => {
4859
+ EditorView18.updateListener.of(() => {
4695
4860
  onUpdate.current?.();
4696
4861
  })
4697
- ].filter(isNotFalsy4)
4862
+ ].filter(isNotFalsy5)
4698
4863
  });
4699
- view2 = new EditorView17({
4864
+ view2 = new EditorView18({
4700
4865
  parent: parentRef.current,
4701
4866
  scrollTo,
4702
- state,
4867
+ state: state2,
4703
4868
  // NOTE: Uncomment to debug/monitor all transactions.
4704
4869
  // https://codemirror.net/docs/ref/#view.EditorView.dispatch
4705
4870
  dispatchTransactions: (trs, view3) => {
@@ -4712,10 +4877,18 @@ var useTextEditor = (cb = () => ({}), deps = []) => {
4712
4877
  setView(view2);
4713
4878
  }
4714
4879
  return () => {
4880
+ log10("destroy", {
4881
+ id
4882
+ }, {
4883
+ F: __dxlog_file13,
4884
+ L: 74,
4885
+ S: void 0,
4886
+ C: (f, a) => f(...a)
4887
+ });
4715
4888
  view2?.destroy();
4716
4889
  };
4717
4890
  }, deps);
4718
- useEffect3(() => {
4891
+ useEffect4(() => {
4719
4892
  if (view) {
4720
4893
  if (!selection && !view.state.selection.main.anchor) {
4721
4894
  selection = EditorSelection2.single(view.state.doc.line(1).to);
@@ -4774,7 +4947,6 @@ export {
4774
4947
  callbackWrapper,
4775
4948
  command,
4776
4949
  comments,
4777
- compareFormatting,
4778
4950
  createBasicExtensions,
4779
4951
  createComment,
4780
4952
  createDataExtensions,
@@ -4791,14 +4963,18 @@ export {
4791
4963
  editorMode,
4792
4964
  editorWithToolbarLayout,
4793
4965
  focusComment,
4966
+ focusEvent,
4967
+ formattingEquals,
4794
4968
  formattingKeymap,
4795
4969
  getFormatting,
4796
4970
  getToken,
4797
4971
  image,
4972
+ imageUpload,
4798
4973
  insertTable,
4799
- keymap9 as keymap,
4974
+ keymap10 as keymap,
4800
4975
  linkTooltip,
4801
4976
  listener,
4977
+ localStorageStateStoreAdapter,
4802
4978
  logChanges,
4803
4979
  markdownHighlightStyle,
4804
4980
  markdownTags,
@@ -4814,6 +4990,7 @@ export {
4814
4990
  setHeading,
4815
4991
  setSelection,
4816
4992
  setStyle,
4993
+ state,
4817
4994
  table,
4818
4995
  tags2 as tags,
4819
4996
  toggleBlockquote,