quoin-editor 1.8.0 → 2.0.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.
Files changed (86) hide show
  1. package/CHANGELOG.md +17 -1
  2. package/MIGRATION.md +137 -23
  3. package/README.md +8 -2
  4. package/dist/ai.cjs +34 -34
  5. package/dist/ai.js +3 -3
  6. package/dist/catalogue.cjs +21 -21
  7. package/dist/catalogue.js +12 -12
  8. package/dist/{chunk-VWYPO6SX.js → chunk-2UKI26JA.js} +5 -5
  9. package/dist/chunk-2UKI26JA.js.map +1 -0
  10. package/dist/{chunk-B3OBM6I4.cjs → chunk-4PIJC4FD.cjs} +16 -16
  11. package/dist/{chunk-5OZX2LV4.js → chunk-5X52BAZB.js} +5 -5
  12. package/dist/{chunk-5OZX2LV4.js.map → chunk-5X52BAZB.js.map} +1 -1
  13. package/dist/{chunk-PVFSJXYQ.cjs → chunk-5YAJZSTI.cjs} +18 -18
  14. package/dist/{chunk-53CNYRVQ.cjs → chunk-66OPHDFA.cjs} +50 -50
  15. package/dist/{chunk-EZX37R5D.js → chunk-6BVE46K7.js} +4 -4
  16. package/dist/{chunk-EZX37R5D.js.map → chunk-6BVE46K7.js.map} +1 -1
  17. package/dist/{chunk-MMS4D4MK.cjs → chunk-77CNOLHY.cjs} +3 -3
  18. package/dist/{chunk-DXUDBPG5.js → chunk-B4VCDJSA.js} +5 -5
  19. package/dist/{chunk-DXUDBPG5.js.map → chunk-B4VCDJSA.js.map} +1 -1
  20. package/dist/{chunk-4MCYQHFL.cjs → chunk-BYYCD54G.cjs} +2 -2
  21. package/dist/{chunk-BA5WXBGE.cjs → chunk-CJVJNMOB.cjs} +107 -101
  22. package/dist/{chunk-TQHFGSTT.cjs → chunk-D6CIIWCU.cjs} +30 -30
  23. package/dist/{chunk-NCDMQCZR.js → chunk-EU5IGKMP.js} +3 -3
  24. package/dist/{chunk-NCDMQCZR.js.map → chunk-EU5IGKMP.js.map} +1 -1
  25. package/dist/{chunk-7X3T7VXV.cjs → chunk-HOSDMHK6.cjs} +25 -25
  26. package/dist/{chunk-XKYNINUX.cjs → chunk-IKZSCTRF.cjs} +3 -3
  27. package/dist/{chunk-D4L5FINK.js → chunk-IW4TMECB.js} +3 -3
  28. package/dist/{chunk-D4L5FINK.js.map → chunk-IW4TMECB.js.map} +1 -1
  29. package/dist/{chunk-MRHDPXQ7.js → chunk-J2SVYBTC.js} +6 -6
  30. package/dist/chunk-J2SVYBTC.js.map +1 -0
  31. package/dist/{chunk-PHQMHXNG.js → chunk-KQPZTSW3.js} +4 -4
  32. package/dist/{chunk-PHQMHXNG.js.map → chunk-KQPZTSW3.js.map} +1 -1
  33. package/dist/{chunk-YVPZDTVZ.cjs → chunk-LEICOZYK.cjs} +2 -2
  34. package/dist/{chunk-SP4F5F6V.js → chunk-M77A3EGD.js} +3 -3
  35. package/dist/{chunk-SP4F5F6V.js.map → chunk-M77A3EGD.js.map} +1 -1
  36. package/dist/{chunk-VM567D6O.js → chunk-MAS4AEF4.js} +4 -4
  37. package/dist/{chunk-VM567D6O.js.map → chunk-MAS4AEF4.js.map} +1 -1
  38. package/dist/{chunk-EZWJRBQT.cjs → chunk-NFO3W644.cjs} +19 -19
  39. package/dist/{chunk-MSWA5ZZM.js → chunk-NL5J77VD.js} +202 -202
  40. package/dist/chunk-NL5J77VD.js.map +1 -0
  41. package/dist/{chunk-53FJFYBR.cjs → chunk-V4WQG7FR.cjs} +64 -64
  42. package/dist/{chunk-WOGCD7JN.js → chunk-YABGWOAV.js} +3 -3
  43. package/dist/{chunk-WOGCD7JN.js.map → chunk-YABGWOAV.js.map} +1 -1
  44. package/dist/{chunk-P6HC7RUN.cjs → chunk-Z6LAARPB.cjs} +199 -204
  45. package/dist/{chunk-Y4EJ6UBV.js → chunk-ZN5DHNSL.js} +15 -9
  46. package/dist/chunk-ZN5DHNSL.js.map +1 -0
  47. package/dist/comments.cjs +30 -30
  48. package/dist/comments.js +3 -3
  49. package/dist/diagram.cjs +6 -6
  50. package/dist/diagram.js +3 -3
  51. package/dist/embed.cjs +5 -5
  52. package/dist/embed.js +3 -3
  53. package/dist/emoji.cjs +6 -6
  54. package/dist/emoji.js +3 -3
  55. package/dist/find.cjs +7 -7
  56. package/dist/find.js +3 -3
  57. package/dist/headless.cjs +11 -31
  58. package/dist/headless.d.cts +4 -41
  59. package/dist/headless.d.ts +4 -41
  60. package/dist/headless.js +1 -1
  61. package/dist/highlight.cjs +3 -3
  62. package/dist/highlight.js +2 -2
  63. package/dist/index.cjs +23 -43
  64. package/dist/index.d.cts +1 -1
  65. package/dist/index.d.ts +1 -1
  66. package/dist/index.js +2 -2
  67. package/dist/math.cjs +6 -6
  68. package/dist/math.js +4 -4
  69. package/dist/mention.cjs +6 -6
  70. package/dist/mention.js +4 -4
  71. package/dist/presence.cjs +12 -12
  72. package/dist/presence.js +3 -3
  73. package/dist/source.cjs +2 -2
  74. package/dist/source.js +2 -2
  75. package/dist/stats.cjs +6 -6
  76. package/dist/stats.js +3 -3
  77. package/dist/styles.css +2 -2
  78. package/dist/table.cjs +22 -22
  79. package/dist/table.js +3 -3
  80. package/dist/toc.cjs +6 -6
  81. package/dist/toc.js +3 -3
  82. package/package.json +1 -1
  83. package/dist/chunk-MRHDPXQ7.js.map +0 -1
  84. package/dist/chunk-MSWA5ZZM.js.map +0 -1
  85. package/dist/chunk-VWYPO6SX.js.map +0 -1
  86. package/dist/chunk-Y4EJ6UBV.js.map +0 -1
package/CHANGELOG.md CHANGED
@@ -1,6 +1,22 @@
1
1
  # Changelog
2
2
 
