quoin-editor 2.0.5 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (215) hide show
  1. package/CHANGELOG.md +105 -0
  2. package/MIGRATION.md +1 -1
  3. package/README.md +4 -3
  4. package/dist/ai.cjs +124 -94
  5. package/dist/ai.d.cts +7 -0
  6. package/dist/ai.d.ts +7 -0
  7. package/dist/ai.js +50 -20
  8. package/dist/ai.js.map +1 -1
  9. package/dist/catalogue.cjs +28 -26
  10. package/dist/catalogue.js +19 -17
  11. package/dist/catalogue.js.map +1 -1
  12. package/dist/{chunk-JGEDJYIV.cjs → chunk-2FUWXPPC.cjs} +92 -98
  13. package/dist/{chunk-NHNW5DK3.js → chunk-3F76QQ65.js} +6 -6
  14. package/dist/{chunk-NHNW5DK3.js.map → chunk-3F76QQ65.js.map} +1 -1
  15. package/dist/{chunk-DBOZFNWU.js → chunk-3QYFAAQR.js} +3 -3
  16. package/dist/{chunk-DBOZFNWU.js.map → chunk-3QYFAAQR.js.map} +1 -1
  17. package/dist/{chunk-I7X7B3WD.cjs → chunk-4JDRDPLC.cjs} +17 -8
  18. package/dist/{chunk-EBSDPHPS.cjs → chunk-4UFWFV43.cjs} +1060 -719
  19. package/dist/{chunk-LM3DHROX.cjs → chunk-5AUIA4B3.cjs} +2 -2
  20. package/dist/{chunk-3INFNIGN.cjs → chunk-5H3BB5VJ.cjs} +131 -78
  21. package/dist/{chunk-DJU25PFC.cjs → chunk-5L2NSIHZ.cjs} +11 -11
  22. package/dist/{chunk-W376L2GM.js → chunk-6Y7JAWYY.js} +8 -8
  23. package/dist/chunk-6Y7JAWYY.js.map +1 -0
  24. package/dist/{chunk-UDBLZILE.js → chunk-ATKDXQEY.js} +12 -7
  25. package/dist/{chunk-UDBLZILE.js.map → chunk-ATKDXQEY.js.map} +1 -1
  26. package/dist/{chunk-A2IQHMIH.js → chunk-BE2SYPK5.js} +49 -54
  27. package/dist/chunk-BE2SYPK5.js.map +1 -0
  28. package/dist/{chunk-PURYVDC3.cjs → chunk-CFICD2JP.cjs} +21 -16
  29. package/dist/{chunk-3HW3WRYC.cjs → chunk-CSIUXYRU.cjs} +2 -2
  30. package/dist/{chunk-N7BSFDDM.cjs → chunk-DBKL524I.cjs} +23 -23
  31. package/dist/{chunk-XE3IYW5B.js → chunk-DEFYFIWW.js} +90 -37
  32. package/dist/chunk-DEFYFIWW.js.map +1 -0
  33. package/dist/{chunk-7E43TURF.cjs → chunk-ECI3PSMS.cjs} +7 -7
  34. package/dist/{chunk-4G645EYO.js → chunk-F73QEF7E.js} +5 -5
  35. package/dist/{chunk-4G645EYO.js.map → chunk-F73QEF7E.js.map} +1 -1
  36. package/dist/{chunk-KHRVCTQQ.cjs → chunk-FUT7XLMP.cjs} +61 -74
  37. package/dist/{chunk-7J63427U.cjs → chunk-GQSBOBOM.cjs} +375 -320
  38. package/dist/{chunk-LU3T3JB4.js → chunk-HPNSQWRD.js} +15 -28
  39. package/dist/chunk-HPNSQWRD.js.map +1 -0
  40. package/dist/{chunk-7Y4B7NHO.js → chunk-I34BFOLL.js} +18 -11
  41. package/dist/chunk-I34BFOLL.js.map +1 -0
  42. package/dist/{chunk-AJT54OMZ.js → chunk-IFRXEFQH.js} +9 -15
  43. package/dist/chunk-IFRXEFQH.js.map +1 -0
  44. package/dist/{chunk-YYW33WUL.cjs → chunk-K3XWVRL4.cjs} +8 -8
  45. package/dist/{chunk-LSI4RMTY.js → chunk-L5L6YNLT.js} +3 -3
  46. package/dist/{chunk-LSI4RMTY.js.map → chunk-L5L6YNLT.js.map} +1 -1
  47. package/dist/{chunk-2BPXVQBP.js → chunk-MO4UMXMT.js} +238 -86
  48. package/dist/chunk-MO4UMXMT.js.map +1 -0
  49. package/dist/{chunk-2QKFRKOP.cjs → chunk-N25EH2GZ.cjs} +351 -205
  50. package/dist/{chunk-CSPCDVCT.js → chunk-NCV2ML25.js} +3 -3
  51. package/dist/{chunk-CSPCDVCT.js.map → chunk-NCV2ML25.js.map} +1 -1
  52. package/dist/{chunk-NUQ64S5B.cjs → chunk-OJSKSQTL.cjs} +26 -26
  53. package/dist/chunk-PIWMU4BV.js +9 -0
  54. package/dist/chunk-PIWMU4BV.js.map +1 -0
  55. package/dist/chunk-QFGWSFOI.cjs +10 -0
  56. package/dist/{chunk-GPTJYWGS.js → chunk-RNBLVGWY.js} +3 -3
  57. package/dist/{chunk-GPTJYWGS.js.map → chunk-RNBLVGWY.js.map} +1 -1
  58. package/dist/{chunk-5RBMZGKV.js → chunk-TL2MJOWM.js} +325 -57
  59. package/dist/chunk-TL2MJOWM.js.map +1 -0
  60. package/dist/{chunk-YEDPBYO5.cjs → chunk-UDQ52EEC.cjs} +4 -4
  61. package/dist/{chunk-EIWPJWY3.js → chunk-UE2NQPNG.js} +5 -5
  62. package/dist/{chunk-EIWPJWY3.js.map → chunk-UE2NQPNG.js.map} +1 -1
  63. package/dist/{chunk-BE2YHZXA.cjs → chunk-ULV3SCYP.cjs} +26 -15
  64. package/dist/{chunk-WDBFGX3U.cjs → chunk-VTTMJGWP.cjs} +72 -77
  65. package/dist/{chunk-556PFYES.js → chunk-WK4UGDZN.js} +290 -235
  66. package/dist/chunk-WK4UGDZN.js.map +1 -0
  67. package/dist/{chunk-JZF6UQH5.cjs → chunk-WMKA37NJ.cjs} +12 -12
  68. package/dist/{chunk-F2ZK6Z52.js → chunk-Y26F2X4E.js} +3 -3
  69. package/dist/{chunk-F2ZK6Z52.js.map → chunk-Y26F2X4E.js.map} +1 -1
  70. package/dist/{chunk-FK7EBZPX.cjs → chunk-Z5EEDCRI.cjs} +549 -276
  71. package/dist/{chunk-G6NBQYMM.js → chunk-ZGFZRVQA.js} +817 -478
  72. package/dist/chunk-ZGFZRVQA.js.map +1 -0
  73. package/dist/{chunk-QLVX7MPS.js → chunk-ZQF45LA4.js} +19 -8
  74. package/dist/chunk-ZQF45LA4.js.map +1 -0
  75. package/dist/comments.cjs +127 -65
  76. package/dist/comments.d.cts +35 -2
  77. package/dist/comments.d.ts +35 -2
  78. package/dist/comments.js +73 -12
  79. package/dist/comments.js.map +1 -1
  80. package/dist/{diagram-extension-22x_njXB.d.ts → diagram-extension-BMlh8GCw.d.ts} +1 -1
  81. package/dist/{diagram-extension-C_FSkoj9.d.cts → diagram-extension-gmndUpti.d.cts} +1 -1
  82. package/dist/diagram.cjs +10 -9
  83. package/dist/diagram.d.cts +2 -2
  84. package/dist/diagram.d.ts +2 -2
  85. package/dist/diagram.js +7 -6
  86. package/dist/diff.cjs +25 -25
  87. package/dist/diff.js +3 -3
  88. package/dist/docx.cjs +31 -30
  89. package/dist/docx.js +5 -4
  90. package/dist/docx.js.map +1 -1
  91. package/dist/embed.cjs +10 -9
  92. package/dist/embed.js +8 -7
  93. package/dist/{emoji-extension-Dk5IQLZf.d.cts → emoji-extension-DD9CQTQj.d.cts} +1 -1
  94. package/dist/{emoji-extension-C832oGeT.d.ts → emoji-extension-U_PWZ2fX.d.ts} +1 -1
  95. package/dist/emoji.cjs +10 -9
  96. package/dist/emoji.d.cts +2 -2
  97. package/dist/emoji.d.ts +2 -2
  98. package/dist/emoji.js +7 -6
  99. package/dist/engine.cjs +58 -58
  100. package/dist/engine.d.cts +3 -3
  101. package/dist/engine.d.ts +3 -3
  102. package/dist/engine.js +3 -3
  103. package/dist/{find-extension-DmK9_N81.d.ts → find-extension-BGlqooZi.d.ts} +1 -1
  104. package/dist/{find-extension-BfruAPLi.d.cts → find-extension-BPnAJ8bY.d.cts} +1 -1
  105. package/dist/find.cjs +11 -10
  106. package/dist/find.d.cts +1 -1
  107. package/dist/find.d.ts +1 -1
  108. package/dist/find.js +7 -6
  109. package/dist/headless.cjs +70 -69
  110. package/dist/headless.d.cts +33 -10
  111. package/dist/headless.d.ts +33 -10
  112. package/dist/headless.js +5 -4
  113. package/dist/highlight.cjs +7 -6
  114. package/dist/highlight.js +6 -5
  115. package/dist/highlight.js.map +1 -1
  116. package/dist/{index-3fYvwqUZ.d.cts → index-DN7vRKFw.d.cts} +1 -1
  117. package/dist/{index-CgxnzCO5.d.ts → index-DyIYI3PM.d.ts} +1 -1
  118. package/dist/index.cjs +82 -81
  119. package/dist/index.d.cts +19 -12
  120. package/dist/index.d.ts +19 -12
  121. package/dist/index.js +6 -5
  122. package/dist/{labels-CiCxj2jP.d.cts → labels-BJORhN93.d.ts} +10 -5
  123. package/dist/{labels-BhRJYo46.d.ts → labels-CVYJ2-1X.d.cts} +10 -5
  124. package/dist/locale-ar.cjs +10 -0
  125. package/dist/locale-ar.d.cts +1 -1
  126. package/dist/locale-ar.d.ts +1 -1
  127. package/dist/locale-ar.js +10 -0
  128. package/dist/locale-ar.js.map +1 -1
  129. package/dist/locale-de.cjs +4 -0
  130. package/dist/locale-de.d.cts +1 -1
  131. package/dist/locale-de.d.ts +1 -1
  132. package/dist/locale-de.js +4 -0
  133. package/dist/locale-de.js.map +1 -1
  134. package/dist/locale-es.cjs +4 -0
  135. package/dist/locale-es.d.cts +1 -1
  136. package/dist/locale-es.d.ts +1 -1
  137. package/dist/locale-es.js +4 -0
  138. package/dist/locale-es.js.map +1 -1
  139. package/dist/locale-fr.cjs +4 -0
  140. package/dist/locale-fr.d.cts +1 -1
  141. package/dist/locale-fr.d.ts +1 -1
  142. package/dist/locale-fr.js +4 -0
  143. package/dist/locale-fr.js.map +1 -1
  144. package/dist/locale-ja.cjs +4 -0
  145. package/dist/locale-ja.d.cts +1 -1
  146. package/dist/locale-ja.d.ts +1 -1
  147. package/dist/locale-ja.js +4 -0
  148. package/dist/locale-ja.js.map +1 -1
  149. package/dist/locale-pt.cjs +4 -0
  150. package/dist/locale-pt.d.cts +1 -1
  151. package/dist/locale-pt.d.ts +1 -1
  152. package/dist/locale-pt.js +4 -0
  153. package/dist/locale-pt.js.map +1 -1
  154. package/dist/locale-zh.cjs +4 -0
  155. package/dist/locale-zh.d.cts +1 -1
  156. package/dist/locale-zh.d.ts +1 -1
  157. package/dist/locale-zh.js +4 -0
  158. package/dist/locale-zh.js.map +1 -1
  159. package/dist/math.cjs +11 -10
  160. package/dist/math.js +9 -8
  161. package/dist/mention.cjs +11 -10
  162. package/dist/mention.js +9 -8
  163. package/dist/{node-reveal-DY6bimv3.d.cts → node-reveal-Be7Xa0BL.d.cts} +1 -1
  164. package/dist/{node-reveal-BnFcnHhz.d.ts → node-reveal-CftIEqF_.d.ts} +1 -1
  165. package/dist/palette-DuIFHYHc.d.cts +10 -0
  166. package/dist/palette-DuIFHYHc.d.ts +10 -0
  167. package/dist/presence.cjs +39 -38
  168. package/dist/presence.js +8 -7
  169. package/dist/presence.js.map +1 -1
  170. package/dist/source.cjs +10 -9
  171. package/dist/source.d.cts +1 -1
  172. package/dist/source.d.ts +1 -1
  173. package/dist/source.js +6 -5
  174. package/dist/source.js.map +1 -1
  175. package/dist/{stats-extension-Dmw0Ujxe.d.ts → stats-extension-Bbpr593p.d.ts} +1 -1
  176. package/dist/{stats-extension-D5HySF0a.d.cts → stats-extension-D7FaeydW.d.cts} +1 -1
  177. package/dist/stats.cjs +12 -10
  178. package/dist/stats.d.cts +2 -2
  179. package/dist/stats.d.ts +2 -2
  180. package/dist/stats.js +9 -7
  181. package/dist/styles.css +1 -1
  182. package/dist/{table-extension-7LkLNK6Q.d.cts → table-extension-DNijjmxV.d.cts} +1 -1
  183. package/dist/{table-extension-DEpV0DnP.d.ts → table-extension-Dp6FYGPh.d.ts} +1 -1
  184. package/dist/table.cjs +26 -25
  185. package/dist/table.d.cts +4 -3
  186. package/dist/table.d.ts +4 -3
  187. package/dist/table.js +7 -6
  188. package/dist/{toc-extension-Dvh37bWQ.d.cts → toc-extension-BGEVAmSY.d.cts} +1 -1
  189. package/dist/{toc-extension-SVckrFmb.d.ts → toc-extension-DZIe40eV.d.ts} +1 -1
  190. package/dist/toc.cjs +10 -9
  191. package/dist/toc.d.cts +2 -2
  192. package/dist/toc.d.ts +2 -2
  193. package/dist/toc.js +7 -6
  194. package/dist/{types-DMuuDBVh.d.ts → types-Byu2a9qk.d.ts} +6 -1
  195. package/dist/{types-Bzhq5AXj.d.cts → types-DdDlCyyZ.d.cts} +6 -1
  196. package/dist/view.cjs +4 -4
  197. package/dist/view.js +2 -2
  198. package/dist/yjs.cjs +121 -121
  199. package/dist/yjs.d.cts +2 -2
  200. package/dist/yjs.d.ts +2 -2
  201. package/dist/yjs.js +5 -5
  202. package/package.json +4 -3
  203. package/dist/chunk-2BPXVQBP.js.map +0 -1
  204. package/dist/chunk-556PFYES.js.map +0 -1
  205. package/dist/chunk-5RBMZGKV.js.map +0 -1
  206. package/dist/chunk-7Y4B7NHO.js.map +0 -1
  207. package/dist/chunk-A2IQHMIH.js.map +0 -1
  208. package/dist/chunk-AJT54OMZ.js.map +0 -1
  209. package/dist/chunk-G6NBQYMM.js.map +0 -1
  210. package/dist/chunk-LU3T3JB4.js.map +0 -1
  211. package/dist/chunk-QLVX7MPS.js.map +0 -1
  212. package/dist/chunk-W376L2GM.js.map +0 -1
  213. package/dist/chunk-XE3IYW5B.js.map +0 -1
  214. package/dist/palette-BrX-hEd-.d.cts +0 -4
  215. package/dist/palette-BrX-hEd-.d.ts +0 -4
