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
@@ -1,9 +1,10 @@
1
- import { useQuoinEditor, EditorView, useIsomorphicLayoutEffect, inlineDirection, useReadOnly, useEditorState, listen, wholeState, NODE_ENTRY, useBlockDomId, useToggleOpen, ToggleChevron, EditableRegion, calloutTone, drawnBlockCount, isComposingKey, NATIVE_INTERACTION, CALLOUT_TONES, useRegionDomId, TaskCheckbox, useOrderedNumber, createAutoScroll, arrowAsWritten, domMapOf, dispatchSelection, regionInline, onBlock, hostKey, BlockErrorContext, textDiff, moveToNeighbour, visibleBlockIndexes } from './chunk-2BPXVQBP.js';
1
+ import { englishRendererLabels, useQuoinEditor, EditorView, useIsomorphicLayoutEffect, inlineDirection, useReadOnly, ToggleBlock, useEditorState, listen, wholeState, NODE_ENTRY, EditableRegion, MarkdownText, drawnBlockCount, isComposingKey, NATIVE_INTERACTION, createAutoScroll, arrowAsWritten, domMapOf, dispatchSelection, regionInline, onBlock, hostKey, BlockErrorContext, textDiff, moveToNeighbour, visibleBlockIndexes } from './chunk-MO4UMXMT.js';
2
2
  import { mergeLabels } from './chunk-DLQCTGC3.js';
3
3
  import { defineBlock, devWarn, highlightedLines, blockByType, paletteBlocks, tokenColour, seedData, parsedData } from './chunk-HDJ653LM.js';
4
- import { createPluginKey, coreExtensions, defineExtension, duplicateBlocks, primaryRegion, regionLength, headBlockId, sameSelection, Transaction, insertBlocksStep, startOf, replaceBlockStep, descendantsEnd, LINE_JOIN, detectMac, selectedBlockIds, blockRangeSelection, runCommands, setSelection, selectedBlockRange, toMarkdown, isCollapsed, createBlock, isParagraph, nodeSelection, setAttrsStep, LINE_SPLIT, validateSelection, endOf, insertBlockAfter, insertText, isTextFlow, markRanges, setTextAlign, orderedRange, unsetLink, textSelection, toggleMark, isActive, setBlockType, getPluginState, rangeContent, unsetMark, setMark, markExtent, setLink, deleteBlocks, setAttrs, HISTORY_GROUP, setBlockColor, removeMarkStep, caretMarks, replaceInlineStep, keyName, normalizeKey, movedBlockIds, moveBlocksUp, moveBlocksDown, selectAll, redo, undo, moveBlocksTo, indent, outdent } from './chunk-5RBMZGKV.js';
5
- import { str, blockPlainText, sanitizeHref, inlineText, parseInline, newId, createDoc, sameItems, headingLevel, sanitizeAssetUrl, blockById, sliceInline, specFor, createBlockCodec, hasMark, alignOrUndefined, optionalNumber, requireBlockIndex, SCHEME, inlineLength, paletteOrUndefined, PALETTE, addMark, text, sameValue } from './chunk-7Y4B7NHO.js';
6
- import { DirectionProvider } from '@radix-ui/react-direction';
4
+ import { calloutTone, CALLOUT_TONES } from './chunk-PIWMU4BV.js';
5
+ import { createPluginKey, coreExtensions, defineExtension, duplicateBlocks, primaryRegion, regionLength, headBlockId, sameSelection, Transaction, insertBlocksStep, startOf, replaceBlockStep, descendantsEnd, PASTED_HTML, LINE_JOIN, detectMac, selectedBlockIds, blockRangeSelection, runCommands, setSelection, selectedBlockRange, toMarkdown, graphemePrefix, isCollapsed, createBlock, isParagraph, nodeSelection, validateSelection, endOf, insertText, isTextFlow, markRanges, setTextAlign, orderedRange, unsetLink, textSelection, setAttrsStep, LINE_SPLIT, toggleMark, isActive, setBlockType, getPluginState, rangeContent, unsetMark, setMark, markExtent, setLink, deleteBlocks, setAttrs, HISTORY_GROUP, setBlockColor, removeMarkStep, caretMarks, replaceInlineStep, keyName, normalizeKey, movedBlockIds, isClosedToggle, selectAll, redo, undo, moveBlocksUp, moveBlocksDown, moveBlocksTo, indent, outdent } from './chunk-TL2MJOWM.js';
6
+ import { str, blockPlainText, sanitizeHref, inlineText, parseInline, newId, createDoc, sameItems, headingLevel, sanitizeAssetUrl, blockById, sliceInline, specFor, createBlockCodec, hasMark, alignOrUndefined, optionalNumber, requireBlockIndex, SCHEME, inlineLength, paletteOrUndefined, PALETTE, addMark, text, sameValue } from './chunk-I34BFOLL.js';
7
+ import { DirectionProvider, useDirection } from '@radix-ui/react-direction';
7
8
  import * as React7 from 'react';
8
9
  import { createContext, forwardRef, useRef, useImperativeHandle, useState, useMemo, useEffect, Fragment as Fragment$1, useContext, useSyncExternalStore, useCallback, useId, createElement } from 'react';
9
10
  import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