3
- Every release of `quoin-editor` is listed here, newest first, in the spirit of [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). From 1.0.0 the package follows [semantic versioning](https://semver.org/): a breaking change waits for the next major version. Until then, Quoin was 0.x and a minor version could contain breaking changes; those known at release were marked "Breaking". The 0.x sections are kept as they were released, "Docs" headings included. Moving from 0.4 to 1.0 is covered step by step in [MIGRATION.md](MIGRATION.md). Review finding ids such as F-17 are those of the 0.4.0 review in [#73](https://github.com/AmineSeboui/quoin-editor/issues/73).
3
+ Every release of `quoin-editor` is listed here, newest first, in the spirit of [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). From 1.0.0 the package follows [semantic versioning](https://semver.org/): a breaking change waits for the next major version. Until then, Quoin was 0.x and a minor version could contain breaking changes; those known at release were marked "Breaking". The 0.x sections are kept as they were released, "Docs" headings included. Moving from 0.4 to 1.0, and from 1.x to 2.0, is covered step by step in [MIGRATION.md](MIGRATION.md). Review finding ids such as F-17 are those of the 0.4.0 review in [#73](https://github.com/AmineSeboui/quoin-editor/issues/73).
4
+
5
+ ## 2.0.1 (2026-11-07)
6
+
7
+ ### Added
8
+ - The playground also switches comments, AI actions and a second collaborator on and off. Comments anchor a thread to a range and keep it in a store of the page’s own, AI streams canned text where a host would pass its model, and collaboration draws a second canvas with a Yjs document of its own that reconciles with the first over a BroadcastChannel. The settings are in two groups now, those a canvas reads as it mounts and those it answers to at once, because the three new ones are extensions and remount it where highlighting and read only do not.
9
+ - A playground page on the site switches the editor between English and the seven locale packs, turns syntax colouring and the markdown source pane on and off, and draws the canvas read only, with the tsx for whatever is set beside it to copy. Picking a language downloads that pack and remounts the canvas, since a canvas reads its extensions once, and the other three take effect where they are. The home page links it from the hero, and two cards there name the code block colouring and the markdown pane, which shipped in 1.5.0 and 1.6.0 and the page had never mentioned. Nothing in the package changes.
10
+ - The FAQ answers how to upgrade from 1.x to 2.0 beside the 0.4 answer it already carried, naming all seven removed exports and pointing at the section of the migration guide that covers them.
11
+
12
+ ## 2.0.0 (2026-11-06)
13
+
14
+ ### Removed
15
+ - Seven names the view layer only ever calls itself leave `quoin-editor/view`, and with it `quoin-editor/headless` and `quoin-editor`. `BlockShell` and `SelectionSink`, with the `BlockShellProps` and `SelectionSinkProps` that typed them, are drawn by `EditorView` itself and register themselves with the controller, so a second copy of either takes the registration from the first. `VoidBlockFallback` is what a block shell already falls back to when no renderer draws a block type, so a renderer table that names it changes nothing. `defaultRenderers` held the three renderers a canvas already reaches for on its own; a renderer of your own draws its text with `EditableRegion`, which is what those three do. `isCrossSelection` answers whether a text selection spans blocks or regions, which is what painting and the arrow keys ask; a host that wants the answer reads `anchor` and `head` of the public `Selection`, and were it to earn a place it belongs beside `textSelection` in `quoin-editor/engine` rather than in the React entry. `BlockRenderProps` and `BlockRenderer` stay, so a renderer table still types. The five values among the seven were marked deprecated in 1.8.0. Removing a published name is breaking, so this lands in 2.0.0; [MIGRATION.md](MIGRATION.md) covers the move.
16
+
17
+ ### Fixed
18
+ - A menu no longer throws where `Element.prototype.scrollIntoView` is absent, which is jsdom with no stub and any webview that omits it. Ten calls guarded the element they had queried but not the method on it, so an element that existed without the method was called anyway and threw a `TypeError` that left Quoin for the host's tree, replacing the page wherever an error boundary caught it. The calls are in the slash and language menus, the code block's caret reveal, a block link's target, a table cell, a heading jumped to from the table of contents and vertical caret movement. Keeping the highlighted entry in view is the whole of what a host without the method now loses. Two `scrollTo` calls that restore a scroll position after a change carried the same shape and are guarded with them.
19
+ - An editable canvas warns in development when a definition of `CODE`, `IMAGE` or `FILE` replaces the preview that edits it and brings no editor of its own. Quoin edits those three in their preview rather than through an `editor`, so such a definition left a block the canvas drew and nobody could write in: no editable node, no language picker, and for an image or a file no toolbar and no way to fill the empty state. Nothing was logged, and the warning that would have caught it covered `MARKDOWN`, `CALLOUT` and `TOGGLE` only, whose consequence is milder since the canvas keeps drawing and typing into those. The migration guide said as much by naming only that first set, and now names both. Nothing about what the canvas draws changes.
4
20
 
5
21
  ## 1.8.0 (2026-11-05)
6
22
 
package/MIGRATION.md CHANGED
@@ -1,4 +1,8 @@
1
- # Migrating from 0.4 to 1.0
1
+ # Migrating Quoin
2
+
3
+ Two upgrades are written up here. [0.4 to 1.0](#migrating-from-04-to-10) is the rewrite; [1.x to 2.0](#migrating-from-1x-to-20) takes seven names the view layer only ever called itself out of the public entries.
4
+
5
+ ## Migrating from 0.4 to 1.0
2
6
 
3
7
  Quoin 1.0 keeps the 0.4 document format and the `BlockCanvas` contract, and replaces everything under them: the card and textarea of each block give way to an editor of Quoin's own that types into the page in place. Most hosts upgrade by changing the version and reading the list below; the [checklist](#checklist) at the end sums it up. Every change is listed in the [changelog](CHANGELOG.md).
4
8
 
@@ -8,13 +12,13 @@ npm install quoin-editor@^1
8
12
 
9
13
  React 18.2 or 19 and TypeScript 5.4 or later, as in 0.4.
10
14
 
11
- ## Data
15
+ ### Data
12
16
 
13
- ### No migration is needed
17
+ #### No migration is needed
14
18
 
15
19
  A document is still a `QuoinBlock[]` of `{ id, type, data }`, and every key of the five 0.4 block types keeps its name and meaning (`MarkdownBlockData`, `CalloutBlockData`, `CodeBlockData`, `ImageBlockData` and `FileBlockData`). Store and load the same arrays. 1.0 adds optional keys, listed [below](#new-optional-keys). Blocks nobody edits are handed back by `onChange` as the very objects you supplied, so a stored 0.4 document is never rewritten by being opened. The one exception is a block whose id repeats another's: it gets a fresh id.
16
20
 
17
- ### Multi-paragraph markdown blocks split on their first edit
21
+ #### Multi-paragraph markdown blocks split on their first edit
18
22
 
19
23
  A 0.4 `MARKDOWN` block could hold a whole page: several paragraphs, a list, a table. A 1.0 text block holds one line: a paragraph, a heading, a list item, a task or a quote. A 0.4 block of several lines is drawn line by line and kept as it is until someone edits it. On that first edit it is split: the first line keeps the block's id, the others take ids derived from it (`<id>-2`, `<id>-3` and on, skipping any id the document already has), a table becomes a `TABLE` block and an image alone on its line an `IMAGE` block. Each piece is written back as the canonical markdown of its line, so a host that diffs stored strings sees those blocks change once.
20
24
 
@@ -33,11 +37,11 @@ export const edited: QuoinBlock[] = [
33
37
  ];
34
38
  ```
35
39
 
36
- ### The `indent` field
40
+ #### The `indent` field
37
41
 
38
42
  `QuoinBlock` gains an optional `indent`, the nesting depth shared by every block type: a nested list item, or a block inside a toggle. It is written only when it is above 0. Code that copies, validates or stores blocks should keep it.
39
43
 
40
- ### New optional keys
44
+ #### New optional keys
41
45
 
42
46
  1.0 writes keys that 0.4 never wrote, each only once a writer uses what it holds:
43
47
 
@@ -50,21 +54,21 @@ export const edited: QuoinBlock[] = [
50
54
 
51
55
  The block itself may carry `indent`, and documents may hold the new block types `DIVIDER`, `TOGGLE`, `TABLE`, `EMBED`, `BOOKMARK`, `MATH` and `TOC`. A host whose schema rejects unknown keys or types would reject a paragraph that was aligned, or an image that was resized. Add the keys above as optional, or let unknown keys through: in zod, `.passthrough()` (zod 3) or `.loose()` (zod 4) on the `data` schema; in JSON Schema, `"additionalProperties": true`.
52
56
 
53
- ### Captions and callout bodies are inline markdown
57
+ #### Captions and callout bodies are inline markdown
54
58
 
55
59
  An image's `caption` and a callout's `body` were plain text in 0.4. In 1.0 they are typed in place and stored as inline markdown, like a paragraph. A 0.4 caption such as `A **sleepy** cat` now shows "sleepy" in bold, and `&amp;` shows as `&`. To keep a character literal in stored data, escape it with a backslash (`\*\*`); text typed in the canvas is escaped for you.
56
60
 
57
- ### Named HTML entities
61
+ #### Named HTML entities
58
62
 
59
63
  Numeric character references (`&#233;`, `&#xE9;`) are always decoded. Of the named entities, only the 23 most common are: `&amp;`, `&lt;`, `&gt;`, `&quot;`, `&apos;`, `&nbsp;`, `&copy;`, `&reg;`, `&trade;`, `&hellip;`, `&mdash;`, `&ndash;`, `&lsquo;`, `&rsquo;`, `&ldquo;`, `&rdquo;`, `&laquo;`, `&raquo;`, `&times;`, `&divide;`, `&deg;`, `&middot;` and `&bull;`. Any other, such as `&eacute;`, stays literal text, and is written back escaped (`\&eacute;`) once its block is edited. The full HTML5 table would add about 10 KB gzip to the engine. Workaround: replace other named entities with the character itself or its numeric reference before you import the markdown.
60
64
 
61
- ### Inline images in imported markdown
65
+ #### Inline images in imported markdown
62
66
 
63
67
  An image alone on its line becomes an `IMAGE` block. An image inside a paragraph, such as `see ![chart](chart.png) below`, stays literal text, since 1.0 has no inline image. Workaround: put each image on a line of its own before you import the markdown, or write an extension that adds an inline image atom.
64
68
 
65
- ## API
69
+ ### API
66
70
 
67
- ### `BlockCanvas`
71
+ #### `BlockCanvas`
68
72
 
69
73
  Every 0.4 prop keeps its name, and its meaning except as the changes below say: `blocks`, `onChange`, `onDelete`, `blockTypes`, `upload`, `resolveAssetUrl`, `onError`, `readOnly`, `className`, `id` and `aria-label`. New ones are optional:
70
74
 
@@ -83,11 +87,11 @@ Two behaviours changed:
83
87
  - **`onDelete` reports more.** Besides the block menu's Delete, it reports whole blocks removed by typing over a selected block or by pasting over selected blocks. A line that Backspace or Delete merges into its neighbour is still not reported. To restore a block, splice it back into `blocks` once they no longer hold it, as on a later click of an Undo toast, not inside the callback.
84
88
  - **A collaborative canvas is uncontrolled.** A canvas given `collaborationExtension` from `quoin-editor/yjs` takes its content from the shared document. Pass the starting blocks once and keep echoing `onChange`; while the shared document is connected, any other array handed to `blocks` is ignored, with a warning in development.
85
89
 
86
- ### `defineBlock`
90
+ #### `defineBlock`
87
91
 
88
92
  `defineBlock` takes the same definition. `icon` now accepts any component that takes a `className` and `aria-hidden`, not only a lucide one. A custom block shows its `preview` at rest, and its `editor` opens on a double click, its Edit button or Enter on the selected block; Escape closes it.
89
93
 
90
- A definition of a type the canvas types into in place, `MARKDOWN`, `CALLOUT` or `TOGGLE`, now draws only on a read-only canvas, and an editable canvas warns in development. To change how one of them looks while it is edited, give an extension a renderer for the type in `ui.renderers`:
94
+ A definition of a type the canvas types into in place, `MARKDOWN`, `CALLOUT` or `TOGGLE`, now draws only on a read-only canvas, and an editable canvas warns in development. `CODE`, `IMAGE` and `FILE` are edited in their `preview` rather than through an `editor`, so a definition that replaces the preview and brings no editor of its own leaves a block an editable canvas draws and nobody can write in; that warns in development too. To draw one of those three your own way on a read-only canvas and keep Quoin's editing where the canvas is editable, register your definition only while `readOnly` is set. To change how one of them looks while it is edited, give an extension a renderer for the type in `ui.renderers`:
91
95
 
92
96
  ```tsx
93
97
  import { BlockCanvas, EditableRegion, type BlockRenderer, type ExtensionUI, type QuoinBlock } from 'quoin-editor';
@@ -107,17 +111,17 @@ export function Editor({ blocks, onChange }: { blocks: QuoinBlock[]; onChange: (
107
111
  }
108
112
  ```
109
113
 
110
- ### `useQuoin`
114
+ #### `useQuoin`
111
115
 
112
116
  `QuoinConfig`, which `useQuoin()` returns to a custom block, gains `highlight` and the required `controller` and `labels`. `QuoinProvider` fills them in, so only a `QuoinConfig` built by hand, such as a test double for a custom block, needs `controller: null` and `labels: englishLabels` added.
113
117
 
114
- ### Entries
118
+ #### Entries
115
119
 
116
120
  - `quoin-editor/headless` now exports the view and the engine (`useQuoinEditor`, `EditorView`, `createEditor` and the rest), and none of the 0.4 helpers. The main entry re-exports all of it.
117
121
  - `formatShortcut` is exported from `quoin-editor` only. Import it from there instead of `quoin-editor/headless`.
118
122
  - `quoin-editor/engine` is the engine alone, with no React and no `'use client'` banner: import `toMarkdown`, `toHTML` and the other pure functions from it in a server component.
119
123
 
120
- ### Removed helpers
124
+ #### Removed helpers
121
125
 
122
126
  The 0.4 helpers worked on the text of a textarea. In 1.0 each is an engine command, run through the canvas's ref (`handle.current?.editor.commands.<name>(...)`) or a headless editor, or is no longer needed.
123
127
 
@@ -163,11 +167,11 @@ export function Editor({ blocks, onChange }: { blocks: QuoinBlock[]; onChange: (
163
167
  }
164
168
  ```
165
169
 
166
- ### Command names of the design spec
170
+ #### Command names of the design spec
167
171
 
168
172
  The 1.0 design spec named `mergeBackward`, `mergeForward` and `insertBlocks` as commands. They are not commands: `deleteBackward` at the start of a block joins it to the one before, `deleteForward` at its end joins the next one to it, and `insertBlockAfter` and `insertSlice` insert blocks. Use those names.
169
173
 
170
- ### Labels
174
+ #### Labels
171
175
 
172
176
  Strings come from `labels`. Two keys are functions rather than strings: `blockDeleted(n)` and `undoHint(shortcut)`. English callout tone names are written in lower case in the tone chip and its menu; give `calloutTones` to write them otherwise:
173
177
 
@@ -185,7 +189,7 @@ export function Editor({ blocks, onChange }: { blocks: QuoinBlock[]; onChange: (
185
189
  }
186
190
  ```
187
191
 
188
- ## Features that now need an import
192
+ ### Features that now need an import
189
193
 
190
194
  In 0.4 the slash palette's Table command wrote a markdown table into the block. In 1.0 a table is a `TABLE` block, drawn and edited by `quoin-editor/table`, which a page loads only when it imports it. Without it, a table, including one split out of a 0.4 block, is drawn as an unsupported block and its data is kept as it was. The one-line change is to pass the extension:
191
195
 
@@ -202,22 +206,22 @@ export function Editor({ blocks, onChange }: { blocks: QuoinBlock[]; onChange: (
202
206
 
203
207
  `catalogue()` from `quoin-editor/catalogue` adds every opt-in feature at once: tables, embeds, math, a table of contents, mentions, emoji, find and replace and stats.
204
208
 
205
- ## Styling
209
+ ### Styling
206
210
 
207
211
  - Every 0.4 `--quoin-` token keeps its name, and the stylesheet still sits in the one `quoin` cascade layer.
208
212
  - New tokens: `--quoin-selection`, the writer's text and background colours `--quoin-color-*` and `--quoin-bg-*`, `--quoin-comment`, `--quoin-comment-active` and `--quoin-comment-edge`, `--quoin-diff-inserted` and `--quoin-diff-deleted`, and `--quoin-link`. Each has a light and a dark default; a host that maps its own tokens onto Quoin's can map these too.
209
213
  - `color-scheme` is declared for light and dark.
210
214
  - The DOM and the class names inside the canvas are new. CSS that targeted 0.4 internals, such as a block card or its textarea, no longer matches; style through the tokens.
211
215
 
212
- ## React 18
216
+ ### React 18
213
217
 
214
218
  React 18.2 and 19 are both supported, as 0.4.1 made them. CI typechecks the packed package against both majors and renders the server entries in plain Node on each, and `BlockCanvas` forwards its ref, so a `QuoinEditorHandle` reaches you on React 18 too.
215
219
 
216
- ## The package
220
+ ### The package
217
221
 
218
222
  The tarball ships ESM and CommonJS builds of every entry, with their sourcemaps. The sourcemaps leave out `sourcesContent`: a debugger maps to the original file names and lines, but shows their text only where it can read the sources, such as from the repository at the release tag.
219
223
 
220
- ## Checklist
224
+ ### Checklist
221
225
 
222
226
  - [ ] Install `quoin-editor@^1`.
223
227
  - [ ] Keep `indent` and the new optional keys when you copy, validate or store blocks, and let a strict schema accept them.
@@ -231,3 +235,113 @@ The tarball ships ESM and CommonJS builds of every entry, with their sourcemaps.
231
235
  - [ ] Pass `tableExtension()` (or `catalogue()`) if your documents hold tables.
232
236
  - [ ] Map the new tokens if you map Quoin's tokens to your own, and drop CSS that targeted 0.4 internals.
233
237
  - [ ] Treat a canvas with a collaboration extension as uncontrolled.
238
+
239
+ ## Migrating from 1.x to 2.0
240
+
241
+ Quoin 2.0 keeps the document format, the props of every component and every command of 1.x. What changes is the published surface: seven names that only the view layer ever called itself stop being exported from `quoin-editor/view`, and so from `quoin-editor/headless` and `quoin-editor` with it. Five of them have carried a deprecation notice since 1.8.0, shown by an editor on hover and on the [headless reference](https://amineseboui.github.io/quoin-editor/docs/headless-reference/); the other two are the prop types of the two components, and go with them. Nothing else is removed and nothing behaves differently, so a host that imports none of the seven upgrades by changing the version.
242
+
243
+ ```bash
244
+ npm install quoin-editor@^2
245
+ ```
246
+
247
+ React 18.2 or 19 and TypeScript 5.4 or later, as in 1.x.
248
+
249
+ ### Exports that are gone
250
+
251
+ | 1.x | 2.0 | Notes |
252
+ | --- | --- | --- |
253
+ | `defaultRenderers` | `EditableRegion` | The one removal with a cost, [below](#defaultrenderers). |
254
+ | `BlockShell`, `BlockShellProps` | Nothing | [`EditorView` draws a shell for every block already.](#blockshell-and-selectionsink) |
255
+ | `SelectionSink`, `SelectionSinkProps` | Nothing | [`EditorView` draws one already.](#blockshell-and-selectionsink) |
256
+ | `VoidBlockFallback` | Nothing | [A block shell falls back to it on its own.](#voidblockfallback) |
257
+ | `isCrossSelection` | `Selection` | [Four lines over `anchor` and `head`.](#iscrossselection) |
258
+
259
+ `BlockRenderProps` and `BlockRenderer` stay public, so a renderer table still types.
260
+
261
+ #### `defaultRenderers`
262
+
263
+ A paragraph, a heading, a bullet, a numbered item, a task and a quote are all `MARKDOWN` blocks drawn by one renderer. A host that wanted its own quote therefore had to name `MARKDOWN` in its table, and could hand every other kind back to the default:
264
+
265
+ ```jsx
266
+ import { defaultRenderers, EditableRegion } from 'quoin-editor';
267
+
268
+ const Markdown = defaultRenderers.MARKDOWN;
269
+
270
+ export const renderers = {
271
+ MARKDOWN: (props) =>
272
+ props.block.attrs.kind === 'quote' ? (
273
+ <figure className="pull-quote">
274
+ <EditableRegion block={props.block.id} region="text" label="Quote" />
275
+ </figure>
276
+ ) : (
277
+ <Markdown {...props} />
278
+ ),
279
+ };
280
+ ```
281
+
282
+ In 2.0 that renderer owns every kind, and draws each one's text with `EditableRegion`, which is what the default renderer does inside:
283
+
284
+ ```tsx
285
+ import { EditableRegion, type BlockRenderer } from 'quoin-editor';
286
+
287
+ const LABELS: Record<string, string> = {
288
+ heading: 'Heading',
289
+ bullet: 'Bullet list item',
290
+ ordered: 'Numbered list item',
291
+ task: 'Task',
292
+ quote: 'Quote',
293
+ };
294
+
295
+ export const renderers: Record<string, BlockRenderer> = {
296
+ MARKDOWN: ({ block }) => {
297
+ const kind = typeof block.attrs.kind === 'string' ? block.attrs.kind : '';
298
+ const text = <EditableRegion block={block.id} region="text" label={LABELS[kind] ?? 'Paragraph'} />;
299
+ if (kind === 'quote') return <figure className="pull-quote">{text}</figure>;
300
+ if (kind === 'heading') {
301
+ const level = typeof block.attrs.level === 'number' ? block.attrs.level : 1;
302
+ return <div role="heading" aria-level={level} className="quoin-heading">{text}</div>;
303
+ }
304
+ if (kind === 'bullet') {
305
+ return (
306
+ <div className="quoin-list-item" data-list="bullet">
307
+ <span className="quoin-marker" aria-hidden="true">{'•'}</span>
308
+ {text}
309
+ </div>
310
+ );
311
+ }
312
+ return text;
313
+ },
314
+ };
315
+ ```
316
+
317
+ `EditableRegion` takes the block's id, the name of the region to draw (`text` for every markdown kind) and an accessible name, and is the same editable host the default renderer mounts, so the caret, the selection and the decorations keep working.
318
+
319
+ Two kinds ask for more than markup, and the sample above leaves both drawn as plain text. A numbered item shows its running number, which counts the ordered blocks above it at the same indent; a task shows a checkbox that runs the `toggleTask` command against its own block. Neither the counter nor the checkbox is public, so a host that overrides `MARKDOWN` and still wants those two drawn the way Quoin draws them writes them itself. A host that only wants to restyle a kind has a cheaper way out: leave `MARKDOWN` out of the renderer table and style the markup the default renderer already writes, which carries `data-kind` on the block and the `quoin-heading`, `quoin-list-item`, `quoin-task` and `quoin-quote` class names.
320
+
321
+ #### `BlockShell` and `SelectionSink`
322
+
323
+ Neither was ever something to render. `EditorView` draws one `SelectionSink` and one `BlockShell` per block, and each registers itself with the `ViewController` on mount (`controller.registerSink`, `controller.registerBlockElement`). A second copy takes that registration from the first, so a host that rendered either lost the selection or the block it meant to help. Drop the import; the editor already draws both. `BlockShellProps` and `SelectionSinkProps` named their props and go with them.
324
+
325
+ #### `VoidBlockFallback`
326
+
327
+ A block shell falls back to it already when no renderer in the table draws a block's type, so a table that named it only restated the default. Drop the entry and the import.
328
+
329
+ #### `isCrossSelection`
330
+
331
+ It asked whether a text selection spans blocks or regions, which is what painting and the arrow keys ask. It reads `anchor` and `head` of the public `Selection`, so a host that wants the answer writes it:
332
+
333
+ ```ts
334
+ import type { Selection } from 'quoin-editor/engine';
335
+
336
+ export const spansBlocks = (selection: Selection | null): boolean =>
337
+ selection?.kind === 'text' &&
338
+ (selection.anchor.block !== selection.head.block || selection.anchor.region !== selection.head.region);
339
+ ```
340
+
341
+ ### 2.0 checklist
342
+
343
+ - [ ] Install `quoin-editor@^2`.
344
+ - [ ] Drop any `BlockShell` or `SelectionSink` you render, and the `BlockShellProps` and `SelectionSinkProps` that typed them.
345
+ - [ ] Drop any `VoidBlockFallback` entry from a renderer table.
346
+ - [ ] Replace `isCrossSelection` with the predicate above.
347
+ - [ ] Draw every `MARKDOWN` kind yourself if your table names `MARKDOWN`, or leave `MARKDOWN` out and style the default markup instead.
package/README.md CHANGED
@@ -76,7 +76,7 @@ What `onChange` hands you, and what you store:
76
76
 
77
77
  Each competitor cell links to the page that shows it; the Tiptap UI cell also draws on the review in [#73](https://github.com/AmineSeboui/quoin-editor/issues/73) (2026-10-12).
78
78
 
79
- | Feature | Quoin 1.8 | Tiptap 3 | BlockNote | Editor.js | Lexical | Plate |
79
+ | Feature | Quoin 2.0 | Tiptap 3 | BlockNote | Editor.js | Lexical | Plate |
80
80
  | --- | --- | --- | --- | --- | --- | --- |
81
81
  | Stored document | Block array, markdown per text block | [JSON, or HTML][tt-store] | [Block JSON][bn-formats] | [JSON][ej-repo] | [Editor state, as JSON][lx-intro] | [Slate JSON][pl-docs] |
82
82
  | Markdown | The storage format | [`@tiptap/markdown`, an early release][tt-md] | [Export and import, documented as lossy][bn-formats] | [Community plugin][ej-awesome] | [`@lexical/markdown`, in and out][lx-md] | [`@platejs/markdown`, in and out][pl-md] |
@@ -131,7 +131,13 @@ Sources, checked on 2026-11-05: Tiptap [persistence][tt-store], [markdown][tt-md
131
131
  - [Collaboration](https://amineseboui.github.io/quoin-editor/docs/collaboration/), [comments](https://amineseboui.github.io/quoin-editor/docs/comments/) and [AI](https://amineseboui.github.io/quoin-editor/docs/ai/)
132
132
  - [Server rendering](https://amineseboui.github.io/quoin-editor/docs/server-rendering/): read-only, `QuoinView`, `QuoinDiff`, `toDocx`
133
133
  - [Accessibility](https://amineseboui.github.io/quoin-editor/docs/accessibility/): what each test suite checks, and what it does not yet
134
- - [FAQ](https://amineseboui.github.io/quoin-editor/docs/faq/): the package name, stability, React versions, and [upgrading from 0.4](MIGRATION.md)
134
+ - [FAQ](https://amineseboui.github.io/quoin-editor/docs/faq/): the package name, stability, React versions, and [upgrading to 2.0](MIGRATION.md#migrating-from-1x-to-20)
135
+
136
+ ## Sponsors
137
+
138
+ Quoin is MIT, with no paid tier and nothing held back behind a licence. It is built and maintained by one person. [Sponsoring it](https://github.com/sponsors/AmineSeboui) pays for the time that keeps issues answered and releases going out.
139
+
140
+ No sponsors yet. Yours would be the first name here.
135
141
 
136
142
  ## The name
137
143
 
package/dist/ai.cjs CHANGED
@@ -2,9 +2,9 @@
2
2
  'use strict';
3
3
 
4
4
  var chunkLE5XMIFJ_cjs = require('./chunk-LE5XMIFJ.cjs');
5
- var chunk4MCYQHFL_cjs = require('./chunk-4MCYQHFL.cjs');
6
- var chunkBA5WXBGE_cjs = require('./chunk-BA5WXBGE.cjs');
7
- var chunkP6HC7RUN_cjs = require('./chunk-P6HC7RUN.cjs');
5
+ var chunkBYYCD54G_cjs = require('./chunk-BYYCD54G.cjs');
6
+ var chunkCJVJNMOB_cjs = require('./chunk-CJVJNMOB.cjs');
7
+ var chunkZ6LAARPB_cjs = require('./chunk-Z6LAARPB.cjs');
8
8
  var chunkZWTIRGY5_cjs = require('./chunk-ZWTIRGY5.cjs');
9
9
  require('./chunk-A6PGQVTR.cjs');
10
10
  var chunkFK7EBZPX_cjs = require('./chunk-FK7EBZPX.cjs');
@@ -377,7 +377,7 @@ async function read(editor, link, run, stream, request) {
377
377
  } catch (error) {
378
378
  if (!live()) return;
379
379
  end("error");
380
- link.report(chunkBA5WXBGE_cjs.toError(error));
380
+ link.report(chunkCJVJNMOB_cjs.toError(error));
381
381
  }
382
382
  }
383
383
  function startStream(editor, link, stream, request, replace) {
@@ -474,7 +474,7 @@ function aiSlashItems(labels, stream) {
474
474
  title: labels.askAi,
475
475
  description: labels.askAiDescription,
476
476
  group: "ai",
477
- icon: chunkBA5WXBGE_cjs.Sparkles,
477
+ icon: chunkCJVJNMOB_cjs.Sparkles,
478
478
  keywords: KEYWORDS,
479
479
  run: ({ controller }) => void openFor(controller)
480
480
  },
@@ -482,7 +482,7 @@ function aiSlashItems(labels, stream) {
482
482
  id: "ai-continue",
483
483
  title: labels.actions.continue,
484
484
  group: "ai",
485
- icon: chunkBA5WXBGE_cjs.PenLine,
485
+ icon: chunkCJVJNMOB_cjs.PenLine,
486
486
  keywords: [...KEYWORDS, "continue"],
487
487
  run: ({ controller }) => {
488
488
  if (openFor(controller)) askFrom({ controller, stream}, "continue");
@@ -493,14 +493,14 @@ function aiSlashItems(labels, stream) {
493
493
  var aiBubbleItem = (labels) => ({
494
494
  id: "ai",
495
495
  title: labels.askAi,
496
- icon: chunkBA5WXBGE_cjs.Sparkles,
496
+ icon: chunkCJVJNMOB_cjs.Sparkles,
497
497
  available: (state) => aiState(state).ghost === null,
498
498
  run: (controller) => void openFor(controller)
499
499
  });
500
500
  var aiBlockMenuItem = (labels) => ({
501
501
  id: "ai",
502
502
  label: labels.askAi,
503
- icon: chunkBA5WXBGE_cjs.Sparkles,
503
+ icon: chunkCJVJNMOB_cjs.Sparkles,
504
504
  available: (state, blockIds) => targetOfBlocks(state, blockIds) !== null,
505
505
  run: (controller, blockIds) => {
506
506
  const target = targetOfBlocks(controller.editor.state, blockIds);
@@ -605,12 +605,12 @@ function AiBar({ env, ghost, gone, barId }) {
605
605
  const [next, setNext] = react.useState("");
606
606
  const { phase } = ghost;
607
607
  const focusPrimary = () => content.current?.querySelector(`[${PRIMARY}]`)?.focus();
608
- chunkP6HC7RUN_cjs.useIsomorphicLayoutEffect(() => {
608
+ chunkZ6LAARPB_cjs.useIsomorphicLayoutEffect(() => {
609
609
  const active = content.current?.ownerDocument.activeElement;
610
610
  inside.current = active == null || active === active.ownerDocument.body;
611
611
  if (inside.current) focusPrimary();
612
612
  }, []);
613
- chunkP6HC7RUN_cjs.useIsomorphicLayoutEffect(() => {
613
+ chunkZ6LAARPB_cjs.useIsomorphicLayoutEffect(() => {
614
614
  const active = content.current?.ownerDocument.activeElement;
615
615
  if (inside.current && !content.current?.contains(active ?? null)) focusPrimary();
616
616
  }, [phase, ghost.key]);
@@ -658,21 +658,21 @@ function AiBar({ env, ghost, gone, barId }) {
658
658
  children: [
659
659
  phase === "streaming" && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
660
660
  /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-sm text-muted-foreground", children: labels.writing }),
661
- /* @__PURE__ */ jsxRuntime.jsx(chunkBA5WXBGE_cjs.Button, { ...primary, size: "sm", variant: "outline", onClick: () => stop(editor), children: labels.stop })
661
+ /* @__PURE__ */ jsxRuntime.jsx(chunkCJVJNMOB_cjs.Button, { ...primary, size: "sm", variant: "outline", onClick: () => stop(editor), children: labels.stop })
662
662
  ] }),
663
663
  phase === "error" && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
664
664
  /* @__PURE__ */ jsxRuntime.jsx("p", { role: "alert", className: "text-sm text-destructive", children: labels.failed }),
665
- /* @__PURE__ */ jsxRuntime.jsx(chunkBA5WXBGE_cjs.Button, { ...primary, size: "sm", onClick: () => retry(editor, stream), children: labels.retry }),
666
- /* @__PURE__ */ jsxRuntime.jsx(chunkBA5WXBGE_cjs.Button, { size: "sm", variant: "ghost", onClick: onDiscard, children: labels.discard })
665
+ /* @__PURE__ */ jsxRuntime.jsx(chunkCJVJNMOB_cjs.Button, { ...primary, size: "sm", onClick: () => retry(editor, stream), children: labels.retry }),
666
+ /* @__PURE__ */ jsxRuntime.jsx(chunkCJVJNMOB_cjs.Button, { size: "sm", variant: "ghost", onClick: onDiscard, children: labels.discard })
667
667
  ] }),
668
668
  (phase === "done" || phase === "stopped") && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
669
669
  phase === "stopped" && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-sm text-muted-foreground", children: labels.stopped }),
670
670
  gone && /* @__PURE__ */ jsxRuntime.jsx("p", { className: "w-full text-xs text-muted-foreground", children: labels.targetGone }),
671
- /* @__PURE__ */ jsxRuntime.jsx(chunkBA5WXBGE_cjs.Button, { ...primary, size: "sm", onClick: onAccept, children: labels.accept }),
672
- /* @__PURE__ */ jsxRuntime.jsx(chunkBA5WXBGE_cjs.Button, { size: "sm", variant: "ghost", onClick: onDiscard, children: labels.discard }),
673
- /* @__PURE__ */ jsxRuntime.jsx(chunkBA5WXBGE_cjs.Button, { size: "sm", variant: "ghost", onClick: () => retry(editor, stream), children: labels.retry }),
671
+ /* @__PURE__ */ jsxRuntime.jsx(chunkCJVJNMOB_cjs.Button, { ...primary, size: "sm", onClick: onAccept, children: labels.accept }),
672
+ /* @__PURE__ */ jsxRuntime.jsx(chunkCJVJNMOB_cjs.Button, { size: "sm", variant: "ghost", onClick: onDiscard, children: labels.discard }),
673
+ /* @__PURE__ */ jsxRuntime.jsx(chunkCJVJNMOB_cjs.Button, { size: "sm", variant: "ghost", onClick: () => retry(editor, stream), children: labels.retry }),
674
674
  /* @__PURE__ */ jsxRuntime.jsx("form", { className: "w-full", onSubmit: onFollowUp, children: /* @__PURE__ */ jsxRuntime.jsx(
675
- chunkBA5WXBGE_cjs.Input,
675
+ chunkCJVJNMOB_cjs.Input,
676
676
  {
677
677
  "aria-label": labels.followUp,
678
678
  placeholder: labels.followUp,
@@ -711,7 +711,7 @@ function fillGhost(ghost, markdown) {
711
711
  if (piece.indent > 0) element.style.setProperty("--quoin-ai-indent", String(piece.indent));
712
712
  const region = chunkFK7EBZPX_cjs.primaryRegion(schema, piece);
713
713
  if (region === null) element.textContent = chunkFK7EBZPX_cjs.toPlainText([codec.toQuoin(piece)], { schema });
714
- else chunkP6HC7RUN_cjs.renderInline(element, piece.regions[region], []);
714
+ else chunkZ6LAARPB_cjs.renderInline(element, piece.regions[region], []);
715
715
  drawn2.push(element);
716
716
  }
717
717
  }
@@ -788,7 +788,7 @@ function clipAside(layer, text, areas, scale) {
788
788
  function useGhostPlace(controller, layer, block, region) {
789
789
  const placeNow = react.useRef(() => {
790
790
  });
791
- chunkP6HC7RUN_cjs.useIsomorphicLayoutEffect(() => {
791
+ chunkZ6LAARPB_cjs.useIsomorphicLayoutEffect(() => {
792
792
  const root = controller.root;
793
793
  const element = layer.current;
794
794
  const view = root?.ownerDocument.defaultView;
@@ -827,7 +827,7 @@ function AiGhost({ env, ghost, target, gone, barId }) {
827
827
  const layer = react.useRef(null);
828
828
  const drawn2 = react.useRef(null);
829
829
  const place2 = useGhostPlace(env.controller, layer, target.to.block, target.to.region);
830
- chunkP6HC7RUN_cjs.useIsomorphicLayoutEffect(() => {
830
+ chunkZ6LAARPB_cjs.useIsomorphicLayoutEffect(() => {
831
831
  const element = drawn2.current;
832
832
  if (element === null) return;
833
833
  element.setAttribute("inert", "");
@@ -841,7 +841,7 @@ function AiGhost({ env, ghost, target, gone, barId }) {
841
841
  }
842
842
  function useTargetAnchor(controller, target) {
843
843
  const latest = react.useRef(target);
844
- chunkP6HC7RUN_cjs.useIsomorphicLayoutEffect(() => {
844
+ chunkZ6LAARPB_cjs.useIsomorphicLayoutEffect(() => {
845
845
  latest.current = target;
846
846
  });
847
847
  return react.useMemo(() => {
@@ -850,8 +850,8 @@ function useTargetAnchor(controller, target) {
850
850
  return { block: to.block, region: to.region, from: to.offset, to: to.offset };
851
851
  };
852
852
  return {
853
- current: chunkBA5WXBGE_cjs.virtualAnchor(
854
- () => chunkBA5WXBGE_cjs.rangeRect(controller, end()),
853
+ current: chunkCJVJNMOB_cjs.virtualAnchor(
854
+ () => chunkCJVJNMOB_cjs.rangeRect(controller, end()),
855
855
  () => controller.registry.get(end().block, end().region)
856
856
  )
857
857
  };
@@ -905,7 +905,7 @@ function AiMenu({ env, target, fromSpace }) {
905
905
  };
906
906
  const fieldLabel = asking === "tone" ? labels.tonePrompt : asking === "translate" ? labels.translatePrompt : labels.prompt;
907
907
  return /* @__PURE__ */ jsxRuntime.jsxs(
908
- chunkBA5WXBGE_cjs.Popover,
908
+ chunkCJVJNMOB_cjs.Popover,
909
909
  {
910
910
  open: true,
911
911
  modal: false,
@@ -913,9 +913,9 @@ function AiMenu({ env, target, fromSpace }) {
913
913
  if (!next) closeMenu(controller.editor);
914
914
  },
915
915
  children: [
916
- /* @__PURE__ */ jsxRuntime.jsx(chunkBA5WXBGE_cjs.PopoverAnchor, { virtualRef: anchor }),
916
+ /* @__PURE__ */ jsxRuntime.jsx(chunkCJVJNMOB_cjs.PopoverAnchor, { virtualRef: anchor }),
917
917
  /* @__PURE__ */ jsxRuntime.jsxs(
918
- chunkBA5WXBGE_cjs.PopoverContent,
918
+ chunkCJVJNMOB_cjs.PopoverContent,
919
919
  {
920
920
  "aria-label": labels.askAi,
921
921
  side: "bottom",
@@ -932,7 +932,7 @@ function AiMenu({ env, target, fromSpace }) {
932
932
  },
933
933
  children: [
934
934
  /* @__PURE__ */ jsxRuntime.jsx(
935
- chunkBA5WXBGE_cjs.Input,
935
+ chunkCJVJNMOB_cjs.Input,
936
936
  {
937
937
  ref: field,
938
938
  role: "combobox",
@@ -956,7 +956,7 @@ function AiMenu({ env, target, fromSpace }) {
956
956
  id: optionId(index),
957
957
  role: "option",
958
958
  "aria-selected": index === current,
959
- className: chunkBA5WXBGE_cjs.cn(chunkBA5WXBGE_cjs.MENU_ITEM, index === current && "bg-accent text-accent-foreground"),
959
+ className: chunkCJVJNMOB_cjs.cn(chunkCJVJNMOB_cjs.MENU_ITEM, index === current && "bg-accent text-accent-foreground"),
960
960
  onMouseDown: (event) => event.preventDefault(),
961
961
  onClick: () => choose(choice),
962
962
  children: choice.label
@@ -976,15 +976,15 @@ var goneOf = (state) => {
976
976
  };
977
977
  var plainText = (markdown) => chunkFK7EBZPX_cjs.toPlainText(chunkI7X7B3WD_cjs.fromMarkdown(markdown));
978
978
  function AiOverlay({ controller, stream, labels }) {
979
- const { onError } = chunkBA5WXBGE_cjs.useQuoin();
979
+ const { onError } = chunkCJVJNMOB_cjs.useQuoin();
980
980
  const link = linkOf(controller.editor);
981
- chunkP6HC7RUN_cjs.useIsomorphicLayoutEffect(() => {
981
+ chunkZ6LAARPB_cjs.useIsomorphicLayoutEffect(() => {
982
982
  link.report = onError;
983
983
  link.ended = (phase, output) => controller.announce((phase === "done" ? labels.written : labels.halted)(plainText(output)));
984
984
  }, [link, onError, controller, labels]);
985
- const value = chunkP6HC7RUN_cjs.useEditorState(controller, aiState);
986
- const gone = chunkP6HC7RUN_cjs.useEditorState(controller, goneOf);
987
- const readOnly = chunkP6HC7RUN_cjs.useReadOnly(controller);
985
+ const value = chunkZ6LAARPB_cjs.useEditorState(controller, aiState);
986
+ const gone = chunkZ6LAARPB_cjs.useEditorState(controller, goneOf);
987
+ const readOnly = chunkZ6LAARPB_cjs.useReadOnly(controller);
988
988
  const env = react.useMemo(() => ({ controller, stream, labels }), [controller, stream, labels]);
989
989
  const barId = react.useId();
990
990
  const showing = value.ghost !== null;
@@ -1003,7 +1003,7 @@ function AiOverlay({ controller, stream, labels }) {
1003
1003
  const root = controller.root;
1004
1004
  if (!root) return;
1005
1005
  let away = true;
1006
- return chunk4MCYQHFL_cjs.watchCanvasFocus(controller, root.ownerDocument, (inside, node) => {
1006
+ return chunkBYYCD54G_cjs.watchCanvasFocus(controller, root.ownerDocument, (inside, node) => {
1007
1007
  if (!inside) away = true;
1008
1008
  else if (root.contains(node)) {
1009
1009
  if (away) noteFocus(controller.editor);
package/dist/ai.js CHANGED
@@ -1,8 +1,8 @@
1
1
  'use client';
2
2
  import { clipBoxes } from './chunk-MQUCCSWG.js';
3
- import { watchCanvasFocus } from './chunk-D4L5FINK.js';
4
- import { Sparkles, PenLine, useQuoin, Popover, PopoverAnchor, PopoverContent, Input, cn, MENU_ITEM, virtualAnchor, rangeRect, Button, toError } from './chunk-Y4EJ6UBV.js';
5
- import { useIsomorphicLayoutEffect, useEditorState, useReadOnly, renderInline } from './chunk-MSWA5ZZM.js';
3
+ import { watchCanvasFocus } from './chunk-IW4TMECB.js';
4
+ import { Sparkles, PenLine, useQuoin, Popover, PopoverAnchor, PopoverContent, Input, cn, MENU_ITEM, virtualAnchor, rangeRect, Button, toError } from './chunk-ZN5DHNSL.js';
5
+ import { useIsomorphicLayoutEffect, useEditorState, useReadOnly, renderInline } from './chunk-NL5J77VD.js';
6
6
  import { mergeLabels } from './chunk-DLQCTGC3.js';
7
7
  import './chunk-HDJ653LM.js';
8
8
  import { createPluginKey, defineExtension, isCollapsed, regionLength, primaryRegion, getPluginState, Transaction, toPlainText, selectedBlockIds, orderedRange, comparePos, sliceFromSelection, textSelection, deleteRange, pasteAtCaret, toMarkdown, descendantsEnd, freshBlocks, insertBlocksStep, capIndents, endOf } from './chunk-5RBMZGKV.js';
@@ -1,21 +1,21 @@
1
1
  'use client';
2
2
  'use strict';
3
3
 
4
- var chunkPVFSJXYQ_cjs = require('./chunk-PVFSJXYQ.cjs');
5
- var chunk7X3T7VXV_cjs = require('./chunk-7X3T7VXV.cjs');
6
- var chunkEZWJRBQT_cjs = require('./chunk-EZWJRBQT.cjs');
7
- var chunkYVPZDTVZ_cjs = require('./chunk-YVPZDTVZ.cjs');
4
+ var chunk5YAJZSTI_cjs = require('./chunk-5YAJZSTI.cjs');
5
+ var chunkHOSDMHK6_cjs = require('./chunk-HOSDMHK6.cjs');
6
+ var chunkNFO3W644_cjs = require('./chunk-NFO3W644.cjs');
7
+ var chunkLEICOZYK_cjs = require('./chunk-LEICOZYK.cjs');
8
8
  require('./chunk-2SWLD6R5.cjs');
9
- var chunk53FJFYBR_cjs = require('./chunk-53FJFYBR.cjs');
10
- var chunk53CNYRVQ_cjs = require('./chunk-53CNYRVQ.cjs');
11
- var chunkTQHFGSTT_cjs = require('./chunk-TQHFGSTT.cjs');
12
- require('./chunk-XKYNINUX.cjs');
9
+ var chunkV4WQG7FR_cjs = require('./chunk-V4WQG7FR.cjs');
10
+ var chunk66OPHDFA_cjs = require('./chunk-66OPHDFA.cjs');
11
+ var chunkD6CIIWCU_cjs = require('./chunk-D6CIIWCU.cjs');
12
+ require('./chunk-IKZSCTRF.cjs');
13
13
  require('./chunk-YEDPBYO5.cjs');
14
- var chunkMMS4D4MK_cjs = require('./chunk-MMS4D4MK.cjs');
15
- var chunkB3OBM6I4_cjs = require('./chunk-B3OBM6I4.cjs');
14
+ var chunk77CNOLHY_cjs = require('./chunk-77CNOLHY.cjs');
15
+ var chunk4PIJC4FD_cjs = require('./chunk-4PIJC4FD.cjs');
16
16
  require('./chunk-LM3DHROX.cjs');
17
- require('./chunk-BA5WXBGE.cjs');
18
- require('./chunk-P6HC7RUN.cjs');
17
+ require('./chunk-CJVJNMOB.cjs');
18
+ require('./chunk-Z6LAARPB.cjs');
19
19
  require('./chunk-ZWTIRGY5.cjs');
20
20
  require('./chunk-A6PGQVTR.cjs');
21
21
  require('./chunk-FK7EBZPX.cjs');
@@ -32,15 +32,15 @@ function createCatalogue(features2) {
32
32
 
33
33
  // src/catalogue.ts
34
34
  var features = {
35
- table: chunk53FJFYBR_cjs.tableExtension,
36
- embed: chunk53CNYRVQ_cjs.embedExtension,
37
- math: chunkTQHFGSTT_cjs.mathExtension,
38
- diagram: chunkMMS4D4MK_cjs.diagramExtension,
39
- toc: chunkB3OBM6I4_cjs.tocExtension,
40
- mention: chunkPVFSJXYQ_cjs.mentionExtension,
41
- emoji: chunk7X3T7VXV_cjs.emojiExtension,
42
- find: chunkEZWJRBQT_cjs.findExtension,
43
- stats: chunkYVPZDTVZ_cjs.statsExtension
35
+ table: chunkV4WQG7FR_cjs.tableExtension,
36
+ embed: chunk66OPHDFA_cjs.embedExtension,
37
+ math: chunkD6CIIWCU_cjs.mathExtension,
38
+ diagram: chunk77CNOLHY_cjs.diagramExtension,
39
+ toc: chunk4PIJC4FD_cjs.tocExtension,
40
+ mention: chunk5YAJZSTI_cjs.mentionExtension,
41
+ emoji: chunkHOSDMHK6_cjs.emojiExtension,
42
+ find: chunkNFO3W644_cjs.findExtension,
43
+ stats: chunkLEICOZYK_cjs.statsExtension
44
44
  };
45
45
  var catalogue = createCatalogue(features);
46
46