package/CHANGELOG.md CHANGED
@@ -2,6 +2,111 @@
2
2
 
3
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
4
 
5
+ ## 2.1.0 (2026-11-13)
6
+
7
+ ### Added
8
+ - `yjsCommentThreads(doc, { currentUser })` in `quoin-editor/comments` keeps comment threads in the `Y.Doc` a canvas already syncs through, so collaborators see each other's threads, replies and resolves. It returns a store and the four callbacks `commentsExtension` takes, and `listen()`, which reads the document before following it and returns how to stop. Each comment is kept under its own id, so two replies sent to one thread at the same moment both stay, where keeping a thread as one value lost one of them, and deleting a thread takes its comments with it. The entry takes the document by its shape, so it imports nothing from `yjs`, types included, and a canvas with comments but no collaboration still needs no `yjs` installed.
9
+ - `aiExtension` takes `describeError` and `isRetryable`, so a failed answer can say why. `describeError` is given what the stream threw or rejected with and returns the text the bar shows under "AI could not answer", announced with it; `isRetryable` returning false leaves Discard alone in the bar, focused, for a failure such as a missing key or an unknown model that asking again cannot fix. Before, the reason reached only `onError` and Retry was always offered. Both are opt-in, so an error the host did not describe stays out of the bar as it did, a throw from either falls back to the label and to Retry, and `onError` still hears every failure.
10
+ - `EditorView` takes `rendererLabels`, the names its renderers give each block's text and the controls beside it (`paragraph`, `heading`, `bulletItem`, `numberedItem`, `task`, `quote`, `callout`, `calloutTones`, `toggle`, `toggleButton`), in place of the English ones; a key left out stays English, and so does each tone `calloutTones` leaves out. The canvas labels and the locale packs carry every one of these keys, so a pack can be passed as it is, and the empty document's paragraph and an empty heading's placeholder follow them unless `paragraphLabel` or `blockPlaceholder` is given. New type `RendererLabels`.
11
+ - `PaletteName` and `CalloutTone`, the types `QuoinLabels` keys the colour names and the callout tone names by, exported from `quoin-editor/engine` and the entries that carry it. A translation typed against the labels could not name either, since both were declared in modules no entry exported.
12
+ - Label `textColorSample`, the letter the colour menus draw in each text colour beside its name: `A` in English and the Latin-script packs, `أ` in Arabic, `あ` in Japanese and `文` in Chinese. It is optional in `QuoinLabels` and English where missing.
13
+ - Two labels in every locale pack: `blocksInserted`, which says how many blocks went in at once and where the first of them now is, and `copyFailed`, which says a copy from the block menu did not reach the clipboard. A third, `blockOptions`, names the block handle on a touch screen. All three are optional in `QuoinLabels` and read as English where missing, so a complete translation written before them still compiles.
14
+ - The mobile toolbar answers Alt+F10 from a hardware keyboard, as the bubble toolbar does: the arrows walk it and Escape goes back to the text. It was tappable only, and out of the tab order, so a keyboard paired with a phone or tablet could not reach it.
15
+ - A paste rule is handed everything that was pasted, the HTML and Quoin flavours beside the plain text, as an optional third argument to its `match`, so a rule can tell a line of text from a table copied out of a spreadsheet and keep what it takes over formatted. A rule written for two arguments works as it did.
16
+ - A paste from a word processor keeps its headings where the source writes them as larger text rather than as heading elements. TextEdit and Pages never write a heading element, Word writes its Title style and any hand-styled heading as a sized paragraph, and Word on the web marks a heading with a role, so all of those arrived as bold or plain paragraphs. A paragraph set 1.8 times larger than most of the text pasted with it becomes a level 1 heading, a bold one 1.4 or 1.15 times larger a level 2 or 3, and a heading role gives its own level. A larger line that is not bold, such as the opening paragraph of an article copied from a browser, stays a paragraph.
17
+ - The canvas uploads the pictures in HTML pasted from another document, such as Google Docs or Notion, when it has an `upload` function: it reads each picture from its address and hands it to `upload` as it would a dropped image, so the document keeps a copy of its own rather than a link the source may later expire. A picture whose server does not let the page read it stays linked as before, and a failed upload is reported to `onError`. This is the one request the canvas makes itself, and only with an `upload` function.
18
+ - A documentation page, Adopting existing documents, for moving a directory of markdown files or a column of HTML into Quoin. It opens with what each conversion loses, measured over 1,319 README, changelog and documentation files from npm packages: an image inside a line of text and HTML outside the few tags the document keeps are shown as text, front matter reads as a divider and a heading, and words after a code fence's language are dropped. It lists the inline HTML the document keeps (underline, highlight, text colour, background, subscript, superscript, line breaks and comment anchors), what storing blocks as markdown keeps and drops, why ids should be assigned once and stored, and the ways back out. Until now nothing said that the package could take an existing document set at all, or what it would cost.
19
+ - A conversion script in the repository, `npm run convert:markdown -- <directory>`, that converts every markdown file under a directory, writes the blocks back as markdown, reads that again, and reports which files came back unchanged, which changed only in spelling, and which would lose something if stored as markdown, naming the first block that differs. `--out` writes the blocks and the markdown beside each other for review, and the script exits with 1 when a file failed, so a migration can be gated on it.
20
+
21
+ ### Changed
22
+ - A list a custom block draws inside the canvas keeps its bullets or numbers. The stylesheet's base reset gave every `ul` and `ol` under `.quoin` `list-style: none`, which a host's layered styles could not undo; Quoin's own lists, in the comments rail, a thread, the comments sheet, the thread chooser, the AI menu and the table of contents, now drop their markers with a class of their own. The reset still zeroes a list's padding, so give such a list a `padding-inline-start` of its own.
23
+
24
+ ### Fixed
25
+ - A collaborator typing exactly at your caret no longer carries your caret along with their text, and their Enter there no longer takes your caret into the new block, where your next keystroke landed inside their text and could open your own slash menu over it. A remote change was carried through the selection the way your own typing is, with the caret following what is put in at it. Text a collaborator puts in at either end of your selection now stays outside it too, and the rule sits in the engine, so an adapter of your own that applies changes with `applyRemote` gets it as well.
26
+ - Undo after a change the editor did not record puts text back on the side of a block boundary it came from. Deleting a selection across two blocks and then undoing, once text had been added earlier in the first block without recording (by a collaborator, an AI answer or a new `blocks` array), put the first block's deleted tail into the second block. An undo step that split one place twice was dropped outright, and two deleted pieces came back in swapped order once the change had deleted the text between them. Carrying the history over the change lost which side of the writer's own edit a position was on wherever the change left nothing between the two, and now keeps it.
27
+ - Undo history is carried over a change of the block list, such as a block added or removed without recording or a new `blocks` array from the host, at about the cost of a text change. Such a change named no block, so every undo and redo step was rewritten, at a cost that grew with the square of the history's depth; now only the steps in the blocks it added, removed, edited or reordered are.
28
+ - The line end key on a wrapped line (End, or Command with the right arrow on a Mac) leaves the caret at the end of that line in the editor's selection too, with `assoc: -1`, as a click past the end of the line already did. The browser drew it there, but the selection read back said it was at the start of the next line, so the editor measured the next ↑ or ↓ from there where the browser did not make that move itself.
29
+ - A caret at the end of a wrapped line is drawn there again when a resize puts the wrap back. Once the caret had been written while no wrap fell where it was, as when focus left and came back at another width, it stayed at the start of the next line until it moved.
30
+ - A slash menu item with neither a `command` nor a `run` is left out of the menu, with a console warning in development. It was listed, and choosing it removed the slash and the query and did nothing else.
31
+ - A slash, mention or emoji item that throws when chosen is reported through `onError`, and the Enter or Tab that chose it stays the menu's. The error escaped the key handler before it claimed the key, so the key went on to the text and the error went uncaught.
32
+ - The bubble toolbar shows again once a press that made a selection ends without a pointerup: a native drag of the selected text, or the window losing focus mid-press. It also shows once a composition that wrote the selected text over again has settled; it stayed hidden until the selection changed.
33
+ - The link hover card is a group, not a dialog, since it never takes focus, and its Edit button names Mod+K, the key that opens the same popover from the text. The toolbar's Link button says it opens a dialog.
34
+ - A refused address in the link popover is said once. The message was both an alert and the field's description, so a screen reader could read it twice; it stays the field's description, and the canvas's live region says it once.
35
+ - A link with an empty address no longer shows an Open button in its hover card, which opened the current page in a new tab. The card and the popover now draw the same Open button.
36
+ - A multi-kilobyte value pasted into the link popover is checked in time that grows with its length. The email check tried every dot of the address against the rest of it, about a third of a second for 32 KB of dots.
37
+ - Tab past the last control of the link popover, which WebKit reaches with Option+Tab, gives focus back to the text, and so does Shift+Tab before its field. Focus fell to the page.
38
+ - A host turning the canvas read-only while focus is in the bubble toolbar, one of its menus, the mobile toolbar or the link popover gives focus back to the text. It fell to the page.
39
+ - Closing a canvas menu leaves focus on a menu button a host's own block draws, which the writer had pressed or tabbed to. Only the canvas's own menu buttons, out of the tab order, hand focus back to the text.
40
+ - An open slash, mention or emoji menu keeps its combobox attributes when React draws a new element for the line it is open on, as when the block changes type.
41
+ - The live region says how many blocks an insert of several put in, rather than one, and no longer counts the empty line a command adds for the caret. After the gutter's +, the slash menu's result count follows the insert announcement rather than replacing it, and a drag says where the block went once without leaning on the announcer's repeat window.
42
+ - Copy and Copy link to block say when the copy failed. They said "Copied" on success and nothing on failure.
43
+ - The gutter's + on an empty paragraph with blocks nested under it adds a line after them, rather than writing the slash into the parent.
44
+ - A right-click on the block handle while its menu is open opens the menu again at the pointer. It closed the menu and opened nothing.
45
+ - The block handle on a touch screen is named "Block options". It was "Drag to move, click for options", though a touch never drags it.
46
+ - The mobile toolbar marks Undo, Redo, Indent and Outdent unavailable while they would do nothing, and stays in place under a pinch zoom: it spans the visible part of the page at its own size, rather than starting at the page's left edge and growing with the zoom.
47
+ - An inline `labels={{ ... }}` holding the same words on each render no longer re-renders everything that reads the canvas configuration. The canvas resolves the labels again only when one of them changes; a label function written inline is still new each time.
48
+ - In `quoin-editor/headless`, a task's checkbox is named after its task's text, and by `Task` while that is empty, and a closed toggle's chevron says how many blocks it hides (`Toggle, 2 blocks hidden`) and points at them with `aria-controls`, as in the canvas. The headless renderers drew their own copy of the canvas markup, which had drifted: the checkbox was always named `Task` and the chevron always `Toggle`.
49
+ - In the canvas, a quote is drawn as a `blockquote`, as `QuoinView`, `toHTML` and the headless view draw it, so assistive technology reports it as a quotation. It was a plain `div`.
50
+ - The chevron of an open toggle, or of a closed one with nothing under it, is named by the `toggle` label. It was named by `toggleButton(0)`, so a host that renamed `toggle` alone still heard "Toggle" on the chevron. `toggleButton` is now asked only for the count a closed toggle hides.
51
+ - The colour menus draw the sample letter of each text colour from the labels. It was a literal `A` no label reached, so a Japanese, Chinese or Arabic canvas showed a Latin letter as the sample of how its text would look.
52
+ - A host's own definition of `IMAGE`, `FILE`, `CODE` or `DIVIDER` is named by its own `label`: in the block's accessible name, its Edit button, the block menu's heading, the slash menu and Turn into. Those read the name the labels give Quoin's block, so a host's "Photo" block was announced and listed as "Image".
53
+ - A callout whose icon is not the one of its tone keeps its tone in words. The icon menu offers any icon to any tone, so a warning given a pin was told by its colour alone until the pointer reached it and showed the tone chip, and on a read-only canvas, which draws no chip, for good. The chip now stays in view while the icon is not the tone's own, and a read-only callout writes the tone's name in the same place.
54
+ - A callout whose tone is none of the five is drawn as `info` by the core callout preview and shaded as `info` by `toDocx`, as the canvas and the view already drew it. The preview drew it with a neutral border and fill, and the Word export shaded it grey.
55
+ - The placeholder of an empty callout stays at 4.5:1 while the callout is selected. A selected block paints the accent under the callout's wash, which brought the muted placeholder to 4.39:1 on the dark warning wash and 4.48:1 on the light danger wash. The default `--quoin-muted-foreground` moves from `oklch(0.48 0.029 263)` to `oklch(0.47 0.029 263)` in light and from `oklch(0.72 0.02 262)` to `oklch(0.73 0.02 262)` in dark, so every other muted text gains a little contrast too. A host that sets the token keeps its own value.
56
+ - A custom block's editor that calls `onDone` after it has closed, as when an upload it started settles once the reader has moved on, no longer selects its block and takes focus back from wherever the reader went. A call from an editor that is no longer mounted does nothing.
57
+ - A press on a page element that stops `pointerdown` from propagating, and takes no focus, closes an open custom block editor like any press outside it. The press never reached the document, where the editor listened for it, so the editor stayed open.
58
+ - Shift+Tab back into a block selected as a whole leaves the page where it is. The element that holds such a selection is fixed at the top of the editor's viewport, and focus the browser gives it itself scrolled to it before anything could put the scroll back: about 9 px on WebKit, and to the top of the editor under a transformed ancestor, where fixed means the top of that ancestor. It now moves over the selected block as it takes focus, so the browser scrolls only to bring that block into view.
59
+ - Undo and redo in a code block bring the caret they leave into view. They put the caret after the text they change by script, which the browser does not scroll to, so a caret left past the right edge of a long line stayed hidden, as Enter, Tab and a typed key no longer left it.
60
+ - The name a selected block is announced by, its first line clipped at 80 characters, is clipped between graphemes. It counted code points, so an emoji family or a flag straddling character 80 was cut in half before the ellipsis, and a line of 80 flags was clipped although a reader sees 80 characters.
61
+ - Redoing an upload into an image block that was undone before its picture was measured brings the picture's size back with it. The size measured once the key had gone was dropped, so the redone image reserved no height until it was measured again; it is now written as soon as the block shows the key again.
62
+ - A file dropped or pasted on the canvas whose upload fails without a message of its own reaches `onError` as "Upload failed.", as an upload into an image or file block already did. It arrived as the rejection turned into a string, such as "undefined". An upload that resolves to an empty key is left out and reported the same way, where it went in as an image or file with nothing behind it.
63
+ - A file dropped right after a click goes in with the empty line after it, as any drop or paste does while the writer stays put. The editor reads a click's caret on the selectionchange that follows it, so a drop that came first saw the caret move during the upload, took it for the writer moving on, and left the empty line out.
64
+ - A file pasted while an empty image or file block is selected as a whole goes into that block, which says it is uploading and shows a failure as its own button does. It went in as a new block after the empty one and left that empty. Other files pasted with it go in after it.
65
+ - An empty image or file block, and an `UploadField` in a custom block, announce an upload through the canvas's live region and show the words as plain text. Each drew a live region of its own, so a document with ten empty images held ten beside the canvas's own. An `UploadField` under a provider with no canvas keeps its own region.
66
+ - `FilePickerButton` leaves focus on the page when the user presses blank space while focus waits to come back to it after a pick. It gave focus back to its input once a host enabled it again, and could not tell the browser dropping focus from the user pressing away.
67
+ - The alt text popover of an image never covers the image's toolbar. Both were anchored to the picture, so near the top of the window, where the toolbar flips below the picture, and near the bottom, where the popover flips above it, the popover covered the toolbar's buttons. It now goes beside the picture and the toolbar together.
68
+ - Where the block decorations of a state are computed in the decorations paint mode, as in a browser without the highlight API, every extension's decorations run once per state rather than twice.
69
+ - The uploads guide says that `toMarkdown`, the plain text a copy puts on the clipboard and the block menu's Copy write the address `resolveAssetUrl` returns in whatever scheme, while the blocks, `toHTML`, `QuoinView` and `QuoinDiff` drop one that is not a web, blob, data or relative address.
70
+ - A nested block dragged straight down below a closed toggle lands beside the toggle, where it stays drawn. A drop could nest one level deeper than the block above the drop line, and below a closed toggle that level is the toggle's hidden content, so the block disappeared into it and the move was announced with the position of the block drawn after it.
71
+ - Dragging a block nests it by the step the page's stylesheet indents a level by. The drag used a step of 1.5rem of its own, so a host that indents blocks by another measure saw the drop line between levels and a drop nest one level more or less than the line showed.
72
+ - The drop line follows the page as the wheel scrolls it during a drag. It was worked out again only when the pointer moved, so a drop after a scroll landed where the pointer had been rather than where it was.
73
+ - Dragging a block, or a selection across blocks, to the edge of the editor scrolls every container round it in turn: once the nearest one reaches its end, the one around it takes over, and the window last. Only the nearest container used to scroll, a page whose body scrolls in place of the window scrolled nothing, and an editor inside a shadow root scrolled the window rather than the container around its host.
74
+ - A tap on a divider drawn without a renderer of its own, as a headless `EditorView` draws it, selects it in Firefox. Firefox leaves no native selection after a touch on a block with no text, and the press that selects such a block was skipped for touch.
75
+ - On a right to left page a menu row is laid out from the right: the bar that marks the highlighted row, the room for a tick, the shortcut at the far end and the chevron of a submenu, which now points the way the submenu opens, were all placed as on a left to right page. The close button of a dialog moves to the top left with them.
76
+ - A canvas inside an element turned round with `dir="rtl"`, on a page that runs left to right, turns round what it draws over the page too. The slash menu, the formatting toolbar, the link and colour popovers, the dialogs, the editing tools on a phone, the find panel and the comments sheet are drawn into the page's body and were laid out left to right, with the arrows walking the toolbars the wrong way; only the block menus followed the canvas. The find panel now sits at the end of the top edge, so at the top left on such a page.
77
+ - A page that turns round while the canvas is on it, through a `dir` attribute, a class or an inline style on the canvas or an element around it, is followed at once. A gutter showing beside a block stayed on the old side until the pointer moved to another block, and the menus kept opening the old way round until the canvas was mounted again. A turn made by a stylesheet alone, such as a media query, still needs a remount.
78
+ - Copying a table's cell range copies those cells and nothing else. It put the whole table on the clipboard, so a cut followed by a paste brought back every cell the cut had left alone. Rich editors and spreadsheets now receive the cells as a table that keeps their columns' alignment and widths, and the header row only when it is one of them, and plain text receives them a row to a line, separated by tabs.
79
+ - Cells copied from a range, or a table copied out of a spreadsheet, paste into a table from the cell the caret is in or from the first cell of a range, adding the rows and columns they reach past, so a cut then a paste puts the cells back. A paste over a range wrote the plain text of anything pasted into its first cell, and a line of formatted text pasted over a range now keeps its formatting there.
80
+ - Text that reaches a cell range without a key replaces the range from its first cell, as a key typed over it does. Dictation, the emoji picker and an input method composing over the range emptied the cells between its two ends in reading order instead, and kept part of the first and last of them. Deleting across cells, through the engine or by a cut, now empties the rectangle of cells the selection spans, as it is painted.
81
+ - A table drops stored column widths that give its columns no usable width, a list of the wrong length or a width of zero or less, as it loads. It kept them, and wrote them back on every change until the first column resize, though it drew the table as if it had none.
82
+ - The table controls are drawn only into the table they belong to. For one render after the caret or the pointer moved to another table, that table's handles were drawn into the previous table and measured against it.
83
+ - An embed's cover lifts after a press that opens a context menu, as Control and the main button do in Chromium on a Mac. That press fires no click, so the block was selected while the cover stayed over its frame until the next click anywhere.
84
+ - A bookmark whose host unfurl threw or rejected asks again the next time it is drawn, as when undo brings it back or the canvas turns editable. A failure marked the address as asked for the rest of the session, so a bookmark placed while the host's service was down kept its bare address until the page was loaded again. An unfurl that answers with nothing is still never asked again.
85
+ - A double click on a formula in a line opens its editor once. The second click, when it reached the formula, opened the editor again after the press before it had closed it, as a mention chip already avoided.
86
+ - Pasted text that starts with a space right after an `@` opens no mention menu. Only a typed space closed it, so a paste kept the menu open and asked the host about the words after it.
87
+ - Escape on a menu that covered a farther trigger closes that one too. An `@` before a slash command came back with its menu once a space ended the dismissed slash command, holding everything typed since.
88
+ - `insertMention` and `insertAtom` refuse attributes that are not text, as code without types can pass, rather than throwing. A kind, an id or a label that was not a string made the check that the atom reads back throw.
89
+ - Typing `:` after an `@` mention query keeps the mention menu open until two characters follow the `:`. The emoji menu waited for those two characters, but a bare `:` already counted as the trigger nearest the caret, so `@Ada :` closed the mention menu and opened nothing in its place.
90
+ - Each grid of the callout icon picker is a tab stop of its own, and the arrows, Home and End stay inside it. The picker drew a grid for each group of emoji but walked them as one, with a single tab stop and arrows that ran from the last row of one group into the next, so a screen reader was told of nine grids that the keys treated as one. Tab now moves from the search field through the grids in turn, and each grid keeps its cell when Tab comes back to it.
91
+ - In regex mode, `^` and `$` in the find panel match at the start and the end of a line only. A line is searched in pieces cut at each formula or mention, and each piece was searched as if it were the whole line, so `^` matched just after a formula and `$` just before one.
92
+ - The find panel's message for a regular expression that cannot be read describes the query field, so a screen reader says why the field is invalid rather than only that it is.
93
+ - The find panel brings a match above the page into view below itself. It is fixed over the top of the page, and a match scrolled to the top edge sat under it.
94
+ - The find panel counts every match and steps to each one. It kept the first 1,000 and dropped the rest, so a match past them could not be reached, the count read "1,000+", and opening the panel with the caret past the thousandth match made the first match current rather than the nearest after the caret. It still marks only the 1,000 matches around the current one, so a search that finds far more costs no more to draw.
95
+ - Typography keeps turning quotes, dashes and arrows after a closed code span that holds a dollar, as in `` `$PATH` ``. The span's dollar was read as a formula opening with nothing to close it, which stopped every typographic replacement for the rest of the line.
96
+ - A spelling replacement, or any other edit the browser targets at a range, that the editor refuses leaves the selection where it was. A refused one, at a character limit for instance, left the replaced word selected, so the next key typed over it.
97
+ - Focus mode stays dimmed while the block menu or another menu the canvas opened has focus. Menus are drawn outside the canvas, so opening one took focus out of it and brought the whole page back to full strength until the menu closed.
98
+ - The character limit's documentation says that Backspace right after a typographic replacement, which undoes it, is let through past the limit as any undo is.
99
+ - A line that opens with a ballot box, as a Word checkbox, a TextEdit or Pages document and a typed checklist write it, is pasted as a task, ticked for a checked box, and a Google Docs checklist pastes as tasks rather than as empty list items with the text beside them. The box was kept as a character in a plain paragraph, and the picture Google Docs draws each box with split its item in two.
100
+ - A list item pasted from Word on the web keeps its depth. Word on the web writes every item as a list of its own and gives its level as an attribute, so a nested list arrived flat.
101
+ - A table with merged cells keeps every cell in its own column. A cell spanning two columns or two rows pushed the cells after it one column across, so a pasted table could put each value under the wrong heading. A merged cell now keeps its text in its first row and column and leaves the cells it covered empty, losing the merge and nothing else.
102
+ - Underline, strikethrough, colours and sizes that Word, LibreOffice, TextEdit and Pages set on a class in the style sheet of the clipboard HTML are read, rather than only those written on the element itself. TextEdit and Pages write all of them that way, so a paste from them lost every underline, strikethrough and colour.
103
+ - Text in a monospaced typeface pastes as inline code, so code set in Consolas, Menlo, Courier or a mono typeface in Word, Google Docs, TextEdit or Pages is no longer pasted as plain text. The typeface of the page a selection was copied from is not read, so prose copied from a page set in a monospaced font stays prose.
104
+ - A highlight from Word or LibreOffice is kept, and so is a colour Word names rather than writes as a number, such as `red`. Both were read only from `background-color` and numeric colours, while Word writes `background:yellow` and LibreOffice a `font` element's `color`.
105
+ - A non-breaking space at the end of a pasted paragraph or table cell is dropped, as an ordinary space already was. Word on the web ends every paragraph with one, so each pasted line carried an invisible trailing space.
106
+ - An AI answer on a right to left page is drawn under the block it is for. It sat 48 px to the right of its block, over the gutter, and inside a block that scrolls sideways it was cut off against the wrong edge: the layer was anchored at the inline start of the canvas, the right on such a page, and then moved by a distance measured from the left.
107
+ - In Safari, the down arrow leaves a paragraph of English words that ends in a full stop or an emoji on a right to left page. WebKit gives the caret no position after such a last character, so the canvas could not tell the caret was on the last line, handed the key to the browser, which had nowhere to go, and the caret stayed where it was.
108
+ - A table's pointer controls work on a right to left page, where its first column is on the right. They measured the columns as if they ran from the left: the + between two columns inserted at the mirrored edge, and none was offered after the last column; the rows offered their + only near the left border, and drew it and their handles there; and each column's border was drawn on its right, the side the column starts from, where dragging it away from the column narrowed it. The handles and the + of the rows now sit outside the right border, each column's border sits on its left and widens the column as it is dragged away, and the up arrow from the block below a table lands in the column under the caret rather than in the first one. The arrows on a column's border still move it the way the key points, so on such a page the left arrow widens the column. The controls are drawn again when the page turns round under them.
109
+
5
110
  ## 2.0.5 (2026-11-11)