@@ -447,21 +448,9 @@ function frozen(labels) {
447
448
  var englishLabels = frozen({
448
449
  document: "Editor",
449
450
  editorHint: "Press Escape, then Tab, to leave the editor.",
450
- paragraph: "Paragraph",
451
- heading: (level) => `Heading ${level}`,
452
- bulletItem: "Bullet list item",
453
- numberedItem: (n) => `Numbered list item ${n}`,
454
- task: "Task",
455
- quote: "Quote",
456
- callout: (tone) => `Callout, ${tone}`,
457
- calloutTones: { info: "info", tip: "tip", success: "success", warning: "warning", danger: "danger" },
451
+ ...englishRendererLabels,
458
452
  calloutIcon: "Callout icon",
459
453
  calloutToneMenu: (tone) => `Callout tone: ${tone}`,
460
- toggle: "Toggle",
461
- toggleButton: (hidden) => {
462
- if (hidden === 0) return "Toggle";
463
- return `Toggle, ${hidden} ${hidden === 1 ? "block" : "blocks"} hidden`;
464
- },
465
454
  placeholderEmpty: "Type '/' for commands",
466
455
  placeholderHeading: (level) => `Heading ${level}`,
467
456
  placeholderList: "List",
@@ -484,18 +473,21 @@ var englishLabels = frozen({
484
473
  color: "Color",
485
474
  textColor: "Text color",
486
475
  backgroundColor: "Background color",
476
+ textColorSample: "A",
487
477
  tone: "Tone",
488
478
  icon: "Icon",
489
479
  duplicate: "Duplicate",
490
480
  copy: "Copy",
491
481
  copyLink: "Copy link to block",
492
482
  copied: "Copied",
483
+ copyFailed: "Could not copy",
493
484
  moveUp: "Move up",
494
485
  moveDown: "Move down",
495
486
  delete: "Delete",
496
487
  askAi: "Ask AI",
497
488
  addBelow: "Add block below",
498
489
  dragHandle: "Drag to move, click for options",
490
+ blockOptions: "Block options",
499
491
  slashMenu: "Commands",
500
492
  noResults: "No results",
501
493
  resultCount: (n) => `${n} ${n === 1 ? "result" : "results"}`,
@@ -559,6 +551,7 @@ var englishLabels = frozen({
559
551
  },
560
552
  blockMoved: (position, total) => `Block moved to position ${position} of ${total}`,
561
553
  blockInserted: (position, total) => `Block inserted at position ${position} of ${total}`,
554
+ blocksInserted: (n, position, total) => `${n} blocks inserted at position ${position} of ${total}`,
562
555
  blockDeleted: (n) => n === 1 ? "Block deleted" : `${n} blocks deleted`,
563
556
  blocksSelected: (n) => `${n} ${n === 1 ? "block" : "blocks"} selected`,
564
557
  undoHint: (shortcut) => `Press ${shortcut} to undo`,
@@ -751,9 +744,8 @@ function CalloutPreview({ data }) {
751
744
  const labels = useLabels();
752
745
  const body = str(data.body);
753
746
  if (body.trim() === "") return /* @__PURE__ */ jsx("p", { className: HINT, children: labels.emptyCallout });
754
- const tone = str(data.tone) || "info";
755
- const paint = TONE[tone] ?? "border-border bg-muted/50";
756
- return /* @__PURE__ */ jsx("div", { "data-tone": tone, className: `rounded-md border-l-4 ${paint} p-3 text-sm`, children: body });
747
+ const tone = calloutTone(data.tone);
748
+ return /* @__PURE__ */ jsx("div", { "data-tone": tone, className: `rounded-md border-l-4 ${TONE[tone]} p-3 text-sm`, children: body });
757
749
  }
758
750
 
759
751
  // src/blocks/core.ts
@@ -873,7 +865,8 @@ function createVoidEditing() {
873
865
  // src/blocks/text/return-focus-to-text.ts
874
866
  function focusBelongsInText(controller, focused) {
875
867
  if (!focused || focused === focused.ownerDocument.body) return true;
876
- return focused.getAttribute("aria-haspopup") === "menu" && controller.root?.contains(focused) === true;
868
+ const canvasMenuButton = focused.getAttribute("aria-haspopup") === "menu" && focused.tabIndex < 0;
869
+ return canvasMenuButton && controller.root?.contains(focused) === true;
877
870
  }
878
871
  function returnFocusToText(controller) {
879
872
  return (event) => {
@@ -949,7 +942,10 @@ var FOCUS_RING = [
949
942
  "focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-solid",
950
943
  "focus-visible:outline-[var(--quoin-ring)]"
951
944
  ].join(" ");
952
- var HIGHLIGHT_BAR = "data-[highlighted]:shadow-[inset_2px_0_0_var(--quoin-ring)]";
945
+ var HIGHLIGHT_BAR = [
946
+ "data-[highlighted]:shadow-[inset_2px_0_0_var(--quoin-ring)]",
947
+ "rtl:data-[highlighted]:shadow-[inset_-2px_0_0_var(--quoin-ring)]"
948
+ ].join(" ");
953
949
 
954
950
  // src/ui/touch-target.ts
955
951
  var TAP_TARGET_FLOOR = "pointer-coarse:min-h-11";
@@ -965,24 +961,24 @@ var FLOATING_TOOLBAR = "flex w-auto items-center gap-0.5 p-1 shadow-md";
965
961
  var MENU_CONTENT = "quoin bg-popover text-popover-foreground z-50 max-h-(--radix-popper-available-height) min-w-[8rem] overflow-x-hidden overflow-y-auto rounded-lg border border-border p-1 shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0";
966
962
  var MENU_SUB_CONTENT = "quoin bg-popover text-popover-foreground z-50 max-h-(--radix-popper-available-height) min-w-[8rem] overflow-x-hidden overflow-y-auto rounded-lg border border-border p-1 shadow-lg";
967
963
  var MENU_ITEM = [
968
- "focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm focus:outline-hidden select-none data-[inset]:pl-8 data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg:not([class*='size-'])]:size-4",
964
+ "focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm focus:outline-hidden select-none data-[inset]:ps-8 data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg:not([class*='size-'])]:size-4",
969
965
  HIGHLIGHT_BAR,
970
966
  TAP_TARGET_FLOOR
971
967
  ].join(" ");
972
968
  var MENU_CHECKABLE_ITEM = [
973
- "focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm focus:outline-hidden select-none",
969
+ "focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pe-2 ps-8 text-sm focus:outline-hidden select-none",
974
970
  HIGHLIGHT_BAR,
975
971
  TAP_TARGET_FLOOR
976
972
  ].join(" ");
977
- var MENU_ITEM_INDICATOR = "pointer-events-none absolute left-2 flex size-3.5 items-center justify-center";
973
+ var MENU_ITEM_INDICATOR = "pointer-events-none absolute start-2 flex size-3.5 items-center justify-center";
978
974
  var MENU_SUB_TRIGGER = [
979
- "focus:bg-accent data-[state=open]:bg-accent flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm focus:outline-hidden select-none data-[inset]:pl-8",
975
+ "focus:bg-accent data-[state=open]:bg-accent flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm focus:outline-hidden select-none data-[inset]:ps-8",
980
976
  HIGHLIGHT_BAR,
981
977
  TAP_TARGET_FLOOR
982
978
  ].join(" ");
983
- var MENU_LABEL = "px-2 py-1.5 text-sm font-medium data-[inset]:pl-8";
979
+ var MENU_LABEL = "px-2 py-1.5 text-sm font-medium data-[inset]:ps-8";
984
980
  var MENU_SEPARATOR = "bg-border -mx-1 my-1 h-px";
985
- var MENU_SHORTCUT = "text-muted-foreground ml-auto text-xs tracking-widest";
981
+ var MENU_SHORTCUT = "text-muted-foreground ms-auto text-xs tracking-widest";
986
982
  function ContextMenu({ modal = false, ...props }) {
987
983
  return /* @__PURE__ */ jsx(ContextMenuPrimitive.Root, { modal, ...props });
988
984
  }
@@ -1073,7 +1069,7 @@ var ContextMenuSubTrigger = /* @__PURE__ */ React7.forwardRef(function ContextMe
1073
1069
  ...props,
1074
1070
  children: [
1075
1071
  children,
1076
- /* @__PURE__ */ jsx(ChevronRight, { className: "ml-auto size-4" })
1072
+ /* @__PURE__ */ jsx(ChevronRight, { className: "ms-auto size-4 rtl:rotate-180" })
1077
1073
  ]
1078
1074
  }
1079
1075
  );
@@ -1090,74 +1086,14 @@ var ContextMenuSubContent = /* @__PURE__ */ React7.forwardRef(function ContextMe
1090
1086
  );
1091
1087
  });
1092
1088
 
1093
- // src/gutter/copy-text.ts
1094
- function copyThroughTextArea(text2, doc) {
1095
- const previous = doc.activeElement instanceof HTMLElement ? doc.activeElement : null;
1096
- const area = doc.createElement("textarea");
1097
- area.value = text2;
1098
- area.readOnly = true;
1099
- area.setAttribute("aria-hidden", "true");
1100
- area.style.cssText = "position:fixed;top:0;left:0;width:1px;height:1px;opacity:0;pointer-events:none";
1101
- const beside = previous !== null && previous !== doc.body ? previous.parentElement : null;
1102
- (beside ?? doc.body).append(area);
1103
- area.focus({ preventScroll: true });
1104
- area.select();
1105
- try {
1106
- return doc.execCommand("copy");
1107
- } catch {
1108
- return false;
1109
- } finally {
1110
- area.remove();
1111
- previous?.focus({ preventScroll: true });
1112
- }
1113
- }
1114
- async function copyText(text2, doc = document) {
1115
- const clipboard = doc.defaultView?.navigator.clipboard;
1116
- if (clipboard === void 0) return copyThroughTextArea(text2, doc);
1117
- try {
1118
- await clipboard.writeText(text2);
1119
- return true;
1120
- } catch {
1121
- return copyThroughTextArea(text2, doc);
1122
- }
1123
- }
1124
-
1125
- // src/gutter/run-on-blocks.ts
1126
- function settle(tr, before) {
1127
- const { doc } = tr;
1128
- const { schema } = tr.before;
1129
- const result = tr.selection;
1130
- if (result?.kind !== "blocks" || before?.kind === "blocks") return result;
1131
- const ids = selectedBlockIds(doc, result);
1132
- const head = headBlockId(before);
1133
- if (head !== null && ids.includes(head) && validateSelection(doc, schema, before) === before) return before;
1134
- return endOf(doc, schema, ids[0]);
1135
- }
1136
- function runOnBlocks(controller, blockIds, command) {
1137
- const { state } = controller.editor;
1138
- const ids = blockIds.filter((id) => state.doc.index.has(id));
1139
- if (ids.length === 0) return false;
1140
- const before = state.selection;
1141
- const exact = sameItems(selectedBlockIds(state.doc, before), ids);
1142
- const ran = exact ? before : blockRangeSelection(ids[0], ids[ids.length - 1]);
1143
- const { ok, tr } = runCommands(state, [setSelection(ran), command]);
1144
- const result = ok && tr !== null ? tr : new Transaction(state).setSelection(ran);
1145
- const settled = exact ? result.selection : settle(result, before);
1146
- if (!sameSelection(settled, result.selection)) result.setSelection(settled);
1147
- if (result.docChanged || !sameSelection(result.selection, before)) controller.dispatch(result);
1148
- return ok;
1149
- }
1150
- var noChange = () => true;
1151
-
1152
- // src/gutter/block-menu-items.ts
1153
- var rangeOf = (blockIds) => blockRangeSelection(blockIds[0], blockIds[blockIds.length - 1]);
1089
+ // src/canvas/block-targets.ts
1154
1090
  function selectedWith(state, blockId) {
1155
1091
  const { doc, selection } = state;
1156
1092
  const selected = selectedBlockIds(doc, selection);
1157
1093
  const ofBlocks = selection?.kind === "blocks" || selection?.kind === "text" && selected.length > 1;
1158
1094
  return ofBlocks && selected.includes(blockId) ? selected : null;
1159
1095
  }
1160
- function menuTargets(state, blockId) {
1096
+ function handleTargets(state, blockId) {
1161
1097
  const selected = selectedWith(state, blockId);
1162
1098
  if (selected !== null) return selected;
1163
1099
  const { doc } = state;
@@ -1167,73 +1103,10 @@ function menuTargets(state, blockId) {
1167
1103
  function styleTargets(state, blockId) {
1168
1104
  return selectedWith(state, blockId) ?? [blockId];
1169
1105
  }
1170
- function menuActions(core, extra = []) {
1171
- return [...core.filter((action) => !action.destructive), ...extra, ...core.filter((action) => action.destructive)];
1172
- }
1173
- function markdownOf(editor, blockIds, resolveAssetUrl) {
1174
- const { doc } = editor.state;
1175
- const range = selectedBlockRange(doc, rangeOf(blockIds));
1176
- if (range === null) return "";
1177
- const blocks = editor.codec.blocksFromDoc(createDoc(doc.blocks.slice(range.start, range.end)));
1178
- return toMarkdown(blocks, { schema: editor.schema, resolveAssetUrl, comments: false });
1179
- }
1180
- var applies = (command) => (state, blockIds) => blockIds.every((id) => state.doc.index.has(id)) && runCommands(state, [setSelection(rangeOf(blockIds)), command]).ok;
1181
- function coreBlockMenuActions(labels, options) {
1182
- const copy = (controller, blockIds, text2) => {
1183
- void copyText(text2).then((copied) => {
1184
- if (copied) options.onCopied?.();
1185
- });
1186
- runOnBlocks(controller, blockIds, noChange);
1187
- };
1188
- return [
1189
- {
1190
- id: "duplicate",
1191
- label: labels.duplicate,
1192
- icon: CopyPlus,
1193
- shortcut: "Mod-d",
1194
- run: (controller, blockIds) => runOnBlocks(controller, blockIds, duplicateBlocks())
1195
- },
1196
- {
1197
- id: "copy",
1198
- label: labels.copy,
1199
- icon: Copy,
1200
- run: (controller, blockIds) => copy(controller, blockIds, markdownOf(controller.editor, blockIds, options.resolveAssetUrl))
1201
- },
1202
- {
1203
- id: "copy-link",
1204
- label: labels.copyLink,
1205
- icon: Link,
1206
- run: (controller, blockIds) => copy(controller, blockIds, options.blockLink(blockIds[0]))
1207
- },
1208
- {
1209
- id: "move-up",
1210
- label: labels.moveUp,
1211
- icon: ArrowUp,
1212
- shortcut: "Mod-Shift-ArrowUp",
1213
- run: (controller, blockIds) => runOnBlocks(controller, blockIds, moveBlocksUp),
1214
- available: applies(moveBlocksUp)
1215
- },
1216
- {
1217
- id: "move-down",
1218
- label: labels.moveDown,
1219
- icon: ArrowDown,
1220
- shortcut: "Mod-Shift-ArrowDown",
1221
- run: (controller, blockIds) => runOnBlocks(controller, blockIds, moveBlocksDown),
1222
- available: applies(moveBlocksDown)
1223
- },
1224
- {
1225
- id: "delete",
1226
- label: labels.delete,
1227
- icon: Trash,
1228
- destructive: true,
1229
- run: (controller, blockIds) => runOnBlocks(controller, blockIds, deleteBlocks)
1230
- }
1231
- ];
1232
- }
1233
1106
 
1234
1107
  // src/gutter/block-menu-state.ts
1235
1108
  var targetsOf = (state, blockId) => ({
1236
- blockIds: menuTargets(state, blockId),
1109
+ blockIds: handleTargets(state, blockId),
1237
1110
  styleIds: styleTargets(state, blockId)
1238
1111
  });
1239
1112
  function openBlockMenu(store, state, blockId) {
@@ -1258,9 +1131,39 @@ var TONE_ICONS = {
1258
1131
  function calloutIcon(attrs, tone) {
1259
1132
  return typeof attrs.icon === "string" && attrs.icon !== "" ? attrs.icon : TONE_ICONS[tone];
1260
1133
  }
1134
+ function iconTellsTone(attrs, tone) {
1135
+ return calloutIcon(attrs, tone) === TONE_ICONS[tone];
1136
+ }
1261
1137
  function MenuIcon({ icon: Icon2 }) {
1262
1138
  return /* @__PURE__ */ jsx("span", { "aria-hidden": "true", className: "flex shrink-0", children: /* @__PURE__ */ jsx(Icon2, { className: "size-4" }) });
1263
1139
  }
1140
+
1141
+ // src/gutter/run-on-blocks.ts
1142
+ function settle(tr, before) {
1143
+ const { doc } = tr;
1144
+ const { schema } = tr.before;
1145
+ const result = tr.selection;
1146
+ if (result?.kind !== "blocks" || before?.kind === "blocks") return result;
1147
+ const ids = selectedBlockIds(doc, result);
1148
+ const head = headBlockId(before);
1149
+ if (head !== null && ids.includes(head) && validateSelection(doc, schema, before) === before) return before;
1150
+ return endOf(doc, schema, ids[0]);
1151
+ }
1152
+ function runOnBlocks(controller, blockIds, command) {
1153
+ const { state } = controller.editor;
1154
+ const ids = blockIds.filter((id) => state.doc.index.has(id));
1155
+ if (ids.length === 0) return false;
1156
+ const before = state.selection;
1157
+ const exact = sameItems(selectedBlockIds(state.doc, before), ids);
1158
+ const ran = exact ? before : blockRangeSelection(ids[0], ids[ids.length - 1]);
1159
+ const { ok, tr } = runCommands(state, [setSelection(ran), command]);
1160
+ const result = ok && tr !== null ? tr : new Transaction(state).setSelection(ran);
1161
+ const settled = exact ? result.selection : settle(result, before);
1162
+ if (!sameSelection(settled, result.selection)) result.setSelection(settled);
1163
+ if (result.docChanged || !sameSelection(result.selection, before)) controller.dispatch(result);
1164
+ return ok;
1165
+ }
1166
+ var noChange = () => true;
1264
1167
  var sharedTone = (blocks) => {
1265
1168
  const tones = new Set(blocks.map((block) => calloutTone(block.attrs.tone)));
1266
1169
  return tones.size === 1 ? [...tones][0] : "";
@@ -1292,7 +1195,7 @@ function sharedColor(blocks, key) {
1292
1195
  return colors.size === 1 ? [...colors][0] : "";
1293
1196
  }
1294
1197
  var swatches = {
1295
- color: (name) => /* @__PURE__ */ jsx("span", { "aria-hidden": "true", "data-color": name, className: "w-4 text-center font-medium", children: "A" }),
1198
+ color: (name, labels) => /* @__PURE__ */ jsx("span", { "aria-hidden": "true", "data-color": name, className: "w-4 text-center font-medium", children: labels.textColorSample ?? englishLabels.textColorSample }),
1296
1199
  background: (name) => /* @__PURE__ */ jsx("span", { "aria-hidden": "true", "data-bg": name, className: "size-4 rounded-sm border border-border" })
1297
1200
  };
1298
1201
  function ColorGroups({ kit, value, onPick }) {
@@ -1302,7 +1205,7 @@ function ColorGroups({ kit, value, onPick }) {
1302
1205
  const group = (key, title, titleId) => /* @__PURE__ */ jsxs(Fragment, { children: [
1303
1206
  /* @__PURE__ */ jsx(kit.Label, { id: titleId, children: title }),
1304
1207
  /* @__PURE__ */ jsx(kit.RadioGroup, { "aria-labelledby": titleId, value: value[key], onValueChange: (name) => onPick(key, name), children: PALETTE.map((name) => /* @__PURE__ */ jsxs(kit.RadioItem, { value: name, indicator: /* @__PURE__ */ jsx(Check, { className: "size-4" }), children: [
1305
- swatches[key](name),
1208
+ swatches[key](name, labels),
1306
1209
  labels.colors[name]
1307
1210
  ] }, name)) })
1308
1211
  ] });
@@ -1623,13 +1526,16 @@ function selectAndFocus({ controller }, command) {
1623
1526
  controller.focus();
1624
1527
  return true;
1625
1528
  }
1626
- function travel({ controller, blockId, area }, command) {
1529
+ function travel({ controller, blockId, area, reveal }, command) {
1627
1530
  const before = area.value;
1628
1531
  controller.run(command);
1629
1532
  const { selection } = controller.editor.state;
1630
1533
  const edit2 = textDiff(before, area.value);
1631
1534
  if (selection?.kind !== "node" || selection.block !== blockId) controller.focus();
1632
- else if (edit2 !== null) area.setSelectionRange(edit2.from + edit2.insert.length, edit2.from + edit2.insert.length);
1535
+ else if (edit2 !== null) {
1536
+ area.setSelectionRange(edit2.from + edit2.insert.length, edit2.from + edit2.insert.length);
1537
+ reveal();
1538
+ }
1633
1539
  return true;
1634
1540
  }
1635
1541
  function runCodeAction(action, context) {
@@ -1714,7 +1620,7 @@ function CodeEditor({ block, controller, field, focusControls }) {
1714
1620
  const area = event.currentTarget;
1715
1621
  if (isComposingKey(controller, event.nativeEvent)) return;
1716
1622
  const action = codeKeyAction(event, fieldOf(area), controller.isMac);
1717
- if (action !== null && runCodeAction(action, { controller, blockId, area, edit: edit2, focusControls })) event.preventDefault();
1623
+ if (action !== null && runCodeAction(action, { controller, blockId, area, edit: edit2, reveal, focusControls })) event.preventDefault();
1718
1624
  };
1719
1625
  return /* @__PURE__ */ jsx("div", { ...{ [NATIVE_INTERACTION]: "" }, dir: "ltr", className: "quoin-code-scroll", children: /* @__PURE__ */ jsxs("div", { className: "quoin-code-lines", children: [
1720
1626
  /* @__PURE__ */ jsx(
@@ -1752,6 +1658,38 @@ function CodeEditor({ block, controller, field, focusControls }) {
1752
1658
  )
1753
1659
  ] }) });
1754
1660
  }
1661
+
1662
+ // src/gutter/copy-text.ts
1663
+ function copyThroughTextArea(text2, doc) {
1664
+ const previous = doc.activeElement instanceof HTMLElement ? doc.activeElement : null;
1665
+ const area = doc.createElement("textarea");
1666
+ area.value = text2;
1667
+ area.readOnly = true;
1668
+ area.setAttribute("aria-hidden", "true");
1669
+ area.style.cssText = "position:fixed;top:0;left:0;width:1px;height:1px;opacity:0;pointer-events:none";
1670
+ const beside = previous !== null && previous !== doc.body ? previous.parentElement : null;
1671
+ (beside ?? doc.body).append(area);
1672
+ area.focus({ preventScroll: true });
1673
+ area.select();
1674
+ try {
1675
+ return doc.execCommand("copy");
1676
+ } catch {
1677
+ return false;
1678
+ } finally {
1679
+ area.remove();
1680
+ previous?.focus({ preventScroll: true });
1681
+ }
1682
+ }
1683
+ async function copyText(text2, doc = document) {
1684
+ const clipboard = doc.defaultView?.navigator.clipboard;
1685
+ if (clipboard === void 0) return copyThroughTextArea(text2, doc);
1686
+ try {
1687
+ await clipboard.writeText(text2);
1688
+ return true;
1689
+ } catch {
1690
+ return copyThroughTextArea(text2, doc);
1691
+ }
1692
+ }
1755
1693
  var BASE = [
1756
1694
  "inline-flex cursor-pointer items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-[color,background-color,border-color,box-shadow,opacity] disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 shrink-0",
1757
1695
  FOCUS_RING
@@ -1848,10 +1786,12 @@ var PopoverAnchor = /* @__PURE__ */ React7.forwardRef(function PopoverAnchor2(pr
1848
1786
  return /* @__PURE__ */ jsx(PopoverPrimitive.Anchor, { ref, "data-slot": "popover-anchor", ...props });
1849
1787
  });
1850
1788
  var PopoverContent = /* @__PURE__ */ React7.forwardRef(function PopoverContent2({ className, align = "center", sideOffset = 4, ...props }, ref) {
1789
+ const dir = useDirection();
1851
1790
  return /* @__PURE__ */ jsx(PopoverPrimitive.Portal, { children: /* @__PURE__ */ jsx(
1852
1791
  PopoverPrimitive.Content,
1853
1792
  {
1854
1793
  ref,
1794
+ dir,
1855
1795
  "data-slot": "popover-content",
1856
1796
  align,
1857
1797
  sideOffset,
@@ -2018,6 +1958,7 @@ var CodeBlock = ({ block, controller }) => {
2018
1958
  };
2019
1959
  function FilePickerButton({ label, accept, disabled, busy = false, onPick }) {
2020
1960
  const inputRef = React7.useRef(null);
1961
+ const wrapper = React7.useRef(null);
2021
1962
  const restoreFocus = React7.useRef(false);
2022
1963
  React7.useEffect(() => {
2023
1964
  if (disabled || !restoreFocus.current) return;
@@ -2025,34 +1966,49 @@ function FilePickerButton({ label, accept, disabled, busy = false, onPick }) {
2025
1966
  const { activeElement } = document;
2026
1967
  if (activeElement === null || activeElement === document.body) inputRef.current?.focus();
2027
1968
  }, [disabled]);
2028
- return /* @__PURE__ */ jsxs("span", { className: "relative inline-flex rounded-md has-[:focus-visible]:outline-2 has-[:focus-visible]:outline-offset-2 has-[:focus-visible]:outline-[var(--quoin-ring)]", children: [
2029
- /* @__PURE__ */ jsx(Button, { asChild: true, variant: "outline", size: "sm", children: /* @__PURE__ */ jsxs("span", { "aria-hidden": "true", className: disabled || busy ? "pointer-events-none opacity-50" : void 0, children: [
2030
- /* @__PURE__ */ jsx(Upload, { "aria-hidden": "true" }),
2031
- label
2032
- ] }) }),
2033
- /* @__PURE__ */ jsx(
2034
- "input",
2035
- {
2036
- ref: inputRef,
2037
- type: "file",
2038
- accept,
2039
- "aria-label": label,
2040
- disabled,
2041
- "aria-disabled": busy || void 0,
2042
- className: "absolute inset-0 cursor-pointer opacity-0 disabled:cursor-not-allowed aria-disabled:cursor-not-allowed",
2043
- onClick: (e) => {
2044
- if (busy) e.preventDefault();
2045
- },
2046
- onChange: (e) => {
2047
- const file = e.target.files?.[0];
2048
- e.target.value = "";
2049
- if (!file || busy) return;
2050
- restoreFocus.current = true;
2051
- onPick(file);
2052
- }
2053
- }
2054
- )
2055
- ] });
1969
+ React7.useEffect(() => {
1970
+ const doc = wrapper.current?.ownerDocument;
1971
+ const pressed = (event) => {
1972
+ if (!wrapper.current?.contains(event.target)) restoreFocus.current = false;
1973
+ };
1974
+ doc?.addEventListener("pointerdown", pressed, true);
1975
+ return () => doc?.removeEventListener("pointerdown", pressed, true);
1976
+ }, []);
1977
+ return /* @__PURE__ */ jsxs(
1978
+ "span",
1979
+ {
1980
+ ref: wrapper,
1981
+ className: "relative inline-flex rounded-md has-[:focus-visible]:outline-2 has-[:focus-visible]:outline-offset-2 has-[:focus-visible]:outline-[var(--quoin-ring)]",
1982
+ children: [
1983
+ /* @__PURE__ */ jsx(Button, { asChild: true, variant: "outline", size: "sm", children: /* @__PURE__ */ jsxs("span", { "aria-hidden": "true", className: disabled || busy ? "pointer-events-none opacity-50" : void 0, children: [
1984
+ /* @__PURE__ */ jsx(Upload, { "aria-hidden": "true" }),
1985
+ label
1986
+ ] }) }),
1987
+ /* @__PURE__ */ jsx(
1988
+ "input",
1989
+ {
1990
+ ref: inputRef,
1991
+ type: "file",
1992
+ accept,
1993
+ "aria-label": label,
1994
+ disabled,
1995
+ "aria-disabled": busy || void 0,
1996
+ className: "absolute inset-0 cursor-pointer opacity-0 disabled:cursor-not-allowed aria-disabled:cursor-not-allowed",
1997
+ onClick: (e) => {
1998
+ if (busy) e.preventDefault();
1999
+ },
2000
+ onChange: (e) => {
2001
+ const file = e.target.files?.[0];
2002
+ e.target.value = "";
2003
+ if (!file || busy) return;
2004
+ restoreFocus.current = true;
2005
+ onPick(file);
2006
+ }
2007
+ }
2008
+ )
2009
+ ]
2010
+ }
2011
+ );
2056
2012
  }
2057
2013
 
2058
2014
  // src/blocks/void/interactive-target.ts
@@ -2099,6 +2055,29 @@ function selectHandlers(controller, blockId, root, onSelect) {
2099
2055
  }
2100
2056
  };
2101
2057
  }
2058
+
2059
+ // src/blocks/upload/upload-targets.ts
2060
+ var targets = /* @__PURE__ */ new WeakMap();
2061
+ function offerUploadTarget(controller, blockId, target) {
2062
+ const offered = targets.get(controller) ?? /* @__PURE__ */ new Map();
2063
+ targets.set(controller, offered.set(blockId, target));
2064
+ return () => {
2065
+ if (offered.get(blockId) === target) offered.delete(blockId);
2066
+ };
2067
+ }
2068
+ function fillSelectedBlock(controller, files) {
2069
+ const { selection } = controller.editor.state;
2070
+ const target = selection?.kind === "node" ? targets.get(controller)?.get(selection.block) : void 0;
2071
+ const taken = target && files.find((file) => target.takes(file));
2072
+ if (taken === void 0) return files;
2073
+ target.start(taken);
2074
+ return files.filter((file) => file !== taken);
2075
+ }
2076
+
2077
+ // src/upload/upload-error.ts
2078
+ var uploadError = (cause, failed) => cause instanceof Error && cause.message ? cause : new Error(failed);
2079
+
2080
+ // src/blocks/upload/use-block-upload.ts
2102
2081
  var IDLE = { busy: false, error: "" };
2103
2082
  function useBlockUpload(controller, blockId, area, uploader) {
2104
2083
  const { upload, resolveAssetUrl, onError, labels } = useQuoin();
@@ -2112,12 +2091,13 @@ function useBlockUpload(controller, blockId, area, uploader) {
2112
2091
  if (started.current === run) setState(next);
2113
2092
  };
2114
2093
  setState({ busy: true, error: "" });
2094
+ controller.announce(labels.uploading);
2115
2095
  const onLand = () => {
2116
2096
  if (area.current?.contains(area.current.ownerDocument.activeElement)) focusNode(controller, blockId);
2117
2097
  };
2118
2098
  uploader(controller, blockId, file, { upload, resolveAssetUrl, onLand }).then(
2119
2099
  () => settle2(IDLE),
2120
- (cause) => settle2({ busy: false, error: cause instanceof Error && cause.message || labels.uploadFailed })
2100
+ (cause) => settle2({ busy: false, error: uploadError(cause, labels.uploadFailed).message })
2121
2101
  );
2122
2102
  },
2123
2103
  [controller, blockId, area, uploader, upload, resolveAssetUrl, onError, labels]
@@ -2142,6 +2122,7 @@ function UploadArea(props) {
2142
2122
  element?.addEventListener("drop", onDrop);
2143
2123
  return () => element?.removeEventListener("drop", onDrop);
2144
2124
  }, [start, takes]);
2125
+ useEffect(() => offerUploadTarget(controller, blockId, { takes, start }), [controller, blockId, takes, start]);
2145
2126
  const onPaste = (event) => {
2146
2127
  const file = fileIn(event.clipboardData, takes);
2147
2128
  if (file === void 0) return;
@@ -2162,7 +2143,7 @@ function UploadArea(props) {
2162
2143
  ] }),
2163
2144
  children,
2164
2145
  /* @__PURE__ */ jsx("p", { className: "quoin-block-message", children: upload ? hint : labels.uploadsUnavailable }),
2165
- /* @__PURE__ */ jsx("p", { role: "status", className: busy ? "quoin-block-message" : "sr-only", children: busy ? labels.uploading : "" }),
2146
+ busy && /* @__PURE__ */ jsx("p", { className: "quoin-block-message", children: labels.uploading }),
2166
2147
  error !== "" && /* @__PURE__ */ jsx("p", { role: "alert", className: "quoin-block-message text-destructive", children: error })
2167
2148
  ]
2168
2149
  }
@@ -2170,8 +2151,8 @@ function UploadArea(props) {
2170
2151
  }
2171
2152
  var CONTROL = "input:not([disabled]), button:not([disabled])";
2172
2153
  var typed = (event) => !event.ctrlKey && !event.metaKey && [...event.key].length === 1;
2173
- function useNodeKeys(controller, blockId, editable, opens, root, { enter, type } = {}) {
2174
- const { editing, stop } = useVoidEditing(controller, blockId, editable && opens);
2154
+ function useNodeKeys(controller, blockId, editable, opens2, root, { enter, type } = {}) {
2155
+ const { editing, stop } = useVoidEditing(controller, blockId, editable && opens2);
2175
2156
  useEffect(() => {
2176
2157
  if (!editing) return;
2177
2158
  stop();
@@ -2191,7 +2172,7 @@ function useNodeKeys(controller, blockId, editable, opens, root, { enter, type }
2191
2172
  }, [controller, blockId, editable, type]);
2192
2173
  }
2193
2174
 
2194
- // src/menus/bubble/bubble-part.ts
2175
+ // src/toolbar/bubble-part.ts
2195
2176
  var BUBBLE_PART = "data-quoin-bubble";
2196
2177
  var partIds = /* @__PURE__ */ new WeakMap();
2197
2178
  var created = 0;
@@ -2275,6 +2256,7 @@ function assetOf(controller, blockId, type) {
2275
2256
  if (block?.type !== type || controller.readOnly) return null;
2276
2257
  return { url: str(block.attrs.url), storageKey: str(block.attrs.storageKey) };
2277
2258
  }
2259
+ var isStored = (stored) => typeof stored?.storageKey === "string" && stored.storageKey !== "";
2278
2260
  var sameAsset = (a, b) => a !== null && b !== null && a.url === b.url && a.storageKey === b.storageKey;
2279
2261
  function writeAttrs(controller, blockId, patch, recorded) {
2280
2262
  const { editor } = controller;
@@ -2285,7 +2267,7 @@ async function uploadInto(controller, blockId, file, { type, category }, { uploa
2285
2267
  const token = claimUpload(controller, blockId);
2286
2268
  const before = assetOf(controller, blockId, type);
2287
2269
  const stored = await upload(file, category);
2288
- if (typeof stored?.storageKey !== "string" || stored.storageKey === "") throw new Error();
2270
+ if (!isStored(stored)) throw new Error();
2289
2271
  if (!isNewestUpload(controller, blockId, token) || !sameAsset(assetOf(controller, blockId, type), before)) return;
2290
2272
  onLand?.();
2291
2273
  await land2(stored, token);
@@ -2335,7 +2317,7 @@ function useReplacePicker(controller, blockId, uploader, accept) {
2335
2317
  if (file === void 0 || !upload) return;
2336
2318
  controller.announce(labels.uploading);
2337
2319
  uploader(controller, blockId, file, { upload, resolveAssetUrl }).catch(
2338
- (cause) => onError(cause instanceof Error && cause.message ? cause : new Error(labels.uploadFailed))
2320
+ (cause) => onError(uploadError(cause, labels.uploadFailed))
2339
2321
  );
2340
2322
  };
2341
2323
  return {
@@ -2385,37 +2367,10 @@ var deleteItem = (blockId, title) => ({
2385
2367
  run: (controller) => deleteBlock(controller, blockId)
2386
2368
  });
2387
2369
  var PRESSED = "aria-pressed:bg-accent aria-pressed:text-accent-foreground";
2388
- function markItem(mark, title, icon) {
2389
- return {
2390
- id: mark,
2391
- title,
2392
- icon,
2393
- isActive: (state) => isActive(state, mark),
2394
- run: (controller) => void controller.run(toggleMark(mark))
2395
- };
2396
- }
2397
- var linkItem = (labels, openLink) => ({
2398
- id: "link",
2399
- title: labels.link,
2400
- icon: Link,
2401
- run: openLink
2402
- });
2403
- function bubbleItems(labels, openLink) {
2404
- return {
2405
- inline: [
2406
- markItem("bold", labels.bold, Bold),
2407
- markItem("italic", labels.italic, Italic),
2408
- markItem("underline", labels.underline, Underline),
2409
- markItem("strike", labels.strike, Strikethrough),
2410
- markItem("code", labels.code, Code),
2411
- linkItem(labels, openLink)
2412
- ],
2413
- scripts: [markItem("sub", labels.subscript, Subscript), markItem("sup", labels.superscript, Superscript)]
2414
- };
2415
- }
2416
2370
  function ToolbarButton({ item, state, controller, buttonRef }) {
2417
2371
  if (item.available?.(state) === false) return null;
2418
2372
  const Icon2 = item.icon;
2373
+ const disabled = item.enabled?.(state) === false;
2419
2374
  return /* @__PURE__ */ jsx(
2420
2375
  Button,
2421
2376
  {
@@ -2425,8 +2380,12 @@ function ToolbarButton({ item, state, controller, buttonRef }) {
2425
2380
  size: "icon-sm",
2426
2381
  "aria-label": item.title,
2427
2382
  "aria-pressed": item.isActive?.(state),
2383
+ "aria-haspopup": item.opensDialog ? "dialog" : void 0,
2384
+ "aria-disabled": disabled || void 0,
2428
2385
  className: PRESSED,
2429
- onClick: () => item.run(controller),
2386
+ onClick: () => {
2387
+ if (!disabled) item.run(controller);
2388
+ },
2430
2389
  children: /* @__PURE__ */ jsx(Icon2, {})
2431
2390
  }
2432
2391
  );
@@ -2535,10 +2494,12 @@ function createSettled(controller) {
2535
2494
  let pressed = false;
2536
2495
  let composing = false;
2537
2496
  const inEditor = (event) => event.target instanceof Node && controller.root?.contains(event.target) === true;
2497
+ const release = () => void (pressed = false);
2538
2498
  const handlers = {
2539
2499
  pointerdown: (event) => void (pressed ||= inEditor(event)),
2540
- pointerup: () => void (pressed = false),
2541
- pointercancel: () => void (pressed = false),
2500
+ pointerup: release,
2501
+ pointercancel: release,
2502
+ dragend: release,
2542
2503
  compositionstart: (event) => void (composing ||= inEditor(event)),
2543
2504
  compositionend: () => void (composing = false),
2544
2505
  focusin: () => {
@@ -2559,9 +2520,27 @@ function createSettled(controller) {
2559
2520
  doc.addEventListener(type, listener, true);
2560
2521
  return () => doc.removeEventListener(type, listener, true);
2561
2522
  });
2523
+ const timers = /* @__PURE__ */ new Set();
2524
+ const afterView = () => {
2525
+ const timer = setTimeout(() => {
2526
+ timers.delete(timer);
2527
+ notify();
2528
+ }, 0);
2529
+ timers.add(timer);
2530
+ };
2531
+ const stopDoc = listen(doc, { compositionend: afterView });
2532
+ const onWindowBlur = () => {
2533
+ release();
2534
+ notify();
2535
+ };
2536
+ const stopWindow = doc.defaultView ? listen(doc.defaultView, { blur: onWindowBlur }) : () => {
2537
+ };
2562
2538
  const stopState = controller.editor.subscribe(() => notify());
2563
2539
  return () => {
2564
2540
  listeners2.forEach((remove) => remove());
2541
+ timers.forEach(clearTimeout);
2542
+ stopDoc();
2543
+ stopWindow();
2565
2544
  stopState();
2566
2545
  };
2567
2546
  }
@@ -2620,6 +2599,23 @@ function requestAlt(controller, blockId) {
2620
2599
  listeners.get(controller)?.(blockId);
2621
2600
  }
2622
2601
 
2602
+ // src/blocks/image/owed-size.ts
2603
+ var owedSizes = /* @__PURE__ */ new WeakMap();
2604
+ var showsKey = (controller, blockId, storageKey) => sameAsset(assetOf(controller, blockId, "IMAGE"), { url: "", storageKey });
2605
+ function writeSize(controller, blockId, measured) {
2606
+ if (!isNewestUpload(controller, blockId, measured.token)) return;
2607
+ if (showsKey(controller, blockId, measured.storageKey)) writeAttrs(controller, blockId, { ...measured.size }, false);
2608
+ else owedSizes.set(controller, (owedSizes.get(controller) ?? /* @__PURE__ */ new Map()).set(blockId, measured));
2609
+ }
2610
+ function payOwedSize(controller, blockId) {
2611
+ const owing = owedSizes.get(controller);
2612
+ const owed = owing?.get(blockId);
2613
+ if (owed === void 0 || !showsKey(controller, blockId, owed.storageKey)) return;
2614
+ owing.delete(blockId);
2615
+ const unsized = blockById(controller.editor.state.doc, blockId)?.attrs.width === void 0;
2616
+ if (unsized && isNewestUpload(controller, blockId, owed.token)) writeAttrs(controller, blockId, { ...owed.size }, false);
2617
+ }
2618
+
2623
2619
  // src/blocks/image/resize-width.ts
2624
2620
  var MIN_WIDTH = 10;
2625
2621
  var MAX_WIDTH = 100;
@@ -2731,6 +2727,7 @@ function ImageFigure({ block, controller, readOnly }) {
2731
2727
  const badgeText = labels.noAltBadge ?? englishLabels.noAltBadge;
2732
2728
  const badgeName = labels.addAltText ?? englishLabels.addAltText;
2733
2729
  const translatedAlone = badgeText !== englishLabels.noAltBadge && badgeName === englishLabels.addAltText;
2730
+ useEffect(() => payOwedSize(controller, id), [controller, id, attrs.storageKey]);
2734
2731
  return /* @__PURE__ */ jsxs(Fragment, { children: [
2735
2732
  /* @__PURE__ */ jsxs("div", { ref: frame, className: "quoin-image-frame", style: width === void 0 ? void 0 : { width: `${width}%` }, children: [
2736
2733
  src !== null && src !== failed ? /* @__PURE__ */ jsx(
@@ -2787,12 +2784,14 @@ function ImageFigure({ block, controller, readOnly }) {
2787
2784
  }
2788
2785
 
2789
2786
  // src/menus/link/link-address.ts
2790
- var BARE_DOMAIN = /^[^\s/#?.]\S*\.\S*$/;
2791
- var EMAIL = /^[^\s@/]+@[^\s@/]+\.[^\s@/]+$/;
2787
+ var DOMAIN_START = /^[^/#?.]/;
2788
+ var EMAIL = /^[^@/]+@([^@/]+)$/;
2792
2789
  var WHITESPACE = /\s/;
2790
+ var isEmail = (address) => EMAIL.exec(address)?.[1].slice(1, -1).includes(".") === true;
2791
+ var isBareDomain = (address) => DOMAIN_START.test(address) && address.includes(".", 1);
2793
2792
  function scheme(address) {
2794
- if (EMAIL.test(address)) return `mailto:${address}`;
2795
- return BARE_DOMAIN.test(address) ? `https://${address}` : address;
2793
+ if (isEmail(address)) return `mailto:${address}`;
2794
+ return isBareDomain(address) ? `https://${address}` : address;
2796
2795
  }
2797
2796
  function linkAddress(typed2) {
2798
2797
  const address = typed2.trim();
@@ -2872,9 +2871,7 @@ var isImageFile = (file) => file.type.startsWith("image/");
2872
2871
  async function measureImage(controller, blockId, storageKey, resolveAssetUrl, token = claimUpload(controller, blockId)) {
2873
2872
  const src = assetHref(resolveAssetUrl, storageKey);
2874
2873
  const size = src === null ? null : await intrinsicSize(src);
2875
- if (size && isNewestUpload(controller, blockId, token) && sameAsset(assetOf(controller, blockId, "IMAGE"), { url: "", storageKey })) {
2876
- writeAttrs(controller, blockId, { ...size }, false);
2877
- }
2874
+ if (size) writeSize(controller, blockId, { storageKey, size, token });
2878
2875
  }
2879
2876
  var uploadImage = (controller, blockId, file, host) => uploadInto(controller, blockId, file, { type: "IMAGE", category: "image" }, host, async ({ storageKey }, token) => {
2880
2877
  writeAttrs(controller, blockId, { storageKey, url: void 0, width: void 0, height: void 0 }, true);
@@ -2960,6 +2957,28 @@ var ImageBlock = ({ block, controller }) => {
2960
2957
  }
2961
2958
  );
2962
2959
  };
2960
+ function useClosing(controller, { store, onClose, onReturn }) {
2961
+ const closed = useRef(false);
2962
+ const finish = (save, refocus = false) => {
2963
+ if (closed.current) return false;
2964
+ closed.current = true;
2965
+ if (save) store(refocus);
2966
+ return true;
2967
+ };
2968
+ const latestFinish = useRef(finish);
2969
+ latestFinish.current = finish;
2970
+ useEffect(() => {
2971
+ closed.current = false;
2972
+ return () => void latestFinish.current(true);
2973
+ }, []);
2974
+ const close = (save, refocus = true) => {
2975
+ if (!refocus) controller.readSelectionFromDom();
2976
+ if (!finish(save, refocus)) return;
2977
+ onClose();
2978
+ if (refocus) onReturn();
2979
+ };
2980
+ return { finish, close };
2981
+ }
2963
2982
  function useOutsidePress(element, ownerDocument, onPress) {
2964
2983
  const latest = useRef(onPress);
2965
2984
  useIsomorphicLayoutEffect(() => {
@@ -2997,29 +3016,18 @@ function AltTextPopover({ blockId, controller, anchor, opener, alt, decorative,
2997
3016
  const [marked, setMarked] = useState(decorative);
2998
3017
  const fieldId = useId();
2999
3018
  const hintId = `${fieldId}-hint`;
3000
- const closed = useRef(false);
3001
- const finish = (store) => {
3002
- if (closed.current) return false;
3003
- closed.current = true;
3004
- if (store && (draft !== alt || marked !== decorative))
3005
- storeAlt(controller, blockId, opened, { alt: draft, decorative: marked || void 0 });
3006
- return true;
3007
- };
3008
- const latestFinish = useRef(finish);
3009
- latestFinish.current = finish;
3010
- useEffect(() => {
3011
- closed.current = false;
3012
- return () => void latestFinish.current(true);
3013
- }, []);
3019
+ const { finish, close } = useClosing(controller, {
3020
+ store() {
3021
+ if (draft !== alt || marked !== decorative) {
3022
+ storeAlt(controller, blockId, opened, { alt: draft, decorative: marked || void 0 });
3023
+ }
3024
+ },
3025
+ onClose,
3026
+ onReturn: () => controller.focus()
3027
+ });
3014
3028
  useEffect(() => {
3015
3029
  if ((alt !== opened.alt || decorative !== opened.decorative) && finish(false)) onClose();
3016
3030
  });
3017
- const close = (store, refocus = true) => {
3018
- if (!refocus) controller.readSelectionFromDom();
3019
- if (!finish(store)) return;
3020
- onClose();
3021
- if (refocus) controller.focus();
3022
- };
3023
3031
  const content = useRef(null);
3024
3032
  const ownerDocument = useCallback(() => controller.root?.ownerDocument ?? document, [controller]);
3025
3033
  useOutsidePress(content, ownerDocument, (target) => {
@@ -3099,6 +3107,21 @@ function AltTextPopover({ blockId, controller, anchor, opener, alt, decorative,
3099
3107
  }
3100
3108
  );
3101
3109
  }
3110
+ function around(a, b) {
3111
+ const left = Math.min(a.left, b.left);
3112
+ const top = Math.min(a.top, b.top);
3113
+ return new DOMRect(left, top, Math.max(a.right, b.right) - left, Math.max(a.bottom, b.bottom) - top);
3114
+ }
3115
+ function useAroundToolbar(part, inToolbar) {
3116
+ return useMemo(() => {
3117
+ const measure2 = () => {
3118
+ const box = part.current.getBoundingClientRect();
3119
+ const toolbar = inToolbar.current?.closest('[role="toolbar"]')?.getBoundingClientRect();
3120
+ return toolbar === void 0 ? box : around(box, toolbar);
3121
+ };
3122
+ return { current: virtualAnchor(measure2, () => part.current.contextElement) };
3123
+ }, [part, inToolbar]);
3124
+ }
3102
3125
 
3103
3126
  // src/blocks/image/image-toolbar-items.ts
3104
3127
  var ALIGNS = [
@@ -3158,6 +3181,7 @@ function ImageToolbar({ blockId, controller, editingAlt, onEditAlt }) {
3158
3181
  const picker = useReplacePicker(controller, blockId, uploadImage, "image/*");
3159
3182
  const altButton = useRef(null);
3160
3183
  const anchor = usePartAnchor(controller, blockId, ".quoin-image-frame");
3184
+ const altAnchor = useAroundToolbar(anchor, altButton);
3161
3185
  const attrs = blockById(state.doc, blockId)?.attrs ?? {};
3162
3186
  const items = imageToolbarItems(blockId, labels, {
3163
3187
  replace: picker.open,
@@ -3182,7 +3206,7 @@ function ImageToolbar({ blockId, controller, editingAlt, onEditAlt }) {
3182
3206
  {
3183
3207
  blockId,
3184
3208
  controller,
3185
- anchor,
3209
+ anchor: altAnchor,
3186
3210
  opener: altButton,
3187
3211
  alt: str(attrs.alt),
3188
3212
  decorative: attrs.decorative === true,
@@ -3312,7 +3336,7 @@ var DropdownMenuSubTrigger = /* @__PURE__ */ React7.forwardRef(function Dropdown
3312
3336
  ...props,
3313
3337
  children: [
3314
3338
  children,
3315
- /* @__PURE__ */ jsx(ChevronRight, { className: "ml-auto size-4" })
3339
+ /* @__PURE__ */ jsx(ChevronRight, { className: "ms-auto size-4 rtl:rotate-180" })
3316
3340
  ]
3317
3341
  }
3318
3342
  );
@@ -3358,7 +3382,7 @@ var SHOWN_WITH_THE_BLOCK = [
3358
3382
  "opacity-0 group-hover/callout:opacity-100 group-focus-within/callout:opacity-100",
3359
3383
  "in-data-[selected=true]:opacity-100 pointer-coarse:opacity-100 focus-visible:opacity-100 data-[state=open]:opacity-100"
3360
3384
  ].join(" ");
3361
- function CalloutToneMenu({ blockId, controller, tone }) {
3385
+ function CalloutToneMenu({ blockId, controller, tone, alwaysShown }) {
3362
3386
  const labels = useLabels();
3363
3387
  const name = labels.calloutTones[tone];
3364
3388
  return /* @__PURE__ */ jsxs(DropdownMenu, { children: [
@@ -3370,7 +3394,7 @@ function CalloutToneMenu({ blockId, controller, tone }) {
3370
3394
  size: "xs",
3371
3395
  tabIndex: -1,
3372
3396
  "aria-label": labels.calloutToneMenu(name),
3373
- className: SHOWN_WITH_THE_BLOCK,
3397
+ className: alwaysShown ? void 0 : SHOWN_WITH_THE_BLOCK,
3374
3398
  children: name
3375
3399
  }
3376
3400
  ) }),
@@ -3389,6 +3413,7 @@ var CalloutBlock = ({ block, controller }) => {
3389
3413
  const readOnly = useReadOnly(controller);
3390
3414
  const tone = calloutTone(block.attrs.tone);
3391
3415
  const icon = calloutIcon(block.attrs, tone);
3416
+ const toneInWords = !iconTellsTone(block.attrs, tone);
3392
3417
  return /* @__PURE__ */ jsxs("div", { className: cn("quoin-callout group/callout flex items-start gap-2", TONE[tone]), "data-tone": tone, children: [
3393
3418
  readOnly ? /* @__PURE__ */ jsx("span", { "aria-hidden": "true", className: "flex size-6 items-center justify-center text-base", children: icon }) : /* @__PURE__ */ jsx(CalloutIconMenu, { blockId: block.id, controller, icon }),
3394
3419
  /* @__PURE__ */ jsx(
@@ -3400,7 +3425,7 @@ var CalloutBlock = ({ block, controller }) => {
3400
3425
  className: "min-w-0 flex-1"
3401
3426
  }
3402
3427
  ),
3403
- !readOnly && /* @__PURE__ */ jsx(CalloutToneMenu, { blockId: block.id, controller, tone })
3428
+ readOnly ? toneInWords && /* @__PURE__ */ jsx("span", { "aria-hidden": "true", className: "flex h-6 items-center px-2 text-xs", children: labels.calloutTones[tone] }) : /* @__PURE__ */ jsx(CalloutToneMenu, { blockId: block.id, controller, tone, alwaysShown: toneInWords })
3404
3429
  ] });
3405
3430
  };
3406
3431
  var DividerBlock = ({ block, controller }) => /* @__PURE__ */ jsx("div", { className: "py-2", contentEditable: false, onClick: () => focusNode(controller, block.id), children: /* @__PURE__ */ jsx("hr", { className: "border-t border-border" }) });
@@ -3454,80 +3479,11 @@ function orderedLabel(n, depth) {
3454
3479
  function ListMarker(props) {
3455
3480
  const text2 = props.list === "bullet" ? bulletGlyph(props.depth) : orderedLabel(props.number, props.depth);
3456
3481
  return /* @__PURE__ */ jsx("span", { className: "quoin-marker", "aria-hidden": "true", children: text2 });
3457
- }
3458
- var REGION = "text";
3459
- var HEADING_LEVELS = ["mt-6 text-3xl", "mt-5 text-2xl", "mt-4 text-xl", "mt-3 text-lg", "mt-3 text-base", "mt-3 text-sm"];
3460
- var headingStyles = (level) => `quoin-heading mb-1 font-semibold leading-tight in-[[data-quoin-block]:first-child]:mt-0 ${HEADING_LEVELS[level - 1]}`;
3461
- function Heading({ block }) {
3462
- const labels = useLabels();
3463
- const level = headingLevel(block.attrs.level);
3464
- return /* @__PURE__ */ jsx("div", { role: "heading", "aria-level": level, className: headingStyles(level), children: /* @__PURE__ */ jsx(EditableRegion, { block: block.id, region: REGION, label: labels.heading(level) }) });
3465
- }
3466
- function NumberedItem({ block, controller }) {
3467
- const labels = useLabels();
3468
- const number = useOrderedNumber(controller, block.id);
3469
- return /* @__PURE__ */ jsxs("div", { className: "quoin-list-item", "data-list": "ordered", children: [
3470
- /* @__PURE__ */ jsx(ListMarker, { list: "ordered", number, depth: block.indent }),
3471
- /* @__PURE__ */ jsx(EditableRegion, { block: block.id, region: REGION, label: labels.numberedItem(number) })
3472
- ] });
3473
- }
3474
- function TaskItem({ block, controller }) {
3475
- const labels = useLabels();
3476
- const hostId = useRegionDomId(block.id, REGION);
3477
- return /* @__PURE__ */ jsxs("div", { className: "quoin-task", "data-checked": block.attrs.checked === true, children: [
3478
- /* @__PURE__ */ jsx(TaskCheckbox, { block, controller, label: labels.task, labelledBy: hostId }),
3479
- /* @__PURE__ */ jsx(EditableRegion, { id: hostId, block: block.id, region: REGION, label: labels.task })
3480
- ] });
3481
- }
3482
- var MarkdownBlock = (props) => {
3483
- const { block } = props;
3484
- const labels = useLabels();
3485
- switch (block.attrs.kind) {
3486
- case "heading":
3487
- return /* @__PURE__ */ jsx(Heading, { block });
3488
- case "bullet":
3489
- return /* @__PURE__ */ jsxs("div", { className: "quoin-list-item", "data-list": "bullet", children: [
3490
- /* @__PURE__ */ jsx(ListMarker, { list: "bullet", depth: block.indent }),
3491
- /* @__PURE__ */ jsx(EditableRegion, { block: block.id, region: REGION, label: labels.bulletItem })
3492
- ] });
3493
- case "ordered":
3494
- return /* @__PURE__ */ jsx(NumberedItem, { ...props });
3495
- case "task":
3496
- return /* @__PURE__ */ jsx(TaskItem, { ...props });
3497
- case "quote":
3498
- return /* @__PURE__ */ jsx("div", { className: "quoin-quote", children: /* @__PURE__ */ jsx(EditableRegion, { block: block.id, region: REGION, label: labels.quote }) });
3499
- default:
3500
- return /* @__PURE__ */ jsx(EditableRegion, { block: block.id, region: REGION, label: labels.paragraph });
3501
- }
3502
- };
3503
-
3504
- // src/blocks/text/use-nested-blocks.ts
3505
- function nestedBlockIds({ doc }, blockId) {
3506
- const index = doc.index.get(blockId);
3507
- if (index === void 0) return [];
3508
- return doc.blocks.slice(index + 1, descendantsEnd(doc, index)).map((block) => block.id);
3509
- }
3510
- function useNestedBlocks(controller, blockId) {
3511
- return useEditorState(controller, (state) => nestedBlockIds(state, blockId), sameItems);
3512
- }
3513
- var ToggleBlock = ({ block, controller }) => {
3514
- const labels = useLabels();
3515
- const domId = useBlockDomId();
3516
- const nested = useNestedBlocks(controller, block.id);
3517
- const open = useToggleOpen(controller, block);
3518
- return /* @__PURE__ */ jsxs("div", { className: "quoin-toggle", "data-open": open, children: [
3519
- /* @__PURE__ */ jsx(
3520
- ToggleChevron,
3521
- {
3522
- block,
3523
- controller,
3524
- label: labels.toggleButton(open ? 0 : nested.length),
3525
- controls: nested.length === 0 ? void 0 : nested.map(domId).join(" ")
3526
- }
3527
- ),
3528
- /* @__PURE__ */ jsx(EditableRegion, { block: block.id, region: "text", label: labels.toggle })
3529
- ] });
3530
- };
3482
+ }
3483
+ var HEADING_LEVELS = ["mt-6 text-3xl", "mt-5 text-2xl", "mt-4 text-xl", "mt-3 text-lg", "mt-3 text-base", "mt-3 text-sm"];
3484
+ var headingStyles = (level) => `quoin-heading mb-1 font-semibold leading-tight in-[[data-quoin-block]:first-child]:mt-0 ${HEADING_LEVELS[level - 1]}`;
3485
+ var CANVAS_LOOK = { Marker: ListMarker, headingClassName: headingStyles };
3486
+ var MarkdownBlock = (props) => /* @__PURE__ */ jsx(MarkdownText, { ...props, look: CANVAS_LOOK });
3531
3487
 
3532
3488
  // src/blocks/text/text-renderers.ts
3533
3489
  var textRenderers = {
@@ -3551,6 +3507,7 @@ function drawnAsText(definitions, type, readOnly) {
3551
3507
  return !hostDefines(definitions, type) || !readOnly && TYPED_IN_PLACE.includes(type);
3552
3508
  }
3553
3509
  var definedTypedInPlace = (definitions) => TYPED_IN_PLACE.filter((type) => hostDefines(definitions, type));
3510
+ var hostLabel = (definitions, type) => !TYPED_IN_PLACE.includes(type) && hostDefines(definitions, type) ? blockByType(definitions, type)?.label : void 0;
3554
3511
  function drawnInPlace(definitions, type) {
3555
3512
  return Object.hasOwn(inPlaceRenderers, type) && blockByType(definitions, type) !== void 0 && !hostDefines(definitions, type);
3556
3513
  }
@@ -3586,10 +3543,10 @@ function turnIntoTargets(labels) {
3586
3543
  }
3587
3544
  function TurnIntoItems({ kit, controller, blockIds }) {
3588
3545
  const { blockTypes, labels } = useQuoin();
3589
- const targets = turnIntoTargets(labels).filter((target) => drawsType(blockTypes, target.type));
3590
- return targets.map((target) => /* @__PURE__ */ jsxs(kit.Item, { onSelect: () => runOnBlocks(controller, blockIds, setBlockType(target.type, target.attrs)), children: [
3546
+ const targets2 = turnIntoTargets(labels).filter((target) => drawsType(blockTypes, target.type));
3547
+ return targets2.map((target) => /* @__PURE__ */ jsxs(kit.Item, { onSelect: () => runOnBlocks(controller, blockIds, setBlockType(target.type, target.attrs)), children: [
3591
3548
  /* @__PURE__ */ jsx(MenuIcon, { icon: target.icon }),
3592
- target.label
3549
+ hostLabel(blockTypes, target.type) ?? target.label
3593
3550
  ] }, target.id));
3594
3551
  }
3595
3552
  function TurnIntoMenu(props) {
@@ -3687,8 +3644,15 @@ function useContextMenuOpener(controller, store, trigger) {
3687
3644
  const onMouseDown = (event) => {
3688
3645
  if (event.button === 2 && blockOf(event) !== null) event.preventDefault();
3689
3646
  };
3647
+ let pressed = null;
3648
+ const onPointerDown = (event) => {
3649
+ const block = event.button === 2 ? blockOf(event) : null;
3650
+ pressed = block === null ? null : { block, x: event.clientX, y: event.clientY };
3651
+ };
3652
+ const pressedAt = ({ clientX, clientY }) => pressed !== null && Math.hypot(clientX - pressed.x, clientY - pressed.y) <= 2 ? pressed.block : null;
3690
3653
  const onContextMenu = (event) => {
3691
- const block = blockOf(event);
3654
+ const block = blockOf(event) ?? pressedAt(event);
3655
+ pressed = null;
3692
3656
  const hidden = trigger.current;
3693
3657
  if (block === null || hidden === null) return;
3694
3658
  event.preventDefault();
@@ -3696,7 +3660,7 @@ function useContextMenuOpener(controller, store, trigger) {
3696
3660
  openBlockMenuAt(store, controller.editor.state, block, clientX, clientY);
3697
3661
  hidden.dispatchEvent(new MouseEvent("contextmenu", { bubbles: true, cancelable: true, clientX, clientY }));
3698
3662
  };
3699
- return listen(root, { mousedown: onMouseDown, contextmenu: onContextMenu });
3663
+ return listen(root, { pointerdown: onPointerDown, mousedown: onMouseDown, contextmenu: onContextMenu });
3700
3664
  }, [controller, store, trigger]);
3701
3665
  }
3702
3666
 
@@ -3720,6 +3684,8 @@ var nameIn = (names, key) => typeof key === "string" && Object.hasOwn(names, key
3720
3684
  function blockTypeLabel(labels, block, definitions, extensionNames = {}) {
3721
3685
  const { kind, level } = block.attrs;
3722
3686
  if (block.type === "MARKDOWN" && kind === "heading") return labels.heading(typeof level === "number" ? level : 1);
3687
+ const own = hostLabel(definitions, block.type);
3688
+ if (own !== void 0) return own;
3723
3689
  const name = block.type === "MARKDOWN" ? nameIn(KIND_NAMES, kind) : nameIn(TYPE_NAMES, block.type);
3724
3690
  if (name !== void 0) return labels.blockNames[name];
3725
3691
  return nameIn(extensionNames, block.type) ?? blockByType(definitions, block.type)?.label ?? labels.unsupportedBlock;
@@ -3782,6 +3748,72 @@ function BlockContextMenu({ controller, store, actions }) {
3782
3748
  );
3783
3749
  }
3784
3750
 
3751
+ // src/gutter/block-menu-items.ts
3752
+ var rangeOf = (blockIds) => blockRangeSelection(blockIds[0], blockIds[blockIds.length - 1]);
3753
+ function menuActions(core, extra = []) {
3754
+ return [...core.filter((action) => !action.destructive), ...extra, ...core.filter((action) => action.destructive)];
3755
+ }
3756
+ function markdownOf(editor, blockIds, resolveAssetUrl) {
3757
+ const { doc } = editor.state;
3758
+ const range = selectedBlockRange(doc, rangeOf(blockIds));
3759
+ if (range === null) return "";
3760
+ const blocks = editor.codec.blocksFromDoc(createDoc(doc.blocks.slice(range.start, range.end)));
3761
+ return toMarkdown(blocks, { schema: editor.schema, resolveAssetUrl, comments: false });
3762
+ }
3763
+ var applies = (command) => (state, blockIds) => blockIds.every((id) => state.doc.index.has(id)) && runCommands(state, [setSelection(rangeOf(blockIds)), command]).ok;
3764
+ function coreBlockMenuActions(labels, options) {
3765
+ const copy = (controller, blockIds, text2) => {
3766
+ void copyText(text2, controller.root?.ownerDocument).then((copied) => {
3767
+ (copied ? options.onCopied : options.onCopyFailed)?.();
3768
+ });
3769
+ runOnBlocks(controller, blockIds, noChange);
3770
+ };
3771
+ return [
3772
+ {
3773
+ id: "duplicate",
3774
+ label: labels.duplicate,
3775
+ icon: CopyPlus,
3776
+ shortcut: "Mod-d",
3777
+ run: (controller, blockIds) => runOnBlocks(controller, blockIds, duplicateBlocks())
3778
+ },
3779
+ {
3780
+ id: "copy",
3781
+ label: labels.copy,
3782
+ icon: Copy,
3783
+ run: (controller, blockIds) => copy(controller, blockIds, markdownOf(controller.editor, blockIds, options.resolveAssetUrl))
3784
+ },
3785
+ {
3786
+ id: "copy-link",
3787
+ label: labels.copyLink,
3788
+ icon: Link,
3789
+ run: (controller, blockIds) => copy(controller, blockIds, options.blockLink(blockIds[0]))
3790
+ },
3791
+ {
3792
+ id: "move-up",
3793
+ label: labels.moveUp,
3794
+ icon: ArrowUp,
3795
+ shortcut: "Mod-Shift-ArrowUp",
3796
+ run: (controller, blockIds) => runOnBlocks(controller, blockIds, moveBlocksUp),
3797
+ available: applies(moveBlocksUp)
3798
+ },
3799
+ {
3800
+ id: "move-down",
3801
+ label: labels.moveDown,
3802
+ icon: ArrowDown,
3803
+ shortcut: "Mod-Shift-ArrowDown",
3804
+ run: (controller, blockIds) => runOnBlocks(controller, blockIds, moveBlocksDown),
3805
+ available: applies(moveBlocksDown)
3806
+ },
3807
+ {
3808
+ id: "delete",
3809
+ label: labels.delete,
3810
+ icon: Trash,
3811
+ destructive: true,
3812
+ run: (controller, blockIds) => runOnBlocks(controller, blockIds, deleteBlocks)
3813
+ }
3814
+ ];
3815
+ }
3816
+
3785
3817
  // src/gutter/block-menu-keymap.ts
3786
3818
  function blockMenuKeymap(store) {
3787
3819
  const openForHead = (state, dispatch) => {
@@ -3855,14 +3887,28 @@ var DragOverlay = forwardRef(function DragOverlay2(_props, ref) {
3855
3887
  ] });
3856
3888
  });
3857
3889
 
3890
+ // src/canvas/removed-blocks.ts
3891
+ function blockRemovals(tr) {
3892
+ return tr.steps.flatMap((step, at) => {
3893
+ if (step.stepType !== "removeBlocks") return [];
3894
+ const { index, count } = step.toJSON();
3895
+ return typeof index === "number" && typeof count === "number" ? [{ step: at, index, count }] : [];
3896
+ });
3897
+ }
3898
+
3858
3899
  // src/canvas/structure-announcement.ts
3859
3900
  var DELETION_STEPS = /* @__PURE__ */ new Set(["removeBlocks", "setIndent"]);
3860
3901
  function drawnPosition(doc, blockId) {
3861
- const index = doc.index.get(blockId) ?? 0;
3862
- return [drawnBlockCount(doc, 0, index) + 1, drawnBlockCount(doc)];
3902
+ const index = doc.index.get(blockId);
3903
+ return index === void 0 ? null : [drawnBlockCount(doc, 0, index) + 1, drawnBlockCount(doc)];
3863
3904
  }
3864
3905
  function blockMovedMessage(doc, blockId, labels) {
3865
- return labels.blockMoved(...drawnPosition(doc, blockId));
3906
+ const position = drawnPosition(doc, blockId);
3907
+ return position && labels.blockMoved(...position);
3908
+ }
3909
+ function announceBlockMoved(controller, blockId, labels) {
3910
+ const message = blockMovedMessage(controller.editor.state.doc, blockId, labels);
3911
+ if (message !== null && controller.announcer.current()?.message !== message) controller.announce(message);
3866
3912
  }
3867
3913
  function movedBlock(tr) {
3868
3914
  const at = tr.steps.findIndex((step) => step.stepType === "moveBlocks");
@@ -3872,19 +3918,39 @@ function movedBlock(tr) {
3872
3918
  }
3873
3919
  function deletedCount(tr) {
3874
3920
  if (!tr.steps.every((step) => DELETION_STEPS.has(step.stepType))) return 0;
3875
- return tr.steps.reduce((deleted, step, i) => {
3876
- const { index, count } = step.toJSON();
3877
- if (step.stepType !== "removeBlocks" || typeof index !== "number" || typeof count !== "number") return deleted;
3878
- return deleted + drawnBlockCount(tr.docs[i], index, index + count);
3879
- }, 0);
3921
+ return blockRemovals(tr).reduce(
3922
+ (deleted, { step, index, count }) => deleted + drawnBlockCount(tr.docs[step], index, index + count),
3923
+ 0
3924
+ );
3880
3925
  }
3881
- function insertedBlock(tr) {
3882
- if (tr.getMeta(LINE_SPLIT) === true || tr.steps.some((step) => step.stepType === "removeBlocks")) return null;
3926
+ function insertedIds(tr) {
3883
3927
  const inserted = tr.steps.flatMap((step) => {
3884
3928
  const { blocks } = step.toJSON();
3885
3929
  return step.stepType === "insertBlocks" && Array.isArray(blocks) ? blocks : [];
3886
3930
  });
3887
- return inserted.find((block) => tr.doc.index.has(block.id))?.id ?? null;
3931
+ return new Set(inserted.map(({ id }) => id));
3932
+ }
3933
+ function isCaretLine(tr, block) {
3934
+ const { selection } = tr;
3935
+ const empty = isParagraph(block) && regionLength(block, primaryRegion(tr.before.schema, block) ?? "") === 0;
3936
+ return empty && selection?.kind === "text" && selection.head.block === block.id;
3937
+ }
3938
+ function insertedBlocks(tr) {
3939
+ if (tr.getMeta(LINE_SPLIT) === true || tr.steps.some((step) => step.stepType === "removeBlocks")) return [];
3940
+ const inserted = insertedIds(tr);
3941
+ const { doc } = tr;
3942
+ const drawn = doc.blocks.filter((block, index) => inserted.has(block.id) && drawnBlockCount(doc, index, index + 1));
3943
+ const last2 = drawn.at(-1);
3944
+ if (drawn.length > 1 && last2 !== void 0 && isCaretLine(tr, last2)) drawn.pop();
3945
+ return drawn.map((block) => block.id);
3946
+ }
3947
+ function insertedMessage(tr, labels) {
3948
+ const [first, ...more] = insertedBlocks(tr);
3949
+ const position = first === void 0 ? null : drawnPosition(tr.doc, first);
3950
+ if (position === null) return null;
3951
+ if (more.length === 0) return labels.blockInserted(...position);
3952
+ const blocksInserted = labels.blocksInserted ?? englishLabels.blocksInserted;
3953
+ return blocksInserted(more.length + 1, ...position);
3888
3954
  }
3889
3955
  function structureAnnouncement(tr, labels, undoKeys) {
3890
3956
  if (tr.origin !== "command" || tr.getMeta(LINE_JOIN) === true) return null;
@@ -3892,36 +3958,56 @@ function structureAnnouncement(tr, labels, undoKeys) {
3892
3958
  if (moved !== null) return blockMovedMessage(tr.doc, moved, labels);
3893
3959
  const deleted = deletedCount(tr);
3894
3960
  if (deleted > 0) return `${labels.blockDeleted(deleted)}. ${labels.undoHint(undoKeys)}`;
3895
- const inserted = insertedBlock(tr);
3896
- return inserted === null ? null : labels.blockInserted(...drawnPosition(tr.doc, inserted));
3961
+ return insertedMessage(tr, labels);
3897
3962
  }
3898
3963
 
3899
3964
  // src/drag/drag-layout.ts
3900
- var INDENT_STEP_REM = 1.5;
3901
3965
  function rootOrigin(root) {
3902
3966
  const box = root.getBoundingClientRect();
3903
3967
  return { x: box.left + root.clientLeft - root.scrollLeft, y: box.top + root.clientTop - root.scrollTop };
3904
3968
  }
3905
3969
  var spanBetween = (a, b) => ({ left: Math.min(a, b), width: Math.abs(b - a) });
3906
- function indentStep(doc) {
3907
- return (parseFloat(getComputedStyle(doc.documentElement).fontSize) || 16) * INDENT_STEP_REM;
3970
+ var leadingMargin = (element, rtl) => parseFloat(getComputedStyle(element)[rtl ? "marginRight" : "marginLeft"]) || 0;
3971
+ function indentStep(shell, rtl) {
3972
+ const copies = [0, 1].map((depth) => {
3973
+ const copy = shell.cloneNode(false);
3974
+ copy.removeAttribute("id");
3975
+ copy.removeAttribute("data-block-id");
3976
+ copy.style.setProperty("--quoin-indent", String(depth));
3977
+ copy.style.display = "none";
3978
+ return copy;
3979
+ });
3980
+ shell.after(...copies);
3981
+ const [flat, nested] = copies.map((copy) => leadingMargin(copy, rtl));
3982
+ for (const copy of copies) copy.remove();
3983
+ return nested > flat ? nested - flat : parseFloat(getComputedStyle(shell).fontSize) || 16;
3908
3984
  }
3909
3985
  function inlineEdges(shell, box, rtl) {
3910
- const style = getComputedStyle(shell);
3911
- if (rtl) return { start: box.right + (parseFloat(style.marginRight) || 0), end: box.left };
3912
- return { start: box.left - (parseFloat(style.marginLeft) || 0), end: box.right };
3986
+ const margin = leadingMargin(shell, rtl);
3987
+ return rtl ? { start: box.right + margin, end: box.left } : { start: box.left - margin, end: box.right };
3913
3988
  }
3914
3989
  function measure(controller, root, moved, rtl) {
3915
3990
  const { doc } = controller.editor.state;
3916
3991
  const origin = rootOrigin(root);
3917
3992
  return visibleBlockIndexes(doc).flatMap((index) => {
3918
- const { id, indent: indent3 } = doc.blocks[index];
3919
- const shell = controller.blockElement(id);
3993
+ const block = doc.blocks[index];
3994
+ const shell = controller.blockElement(block.id);
3920
3995
  if (shell === void 0) return [];
3921
3996
  const box = shell.getBoundingClientRect();
3922
3997
  const { start, end } = inlineEdges(shell, box, rtl);
3923
- const [top, bottom] = [box.top - origin.y, box.bottom - origin.y];
3924
- return [{ id, top, bottom, start: start - origin.x, end: end - origin.x, indent: indent3, dragged: moved.has(id), shell }];
3998
+ return [
3999
+ {
4000
+ id: block.id,
4001
+ top: box.top - origin.y,
4002
+ bottom: box.bottom - origin.y,
4003
+ start: start - origin.x,
4004
+ end: end - origin.x,
4005
+ indent: block.indent,
4006
+ dragged: moved.has(block.id),
4007
+ hidesNested: isClosedToggle(block),
4008
+ shell
4009
+ }
4010
+ ];
3925
4011
  });
3926
4012
  }
3927
4013
  var furthestEnd = (blocks, rtl) => blocks.reduce((end, block) => rtl ? Math.min(end, block.end) : Math.max(end, block.end), blocks[0].end);
@@ -3936,7 +4022,7 @@ function measureDrag(controller, root, ids) {
3936
4022
  const [first] = dragged;
3937
4023
  const origin = rootOrigin(root);
3938
4024
  const viewport = root.ownerDocument.defaultView?.innerHeight ?? Infinity;
3939
- const step = indentStep(root.ownerDocument);
4025
+ const step = indentStep(first.shell, rtl);
3940
4026
  const rest = doc.blocks.filter((block) => !moved.has(block.id));
3941
4027
  const restIndex = new Map(rest.map((block, index) => [block.id, index]));
3942
4028
  const staying = blocks.filter((block) => !block.dragged);
@@ -3967,6 +4053,7 @@ function nearestGap(blocks, pointerY) {
3967
4053
  return nearest2;
3968
4054
  }
3969
4055
  function computeDropTarget({ pointerX, pointerY, blocks, indentStep: indentStep2, direction }) {
4056
+ if (blocks.length === 0) return null;
3970
4057
  const staying = blocks.filter((block) => !block.dragged);
3971
4058
  if (staying.length === 0) return { index: 0, indent: 0, lineTop: blocks[0].top, lineStart: blocks[0].start };
3972
4059
  const index = nearestGap(staying, pointerY);
@@ -3974,7 +4061,8 @@ function computeDropTarget({ pointerX, pointerY, blocks, indentStep: indentStep2
3974
4061
  if (index === 0) return { index, indent: 0, lineTop, lineStart: staying[0].start };
3975
4062
  const above = staying[index - 1];
3976
4063
  const step = direction === "rtl" ? -indentStep2 : indentStep2;
3977
- const indent3 = Math.min(Math.max(Math.round((pointerX - above.start) / step), 0), above.indent + 1);
4064
+ const deepest = above.hidesNested ? above.indent : above.indent + 1;
4065
+ const indent3 = Math.min(Math.max(Math.round((pointerX - above.start) / step), 0), deepest);
3978
4066
  return { index, indent: indent3, lineTop, lineStart: above.start + indent3 * step };
3979
4067
  }
3980
4068
 
@@ -3984,7 +4072,7 @@ var SCROLL_EDGE = 48;
3984
4072
  var sameTarget = (a, b) => a?.index === b.index && a.indent === b.indent;
3985
4073
  function draggedBlocks(controller, blockId) {
3986
4074
  const { state } = controller.editor;
3987
- return state.doc.index.has(blockId) ? menuTargets(state, blockId) : [];
4075
+ return state.doc.index.has(blockId) ? handleTargets(state, blockId) : [];
3988
4076
  }
3989
4077
  function followPress(press, root, view) {
3990
4078
  const { controller, store, overlay, handle: handle2, pointerId } = press;
@@ -4017,6 +4105,7 @@ function followPress(press, root, view) {
4017
4105
  const { indentStep: indentStep2, blocks, direction } = layout;
4018
4106
  const target = computeDropTarget({ pointerX: layout.draggedStart + dx, pointerY, blocks, indentStep: indentStep2, direction });
4019
4107
  overlay.follow(dx, pointer.y - press.y);
4108
+ if (target === null) return;
4020
4109
  overlay.mark({ top: target.lineTop, ...spanBetween(target.lineStart, layout.end) });
4021
4110
  if (!sameTarget(active.target, target)) {
4022
4111
  store.set({ drag: { blockIds: ids, targetIndent: target.indent } });
@@ -4027,6 +4116,7 @@ function followPress(press, root, view) {
4027
4116
  if (done) return;
4028
4117
  done = true;
4029
4118
  unlistenDocument();
4119
+ unlistenScroll();
4030
4120
  unlistenBlur();
4031
4121
  unlistenKeys();
4032
4122
  scroll.stop();
@@ -4065,6 +4155,7 @@ function followPress(press, root, view) {
4065
4155
  finish();
4066
4156
  }
4067
4157
  const unlistenDocument = listen(doc, { pointermove: onPointerMove, pointerup: onPointerUp, pointercancel: finish });
4158
+ const unlistenScroll = listen(doc, { scroll: follow }, true);
4068
4159
  const unlistenBlur = listen(view, { blur: finish });
4069
4160
  const unlistenKeys = listen(view, { keydown: onKeyDown }, true);
4070
4161
  return finish;
@@ -4101,9 +4192,9 @@ function useBlockDrag(controller, store, blockId, overlay) {
4101
4192
  onDragStart: () => {
4102
4193
  dragged.current = true;
4103
4194
  },
4104
- // The canvas has already said where a drop moved the block, and the announcer says the same words for the
4105
- // same state once, so this is heard only for a drop that nested the block in place.
4106
- onMoved: (movedId) => controller.announce(blockMovedMessage(controller.editor.state.doc, movedId, labels))
4195
+ // The canvas has already said where a drop moved the block, so this is heard only for a drop that nested the
4196
+ // block in place.
4197
+ onMoved: (movedId) => announceBlockMoved(controller, movedId, labels)
4107
4198
  });
4108
4199
  },
4109
4200
  takeClick(event) {
@@ -4113,15 +4204,42 @@ function useBlockDrag(controller, store, blockId, overlay) {
4113
4204
  }
4114
4205
  };
4115
4206
  }
4207
+ var listOf = (query) => typeof window.matchMedia === "function" ? window.matchMedia(query) : null;
4208
+ var readOnServer = () => false;
4209
+ function useMediaQuery(query, unsupported) {
4210
+ const subscribe3 = useCallback(
4211
+ (notify) => {
4212
+ const list2 = listOf(query);
4213
+ list2?.addEventListener("change", notify);
4214
+ return () => list2?.removeEventListener("change", notify);
4215
+ },
4216
+ [query]
4217
+ );
4218
+ const read2 = () => listOf(query)?.matches ?? unsupported;
4219
+ return useSyncExternalStore(subscribe3, read2, readOnServer);
4220
+ }
4221
+
4222
+ // src/mobile/use-coarse-pointer.ts
4223
+ var useCoarsePointer = () => useMediaQuery("(pointer: coarse)", false);
4116
4224
 
4117
4225
  // src/gutter/add-block-below.ts
4226
+ var paragraphAfter = (blockId) => (state, dispatch) => {
4227
+ const { doc, schema } = state;
4228
+ const index = requireBlockIndex(doc, blockId);
4229
+ const paragraph = createBlock(schema, "MARKDOWN", { kind: "paragraph" }, doc.blocks[index].indent);
4230
+ const tr = new Transaction(state).step(insertBlocksStep(descendantsEnd(doc, index), [paragraph]));
4231
+ dispatch?.(tr.setSelection(startOf(tr.doc, schema, paragraph.id)));
4232
+ return true;
4233
+ };
4118
4234
  function addBlockBelow(controller, blockId) {
4119
4235
  const { state } = controller.editor;
4120
4236
  const { doc, schema } = state;
4121
- const block = blockById(doc, blockId);
4122
- if (block === void 0) return false;
4123
- const empty = isParagraph(block) && regionLength(block, primaryRegion(schema, block) ?? "") === 0;
4124
- const fill = empty ? setSelection(startOf(doc, schema, blockId)) : insertBlockAfter("MARKDOWN", { kind: "paragraph" });
4237
+ const index = doc.index.get(blockId);
4238
+ if (index === void 0) return false;
4239
+ const block = doc.blocks[index];
4240
+ const alone = descendantsEnd(doc, index) === index + 1;
4241
+ const empty = alone && isParagraph(block) && regionLength(block, primaryRegion(schema, block) ?? "") === 0;
4242
+ const fill = empty ? setSelection(startOf(doc, schema, blockId)) : paragraphAfter(blockId);
4125
4243
  const slash = insertText("/", { origin: "command" });
4126
4244
  const { ok, tr } = runCommands(state, [setSelection(blockRangeSelection(blockId)), fill, slash]);
4127
4245
  if (ok && tr !== null) controller.dispatch(tr);
@@ -4172,6 +4290,20 @@ function BlockMenu({ controller, store, actions, onOpen, children }) {
4172
4290
  );
4173
4291
  }
4174
4292
 
4293
+ // src/ui/watch-direction.ts
4294
+ var TURNS_ROUND = { attributeFilter: ["dir", "class", "style"] };
4295
+ function watchDirection(element, onChange) {
4296
+ let current = inlineDirection(element);
4297
+ const observer = new MutationObserver(() => {
4298
+ const next = inlineDirection(element);
4299
+ if (next === current) return;
4300
+ current = next;
4301
+ onChange(next);
4302
+ });
4303
+ for (let node = element; node !== null; node = node.parentElement) observer.observe(node, TURNS_ROUND);
4304
+ return () => observer.disconnect();
4305
+ }
4306
+
4175
4307
  // src/gutter/use-gutter-position.ts
4176
4308
  var GAP = 4;
4177
4309
  var VOID_LINE = 24;
@@ -4214,12 +4346,14 @@ function useGutterPosition(controller, gutter, block) {
4214
4346
  frame = view.requestAnimationFrame(update);
4215
4347
  };
4216
4348
  const unsubscribe = controller.editor.subscribe(later);
4349
+ const unwatch = watchDirection(root, later);
4217
4350
  const observer = new ResizeObserver(later);
4218
4351
  observer.observe(root);
4219
4352
  view.addEventListener("resize", later);
4220
4353
  return () => {
4221
4354
  view.cancelAnimationFrame(frame);
4222
4355
  unsubscribe();
4356
+ unwatch();
4223
4357
  observer.disconnect();
4224
4358
  view.removeEventListener("resize", later);
4225
4359
  };
@@ -4261,6 +4395,7 @@ var GUTTER = "absolute top-0 left-0 z-10 flex items-center gap-0.5 pointer-coars
4261
4395
  var ADD_BELOW = "text-muted-foreground pointer-coarse:hidden";
4262
4396
  function Gutter({ controller, store, actions }) {
4263
4397
  const labels = useLabels();
4398
+ const coarse = useCoarsePointer();
4264
4399
  const element = useRef(null);
4265
4400
  const overlay = useRef(null);
4266
4401
  const menuOpenAtPress = useRef(false);
@@ -4306,7 +4441,7 @@ function Gutter({ controller, store, actions }) {
4306
4441
  variant: "ghost",
4307
4442
  size: null,
4308
4443
  tabIndex: -1,
4309
- "aria-label": labels.dragHandle,
4444
+ "aria-label": coarse ? labels.blockOptions ?? englishLabels.blockOptions : labels.dragHandle,
4310
4445
  className: cn("text-muted-foreground h-6 w-4 cursor-grab rounded-md", SQUARE_TAP_TARGET_FLOOR),
4311
4446
  onPointerDown: (event) => {
4312
4447
  menuOpenAtPress.current = event.button === 0 && store.get().blockMenu?.anchor === "handle";
@@ -4328,17 +4463,6 @@ function Gutter({ controller, store, actions }) {
4328
4463
  ] });
4329
4464
  }
4330
4465
 
4331
- // src/i18n/resolve-labels.ts
4332
- var isRecord = (value) => typeof value === "object" && value !== null;
4333
- function ontoRecords(overrides) {
4334
- const taken = Object.entries(overrides).map(([key, value]) => {
4335
- const fallback = englishLabels[key];
4336
- return [key, isRecord(value) && isRecord(fallback) ? { ...fallback, ...value } : value];
4337
- });
4338
- return Object.fromEntries(taken);
4339
- }
4340
- var resolveLabels = (overrides) => mergeLabels(englishLabels, overrides && ontoRecords(overrides));
4341
-
4342
4466
  // src/menus/floating/range-rect.ts
4343
4467
  var TEXT_NODE = 3;
4344
4468
  function lineBox(rect, host) {
@@ -4457,6 +4581,39 @@ function AlignDropdown({ controller, state }) {
4457
4581
  labels[LABEL_KEYS[align]]
4458
4582
  ] }, align)) }) });
4459
4583
  }
4584
+
4585
+ // src/toolbar/toolbar-items.ts
4586
+ function markItem(mark, title, icon) {
4587
+ return {
4588
+ id: mark,
4589
+ title,
4590
+ icon,
4591
+ isActive: (state) => isActive(state, mark),
4592
+ run: (controller) => void controller.run(toggleMark(mark))
4593
+ };
4594
+ }
4595
+ var linkItem = (labels, openLink) => ({
4596
+ id: "link",
4597
+ title: labels.link,
4598
+ icon: Link,
4599
+ opensDialog: true,
4600
+ run: openLink
4601
+ });
4602
+
4603
+ // src/menus/bubble/bubble-items.ts
4604
+ function bubbleItems(labels, openLink) {
4605
+ return {
4606
+ inline: [
4607
+ markItem("bold", labels.bold, Bold),
4608
+ markItem("italic", labels.italic, Italic),
4609
+ markItem("underline", labels.underline, Underline),
4610
+ markItem("strike", labels.strike, Strikethrough),
4611
+ markItem("code", labels.code, Code),
4612
+ linkItem(labels, openLink)
4613
+ ],
4614
+ scripts: [markItem("sub", labels.subscript, Subscript), markItem("sup", labels.superscript, Superscript)]
4615
+ };
4616
+ }
4460
4617
  var MARKS = { color: "color", background: "highlight" };
4461
4618
  function selectionColor(state, mark) {
4462
4619
  const names = /* @__PURE__ */ new Set();
@@ -4486,6 +4643,22 @@ function TurnIntoDropdown({ controller, state }) {
4486
4643
  ] });
4487
4644
  return /* @__PURE__ */ jsx(ToolbarMenu, { controller, trigger, children: /* @__PURE__ */ jsx(TurnIntoItems, { kit: DROPDOWN_KIT, controller, blockIds: selectedBlockIds(state.doc, state.selection) }) });
4488
4645
  }
4646
+ function useFocusBackToText(controller, holds = isBubblePart) {
4647
+ const latest = useRef(holds);
4648
+ useIsomorphicLayoutEffect(() => {
4649
+ latest.current = holds;
4650
+ });
4651
+ useIsomorphicLayoutEffect(
4652
+ () => () => {
4653
+ const doc = controller.root?.ownerDocument;
4654
+ if (!doc || !latest.current(doc.activeElement, controller)) return;
4655
+ queueMicrotask(() => {
4656
+ if (doc.activeElement === null || doc.activeElement === doc.body) controller.focus();
4657
+ });
4658
+ },
4659
+ [controller]
4660
+ );
4661
+ }
4489
4662
  var DIVIDER = "mx-0.5 h-5 w-px shrink-0 bg-border";
4490
4663
  function BubbleToolbar({ controller, labels, store, extraItems }) {
4491
4664
  const state = useEditorState(controller, wholeState);
@@ -4493,6 +4666,7 @@ function BubbleToolbar({ controller, labels, store, extraItems }) {
4493
4666
  const roving = useRovingFocus();
4494
4667
  const items = useMemo(() => bubbleItems(labels, () => store.set({ linkPopover: { open: true } })), [labels, store]);
4495
4668
  useFocusKey(controller, roving.focusCurrent);
4669
+ useFocusBackToText(controller);
4496
4670
  const onKeyDown = (event) => {
4497
4671
  if (!roving.owns(event.target)) return;
4498
4672
  if (event.key === "Escape" || event.key === "Tab" && !event.altKey && !event.ctrlKey && !event.metaKey) {
@@ -4531,20 +4705,6 @@ function BubbleToolbar({ controller, labels, store, extraItems }) {
4531
4705
  )
4532
4706
  ] });
4533
4707
  }
4534
- var listOf = (query) => typeof window.matchMedia === "function" ? window.matchMedia(query) : null;
4535
- var readOnServer = () => false;
4536
- function useMediaQuery(query, unsupported) {
4537
- const subscribe3 = useCallback(
4538
- (notify) => {
4539
- const list2 = listOf(query);
4540
- list2?.addEventListener("change", notify);
4541
- return () => list2?.removeEventListener("change", notify);
4542
- },
4543
- [query]
4544
- );
4545
- const read2 = () => listOf(query)?.matches ?? unsupported;
4546
- return useSyncExternalStore(subscribe3, read2, readOnServer);
4547
- }
4548
4708
 
4549
4709
  // src/menus/bubble/use-fine-pointer.ts
4550
4710
  var useFinePointer = () => useMediaQuery("(pointer: fine)", true);
@@ -4590,6 +4750,12 @@ function linkAtSelection(state) {
4590
4750
  const { from, to } = orderedRange(state.doc, selection.anchor, selection.head);
4591
4751
  return linkAround(state, from, to);
4592
4752
  }
4753
+ function sameLink(a, b) {
4754
+ if (a === null || b === null) return a === b;
4755
+ const [x, y] = [a.range, b.range];
4756
+ const sameRange2 = x.block === y.block && x.region === y.region && x.from === y.from && x.to === y.to;
4757
+ return sameRange2 && a.href === b.href && a.title === b.title;
4758
+ }
4593
4759
 
4594
4760
  // src/menus/link/link-element.ts
4595
4761
  var LINK = "a[data-quoin-link]";
@@ -4604,13 +4770,18 @@ function linkAtElement(controller, state, element) {
4604
4770
  return linkAround(state, { block: host.block, region: host.region, offset });
4605
4771
  }
4606
4772
  var sameStart = (a, b) => a.block === b.block && a.region === b.region && a.from === b.from;
4607
- function sameLink(controller, a, b) {
4773
+ function sameLink2(controller, a, b) {
4608
4774
  if (a === b || a === null || b === null) return a === b;
4609
4775
  const { state } = controller.editor;
4610
4776
  const first = linkAtElement(controller, state, a)?.range;
4611
4777
  const second = linkAtElement(controller, state, b)?.range;
4612
4778
  return first !== void 0 && second !== void 0 && sameStart(first, second);
4613
4779
  }
4780
+ function OpenLinkButton({ href, label }) {
4781
+ const safe = sanitizeHref(href);
4782
+ if (!safe) return null;
4783
+ return /* @__PURE__ */ jsx(Button, { asChild: true, variant: "ghost", size: "icon-sm", children: /* @__PURE__ */ jsx("a", { href: safe, target: "_blank", rel: "noopener noreferrer", "aria-label": label, children: /* @__PURE__ */ jsx(ExternalLink, {}) }) });
4784
+ }
4614
4785
 
4615
4786
  // src/menus/link/truncate-middle.ts
4616
4787
  var ELLIPSIS = "\u2026";
@@ -4671,7 +4842,7 @@ function usePointerLink(controller, card, delay) {
4671
4842
  const onCard = !!card.current?.contains(event.target);
4672
4843
  const next = onCard ? shown.current : linkElementAt(controller, event.target);
4673
4844
  clearTimeout(timer.current);
4674
- if (!sameLink(controller, next, shown.current)) timer.current = setTimeout(() => show(next), delay);
4845
+ if (!sameLink2(controller, next, shown.current)) timer.current = setTimeout(() => show(next), delay);
4675
4846
  };
4676
4847
  document.addEventListener("pointerover", onOver);
4677
4848
  return () => {
@@ -4682,15 +4853,16 @@ function usePointerLink(controller, card, delay) {
4682
4853
  return { element, dismiss: useCallback(() => show(null), [show]) };
4683
4854
  }
4684
4855
  var REST_MS = 300;
4856
+ var OPEN_POPOVER = "Mod-k";
4685
4857
  var URL_LENGTH = 48;
4686
4858
  var CARD = "flex w-auto items-center gap-0.5 py-1 ps-3 pe-1 shadow-md";
4687
4859
  var ADDRESS = "me-1 whitespace-nowrap text-sm text-muted-foreground";
4688
4860
  function LinkCard({ controller, labels, store, cardRef, element, onUnlinked }) {
4689
- const state = useEditorState(controller, wholeState);
4690
- const link = useMemo(
4691
- () => element === null ? linkAtSelection(state) : linkAtElement(controller, state, element),
4692
- [controller, state, element]
4861
+ const select = useCallback(
4862
+ (state) => element === null ? linkAtSelection(state) : linkAtElement(controller, state, element),
4863
+ [controller, element]
4693
4864
  );
4865
+ const link = useEditorState(controller, select, sameLink);
4694
4866
  const anchor = useMemo(() => {
4695
4867
  if (link === null) return { current: null };
4696
4868
  const { range } = link;
@@ -4699,7 +4871,6 @@ function LinkCard({ controller, labels, store, cardRef, element, onUnlinked }) {
4699
4871
  }, [controller, link]);
4700
4872
  if (link === null) return null;
4701
4873
  const { block, region, from, to } = link.range;
4702
- const href = sanitizeHref(link.href);
4703
4874
  const address = truncateMiddle(link.href, URL_LENGTH);
4704
4875
  const edit2 = () => {
4705
4876
  if (element !== null) {
@@ -4718,6 +4889,7 @@ function LinkCard({ controller, labels, store, cardRef, element, onUnlinked }) {
4718
4889
  PopoverContent,
4719
4890
  {
4720
4891
  ref: cardRef,
4892
+ role: "group",
4721
4893
  "aria-label": labels.link,
4722
4894
  side: "bottom",
4723
4895
  align: "start",
@@ -4728,8 +4900,19 @@ function LinkCard({ controller, labels, store, cardRef, element, onUnlinked }) {
4728
4900
  onMouseDown: keepFocus,
4729
4901
  children: [
4730
4902
  /* @__PURE__ */ jsx("span", { className: ADDRESS, title: address === link.href ? void 0 : link.href, children: address }),
4731
- href !== null && /* @__PURE__ */ jsx(Button, { asChild: true, variant: "ghost", size: "icon-sm", children: /* @__PURE__ */ jsx("a", { href, target: "_blank", rel: "noopener noreferrer", "aria-label": labels.linkOpen, children: /* @__PURE__ */ jsx(ExternalLink, {}) }) }),
4732
- /* @__PURE__ */ jsx(Button, { type: "button", variant: "ghost", size: "icon-sm", "aria-label": labels.linkEdit, onClick: edit2, children: /* @__PURE__ */ jsx(Pencil, {}) }),
4903
+ /* @__PURE__ */ jsx(OpenLinkButton, { href: link.href, label: labels.linkOpen }),
4904
+ /* @__PURE__ */ jsx(
4905
+ Button,
4906
+ {
4907
+ type: "button",
4908
+ variant: "ghost",
4909
+ size: "icon-sm",
4910
+ "aria-label": labels.linkEdit,
4911
+ "aria-keyshortcuts": shortcutHint(OPEN_POPOVER, controller.isMac, labels).aria,
4912
+ onClick: edit2,
4913
+ children: /* @__PURE__ */ jsx(Pencil, {})
4914
+ }
4915
+ ),
4733
4916
  /* @__PURE__ */ jsx(Button, { type: "button", variant: "ghost", size: "icon-sm", "aria-label": labels.linkRemove, onClick: unlink, children: /* @__PURE__ */ jsx(Unlink, {}) })
4734
4917
  ]
4735
4918
  }
@@ -4769,6 +4952,7 @@ function applyLink(href, value) {
4769
4952
  var CONTENT2 = "w-80 p-2 shadow-md";
4770
4953
  var FIELD2 = "h-8";
4771
4954
  var MESSAGE = "mt-1.5 px-1 text-sm text-destructive";
4955
+ var CONTROLS = "input, button, a[href]";
4772
4956
  function popoverAnchor(controller, link) {
4773
4957
  const { selection } = controller.editor.state;
4774
4958
  if (selection?.kind !== "text") return null;
@@ -4786,7 +4970,8 @@ function LinkPopover({ controller, labels, onClose }) {
4786
4970
  const [invalid, setInvalid] = useState(false);
4787
4971
  const messageId = useId();
4788
4972
  const anchor = useMemo(() => ({ current: popoverAnchor(controller, link) }), [controller, link]);
4789
- const openHref = link && sanitizeHref(link.href);
4973
+ const content = useRef(null);
4974
+ useFocusBackToText(controller, (focused) => focused !== null && content.current?.contains(focused) === true);
4790
4975
  const backToText = () => {
4791
4976
  onClose();
4792
4977
  controller.focus();
@@ -4798,7 +4983,11 @@ function LinkPopover({ controller, labels, onClose }) {
4798
4983
  const apply = (event) => {
4799
4984
  event.preventDefault();
4800
4985
  const href = linkAddress(address);
4801
- if (href === null) return setInvalid(true);
4986
+ if (href === null) {
4987
+ setInvalid(true);
4988
+ controller.announce(labels.invalidLink);
4989
+ return;
4990
+ }
4802
4991
  controller.run(applyLink(href, address.trim()));
4803
4992
  backToText();
4804
4993
  };
@@ -4806,6 +4995,13 @@ function LinkPopover({ controller, labels, onClose }) {
4806
4995
  controller.run(unsetLink);
4807
4996
  backToText();
4808
4997
  };
4998
+ const tabOut = (event) => {
4999
+ if (event.key !== "Tab" || event.ctrlKey || event.metaKey) return;
5000
+ const controls = [...event.currentTarget.querySelectorAll(CONTROLS)];
5001
+ if (event.target !== (event.shiftKey ? controls[0] : controls.at(-1))) return;
5002
+ event.preventDefault();
5003
+ backToText();
5004
+ };
4809
5005
  return /* @__PURE__ */ jsxs(
4810
5006
  Popover,
4811
5007
  {
@@ -4819,6 +5015,7 @@ function LinkPopover({ controller, labels, onClose }) {
4819
5015
  /* @__PURE__ */ jsxs(
4820
5016
  PopoverContent,
4821
5017
  {
5018
+ ref: content,
4822
5019
  "aria-label": labels.link,
4823
5020
  side: "bottom",
4824
5021
  align: "start",
@@ -4828,6 +5025,7 @@ function LinkPopover({ controller, labels, onClose }) {
4828
5025
  event.preventDefault();
4829
5026
  backToText();
4830
5027
  },
5028
+ onKeyDown: tabOut,
4831
5029
  children: [
4832
5030
  /* @__PURE__ */ jsxs("form", { noValidate: true, className: "flex items-center gap-0.5", onSubmit: apply, children: [
4833
5031
  /* @__PURE__ */ jsx(
@@ -4843,10 +5041,10 @@ function LinkPopover({ controller, labels, onClose }) {
4843
5041
  }
4844
5042
  ),
4845
5043
  /* @__PURE__ */ jsx(Button, { type: "submit", variant: "ghost", size: "icon-sm", "aria-label": labels.linkApply, children: /* @__PURE__ */ jsx(Check, {}) }),
4846
- openHref && /* @__PURE__ */ jsx(Button, { asChild: true, variant: "ghost", size: "icon-sm", children: /* @__PURE__ */ jsx("a", { href: openHref, target: "_blank", rel: "noopener noreferrer", "aria-label": labels.linkOpen, children: /* @__PURE__ */ jsx(ExternalLink, {}) }) }),
5044
+ link !== null && /* @__PURE__ */ jsx(OpenLinkButton, { href: link.href, label: labels.linkOpen }),
4847
5045
  link !== null && /* @__PURE__ */ jsx(Button, { type: "button", variant: "ghost", size: "icon-sm", "aria-label": labels.linkRemove, onClick: remove, children: /* @__PURE__ */ jsx(Unlink, {}) })
4848
5046
  ] }),
4849
- invalid && /* @__PURE__ */ jsx("p", { id: messageId, role: "alert", className: MESSAGE, children: labels.invalidLink })
5047
+ invalid && /* @__PURE__ */ jsx("p", { id: messageId, className: MESSAGE, children: labels.invalidLink })
4850
5048
  ]
4851
5049
  }
4852
5050
  )
@@ -4903,7 +5101,8 @@ function triggerOffset(text2, start, config) {
4903
5101
  if (character === "\n" || character === ATOM) return null;
4904
5102
  if (character === char) {
4905
5103
  const atRegionStart = start + i === 0;
4906
- if (atRegionStart || !startOfLine && i > 0 && WHITESPACE2.test(text2[i - 1])) return start + i;
5104
+ const opens2 = atRegionStart || !startOfLine && i > 0 && WHITESPACE2.test(text2[i - 1]);
5105
+ if (opens2) return WHITESPACE2.test(text2[i + 1] ?? "") ? null : start + i;
4907
5106
  }
4908
5107
  if (!allowSpaces && WHITESPACE2.test(character)) return null;
4909
5108
  }
@@ -4934,11 +5133,13 @@ function stillDismissed(tr, dismissed, found) {
4934
5133
  const same = found.block === at.block && found.region === at.region && found.from === at.offset;
4935
5134
  return same ? rangeOf2(found) : null;
4936
5135
  }
4937
- function nearest(state, found) {
4938
- if (found === null) return null;
5136
+ var opens = (found, { minLength = 0 }) => found !== null && found.query.length >= minLength;
5137
+ function nearest(state, found, config) {
5138
+ if (!opens(found, config)) return null;
4939
5139
  for (const { trigger } of state.plugins) {
4940
- const other = trigger && findTrigger(state, trigger);
4941
- if (other && other.from > found.from) return null;
5140
+ if (trigger === void 0) continue;
5141
+ const other = findTrigger(state, trigger);
5142
+ if (opens(other, trigger) && other.from > found.from) return null;
4942
5143
  }
4943
5144
  return found;
4944
5145
  }
@@ -4946,15 +5147,15 @@ function suggestionPlugin(key, config) {
4946
5147
  return {
4947
5148
  key,
4948
5149
  trigger: config,
4949
- init: (state) => ({ active: nearest(state, findTrigger(state, config)), dismissedAt: null }),
5150
+ init: (state) => ({ active: nearest(state, findTrigger(state, config), config), dismissedAt: null }),
4950
5151
  apply(tr, value, oldState, newState) {
4951
- const dismissing = tr.getMeta(DISMISS) === key.name;
5152
+ const dismissing = tr.getMeta(DISMISS) !== void 0;
4952
5153
  if (!dismissing && !tr.docChanged && sameSelection(oldState.selection, newState.selection)) return value;
4953
5154
  const found = findTrigger(newState, config);
4954
5155
  let dismissedAt = null;
4955
5156
  if (dismissing) dismissedAt = found && rangeOf2(found);
4956
5157
  else if (value.dismissedAt !== null) dismissedAt = stillDismissed(tr, value.dismissedAt, found);
4957
- const active = dismissedAt === null ? nearest(newState, found) : null;
5158
+ const active = dismissedAt === null ? nearest(newState, found, config) : null;
4958
5159
  const unchanged = sameTrigger(active, value.active) && sameRange(dismissedAt, value.dismissedAt);
4959
5160
  if (unchanged) return value;
4960
5161
  return {
@@ -4997,7 +5198,7 @@ function insertionItem(id, insertion, labels, definitions) {
4997
5198
  const { label, icon, type, attrs } = insertion;
4998
5199
  return {
4999
5200
  id,
5000
- title: label,
5201
+ title: hostLabel(definitions, type) ?? label,
5001
5202
  description: labels.slashDescriptions[id],
5002
5203
  icon,
5003
5204
  ...DETAILS[id],
@@ -5061,33 +5262,59 @@ function useRangeAnchor(controller, range) {
5061
5262
  }, [controller, start]);
5062
5263
  return { anchor, side };
5063
5264
  }
5265
+
5266
+ // src/to-error.ts
5267
+ var toError = (cause) => cause instanceof Error ? cause : new Error(String(cause));
5064
5268
  var ADDED = ["aria-expanded", "aria-controls", "aria-autocomplete", "aria-activedescendant"];
5065
5269
  var SWAPPED = ["role", "aria-multiline"];
5066
5270
  function restore(host, name, value) {
5067
5271
  if (value === null) host.removeAttribute(name);
5068
5272
  else host.setAttribute(name, value);
5069
5273
  }
5070
- function openCombobox(host, listboxId) {
5071
- const saved = SWAPPED.map((name) => [name, host.getAttribute(name)]);
5274
+ var held = /* @__PURE__ */ new WeakMap();
5275
+ function makeCombobox(host, listboxId) {
5072
5276
  host.setAttribute("role", "combobox");
5073
5277
  host.removeAttribute("aria-multiline");
5074
5278
  host.setAttribute("aria-expanded", "true");
5075
5279
  host.setAttribute("aria-controls", listboxId);
5076
5280
  host.setAttribute("aria-autocomplete", "list");
5281
+ }
5282
+ function openCombobox(host, listboxId) {
5283
+ const saved = () => SWAPPED.map((name) => [name, host.getAttribute(name)]);
5284
+ const entry = held.get(host) ?? { saved: saved(), listboxes: [] };
5285
+ held.set(host, entry);
5286
+ entry.listboxes.push(listboxId);
5287
+ makeCombobox(host, listboxId);
5077
5288
  return () => {
5289
+ entry.listboxes.splice(entry.listboxes.lastIndexOf(listboxId), 1);
5290
+ const remaining = entry.listboxes.at(-1);
5291
+ if (remaining !== void 0) return makeCombobox(host, remaining);
5292
+ held.delete(host);
5078
5293
  for (const name of ADDED) host.removeAttribute(name);
5079
- for (const [name, value] of saved) restore(host, name, value);
5294
+ for (const [name, value] of entry.saved) restore(host, name, value);
5080
5295
  };
5081
5296
  }
5082
5297
  function useComboboxHost(controller, block, region, listboxId, activeOptionId) {
5083
- const host = useRef(null);
5084
- useIsomorphicLayoutEffect(() => {
5085
- host.current = block === null || region === null ? null : controller.registry.get(block, region) ?? null;
5086
- return host.current === null ? void 0 : openCombobox(host.current, listboxId);
5087
- }, [controller, block, region, listboxId]);
5298
+ const open = block !== null && region !== null;
5299
+ const docWhileOpen = useCallback((state) => open ? state.doc : null, [open]);
5300
+ useEditorState(controller, docWhileOpen);
5301
+ const opened = useRef(null);
5088
5302
  useIsomorphicLayoutEffect(() => {
5089
- if (host.current !== null) restore(host.current, "aria-activedescendant", activeOptionId);
5090
- }, [block, region, activeOptionId]);
5303
+ const host = open ? controller.registry.get(block, region) ?? null : null;
5304
+ const last2 = opened.current;
5305
+ if (last2?.host !== host || last2.listboxId !== listboxId) {
5306
+ last2?.close();
5307
+ opened.current = host && { host, listboxId, close: openCombobox(host, listboxId) };
5308
+ }
5309
+ if (opened.current !== null) restore(opened.current.host, "aria-activedescendant", activeOptionId);
5310
+ });
5311
+ useIsomorphicLayoutEffect(
5312
+ () => () => {
5313
+ opened.current?.close();
5314
+ opened.current = null;
5315
+ },
5316
+ []
5317
+ );
5091
5318
  }
5092
5319
 
5093
5320
  // src/menus/suggestion/group-items.ts
@@ -5144,11 +5371,6 @@ function useSuggestionKeys(controller, open, menu) {
5144
5371
  return interceptKeys(controller, intercept);
5145
5372
  }, [controller, open]);
5146
5373
  }
5147
-
5148
- // src/to-error.ts
5149
- var toError = (cause) => cause instanceof Error ? cause : new Error(String(cause));
5150
-
5151
- // src/menus/suggestion/use-suggestion-results.ts
5152
5374
  var FORGOTTEN = { source: null, query: null, items: [] };
5153
5375
  var CLOSED = { items: [], pending: false };
5154
5376
  var isPromise = (value) => typeof value?.then === "function";
@@ -5199,7 +5421,8 @@ function useSuggestion({
5199
5421
  itemKey: itemKey2,
5200
5422
  groupOf: groupOf2,
5201
5423
  onSelect,
5202
- emptyLabel
5424
+ emptyLabel,
5425
+ countLabel
5203
5426
  }) {
5204
5427
  const { labels, onError } = useQuoin();
5205
5428
  const readOnly = useReadOnly(controller);
@@ -5227,19 +5450,31 @@ function useSuggestion({
5227
5450
  const select = useCallback(
5228
5451
  (index) => {
5229
5452
  const current = getPluginState(controller.editor.state, pluginKey).active;
5230
- if (current !== null && index < ordered.length) onSelect(ordered[index], rangeOf2(current));
5453
+ if (current === null || index >= ordered.length) return;
5454
+ try {
5455
+ onSelect(ordered[index], rangeOf2(current));
5456
+ } catch (error) {
5457
+ onError(toError(error));
5458
+ }
5231
5459
  },
5232
- [controller, pluginKey, ordered, onSelect]
5460
+ [controller, pluginKey, ordered, onSelect, onError]
5233
5461
  );
5234
5462
  const dismiss = useCallback(() => void controller.run(dismissSuggestion(pluginKey)), [controller, pluginKey]);
5235
5463
  useSuggestionKeys(controller, trigger !== null, { count: ordered.length, activeIndex, highlight, select, dismiss });
5236
5464
  useComboboxHost(controller, trigger?.block ?? null, trigger?.region ?? null, listboxId, activeId);
5237
5465
  const query = trigger?.query ?? null;
5238
5466
  const count = ordered.length;
5239
- const resultMessage = count === 0 ? emptyLabel : labels.resultCount(count);
5467
+ const resultMessage = count === 0 ? emptyLabel : (countLabel ?? labels.resultCount)(count);
5468
+ const said = useRef(null);
5240
5469
  useEffect(() => {
5241
5470
  if (query === null || results.pending) return;
5242
- const timer = setTimeout(() => controller.announce(resultMessage), SETTLE_MS);
5471
+ const timer = setTimeout(() => {
5472
+ const current = controller.announcer.current()?.message;
5473
+ const own = current === void 0 || current === said.current;
5474
+ const message = own ? resultMessage : `${current}. ${resultMessage}`;
5475
+ said.current = message;
5476
+ controller.announce(message);
5477
+ }, SETTLE_MS);
5243
5478
  return () => clearTimeout(timer);
5244
5479
  }, [controller, query, resultMessage, results.pending]);
5245
5480
  return { trigger, sections, pending: results.pending, activeIndex, listboxId, optionId, highlight, select, dismiss };
@@ -5469,7 +5704,7 @@ function slashExtension() {
5469
5704
 
5470
5705
  // src/mobile/mobile-items.tsx
5471
5706
  function commandItem(id, title, icon, command) {
5472
- return { id, title, icon, run: (controller) => void controller.run(command) };
5707
+ return { id, title, icon, enabled: (state) => command(state), run: (controller) => void controller.run(command) };
5473
5708
  }
5474
5709
  function addBlockAtCaret(controller) {
5475
5710
  const block = headBlockId(controller.editor.state.selection);
@@ -5491,9 +5726,6 @@ function mobileItems(labels, openLink) {
5491
5726
  ]
5492
5727
  };
5493
5728
  }
5494
-
5495
- // src/mobile/use-coarse-pointer.ts
5496
- var useCoarsePointer = () => useMediaQuery("(pointer: coarse)", false);
5497
5729
  var FOCUS_EVENTS = ["focusin", "focusout"];
5498
5730
  function subscribe(notify) {
5499
5731
  for (const type of FOCUS_EVENTS) document.addEventListener(type, notify);
@@ -5506,6 +5738,7 @@ function useEditorFocus(controller) {
5506
5738
  const read2 = useCallback(() => focusInEditor(controller), [controller]);
5507
5739
  return useSyncExternalStore(subscribe, read2, NOT_FOCUSED);
5508
5740
  }
5741
+ var KEYS3 = ["offsetTop", "offsetLeft", "height", "width", "scale"];
5509
5742
  var EVENTS = ["resize", "scroll"];
5510
5743
  function subscribe2(notify) {
5511
5744
  const viewport = window.visualViewport;
@@ -5520,10 +5753,8 @@ var last = null;
5520
5753
  function read() {
5521
5754
  const viewport = window.visualViewport;
5522
5755
  if (!viewport) return null;
5523
- const { offsetTop, height, width } = viewport;
5524
- if (last?.offsetTop !== offsetTop || last.height !== height || last.width !== width) {
5525
- last = { offsetTop, height, width };
5526
- }
5756
+ const box = Object.fromEntries(KEYS3.map((key) => [key, viewport[key]]));
5757
+ if (last === null || KEYS3.some((key) => last?.[key] !== box[key])) last = box;
5527
5758
  return last;
5528
5759
  }
5529
5760
  var readOnServer2 = () => null;
@@ -5536,7 +5767,16 @@ var TOOLBAR = [
5536
5767
  ].join(" ");
5537
5768
  function placement(viewport) {
5538
5769
  if (viewport === null) return { bottom: 0 };
5539
- return { top: viewport.offsetTop + viewport.height, width: viewport.width, transform: "translateY(-100%)" };
5770
+ const { offsetLeft, offsetTop, height, width, scale } = viewport;
5771
+ const shift = "translateY(-100%)";
5772
+ return {
5773
+ top: offsetTop + height,
5774
+ left: offsetLeft,
5775
+ right: "auto",
5776
+ width: width * scale,
5777
+ transform: scale === 1 ? shift : `scale(${1 / scale}) ${shift}`,
5778
+ transformOrigin: "0 0"
5779
+ };
5540
5780
  }
5541
5781
  function EditingTools({ controller }) {
5542
5782
  const labels = useLabels();
@@ -5544,6 +5784,9 @@ function EditingTools({ controller }) {
5544
5784
  const state = useEditorState(controller, wholeState);
5545
5785
  const viewport = useVisualViewport();
5546
5786
  const roving = useRovingFocus();
5787
+ const dir = useDirection();
5788
+ useFocusBackToText(controller);
5789
+ useFocusKey(controller, roving.focusCurrent);
5547
5790
  const items = useMemo(() => mobileItems(labels, () => store.set({ linkPopover: { open: true } })), [labels, store]);
5548
5791
  const onKeyDown = (event) => {
5549
5792
  if (event.key === "Escape" && roving.owns(event.target)) {
@@ -5557,6 +5800,7 @@ function EditingTools({ controller }) {
5557
5800
  "div",
5558
5801
  {
5559
5802
  ref: roving.ref,
5803
+ dir,
5560
5804
  role: "toolbar",
5561
5805
  "aria-label": labels.mobileToolbar,
5562
5806
  className: TOOLBAR,
@@ -5603,12 +5847,14 @@ var DialogOverlay = /* @__PURE__ */ React7.forwardRef(function DialogOverlay2({
5603
5847
  );
5604
5848
  });
5605
5849
  var DialogContent = /* @__PURE__ */ React7.forwardRef(function DialogContent2({ className, children, closeLabel = "Close", ...props }, ref) {
5850
+ const dir = useDirection();
5606
5851
  return /* @__PURE__ */ jsxs(DialogPortal, { children: [
5607
5852
  /* @__PURE__ */ jsx(DialogOverlay, {}),
5608
5853
  /* @__PURE__ */ jsxs(
5609
5854
  DialogPrimitive.Content,
5610
5855
  {
5611
5856
  ref,
5857
+ dir,
5612
5858
  "data-slot": "dialog-content",
5613
5859
  className: cn(
5614
5860
  "quoin bg-popover fixed top-[50%] left-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 rounded-xl border p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
@@ -5621,7 +5867,7 @@ var DialogContent = /* @__PURE__ */ React7.forwardRef(function DialogContent2({
5621
5867
  DialogPrimitive.Close,
5622
5868
  {
5623
5869
  className: cn(
5624
- "absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100",
5870
+ "absolute top-4 end-4 rounded-xs opacity-70 transition-opacity hover:opacity-100",
5625
5871
  FOCUS_RING,
5626
5872
  ICON_TAP_TARGET_FLOOR
5627
5873
  ),
@@ -5738,14 +5984,14 @@ function shortcutGroups(labels, isMac) {
5738
5984
  }
5739
5985
  ];
5740
5986
  }
5741
- var KEYS3 = "rounded border bg-muted px-1.5 py-0.5 font-sans text-xs whitespace-nowrap";
5987
+ var KEYS4 = "rounded border bg-muted px-1.5 py-0.5 font-sans text-xs whitespace-nowrap";
5742
5988
  function ShortcutSection({ group }) {
5743
5989
  const titleId = useId();
5744
5990
  return /* @__PURE__ */ jsxs("section", { "aria-labelledby": titleId, children: [
5745
5991
  /* @__PURE__ */ jsx("h3", { id: titleId, className: "mb-2 text-sm font-semibold", children: group.title }),
5746
5992
  /* @__PURE__ */ jsx("dl", { className: "grid gap-1.5 text-sm", children: group.items.map(({ label, keys }) => /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between gap-4", children: [
5747
5993
  /* @__PURE__ */ jsx("dt", { children: label }),
5748
- /* @__PURE__ */ jsx("dd", { children: /* @__PURE__ */ jsx("kbd", { className: KEYS3, children: keys }) })
5994
+ /* @__PURE__ */ jsx("dd", { children: /* @__PURE__ */ jsx("kbd", { className: KEYS4, children: keys }) })
5749
5995
  ] }, `${label} ${keys}`)) })
5750
5996
  ] });
5751
5997
  }
@@ -5808,7 +6054,9 @@ function useRootDirection(controller) {
5808
6054
  const [direction, setDirection] = useState("ltr");
5809
6055
  useIsomorphicLayoutEffect(() => {
5810
6056
  const root = controller.root;
5811
- if (root !== null) setDirection(inlineDirection(root));
6057
+ if (root === null) return;
6058
+ setDirection(inlineDirection(root));
6059
+ return watchDirection(root, setDirection);
5812
6060
  }, [controller]);
5813
6061
  return direction;
5814
6062
  }
@@ -5893,16 +6141,20 @@ function useEditingFocus(root, editing, stop) {
5893
6141
  (element.querySelector(FOCUSABLE) ?? element).focus();
5894
6142
  const doc = element.ownerDocument;
5895
6143
  const canvas = element.closest(CANVAS);
5896
- const leaveForCanvas = (event) => {
5897
- const target = event.target;
5898
- if (!element.contains(target) && canvas?.contains(target)) stop();
5899
- };
5900
6144
  const leaveUnlessInside = (event) => {
5901
6145
  if (!inside.current.has(event)) stop();
5902
6146
  };
5903
- const unlistenCapture = listen(doc, { pointerdown: leaveForCanvas }, true);
6147
+ let afterDispatch;
6148
+ const onPress = (event) => {
6149
+ const target = event.target;
6150
+ if (!element.contains(target) && canvas?.contains(target)) return stop();
6151
+ clearTimeout(afterDispatch);
6152
+ afterDispatch = setTimeout(() => leaveUnlessInside(event));
6153
+ };
6154
+ const unlistenCapture = listen(doc, { pointerdown: onPress }, true);
5904
6155
  const unlisten = listen(doc, { pointerdown: leaveUnlessInside, focusin: leaveUnlessInside });
5905
6156
  return () => {
6157
+ clearTimeout(afterDispatch);
5906
6158
  unlistenCapture();
5907
6159
  unlisten();
5908
6160
  };
@@ -5912,6 +6164,18 @@ function useEditingFocus(root, editing, stop) {
5912
6164
  return { onPointerDownCapture: mark, onFocusCapture: mark };
5913
6165
  }, []);
5914
6166
  }
6167
+ function useWhileMounted(callback) {
6168
+ const mounted = useRef(false);
6169
+ useEffect(() => {
6170
+ mounted.current = true;
6171
+ return () => {
6172
+ mounted.current = false;
6173
+ };
6174
+ }, []);
6175
+ return useCallback(() => {
6176
+ if (mounted.current) callback();
6177
+ }, [callback]);
6178
+ }
5915
6179
  function useMissingPreviewWarning(readOnly, { type, preview }) {
5916
6180
  useEffect(() => {
5917
6181
  if (!readOnly || preview !== void 0) return;
@@ -5924,9 +6188,10 @@ function VoidPreview({ definition: { preview: Preview }, name, ...props }) {
5924
6188
  if (Preview === void 0) return /* @__PURE__ */ jsx("p", { className: "py-1 text-sm text-muted-foreground", children: name });
5925
6189
  return /* @__PURE__ */ jsx(Preview, { ...props });
5926
6190
  }
5927
- function VoidEditor({ Editor, controller, ...props }) {
6191
+ function VoidEditor({ Editor, controller, onDone, ...props }) {
5928
6192
  const index = useBlockIndex(controller, props.blockId);
5929
- return /* @__PURE__ */ jsx(Editor, { ...props, index });
6193
+ const done = useWhileMounted(onDone);
6194
+ return /* @__PURE__ */ jsx(Editor, { ...props, index, onDone: done });
5930
6195
  }
5931
6196
  function VoidBlock({ block, controller, definition }) {
5932
6197
  const { blockTypes, labels } = useQuoin();
@@ -6066,6 +6331,11 @@ function extensionUi(extension) {
6066
6331
  }
6067
6332
  var list = (value) => Array.isArray(value) ? value : [];
6068
6333
  var record = (value) => typeof value === "object" && value !== null && !Array.isArray(value) ? value : {};
6334
+ function doesSomething(item) {
6335
+ if (item.command !== void 0 || item.run !== void 0) return true;
6336
+ devWarn(`the slash command "${item.id}" has neither a command nor a run, so the slash menu leaves it out.`);
6337
+ return false;
6338
+ }
6069
6339
  function mergeRender(uis) {
6070
6340
  const given = uis.flatMap(({ render }) => render === void 0 ? [] : [render]);
6071
6341
  if (given.length === 0) return void 0;
@@ -6078,7 +6348,10 @@ function mergeRender(uis) {
6078
6348
  function collectUi(extensions, definitions, readOnly = false, onError) {
6079
6349
  const uis = extensions.map(extensionUi);
6080
6350
  return {
6081
- slashItems: [...uis.flatMap((ui) => list(ui.slashItems)), ...blockTypeSlashItems(definitions, onError)],
6351
+ slashItems: [
6352
+ ...uis.flatMap((ui) => list(ui.slashItems)).filter(doesSomething),
6353
+ ...blockTypeSlashItems(definitions, onError)
6354
+ ],
6082
6355
  bubbleItems: uis.flatMap((ui) => list(ui.bubbleItems)),
6083
6356
  blockMenuItems: uis.flatMap((ui) => list(ui.blockMenuItems)),
6084
6357
  renderers: Object.assign({}, canvasRenderers(definitions, readOnly), ...uis.map((ui) => record(ui.renderers))),
@@ -6111,12 +6384,15 @@ function movedOn(controller, before) {
6111
6384
  }
6112
6385
  async function insertUploads(controller, files, at, { upload, resolveAssetUrl, onError, unavailable, failed }) {
6113
6386
  if (!upload) return onError(new Error(unavailable));
6387
+ controller.readSelectionFromDom();
6388
+ const rest = at === null ? fillSelectedBlock(controller, files) : files;
6389
+ if (rest.length === 0) return;
6114
6390
  const writer = writerOf(controller);
6115
- const outcomes = await Promise.allSettled(files.map(async (file) => upload(file, isImageFile(file) ? "image" : "file")));
6391
+ const outcomes = await Promise.allSettled(rest.map(async (file) => upload(file, isImageFile(file) ? "image" : "file")));
6116
6392
  const blocks = [];
6117
6393
  outcomes.forEach((outcome, index) => {
6118
- if (outcome.status === "rejected") onError(toError(outcome.reason));
6119
- else if (typeof outcome.value?.storageKey === "string") blocks.push(fileBlock(files[index], outcome.value));
6394
+ if (outcome.status === "rejected") onError(uploadError(outcome.reason, failed));
6395
+ else if (isStored(outcome.value)) blocks.push(fileBlock(rest[index], outcome.value));
6120
6396
  else onError(new Error(failed));
6121
6397
  });
6122
6398
  if (blocks.length === 0 || controller.root === null || controller.readOnly) return;
@@ -6159,8 +6435,8 @@ function placeholderFor(state, block, labels) {
6159
6435
  // src/canvas/selected-block-label.ts
6160
6436
  var LONGEST = 80;
6161
6437
  function firstLine2(text2) {
6162
- const characters = [...text2.split("\n", 1)[0].trim()];
6163
- return characters.length > LONGEST ? `${characters.slice(0, LONGEST - 1).join("")}\u2026` : characters.join("");
6438
+ const line = text2.split("\n", 1)[0].trim();
6439
+ return graphemePrefix(line, LONGEST) === line ? line : `${graphemePrefix(line, LONGEST - 1)}\u2026`;
6164
6440
  }
6165
6441
  var selectedBlockLabel = (labels, definitions, extensionNames) => (block, schema) => {
6166
6442
  const { attrs } = block;
@@ -6205,11 +6481,8 @@ function filledLine({ origin, before }, schema) {
6205
6481
  }
6206
6482
  function deletions(codec, tr) {
6207
6483
  const filled = filledLine(tr, tr.before.schema);
6208
- return tr.steps.flatMap((step, i) => {
6209
- if (step.stepType !== "removeBlocks") return [];
6210
- const { index, count } = step.toJSON();
6211
- if (typeof index !== "number" || typeof count !== "number") return [];
6212
- const { blocks } = tr.docs[i];
6484
+ return blockRemovals(tr).flatMap(({ step, index, count }) => {
6485
+ const { blocks } = tr.docs[step];
6213
6486
  const first = codec.blocksFromDoc(createDoc(blocks.slice(0, index))).length;
6214
6487
  return blocks.slice(index, index + count).flatMap((block, offset) => filled(block) ? [] : [{ block: codec.toQuoin(block), index: first + offset }]);
6215
6488
  });
@@ -6228,6 +6501,61 @@ function useDeleteReporter(controller, onDelete) {
6228
6501
  });
6229
6502
  }, [controller]);
6230
6503
  }
6504
+
6505
+ // src/canvas/upload-pasted-images.ts
6506
+ var FETCHABLE = /^https?:\/\//i;
6507
+ function pastedPictures(tr, state) {
6508
+ if (tr.getMeta(PASTED_HTML) !== true) return [];
6509
+ return state.doc.blocks.flatMap(({ id, type, attrs }) => {
6510
+ const url = str(attrs.url);
6511
+ const pasted = type === "IMAGE" && FETCHABLE.test(url) && !str(attrs.storageKey) && !tr.before.doc.index.has(id);
6512
+ return pasted ? [{ id, url }] : [];
6513
+ });
6514
+ }
6515
+ async function pictureAt(url) {
6516
+ const response = await fetch(url).catch(() => null);
6517
+ const picture = response?.ok ? await response.blob().catch(() => null) : null;
6518
+ if (!picture?.type.startsWith("image/")) return null;
6519
+ return new File([picture], url.split(/[?#]/)[0].split("/").pop() || "image", { type: picture.type });
6520
+ }
6521
+ function uploadPastedImages(controller, tr, state, host) {
6522
+ for (const { id, url } of pastedPictures(tr, state)) {
6523
+ void pictureAt(url).then(async (file) => {
6524
+ if (file === null) return;
6525
+ await uploadImage(controller, id, file, host).catch(
6526
+ (cause) => host.onError(new Error(cause instanceof Error && cause.message || host.failed))
6527
+ );
6528
+ });
6529
+ }
6530
+ }
6531
+
6532
+ // src/i18n/resolve-labels.ts
6533
+ var isRecord = (value) => typeof value === "object" && value !== null;
6534
+ function ontoRecords(overrides) {
6535
+ const taken = Object.entries(overrides).map(([key, value]) => {
6536
+ const fallback = englishLabels[key];
6537
+ return [key, isRecord(value) && isRecord(fallback) ? { ...fallback, ...value } : value];
6538
+ });
6539
+ return Object.fromEntries(taken);
6540
+ }
6541
+ var resolveLabels = (overrides) => mergeLabels(englishLabels, overrides && ontoRecords(overrides));
6542
+
6543
+ // src/canvas/use-stable-labels.ts
6544
+ var isRecord2 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
6545
+ function sameEntries(a, b, deeper) {
6546
+ const keys = Object.keys(a);
6547
+ if (keys.length !== Object.keys(b).length) return false;
6548
+ return keys.every((key) => {
6549
+ const [x, y] = [a[key], b[key]];
6550
+ return Object.is(x, y) || deeper && isRecord2(x) && isRecord2(y) && sameEntries(x, y, false);
6551
+ });
6552
+ }
6553
+ var sameOverrides = (a, b) => a === b || a !== void 0 && b !== void 0 && sameEntries(a, b, true);
6554
+ function useStableLabels(overrides) {
6555
+ const [kept, setKept] = useState(overrides);
6556
+ if (!sameOverrides(kept, overrides)) setKept(overrides);
6557
+ return useMemo(() => resolveLabels(kept), [kept]);
6558
+ }
6231
6559
  function useStructureAnnouncements(controller, labels) {
6232
6560
  useEffect(() => {
6233
6561
  const undoKeys = formatShortcut("Mod-z", { isMac: controller.isMac, labels });
@@ -6284,7 +6612,7 @@ var BlockCanvas = forwardRef(function BlockCanvas2({
6284
6612
  shortcutsExtension(uiStore),
6285
6613
  ...extensions ?? []
6286
6614
  ]);
6287
- const labels = useMemo(() => resolveLabels(labelOverrides), [labelOverrides]);
6615
+ const labels = useStableLabels(labelOverrides);
6288
6616
  const latestOnError = useRef(onError);
6289
6617
  latestOnError.current = onError;
6290
6618
  const ui = useMemo(
@@ -6299,6 +6627,11 @@ var BlockCanvas = forwardRef(function BlockCanvas2({
6299
6627
  readOnly,
6300
6628
  extensions: allExtensions,
6301
6629
  render: ui.render,
6630
+ onTransaction(tr, state) {
6631
+ if (!upload) return;
6632
+ const host = { upload, resolveAssetUrl: resolveAssetUrl ?? String, onError: onError ?? console.error };
6633
+ uploadPastedImages(controller, tr, state, { ...host, failed: labels.uploadFailed });
6634
+ },
6302
6635
  onFiles(files, at) {
6303
6636
  const report = onError ?? console.error;
6304
6637
  void insertUploads(controller, files, at, {
@@ -6315,7 +6648,9 @@ var BlockCanvas = forwardRef(function BlockCanvas2({
6315
6648
  useStructureAnnouncements(controller, labels);
6316
6649
  const actions = useMemo(() => {
6317
6650
  const onCopied = () => controller.announce(labels.copied);
6318
- return menuActions(coreBlockMenuActions(labels, { blockLink, onCopied, resolveAssetUrl }), ui.blockMenuItems);
6651
+ const onCopyFailed = () => controller.announce(labels.copyFailed ?? englishLabels.copyFailed);
6652
+ const core = coreBlockMenuActions(labels, { blockLink, onCopied, onCopyFailed, resolveAssetUrl });
6653
+ return menuActions(core, ui.blockMenuItems);
6319
6654
  }, [controller, labels, blockLink, resolveAssetUrl, ui.blockMenuItems]);
6320
6655
  const slashItems = useMemo(
6321
6656
  () => [shortcutsSlashItem(labels, uiStore), ...ui.slashItems],
@@ -6349,6 +6684,7 @@ var BlockCanvas = forwardRef(function BlockCanvas2({
6349
6684
  id,
6350
6685
  "aria-label": ariaLabel ?? labels.document,
6351
6686
  hint: labels.editorHint,
6687
+ rendererLabels: labels,
6352
6688
  paragraphLabel: labels.paragraph,
6353
6689
  selectedContentLabel: labels.selectedContent,
6354
6690
  blockLabel,
@@ -6375,22 +6711,25 @@ function progressText(state, labels) {
6375
6711
  return "";
6376
6712
  }
6377
6713
  function UploadField({ category, label, accept, onUploaded }) {
6378
- const { upload, onError } = useQuoin();
6714
+ const { upload, onError, controller } = useQuoin();
6379
6715
  const labels = useLabels();
6380
6716
  const [state, setState] = React7.useState({ phase: "idle" });
6381
6717
  async function run(file) {
6382
6718
  if (!upload) return;
6383
6719
  setState({ phase: "busy" });
6720
+ controller?.announce(labels.uploading);
6384
6721
  let result;
6385
6722
  try {
6386
6723
  result = await upload(file, category);
6387
6724
  } catch (cause) {
6388
- setState({ phase: "failed", message: cause instanceof Error && cause.message || labels.uploadFailed });
6725
+ setState({ phase: "failed", message: uploadError(cause, labels.uploadFailed).message });
6389
6726
  return;
6390
6727
  }
6391
6728
  try {
6392
6729
  await onUploaded(result);
6393
- setState({ phase: "uploaded", name: result.filename || file.name });
6730
+ const name = result.filename || file.name;
6731
+ setState({ phase: "uploaded", name });
6732
+ controller?.announce(labels.uploaded(name));
6394
6733
  } catch (cause) {
6395
6734
  onError(toError(cause));
6396
6735
  setState({ phase: "idle" });
@@ -6409,7 +6748,7 @@ function UploadField({ category, label, accept, onUploaded }) {
6409
6748
  }
6410
6749
  ),
6411
6750
  !upload && /* @__PURE__ */ jsx("span", { role: "note", className: "text-xs text-muted-foreground", children: labels.uploadsUnavailable }),
6412
- /* @__PURE__ */ jsx("span", { role: "status", className: progress === "" ? "sr-only" : "text-xs text-muted-foreground", children: progress }),
6751
+ /* @__PURE__ */ jsx("span", { role: controller ? void 0 : "status", className: progress === "" ? "sr-only" : "text-xs text-muted-foreground", children: progress }),
6413
6752
  state.phase === "failed" && /* @__PURE__ */ jsx("span", { role: "alert", className: "text-xs text-destructive", children: state.message })
6414
6753
  ] });
6415
6754
  }
@@ -6567,6 +6906,6 @@ function useAutosave({
6567
6906
  return { status, error, retry: runner.retry, flush: runner.flush };
6568
6907
  }
6569
6908
 
6570
- export { AlignCenter, AlignLeft, AlignRight, AppWindow, ArrowDown, ArrowDownToLine, ArrowLeft, ArrowLeftToLine, ArrowRight, ArrowRightToLine, ArrowUp, ArrowUpToLine, BlockCanvas, Bookmark, Button, CODE_LANGUAGES, CaseSensitive, ChevronDown, ChevronUp, CodeBlock, DropdownMenu, DropdownMenuCheckboxItem, DropdownMenuContent, DropdownMenuItem, DropdownMenuRadioGroup, DropdownMenuRadioItem, DropdownMenuSeparator, DropdownMenuTrigger, FLOATING_TOOLBAR, FOCUS_RING, FilePickerButton, GripHorizontal, GripVertical, Input, MENU_ITEM, MenuIcon, MessageSquare, MessageSquarePlus, PenLine, Plus, Popover, PopoverAnchor, PopoverContent, QuoinProvider, Radical, Regex, SendHorizontal, Sigma, SmilePlus, Sparkles, SuggestionMenu, Table, TableOfContents, Trash, UploadField, WholeWord, X, blockFragment, bubblePart, cn, coreBlocks, dismissSuggestion, englishLabels, focusNode, formatShortcut, insertBlockToWrite, insertBlocks, interceptKeys, keepFocus, linkAddress, linkedBlockId, noChange, preventDefault, rangeRect, replaceTrigger, resolveLabels, returnFocusToText, runOnBlocks, selectHandlers, selectNode, styleTargets, suggestionPlugin, toError, useAutosave, useFocusKey, useLabels, useNodeKeys, useOutsidePress, usePartAnchor, useQuoin, useRangeAnchor, useRovingFocus, virtualAnchor };
6571
- //# sourceMappingURL=chunk-G6NBQYMM.js.map
6572
- //# sourceMappingURL=chunk-G6NBQYMM.js.map
6909
+ export { AlignCenter, AlignLeft, AlignRight, AppWindow, ArrowDown, ArrowDownToLine, ArrowLeft, ArrowLeftToLine, ArrowRight, ArrowRightToLine, ArrowUp, ArrowUpToLine, BlockCanvas, Bookmark, Button, CODE_LANGUAGES, CaseSensitive, ChevronDown, ChevronUp, CodeBlock, DropdownMenu, DropdownMenuCheckboxItem, DropdownMenuContent, DropdownMenuItem, DropdownMenuRadioGroup, DropdownMenuRadioItem, DropdownMenuSeparator, DropdownMenuTrigger, FLOATING_TOOLBAR, FOCUS_RING, FilePickerButton, GripHorizontal, GripVertical, Input, MENU_ITEM, MenuIcon, MessageSquare, MessageSquarePlus, PenLine, Plus, Popover, PopoverAnchor, PopoverContent, QuoinProvider, Radical, Regex, SendHorizontal, Sigma, SmilePlus, Sparkles, SuggestionMenu, Table, TableOfContents, Trash, UploadField, WholeWord, X, blockFragment, bubblePart, cn, coreBlocks, dismissSuggestion, englishLabels, focusNode, formatShortcut, insertBlockToWrite, insertBlocks, interceptKeys, keepFocus, linkAddress, linkedBlockId, noChange, preventDefault, rangeRect, replaceTrigger, resolveLabels, returnFocusToText, runOnBlocks, selectHandlers, selectNode, styleTargets, suggestionPlugin, toError, useAutosave, useClosing, useFocusKey, useLabels, useNodeKeys, useOutsidePress, usePartAnchor, useQuoin, useRangeAnchor, useRovingFocus, virtualAnchor, watchDirection };
6910
+ //# sourceMappingURL=chunk-ZGFZRVQA.js.map
6911
+ //# sourceMappingURL=chunk-ZGFZRVQA.js.map