quoin-editor 0.1.0 → 0.1.1

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.
package/dist/index.d.cts CHANGED
@@ -87,9 +87,12 @@ declare function paletteBlocks(definitions: AnyBlockDefinition[]): AnyBlockDefin
87
87
  /** Props for `BlockCanvas`; `blocks` and `onChange` are the whole document contract. */
88
88
  type BlockCanvasProps = {
89
89
  /**
90
- * The document. Update it synchronously from `onChange`, as with a controlled `<input value onChange>`:
91
- * a host that applies edits late (a debounce, a transition, an async store) will see typing revert,
92
- * and should treat the canvas as uncontrolled by passing the initial document and ignoring later echoes.
90
+ * The document. Update it synchronously from `onChange`, as with a controlled `<input value onChange>`.
91
+ * Applying the edit late is tolerated as long as what comes back is one of the arrays the canvas emitted:
92
+ * it recognises its own output even several keystrokes behind, so a transition or a debounced setter is safe.
93
+ * A store that rebuilds the document on the way through hands back a structural copy instead, which arrives
94
+ * behind the canvas's own state and reads as a foreign document. That does not revert the edit cleanly: the
95
+ * characters the copy predates are dropped and the ones typed after them are kept.
93
96
  * A new array that differs from the last one you supplied replaces what the canvas shows, which is how a host
94
97
  * loads, undoes or merges. An array structurally equal to the last one you supplied is ignored, so a "discard
95
98
  * changes" button that hands back the original document does nothing; remount the canvas with a React `key` to
@@ -145,6 +148,7 @@ type BlockList = {
145
148
  blocks: QuoinBlock[];
146
149
  insertAt: (index: number, type: string, data?: Record<string, unknown>) => string;
147
150
  update: (id: string, data: Record<string, unknown>) => void;
151
+ /** Returns a token `undo` restores that block with; only the last 20 removals stay restorable. */
148
152
  remove: (id: string) => UndoToken;
149
153
  undo: (token: UndoToken) => void;
150
154
  move: (index: number, dir: -1 | 1) => void;
@@ -203,4 +207,76 @@ type TextCommand = {
203
207
  /** The built-in slash-palette commands, in display order. */
204
208
  declare const TEXT_COMMANDS: TextCommand[];
205
209
 
206
- export { type AnyBlockDefinition, type AutosaveStatus, BlockCanvas, type BlockCanvasProps, type BlockDefinition, type BlockEditorProps, type BlockList, type BlockPreviewProps, type QuoinBlock, type QuoinConfig, type QuoinConfigInput, QuoinProvider, TEXT_COMMANDS, type TextCommand, type UndoToken, type UploadFn, type UploadResult, type UseAutosaveInput, type UseAutosaveResult, blockByType, coreBlocks, defineBlock, paletteBlocks, useAutoGrow, useAutosave, useBlockList, useQuoin };
210
+ /** A text transform result: the next value and where the selection should land. */
211
+ type Edit = {
212
+ value: string;
213
+ selectionStart: number;
214
+ selectionEnd: number;
215
+ };
216
+ /** Applies an edit to a textarea through the native undo stack when the browser allows it, so Ctrl+Z still works. */
217
+ declare function applyEdit(el: HTMLTextAreaElement, edit: Edit, commit: (value: string) => void): void;
218
+
219
+ type Line = {
220
+ start: number;
221
+ end: number;
222
+ text: string;
223
+ };
224
+ /** The line containing the offset, with its start and end offsets and its text (no trailing newline). */
225
+ declare function lineAt(value: string, offset: number): Line;
226
+
227
+ /** A line-level markdown prefix: a heading level, a bullet or a numbered item. */
228
+ type LinePrefix = 'h1' | 'h2' | 'h3' | 'bullet' | 'ordered';
229
+ /** Adds the prefix to every line in the selection, or removes it when every line already carries it; fenced code is left alone. */
230
+ declare function toggleLinePrefix(kind: LinePrefix, value: string, selectionStart: number, selectionEnd: number): Edit;
231
+
232
+ /** Continues a list item on Enter, or climbs out of the list on an empty item or Backspace at the marker; returns null when the caret is not in a list item or is inside a code fence, so the default key behaviour runs. */
233
+ declare function continueList(value: string, caret: number, key: 'Enter' | 'Backspace'): Edit | null;
234
+
235
+ /** Indents or outdents the selected list items; returns null when the selection is not entirely list items, is inside a code fence, or cannot move further, so Tab keeps its default behaviour. */
236
+ declare function indentList(value: string, selectionStart: number, selectionEnd: number, direction: 1 | -1): Edit | null;
237
+
238
+ /** An inline markdown wrapper: bold, italic, code or a link. */
239
+ type WrapKind = 'bold' | 'italic' | 'code' | 'link';
240
+ /** Wraps the selection in the markers for the kind, or removes them when it is already wrapped. */
241
+ declare function wrapSelection(kind: WrapKind, value: string, selectionStart: number, selectionEnd: number): Edit;
242
+ /** Turns a pasted URL over a selection into a markdown link; returns null when nothing is selected or the paste is not a bare URL, so the paste goes through unchanged. */
243
+ declare function linkPaste(value: string, selectionStart: number, selectionEnd: number, pasted: string): Edit | null;
244
+
245
+ /** The fields `insertCommand` needs. `TextCommand` satisfies this structurally. */
246
+ type InsertableCommand = {
247
+ insert: string;
248
+ caretOffset: number;
249
+ selectLength?: number;
250
+ separate: boolean;
251
+ };
252
+ /** Inserts a command's markdown at the caret, starting a new line (or a blank line if it asks) when the caret is mid-line. */
253
+ declare function insertCommand(command: InsertableCommand, value: string, caret: number): Edit;
254
+
255
+ /** Whether typing "/" here should open the command palette: only at a word start, with no selection and outside a code fence. */
256
+ declare function shouldOpenPalette(value: string, selectionStart: number, selectionEnd: number): boolean;
257
+
258
+ /** Renders a "mod+shift+b" style hint as the platform's own label, such as ⌘⇧B on a Mac and Ctrl+Shift+B elsewhere. */
259
+ declare function formatShortcut(hint: string): string;
260
+
261
+ /** The caret's position and line height, in pixels relative to the textarea's top left corner. */
262
+ type CaretRect = {
263
+ top: number;
264
+ left: number;
265
+ height: number;
266
+ };
267
+ /** A textarea exposes no caret geometry, so the text before the caret is laid
268
+ * out again in a hidden div that copies the box's own metrics, and a marker at
269
+ * the end of it is measured. Returns null when the textarea is not laid out
270
+ * (hidden or detached), because there is nothing to measure. */
271
+ declare function caretRect(el: HTMLTextAreaElement): CaretRect | null;
272
+
273
+ /** Keyboard and paste handlers that give a plain textarea markdown shortcuts, list continuation, Tab indent and the slash palette trigger. */
274
+ declare function useMarkdownKeymap({ commit, onOpenPalette, }: {
275
+ commit: (markdown: string) => void;
276
+ onOpenPalette: () => void;
277
+ }): {
278
+ onKeyDown: (event: React.KeyboardEvent<HTMLTextAreaElement>) => void;
279
+ onPaste: (event: React.ClipboardEvent<HTMLTextAreaElement>) => void;
280
+ };
281
+
282
+ export { type AnyBlockDefinition, type AutosaveStatus, BlockCanvas, type BlockCanvasProps, type BlockDefinition, type BlockEditorProps, type BlockList, type BlockPreviewProps, type CaretRect, type Edit, type InsertableCommand, type LinePrefix, type QuoinBlock, type QuoinConfig, type QuoinConfigInput, QuoinProvider, TEXT_COMMANDS, type TextCommand, type UndoToken, type UploadFn, type UploadResult, type UseAutosaveInput, type UseAutosaveResult, type WrapKind, applyEdit, blockByType, caretRect, continueList, coreBlocks, defineBlock, formatShortcut, indentList, insertCommand, lineAt, linkPaste, paletteBlocks, shouldOpenPalette, toggleLinePrefix, useAutoGrow, useAutosave, useBlockList, useMarkdownKeymap, useQuoin, wrapSelection };
package/dist/index.d.ts CHANGED
@@ -87,9 +87,12 @@ declare function paletteBlocks(definitions: AnyBlockDefinition[]): AnyBlockDefin
87
87
  /** Props for `BlockCanvas`; `blocks` and `onChange` are the whole document contract. */
88
88
  type BlockCanvasProps = {
89
89
  /**
90
- * The document. Update it synchronously from `onChange`, as with a controlled `<input value onChange>`:
91
- * a host that applies edits late (a debounce, a transition, an async store) will see typing revert,
92
- * and should treat the canvas as uncontrolled by passing the initial document and ignoring later echoes.
90
+ * The document. Update it synchronously from `onChange`, as with a controlled `<input value onChange>`.
91
+ * Applying the edit late is tolerated as long as what comes back is one of the arrays the canvas emitted:
92
+ * it recognises its own output even several keystrokes behind, so a transition or a debounced setter is safe.
93
+ * A store that rebuilds the document on the way through hands back a structural copy instead, which arrives
94
+ * behind the canvas's own state and reads as a foreign document. That does not revert the edit cleanly: the
95
+ * characters the copy predates are dropped and the ones typed after them are kept.
93
96
  * A new array that differs from the last one you supplied replaces what the canvas shows, which is how a host
94
97
  * loads, undoes or merges. An array structurally equal to the last one you supplied is ignored, so a "discard
95
98
  * changes" button that hands back the original document does nothing; remount the canvas with a React `key` to
@@ -145,6 +148,7 @@ type BlockList = {
145
148
  blocks: QuoinBlock[];
146
149
  insertAt: (index: number, type: string, data?: Record<string, unknown>) => string;
147
150
  update: (id: string, data: Record<string, unknown>) => void;
151
+ /** Returns a token `undo` restores that block with; only the last 20 removals stay restorable. */
148
152
  remove: (id: string) => UndoToken;
149
153
  undo: (token: UndoToken) => void;
150
154
  move: (index: number, dir: -1 | 1) => void;
@@ -203,4 +207,76 @@ type TextCommand = {
203
207
  /** The built-in slash-palette commands, in display order. */
204
208
  declare const TEXT_COMMANDS: TextCommand[];
205
209
 
206
- export { type AnyBlockDefinition, type AutosaveStatus, BlockCanvas, type BlockCanvasProps, type BlockDefinition, type BlockEditorProps, type BlockList, type BlockPreviewProps, type QuoinBlock, type QuoinConfig, type QuoinConfigInput, QuoinProvider, TEXT_COMMANDS, type TextCommand, type UndoToken, type UploadFn, type UploadResult, type UseAutosaveInput, type UseAutosaveResult, blockByType, coreBlocks, defineBlock, paletteBlocks, useAutoGrow, useAutosave, useBlockList, useQuoin };
210
+ /** A text transform result: the next value and where the selection should land. */
211
+ type Edit = {
212
+ value: string;
213
+ selectionStart: number;
214
+ selectionEnd: number;
215
+ };
216
+ /** Applies an edit to a textarea through the native undo stack when the browser allows it, so Ctrl+Z still works. */
217
+ declare function applyEdit(el: HTMLTextAreaElement, edit: Edit, commit: (value: string) => void): void;
218
+
219
+ type Line = {
220
+ start: number;
221
+ end: number;
222
+ text: string;
223
+ };
224
+ /** The line containing the offset, with its start and end offsets and its text (no trailing newline). */
225
+ declare function lineAt(value: string, offset: number): Line;
226
+
227
+ /** A line-level markdown prefix: a heading level, a bullet or a numbered item. */
228
+ type LinePrefix = 'h1' | 'h2' | 'h3' | 'bullet' | 'ordered';
229
+ /** Adds the prefix to every line in the selection, or removes it when every line already carries it; fenced code is left alone. */
230
+ declare function toggleLinePrefix(kind: LinePrefix, value: string, selectionStart: number, selectionEnd: number): Edit;
231
+
232
+ /** Continues a list item on Enter, or climbs out of the list on an empty item or Backspace at the marker; returns null when the caret is not in a list item or is inside a code fence, so the default key behaviour runs. */
233
+ declare function continueList(value: string, caret: number, key: 'Enter' | 'Backspace'): Edit | null;
234
+
235
+ /** Indents or outdents the selected list items; returns null when the selection is not entirely list items, is inside a code fence, or cannot move further, so Tab keeps its default behaviour. */
236
+ declare function indentList(value: string, selectionStart: number, selectionEnd: number, direction: 1 | -1): Edit | null;
237
+
238
+ /** An inline markdown wrapper: bold, italic, code or a link. */
239
+ type WrapKind = 'bold' | 'italic' | 'code' | 'link';
240
+ /** Wraps the selection in the markers for the kind, or removes them when it is already wrapped. */
241
+ declare function wrapSelection(kind: WrapKind, value: string, selectionStart: number, selectionEnd: number): Edit;
242
+ /** Turns a pasted URL over a selection into a markdown link; returns null when nothing is selected or the paste is not a bare URL, so the paste goes through unchanged. */
243
+ declare function linkPaste(value: string, selectionStart: number, selectionEnd: number, pasted: string): Edit | null;
244
+
245
+ /** The fields `insertCommand` needs. `TextCommand` satisfies this structurally. */
246
+ type InsertableCommand = {
247
+ insert: string;
248
+ caretOffset: number;
249
+ selectLength?: number;
250
+ separate: boolean;
251
+ };
252
+ /** Inserts a command's markdown at the caret, starting a new line (or a blank line if it asks) when the caret is mid-line. */
253
+ declare function insertCommand(command: InsertableCommand, value: string, caret: number): Edit;
254
+
255
+ /** Whether typing "/" here should open the command palette: only at a word start, with no selection and outside a code fence. */
256
+ declare function shouldOpenPalette(value: string, selectionStart: number, selectionEnd: number): boolean;
257
+
258
+ /** Renders a "mod+shift+b" style hint as the platform's own label, such as ⌘⇧B on a Mac and Ctrl+Shift+B elsewhere. */
259
+ declare function formatShortcut(hint: string): string;
260
+
261
+ /** The caret's position and line height, in pixels relative to the textarea's top left corner. */
262
+ type CaretRect = {
263
+ top: number;
264
+ left: number;
265
+ height: number;
266
+ };
267
+ /** A textarea exposes no caret geometry, so the text before the caret is laid
268
+ * out again in a hidden div that copies the box's own metrics, and a marker at
269
+ * the end of it is measured. Returns null when the textarea is not laid out
270
+ * (hidden or detached), because there is nothing to measure. */
271
+ declare function caretRect(el: HTMLTextAreaElement): CaretRect | null;
272
+
273
+ /** Keyboard and paste handlers that give a plain textarea markdown shortcuts, list continuation, Tab indent and the slash palette trigger. */
274
+ declare function useMarkdownKeymap({ commit, onOpenPalette, }: {
275
+ commit: (markdown: string) => void;
276
+ onOpenPalette: () => void;
277
+ }): {
278
+ onKeyDown: (event: React.KeyboardEvent<HTMLTextAreaElement>) => void;
279
+ onPaste: (event: React.ClipboardEvent<HTMLTextAreaElement>) => void;
280
+ };
281
+
282
+ export { type AnyBlockDefinition, type AutosaveStatus, BlockCanvas, type BlockCanvasProps, type BlockDefinition, type BlockEditorProps, type BlockList, type BlockPreviewProps, type CaretRect, type Edit, type InsertableCommand, type LinePrefix, type QuoinBlock, type QuoinConfig, type QuoinConfigInput, QuoinProvider, TEXT_COMMANDS, type TextCommand, type UndoToken, type UploadFn, type UploadResult, type UseAutosaveInput, type UseAutosaveResult, type WrapKind, applyEdit, blockByType, caretRect, continueList, coreBlocks, defineBlock, formatShortcut, indentList, insertCommand, lineAt, linkPaste, paletteBlocks, shouldOpenPalette, toggleLinePrefix, useAutoGrow, useAutosave, useBlockList, useMarkdownKeymap, useQuoin, wrapSelection };
package/dist/index.js CHANGED
@@ -1117,7 +1117,10 @@ function MarkdownBlockEditor({ data, onChange, onInsertBlock, onDone }) {
1117
1117
  const palette = useSlashPalette(ref);
1118
1118
  const [shortcutsOpen, setShortcutsOpen] = React5.useState(false);
1119
1119
  const set = (patch) => onChange({ ...data, ...patch });
1120
- const commit = (next) => onChange({ ...data, markdown: next });
1120
+ const commit = React5.useCallback(
1121
+ (next) => onChange({ ...data, markdown: next }),
1122
+ [data, onChange]
1123
+ );
1121
1124
  const keymap = useMarkdownKeymap({ commit, onOpenPalette: palette.openAtCaret });
1122
1125
  function reopenBox() {
1123
1126
  const el = ref.current;
@@ -1578,6 +1581,7 @@ function newBlockId() {
1578
1581
 
1579
1582
  // src/hooks/useBlockList.ts
1580
1583
  var removalSeq = 0;
1584
+ var UNDO_DEPTH = 20;
1581
1585
  function reducer(state, action) {
1582
1586
  switch (action.type) {
1583
1587
  case "insertAt": {
@@ -1596,6 +1600,11 @@ function reducer(state, action) {
1596
1600
  if (index === -1) return state;
1597
1601
  const removals = new Map(state.removals);
1598
1602
  removals.set(action.token, { block: state.blocks[index], index });
1603
+ while (removals.size > UNDO_DEPTH) {
1604
+ const oldest = removals.keys().next();
1605
+ if (oldest.done) break;
1606
+ removals.delete(oldest.value);
1607
+ }
1599
1608
  return { blocks: state.blocks.filter((b) => b.id !== action.id), removals };
1600
1609
  }
1601
1610
  case "undo": {
@@ -1833,18 +1842,48 @@ function BlockEditor({
1833
1842
  }
1834
1843
 
1835
1844
  // src/canvas/source-offset.ts
1845
+ function caretPositionAt(x, y) {
1846
+ const doc = document;
1847
+ const position = doc.caretPositionFromPoint?.(x, y);
1848
+ if (position) return position;
1849
+ const range = doc.caretRangeFromPoint?.(x, y);
1850
+ return range ? { offsetNode: range.startContainer, offset: range.startOffset } : null;
1851
+ }
1852
+ function renderedOffsetAt(container, x, y) {
1853
+ const position = caretPositionAt(x, y);
1854
+ if (!position || position.offsetNode.nodeType !== Node.TEXT_NODE) return null;
1855
+ if (!container.contains(position.offsetNode)) return null;
1856
+ const walker = document.createTreeWalker(container, NodeFilter.SHOW_TEXT);
1857
+ let before = 0;
1858
+ while (walker.nextNode()) {
1859
+ if (walker.currentNode === position.offsetNode) return before + position.offset;
1860
+ before += walker.currentNode.textContent?.length ?? 0;
1861
+ }
1862
+ return null;
1863
+ }
1836
1864
  var PROBE_LENGTHS = [40, 20, 8];
1837
- function sourceOffsetFor(source, clickedText) {
1838
- const text = clickedText.replace(/\s+/g, " ").trim();
1839
- if (text === "") return 0;
1840
- const whole = source.indexOf(text);
1841
- if (whole !== -1) return whole;
1865
+ function search(source, probe) {
1866
+ const literal = probe.clickedText.trim();
1867
+ if (literal === "" || literal === probe.rendered.trim()) return source.length;
1868
+ const candidates = [literal, literal.replace(/\s+/g, " ")];
1869
+ for (const text of candidates) {
1870
+ const whole = source.indexOf(text);
1871
+ if (whole !== -1) return whole;
1872
+ }
1842
1873
  for (const length of PROBE_LENGTHS) {
1843
- if (text.length < length) continue;
1844
- const at = source.indexOf(text.slice(0, length));
1845
- if (at !== -1) return at;
1874
+ for (const text of candidates) {
1875
+ if (text.length < length) continue;
1876
+ const at = source.indexOf(text.slice(0, length));
1877
+ if (at !== -1) return at;
1878
+ }
1879
+ }
1880
+ return source.length;
1881
+ }
1882
+ function sourceOffsetFor(source, probe) {
1883
+ if (probe.renderedOffset !== null && probe.rendered === source) {
1884
+ return Math.max(0, Math.min(probe.renderedOffset, source.length));
1846
1885
  }
1847
- return 0;
1886
+ return search(source, probe);
1848
1887
  }
1849
1888
 
1850
1889
  // src/canvas/BlockCard.tsx
@@ -1865,14 +1904,14 @@ function BlockCardImpl({
1865
1904
  onUpdate,
1866
1905
  onInsertBlock
1867
1906
  }) {
1868
- const { blockTypes } = useQuoin();
1907
+ const { blockTypes, onError } = useQuoin();
1869
1908
  const definition = blockByType(blockTypes, block.type);
1870
1909
  const Preview = definition?.preview;
1871
1910
  const [editing, setEditing] = React8.useState(editingByDefault && !readOnly);
1872
1911
  const editButtonRef = React8.useRef(null);
1873
1912
  const editorContainerRef = React8.useRef(null);
1874
1913
  const returnFocusOnClose = React8.useRef(false);
1875
- const clickedTextRef = React8.useRef(null);
1914
+ const clickedRef = React8.useRef(null);
1876
1915
  const scrollTopRef = React8.useRef(null);
1877
1916
  const handleInsertBlock = React8.useCallback(
1878
1917
  (type) => onInsertBlock?.(index + 1, type),
@@ -1916,8 +1955,8 @@ function BlockCardImpl({
1916
1955
  'textarea, input, [role="combobox"]'
1917
1956
  );
1918
1957
  if (!field) return;
1919
- const clicked = clickedTextRef.current;
1920
- clickedTextRef.current = null;
1958
+ const clicked = clickedRef.current;
1959
+ clickedRef.current = null;
1921
1960
  if (clicked === null || !(field instanceof HTMLTextAreaElement)) {
1922
1961
  field.focus();
1923
1962
  if (field instanceof HTMLTextAreaElement) {
@@ -1942,6 +1981,12 @@ function BlockCardImpl({
1942
1981
  returnFocusOnClose.current = false;
1943
1982
  editButtonRef.current?.focus();
1944
1983
  }, [editing]);
1984
+ const reportedType = React8.useRef(null);
1985
+ React8.useEffect(() => {
1986
+ if (definition || reportedType.current === block.type) return;
1987
+ reportedType.current = block.type;
1988
+ onError(new Error(`No block type is registered for "${block.type}".`));
1989
+ }, [definition, block.type, onError]);
1945
1990
  if (definition?.alwaysEditing && !readOnly) {
1946
1991
  return /* @__PURE__ */ jsx22(
1947
1992
  BlockEditor,
@@ -1981,7 +2026,7 @@ function BlockCardImpl({
1981
2026
  }
1982
2027
  );
1983
2028
  }
1984
- const rest = Preview ? /* @__PURE__ */ jsx22(Preview, { data: block.data, blockId: block.id }) : definition ? /* @__PURE__ */ jsx22("p", { className: "py-1 text-sm text-muted-foreground", children: definition.label }) : null;
2029
+ const rest = Preview ? /* @__PURE__ */ jsx22(Preview, { data: block.data, blockId: block.id }) : /* @__PURE__ */ jsx22("p", { className: "py-1 text-sm text-muted-foreground", children: definition?.label ?? block.type });
1985
2030
  if (readOnly) return /* @__PURE__ */ jsx22("div", { children: rest });
1986
2031
  return /* @__PURE__ */ jsxs15("div", { className: "group/card relative", children: [
1987
2032
  /* @__PURE__ */ jsx22(
@@ -1991,7 +2036,11 @@ function BlockCardImpl({
1991
2036
  const target = e.target;
1992
2037
  if (target.closest(INTERACTIVE_SELECTOR)) return;
1993
2038
  if (hasSelectionWithin(e.currentTarget)) return;
1994
- clickedTextRef.current = target.textContent ?? "";
2039
+ clickedRef.current = {
2040
+ rendered: e.currentTarget.textContent ?? "",
2041
+ clickedText: target.textContent ?? "",
2042
+ renderedOffset: renderedOffsetAt(e.currentTarget, e.clientX, e.clientY)
2043
+ };
1995
2044
  scrollTopRef.current = (document.scrollingElement ?? document.documentElement).scrollTop;
1996
2045
  setEditing(true);
1997
2046
  },
@@ -2225,6 +2274,15 @@ function SortableBlock({
2225
2274
 
2226
2275
  // src/canvas/BlockCanvas.tsx
2227
2276
  import { jsx as jsx26, jsxs as jsxs19 } from "react/jsx-runtime";
2277
+ var UNACKNOWLEDGED_LIMIT = 64;
2278
+ function remember(outstanding, blocks) {
2279
+ outstanding.add(blocks);
2280
+ while (outstanding.size > UNACKNOWLEDGED_LIMIT) {
2281
+ const oldest = outstanding.values().next();
2282
+ if (oldest.done) return;
2283
+ outstanding.delete(oldest.value);
2284
+ }
2285
+ }
2228
2286
  function Canvas({
2229
2287
  blocks,
2230
2288
  onChange,
@@ -2251,16 +2309,22 @@ function Canvas({
2251
2309
  );
2252
2310
  const lastEmitted = React11.useRef(blocks);
2253
2311
  const lastSupplied = React11.useRef(blocks);
2312
+ const unacknowledged = React11.useRef(/* @__PURE__ */ new Set());
2254
2313
  React11.useEffect(() => {
2255
2314
  if (list.blocks === lastEmitted.current) return;
2256
2315
  lastEmitted.current = list.blocks;
2316
+ remember(unacknowledged.current, list.blocks);
2257
2317
  onChange(list.blocks);
2258
2318
  }, [list.blocks, onChange]);
2259
2319
  React11.useEffect(() => {
2260
2320
  const previous = lastSupplied.current;
2261
2321
  lastSupplied.current = blocks;
2262
- if (blocks === lastEmitted.current) return;
2322
+ if (unacknowledged.current.has(blocks)) {
2323
+ if (blocks === lastEmitted.current) unacknowledged.current.clear();
2324
+ return;
2325
+ }
2263
2326
  if (sameBlocks(blocks, previous) || sameBlocks(blocks, list.blocks)) return;
2327
+ unacknowledged.current.clear();
2264
2328
  lastEmitted.current = blocks;
2265
2329
  reset(blocks);
2266
2330
  }, [blocks, reset]);
@@ -2465,13 +2529,25 @@ export {
2465
2529
  BlockCanvas,
2466
2530
  QuoinProvider,
2467
2531
  TEXT_COMMANDS,
2532
+ applyEdit,
2468
2533
  blockByType,
2534
+ caretRect,
2535
+ continueList,
2469
2536
  coreBlocks,
2470
2537
  defineBlock,
2538
+ formatShortcut,
2539
+ indentList,
2540
+ insertCommand,
2541
+ lineAt,
2542
+ linkPaste,
2471
2543
  paletteBlocks,
2544
+ shouldOpenPalette,
2545
+ toggleLinePrefix,
2472
2546
  useAutoGrow,
2473
2547
  useAutosave,
2474
2548
  useBlockList,
2475
- useQuoin
2549
+ useMarkdownKeymap,
2550
+ useQuoin,
2551
+ wrapSelection
2476
2552
  };
2477
2553
  //# sourceMappingURL=index.js.map