6
111
 
7
112
  ### Changed
package/MIGRATION.md CHANGED
@@ -238,7 +238,7 @@ The tarball ships ESM and CommonJS builds of every entry, with their sourcemaps.
238
238
 
239
239
  ## Migrating from 1.x to 2.0
240
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.
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://quoincloud.com/docs/editor/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
242
 
243
243
  ```bash
244
244
  npm install quoin-editor@^2
package/README.md CHANGED
@@ -6,7 +6,7 @@ Quoin is a rich markdown editor and block editor for React. Every text block sta
6
6
 
7
7
  [![npm](https://img.shields.io/npm/v/quoin-editor)](https://www.npmjs.com/package/quoin-editor) [![CI](https://github.com/AmineSeboui/quoin-editor/actions/workflows/ci.yml/badge.svg)](https://github.com/AmineSeboui/quoin-editor/actions/workflows/ci.yml) [![licence](https://img.shields.io/npm/l/quoin-editor)](LICENSE)
8
8
 
9
- [Live demo](https://amineseboui.github.io/quoin-editor/) | [Docs](https://quoincloud.com/docs/editor/getting-started) | [Changelog](CHANGELOG.md)
9
+ [Home](https://quoincloud.com) | [Docs](https://quoincloud.com/docs/editor/getting-started) | [Live demo](https://amineseboui.github.io/quoin-editor/playground/) | [Changelog](CHANGELOG.md)
10
10
 
11
11
  ![A heading typed with a hash and a space, a bulleted list, two words made bold from the toolbar, a callout inserted from the slash menu and dragged into place, while the panel beside shows the markdown onChange hands back](demo/public/quoin-demo.gif)
12
12
 
@@ -76,14 +76,14 @@ 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 2.0 | Tiptap 3 | BlockNote | Editor.js | Lexical | Plate |
79
+ | Feature | Quoin 2.1 | 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] |
83
83
  | Notion-style UI | Included, MIT | [MIT menus and drag handle][tt-review]; [Notion-like template on a paid plan][tt-notion] | [Included][bn-home] | [Toolbox and inline toolbar][ej-repo] | [None; you draw it][lx-intro] | [MIT components][pl-repo]; [Notion-like set on a paid licence][pl-pro] |
84
84
  | Licence | MIT | [MIT core, paid Pro extensions][tt-repo] | [MPL-2.0, `xl-*` packages GPL-3.0 or commercial][bn-repo] | [Apache-2.0][ej-repo] | [MIT][lx-repo] | [MIT, with paid Plus components][pl-pro] |
85
85
  | Real-time collaboration | Yjs, opt-in entry | [Yjs][tt-collab] | [Yjs][bn-collab] | [On its roadmap, in progress][ej-repo] | [Yjs][lx-collab] | [Yjs][pl-yjs] |
86
- | Comments | Opt-in, your store | [Paid plan][tt-comments] | [Built in, on Yjs or a thread store][bn-comments] | [Community plugin][ej-awesome] | [None shipped; a playground example][lx-repo] | [`@platejs/comment`, as text marks][pl-comment] |
86
+ | Comments | Opt-in, on your store or on Yjs | [Paid plan][tt-comments] | [Built in, on Yjs or a thread store][bn-comments] | [Community plugin][ej-awesome] | [None shipped; a playground example][lx-repo] | [`@platejs/comment`, as text marks][pl-comment] |
87
87
  | Localization | Seven packs, every string | [None shipped; the UI is yours][tt-ui] | [24 locales built in][bn-i18n] | [An i18n API; you bring the words][ej-i18n] | [None shipped; it draws no UI][lx-repo] | [None shipped; the UI is yours][pl-docs] |
88
88
  | Framework | React | [React, Vue, Svelte, plain JavaScript and more][tt-install] | [React, or vanilla JS with your own UI][bn-vanilla] | [Plain JavaScript][ej-start] | [Any, with React bindings][lx-repo] | [React][pl-repo] |
89
89
 
@@ -125,6 +125,7 @@ Sources, checked on 2026-11-05: Tiptap [persistence][tt-store], [markdown][tt-md
125
125
  ## Learn more
126
126
 
127
127
  - [The controlled contract](https://quoincloud.com/docs/editor/contract): loading, autosave, discarding, undoing a deletion
128
+ - [Adopting existing documents](https://quoincloud.com/docs/editor/adoption): what markdown files and HTML lose on the way in, ids that last, the way back out
128
129
  - [Theming](https://quoincloud.com/docs/editor/theming): `--quoin-` tokens, dark mode, cascade layers, Tailwind
129
130
  - [Custom blocks](https://quoincloud.com/docs/editor/custom-blocks) and [uploads](https://quoincloud.com/docs/editor/uploads)
130
131
  - [Extensions](https://quoincloud.com/docs/editor/extensions): every opt-in feature, and writing your own