@voithos-labs/aragonite 0.10.2 → 0.10.4

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 (300) hide show
  1. package/README.md +6 -21
  2. package/THIRD-PARTY-NOTICES.md +25 -0
  3. package/dist/a11y-strings.d.ts +19 -0
  4. package/dist/a11y-strings.js +19 -0
  5. package/dist/action-contracts.d.ts +7 -1
  6. package/dist/ambient/ambient-dom.js +5 -1
  7. package/dist/block-component.d.ts +14 -0
  8. package/dist/components/BlockDragHandle.svelte +37 -24
  9. package/dist/components/BlockHost.svelte +18 -11
  10. package/dist/components/Editor.svelte +434 -198
  11. package/dist/components/Editor.svelte.d.ts +1 -1
  12. package/dist/components/SelectionOverlay.svelte +25 -17
  13. package/dist/components/SelectionOverlay.svelte.d.ts +1 -1
  14. package/dist/components/TailInsert.svelte +107 -0
  15. package/dist/components/TailInsert.svelte.d.ts +17 -0
  16. package/dist/components/block-content-selector.d.ts +6 -2
  17. package/dist/components/block-content-selector.js +6 -2
  18. package/dist/components/blocks/ThematicBreakBlock.svelte +19 -6
  19. package/dist/components/blocks/ThematicBreakBlock.svelte.d.ts +1 -0
  20. package/dist/components/blocks/code/CodeBlock.svelte +211 -30
  21. package/dist/components/blocks/code/CodeBlockRail.svelte +704 -0
  22. package/dist/components/blocks/code/CodeBlockRail.svelte.d.ts +26 -0
  23. package/dist/components/blocks/code/code-bootstrap.js +16 -0
  24. package/dist/components/blocks/code/code-context-actions.d.ts +1 -0
  25. package/dist/components/blocks/code/code-context-actions.js +24 -0
  26. package/dist/components/blocks/code/code-fence-exit.d.ts +15 -0
  27. package/dist/components/blocks/code/code-fence-exit.js +26 -0
  28. package/dist/components/blocks/code/code-languages.d.ts +6 -0
  29. package/dist/components/blocks/code/code-languages.js +22 -3
  30. package/dist/components/blocks/code/code-renderer.js +11 -0
  31. package/dist/components/blocks/directive/DirectiveContainerBlock.svelte +1 -1
  32. package/dist/components/blocks/editable-leaf.d.ts +37 -6
  33. package/dist/components/blocks/editable-leaf.js +248 -30
  34. package/dist/components/blocks/editable-surface.d.ts +9 -0
  35. package/dist/components/blocks/editable-surface.js +22 -3
  36. package/dist/components/blocks/list/ListBlock.svelte +1 -0
  37. package/dist/components/blocks/list/ListItemBlock.svelte +14 -5
  38. package/dist/components/blocks/list/ListItemBlock.svelte.d.ts +2 -0
  39. package/dist/components/blocks/list/task-checkbox.d.ts +2 -0
  40. package/dist/components/blocks/list/task-checkbox.js +11 -2
  41. package/dist/components/blocks/surface-wiring.svelte.d.ts +4 -0
  42. package/dist/components/blocks/surface-wiring.svelte.js +8 -1
  43. package/dist/components/blocks/table/TableActionMenu.svelte +210 -86
  44. package/dist/components/blocks/table/TableActionMenu.svelte.d.ts +5 -0
  45. package/dist/components/blocks/table/TableBlock.svelte +205 -186
  46. package/dist/components/blocks/table/TableBlock.svelte.d.ts +1 -0
  47. package/dist/components/blocks/table/TableCellBlock.svelte +87 -22
  48. package/dist/components/blocks/table/TableRowBlock.svelte +4 -18
  49. package/dist/components/blocks/table/TableRowBlock.svelte.d.ts +0 -5
  50. package/dist/components/blocks/table/cell-clipboard.d.ts +10 -0
  51. package/dist/components/blocks/table/cell-clipboard.js +34 -1
  52. package/dist/components/blocks/table/cell-keydown-plan.d.ts +1 -1
  53. package/dist/components/blocks/table/cell-keydown-plan.js +3 -1
  54. package/dist/components/blocks/table/table-cell-paste.js +2 -1
  55. package/dist/components/blocks/table/table-menu-model.d.ts +35 -8
  56. package/dist/components/blocks/table/table-menu-model.js +36 -17
  57. package/dist/components/blocks/text/TextEditableBlock.svelte +58 -11
  58. package/dist/components/blocks/text/click-snap-guard.d.ts +3 -0
  59. package/dist/components/blocks/text/click-snap-guard.js +8 -0
  60. package/dist/components/blocks/text/delimiter-autopair.d.ts +71 -0
  61. package/dist/components/blocks/text/delimiter-autopair.js +216 -0
  62. package/dist/components/blocks/text/edge-policy-dispatch.d.ts +4 -0
  63. package/dist/components/blocks/text/edge-policy-dispatch.js +89 -20
  64. package/dist/components/blocks/text/live-selection-edit.d.ts +5 -5
  65. package/dist/components/blocks/text/live-selection-edit.js +37 -7
  66. package/dist/components/blocks/text/text-clipboard.js +2 -2
  67. package/dist/components/blocks/text/text-keydown.d.ts +7 -1
  68. package/dist/components/blocks/text/text-keydown.js +11 -1
  69. package/dist/components/blocks/text/text-render.d.ts +1 -1
  70. package/dist/components/blocks/text/text-render.js +3 -1
  71. package/dist/components/blocks/text/widget-interaction.d.ts +3 -0
  72. package/dist/components/blocks/text/widget-interaction.js +181 -37
  73. package/dist/components/drag-handle.d.ts +41 -0
  74. package/dist/components/drag-handle.js +134 -0
  75. package/dist/components/editor-root-focus.d.ts +19 -0
  76. package/dist/components/editor-root-focus.js +67 -0
  77. package/dist/components/editor-root-geometry.d.ts +39 -0
  78. package/dist/components/editor-root-geometry.js +91 -0
  79. package/dist/components/editor-root-keydown.d.ts +1 -1
  80. package/dist/components/editor-root-keydown.js +12 -3
  81. package/dist/components/editor-root-listeners.d.ts +3 -6
  82. package/dist/components/editor-root-listeners.js +3 -24
  83. package/dist/components/editor-root-mode-flip.d.ts +36 -0
  84. package/dist/components/editor-root-mode-flip.js +92 -0
  85. package/dist/components/image/ImageOverlayHost.svelte +14 -6
  86. package/dist/components/image/ImageProperties.svelte +512 -70
  87. package/dist/components/image/ImageProperties.svelte.d.ts +6 -1
  88. package/dist/components/image/ImageResizeHandles.svelte +67 -34
  89. package/dist/components/image/image-crop.d.ts +39 -0
  90. package/dist/components/image/image-crop.js +74 -0
  91. package/dist/components/image/image-edit-commit.d.ts +1 -0
  92. package/dist/components/image/image-edit-commit.js +21 -5
  93. package/dist/components/image/image-source-bytes.js +10 -3
  94. package/dist/components/image/image-widget-editing.js +1 -0
  95. package/dist/components/image/widget-dom.js +5 -1
  96. package/dist/components/link-card/link-card-commit.js +1 -1
  97. package/dist/components/lrd-map-gate.js +1 -1
  98. package/dist/components/menu/BlockMenu.svelte +315 -0
  99. package/dist/components/menu/BlockMenu.svelte.d.ts +34 -0
  100. package/dist/components/menu/MenuIcon.svelte +153 -0
  101. package/dist/components/menu/MenuIcon.svelte.d.ts +51 -0
  102. package/dist/components/menu/SelectionToolbar.svelte +385 -0
  103. package/dist/components/menu/SelectionToolbar.svelte.d.ts +16 -0
  104. package/dist/components/menu/clipboard-actions.d.ts +12 -0
  105. package/dist/components/menu/clipboard-actions.js +42 -0
  106. package/dist/components/menu/default-context-actions.d.ts +15 -0
  107. package/dist/components/menu/default-context-actions.js +76 -0
  108. package/dist/components/menu/flyout-placement.d.ts +6 -0
  109. package/dist/components/menu/flyout-placement.js +25 -0
  110. package/dist/core/inline/format-toggle.d.ts +12 -4
  111. package/dist/core/inline/format-toggle.js +94 -40
  112. package/dist/core/inline/image-dimensions.d.ts +3 -0
  113. package/dist/core/inline/image-dimensions.js +46 -10
  114. package/dist/core/inline/inline-widgets.d.ts +19 -0
  115. package/dist/core/inline/inline-widgets.js +5 -0
  116. package/dist/core/inline/scan/brackets.js +1 -0
  117. package/dist/core/inline/scan/plugin-syntax.d.ts +8 -0
  118. package/dist/core/inline/scan/plugin-syntax.js +15 -1
  119. package/dist/core/inline/transparency.js +3 -3
  120. package/dist/core/inline-render.d.ts +6 -0
  121. package/dist/core/inline-render.js +32 -0
  122. package/dist/core/nodes.d.ts +14 -0
  123. package/dist/cursor/edge-affinity.js +2 -1
  124. package/dist/cursor/height-oracle.d.ts +2 -3
  125. package/dist/cursor/height-oracle.js +0 -1
  126. package/dist/cursor/overlay-remeasure.js +8 -0
  127. package/dist/cursor/reveal-source.js +7 -2
  128. package/dist/cursor/scroll-hold.d.ts +10 -0
  129. package/dist/cursor/scroll-hold.js +22 -0
  130. package/dist/cursor/scrollport.d.ts +9 -0
  131. package/dist/cursor/scrollport.js +30 -1
  132. package/dist/cursor/visual-lines.d.ts +5 -4
  133. package/dist/cursor/visual-lines.js +56 -11
  134. package/dist/cursor/widget-edge-snap.d.ts +28 -0
  135. package/dist/cursor/widget-edge-snap.js +38 -0
  136. package/dist/cursor/widget-offset.d.ts +9 -0
  137. package/dist/cursor/widget-offset.js +71 -4
  138. package/dist/debug/interaction-trace.d.ts +4 -0
  139. package/dist/debug/interaction-trace.js +15 -0
  140. package/dist/decorations/decoration-state.svelte.js +1 -1
  141. package/dist/decorations/reserved-attrs.js +1 -0
  142. package/dist/editor-actions/ancestry-folds.d.ts +2 -2
  143. package/dist/editor-actions/ancestry-folds.js +1 -1
  144. package/dist/editor-actions/block-edit-scope.js +1 -1
  145. package/dist/editor-actions/commit/text-batch.d.ts +3 -2
  146. package/dist/editor-actions/commit/text-batch.js +1 -1
  147. package/dist/editor-actions/commit/undo-controller.js +4 -2
  148. package/dist/editor-actions/container-edit.js +2 -1
  149. package/dist/editor-actions/enter-completion.d.ts +2 -0
  150. package/dist/editor-actions/enter-completion.js +23 -2
  151. package/dist/editor-actions/focus/focus-dispatch.js +7 -4
  152. package/dist/editor-actions/focus/focus-landing.d.ts +10 -1
  153. package/dist/editor-actions/focus/focus-landing.js +20 -6
  154. package/dist/editor-actions/inline-range-commit.js +1 -1
  155. package/dist/editor-actions/plugin/container.d.ts +7 -0
  156. package/dist/editor-actions/plugin/container.js +3 -1
  157. package/dist/editor-actions/reorder-action.js +19 -10
  158. package/dist/editor-actions/reorder-drag.js +29 -1
  159. package/dist/editor-actions/replacement-focus.d.ts +1 -1
  160. package/dist/editor-actions/replacement-focus.js +1 -1
  161. package/dist/editor-actions/search-replace.js +1 -1
  162. package/dist/editor-actions/table-context.d.ts +4 -1
  163. package/dist/editor-actions/table-context.js +57 -1
  164. package/dist/editor-events.d.ts +3 -0
  165. package/dist/editor-keys.d.ts +33 -0
  166. package/dist/editor-props.d.ts +24 -12
  167. package/dist/index.d.ts +1 -1
  168. package/dist/plugin.d.ts +7 -0
  169. package/dist/plugin.js +16 -0
  170. package/dist/plugins/latex/BlockMath.svelte +264 -29
  171. package/dist/plugins/latex/BlockMath.svelte.d.ts +2 -0
  172. package/dist/plugins/latex/index.d.ts +2 -1
  173. package/dist/plugins/latex/latex-kind.js +60 -10
  174. package/dist/plugins/latex/math-completion.js +4 -1
  175. package/dist/plugins/latex/math-layout.d.ts +15 -0
  176. package/dist/plugins/latex/math-layout.js +13 -0
  177. package/dist/plugins/latex/math-source.d.ts +19 -0
  178. package/dist/plugins/latex/math-source.js +103 -0
  179. package/dist/plugins/latex/register.d.ts +11 -2
  180. package/dist/plugins/latex/register.js +3 -1
  181. package/dist/plugins/latex/renderer.d.ts +3 -3
  182. package/dist/plugins/latex/renderer.js +13 -6
  183. package/dist/plugins/mermaid/MermaidBlock.svelte +19 -9
  184. package/dist/plugins/parrot/ParrotBlock.svelte +3 -1
  185. package/dist/reactivity/list-windowing.svelte.d.ts +8 -8
  186. package/dist/reactivity/list-windowing.svelte.js +75 -30
  187. package/dist/schema/block-completions.d.ts +8 -0
  188. package/dist/schema/block-completions.js +11 -0
  189. package/dist/schema/commands.d.ts +9 -5
  190. package/dist/schema/commands.js +12 -7
  191. package/dist/schema/context-actions.d.ts +31 -0
  192. package/dist/schema/context-actions.js +23 -0
  193. package/dist/schema/fenced-code-raw.js +31 -1
  194. package/dist/schema/operations.d.ts +8 -1
  195. package/dist/schema/reserved-chords.js +37 -4
  196. package/dist/schema/table-cell-raw.d.ts +1 -1
  197. package/dist/schema/table-cell-raw.js +1 -1
  198. package/dist/selection/block-hit-test.js +3 -2
  199. package/dist/selection/char-endpoint-snap.js +1 -1
  200. package/dist/selection/clipboard-text.js +6 -1
  201. package/dist/selection/covered-block.d.ts +10 -0
  202. package/dist/selection/covered-block.js +24 -0
  203. package/dist/selection/cross-block/dispatch.d.ts +3 -0
  204. package/dist/selection/cross-block/dispatch.js +13 -1
  205. package/dist/selection/cross-block/format-range.d.ts +1 -1
  206. package/dist/selection/cross-block/format-range.js +4 -15
  207. package/dist/selection/cross-block/format-toggle.js +1 -1
  208. package/dist/selection/cross-block/keydown.js +3 -29
  209. package/dist/selection/cross-block/ops.js +1 -1
  210. package/dist/selection/cross-block/paste.js +31 -35
  211. package/dist/selection/cross-block/type-replace.d.ts +3 -2
  212. package/dist/selection/cross-block/type-replace.js +52 -13
  213. package/dist/selection/dead-space-caret.d.ts +13 -0
  214. package/dist/selection/dead-space-caret.js +57 -1
  215. package/dist/selection/drag-pointer.d.ts +26 -2
  216. package/dist/selection/drag-pointer.js +103 -2
  217. package/dist/selection/gap-caret.js +1 -1
  218. package/dist/selection/keyboard-extend.d.ts +3 -2
  219. package/dist/selection/keyboard-extend.js +4 -3
  220. package/dist/selection/multi-click.d.ts +42 -0
  221. package/dist/selection/multi-click.js +203 -0
  222. package/dist/selection/native-bridge.d.ts +3 -0
  223. package/dist/selection/native-bridge.js +31 -2
  224. package/dist/selection/path-lookup.d.ts +10 -2
  225. package/dist/selection/path-lookup.js +11 -4
  226. package/dist/selection/pointer-gesture.d.ts +9 -0
  227. package/dist/selection/pointer-gesture.js +11 -0
  228. package/dist/selection/primitives.d.ts +7 -0
  229. package/dist/selection/primitives.js +19 -1
  230. package/dist/selection/range-delete-ceremony.js +4 -2
  231. package/dist/selection/range-delete-chrome.js +2 -1
  232. package/dist/selection/range-delete-table-coverage.js +2 -1
  233. package/dist/selection/range-delete-table.js +3 -2
  234. package/dist/selection/range-delete.d.ts +4 -0
  235. package/dist/selection/range-delete.js +31 -3
  236. package/dist/selection/selection-drop.d.ts +34 -0
  237. package/dist/selection/selection-drop.js +253 -0
  238. package/dist/selection/selection-restore.js +1 -1
  239. package/dist/selection/selection-state.svelte.d.ts +6 -0
  240. package/dist/selection/selection-state.svelte.js +36 -1
  241. package/dist/selection/table-endpoint-snap.js +1 -1
  242. package/dist/selection/table-rect-extend.js +1 -1
  243. package/dist/styles/editor-theme.css +75 -31
  244. package/dist/styles/editor.css +268 -18
  245. package/dist/testing/container-conformance.js +2 -2
  246. package/dist/testing/inline-conformance.js +2 -1
  247. package/dist/tree-operations/blockquote.js +1 -1
  248. package/dist/tree-operations/chain-rebuild.d.ts +63 -0
  249. package/dist/tree-operations/chain-rebuild.js +142 -0
  250. package/dist/tree-operations/children.d.ts +1 -1
  251. package/dist/tree-operations/children.js +1 -1
  252. package/dist/tree-operations/cleanup.js +1 -1
  253. package/dist/tree-operations/content-write.d.ts +50 -0
  254. package/dist/tree-operations/content-write.js +263 -0
  255. package/dist/tree-operations/index.d.ts +8 -3
  256. package/dist/tree-operations/index.js +6 -2
  257. package/dist/tree-operations/list/exit-replacement.js +1 -1
  258. package/dist/tree-operations/list/unwrap-merge.js +3 -3
  259. package/dist/tree-operations/node-ops.d.ts +17 -234
  260. package/dist/tree-operations/node-ops.js +47 -1113
  261. package/dist/tree-operations/node-primitives.d.ts +75 -0
  262. package/dist/tree-operations/node-primitives.js +117 -0
  263. package/dist/tree-operations/paste/apply.js +1 -1
  264. package/dist/tree-operations/paste/body-write.d.ts +1 -1
  265. package/dist/tree-operations/paste/body-write.js +2 -2
  266. package/dist/tree-operations/paste/container-match.js +4 -2
  267. package/dist/tree-operations/paste/dispatch.js +2 -1
  268. package/dist/tree-operations/paste/find-enclosing-list.js +1 -1
  269. package/dist/tree-operations/paste/focus-target.d.ts +1 -1
  270. package/dist/tree-operations/paste/list-absorb.js +1 -1
  271. package/dist/tree-operations/paste/list-break-out.js +1 -1
  272. package/dist/tree-operations/paste/parent-scope.js +1 -1
  273. package/dist/tree-operations/paste/paste-replacement.js +1 -1
  274. package/dist/tree-operations/paste/replace-block-at-parent.d.ts +3 -1
  275. package/dist/tree-operations/paste/replace-block-at-parent.js +5 -2
  276. package/dist/tree-operations/paste/replacement-parse.d.ts +17 -0
  277. package/dist/tree-operations/paste/replacement-parse.js +22 -0
  278. package/dist/tree-operations/path-mutate.d.ts +1 -1
  279. package/dist/tree-operations/path-mutate.js +2 -1
  280. package/dist/tree-operations/reorder-unit.js +1 -1
  281. package/dist/tree-operations/reorder.d.ts +5 -2
  282. package/dist/tree-operations/reorder.js +55 -2
  283. package/dist/tree-operations/settle.d.ts +105 -0
  284. package/dist/tree-operations/settle.js +660 -0
  285. package/dist/tree-operations/table-grid-clipboard.d.ts +21 -0
  286. package/dist/tree-operations/table-grid-clipboard.js +90 -0
  287. package/dist/tree-operations/unshare.d.ts +15 -77
  288. package/dist/tree-operations/unshare.js +15 -166
  289. package/docs/guide/consumer-guide.md +136 -104
  290. package/docs/guide/plugin-api.md +73 -29
  291. package/docs/guide/plugin-guide.md +55 -23
  292. package/package.json +9 -7
  293. package/dist/components/blocks/code/CodeLanguageChip.svelte +0 -127
  294. package/dist/components/blocks/code/CodeLanguageChip.svelte.d.ts +0 -14
  295. package/dist/components/blocks/table/TableGrip.svelte +0 -91
  296. package/dist/components/blocks/table/TableGrip.svelte.d.ts +0 -8
  297. package/dist/components/blocks/table/table-drop-target.d.ts +0 -1
  298. package/dist/components/blocks/table/table-drop-target.js +0 -16
  299. package/dist/components/blocks/table/table-reorder-drag.d.ts +0 -78
  300. package/dist/components/blocks/table/table-reorder-drag.js +0 -97
@@ -64,10 +64,25 @@
64
64
  return getWidgetEl()?.querySelector('img') ?? null;
65
65
  }
66
66
 
67
+ /**
68
+ * What the drag measures and previews on. A cropped image is a FRAME (the widget) with the
69
+ * picture panned inside it, so the `<img>` is neither the size being changed nor the box the
70
+ * user is dragging: previewing on it bulges the picture out of its frame, and at a zoom above
71
+ * 1 the drag would start from the painted image's width and jump.
72
+ */
73
+ function isCropped(): boolean {
74
+ return getWidgetEl()?.classList.contains('md-image-cropped') ?? false;
75
+ }
76
+
77
+ function previewEl(): HTMLElement | null {
78
+ return isCropped() ? getWidgetEl() : imgEl();
79
+ }
80
+
67
81
  function startDrag(e: PointerEvent) {
68
82
  const img = imgEl();
69
- if (!img) return;
70
- const { width: startWidth, height: startHeight } = img.getBoundingClientRect();
83
+ const preview = previewEl();
84
+ if (!img || !preview) return;
85
+ const { width: startWidth, height: startHeight } = preview.getBoundingClientRect();
71
86
  // Unmeasurable width makes every snap run against 0 and commit a tiny image
72
87
  // whichever way the drag goes; bail before pointer capture.
73
88
  if (startWidth < MIN_WIDTH || editorContentWidth < MIN_WIDTH) return;
@@ -79,7 +94,10 @@
79
94
  startWidth,
80
95
  startHeight,
81
96
  naturalWidth: img.naturalWidth,
82
- aspectLocked: !e.shiftKey,
97
+ // A crop's frame keeps its shape through a resize (`commitImageResize` derives the
98
+ // height from it), so Shift has nothing to unlock here; the crop session's corner
99
+ // brackets are where a frame's aspect changes.
100
+ aspectLocked: isCropped() || !e.shiftKey,
83
101
  currentWidth: startWidth
84
102
  };
85
103
  }
@@ -95,12 +113,13 @@
95
113
  const clamped = clampWidth(proposed, editorContentWidth);
96
114
  const snapped = snapWidth(clamped, editorContentWidth, SNAP_THRESHOLD_PX);
97
115
  dragState.currentWidth = snapped;
98
- const img = imgEl();
99
- if (!img) return;
100
- img.style.width = `${snapped}px`;
116
+ const preview = previewEl();
117
+ if (!preview) return;
118
+ preview.style.width = `${snapped}px`;
101
119
  // Shift unlocks the aspect: the height stays where the user found it and the image
102
- // distorts. Locked, the stylesheet's `height: auto` derives it from the new width.
103
- img.style.height = dragState.aspectLocked ? '' : `${dragState.startHeight}px`;
120
+ // distorts. Locked, the frame's own `aspect-ratio` (cropped) or the stylesheet's
121
+ // `height: auto` (plain) derives it from the new width.
122
+ preview.style.height = dragState.aspectLocked ? '' : `${dragState.startHeight}px`;
104
123
  }
105
124
 
106
125
  function endDrag(e: PointerEvent) {
@@ -126,26 +145,36 @@
126
145
  dx: e.clientX - startX,
127
146
  editorContentWidth,
128
147
  naturalWidth,
129
- imgRectWidth: imgEl()?.getBoundingClientRect().width,
148
+ imgRectWidth: previewEl()?.getBoundingClientRect().width,
130
149
  imgWidthAttr: imgEl()?.getAttribute('width')
131
150
  });
132
151
  }
133
152
  onCommit(Math.round(finalWidth), resolveDraggedHeight(aspectLocked, startHeight));
134
153
  }
135
154
 
155
+ /** The frame's committed width, from the `|WxH` hint the widget was built with. */
156
+ function restoreFrameWidth(widget: HTMLElement): void {
157
+ const width = imgEl()?.getAttribute('width');
158
+ if (width) widget.style.width = `${width}px`;
159
+ }
160
+
136
161
  function cancelDrag(e: PointerEvent) {
137
162
  if (!dragState) return;
138
- // Fall back to the widget's committed width/height attributes.
139
- const img = imgEl();
140
- if (img) {
141
- img.style.width = '';
142
- img.style.height = '';
163
+ // Fall back to the committed geometry: the widget's width/height attributes, or for a
164
+ // cropped image the frame width `applyCropToWidget` wrote.
165
+ const preview = previewEl();
166
+ if (preview) {
167
+ preview.style.height = '';
168
+ if (preview === imgEl()) preview.style.width = '';
169
+ else restoreFrameWidth(preview);
143
170
  }
144
171
  dragState = null;
145
172
  (e.target as HTMLElement).releasePointerCapture(e.pointerId);
146
173
  }
147
174
  </script>
148
175
 
176
+ <!-- One grip, on the right edge: width is the only thing a drag sets (Shift unlocks the
177
+ aspect), so a second corner grip was the same gesture twice. -->
149
178
  {#if !isBroken}
150
179
  <div
151
180
  class="md-resize-handle md-resize-handle-right"
@@ -155,34 +184,38 @@
155
184
  onpointerup={endDrag}
156
185
  onpointercancel={cancelDrag}
157
186
  ></div>
158
- <div
159
- class="md-resize-handle md-resize-handle-corner"
160
- role="presentation"
161
- onpointerdown={startDrag}
162
- onpointermove={moveDrag}
163
- onpointerup={endDrag}
164
- onpointercancel={cancelDrag}
165
- ></div>
166
187
  {/if}
167
188
 
168
189
  <style>
190
+ /* A neutral grip sitting inside the picture's right edge: the selection ring owns the edge
191
+ itself, and the accent is the ring's, so a second accent shape beside it reads as decoration
192
+ rather than as a control. White over a hairline rim so it carries on a dark picture and a
193
+ pale one alike. The hit strip is wider than the paint, so on a picture too narrow to hold
194
+ both the strip and a clickable middle (an icon-sized image) the grip steps back outside
195
+ the edge, where it stood before. */
169
196
  .md-resize-handle {
170
197
  position: absolute;
171
- width: 8px;
172
- height: 8px;
173
- background: var(--color-accent, #567b67);
174
- border: 1px solid #fff;
198
+ right: clamp(-3px, calc(50% - 20px), 7px);
199
+ top: 50%;
200
+ width: 4px;
201
+ height: 36px;
202
+ max-height: 40%;
203
+ transform: translateY(-50%);
204
+ border-radius: 999px;
205
+ background: rgba(255, 255, 255, 0.92);
206
+ box-shadow:
207
+ 0 0 0 0.5px rgba(0, 0, 0, 0.35),
208
+ 0 1px 3px rgba(0, 0, 0, 0.35);
209
+ opacity: 0.85;
175
210
  z-index: 10;
176
211
  cursor: ew-resize;
177
212
  }
178
- .md-resize-handle-right {
179
- right: -4px;
180
- top: 50%;
181
- transform: translateY(-50%);
213
+ .md-resize-handle:hover {
214
+ opacity: 1;
182
215
  }
183
- .md-resize-handle-corner {
184
- right: -4px;
185
- bottom: -4px;
186
- cursor: nwse-resize;
216
+ .md-resize-handle::before {
217
+ content: '';
218
+ position: absolute;
219
+ inset: -6px -8px;
187
220
  }
188
221
  </style>
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Pure math for the image crop: how a `{x, y, z}` window maps to the styles that paint it, and
3
+ * how a pan or a wheel turn moves it. No DOM here beyond a style bag; the properties surface
4
+ * owns the pointer events and `widget-dom.ts` paints a committed crop from the same styles.
5
+ *
6
+ * The model is CSS `background-size: cover` at `z` times the fill scale, with the image point
7
+ * (`x`%, `y`%) pinned to that point of the frame, so `{50, 50, 1}` is a centred cover fit.
8
+ */
9
+ import type { ImageCrop } from '../../core/nodes';
10
+ export declare const DEFAULT_CROP: ImageCrop;
11
+ export interface Size {
12
+ width: number;
13
+ height: number;
14
+ }
15
+ export declare function clampCrop(crop: ImageCrop): ImageCrop;
16
+ /**
17
+ * The styles an `<img>` inside an `overflow: hidden` frame takes. The image box is `z` times
18
+ * the frame with `object-fit: cover`, offset so the pinned point stays put: at `z` 1 the offset
19
+ * is zero and `object-position` alone pans the cover overflow.
20
+ */
21
+ export declare function cropImageStyle(crop: ImageCrop): {
22
+ width: string;
23
+ height: string;
24
+ left: string;
25
+ top: string;
26
+ objectPosition: string;
27
+ };
28
+ /** The painted image size at this crop: cover-scaled to the frame, then times `z`. */
29
+ export declare function paintedSize(frame: Size, natural: Size, z: number): Size;
30
+ /**
31
+ * A drag of (`dx`, `dy`) px moves the window the other way, by the share of the overflow the
32
+ * pointer covered; an axis with nothing to pan on ignores its component.
33
+ */
34
+ export declare function panCrop(crop: ImageCrop, dx: number, dy: number, frame: Size, natural: Size): ImageCrop;
35
+ /** A wheel turn zooms about the pinned point; positive `deltaY` (scrolling down) zooms out. */
36
+ export declare function zoomCrop(crop: ImageCrop, deltaY: number): ImageCrop;
37
+ export declare function isDefaultCrop(crop: ImageCrop): boolean;
38
+ /** Paint a crop onto a widget and its image, the way a committed one renders. */
39
+ export declare function applyCropToWidget(widget: HTMLElement, img: HTMLImageElement, frame: Size, crop: ImageCrop): void;
@@ -0,0 +1,74 @@
1
+ /**
2
+ * Pure math for the image crop: how a `{x, y, z}` window maps to the styles that paint it, and
3
+ * how a pan or a wheel turn moves it. No DOM here beyond a style bag; the properties surface
4
+ * owns the pointer events and `widget-dom.ts` paints a committed crop from the same styles.
5
+ *
6
+ * The model is CSS `background-size: cover` at `z` times the fill scale, with the image point
7
+ * (`x`%, `y`%) pinned to that point of the frame, so `{50, 50, 1}` is a centred cover fit.
8
+ */
9
+ import { MAX_CROP_ZOOM } from '../../core/inline/image-dimensions';
10
+ export const DEFAULT_CROP = { x: 50, y: 50, z: 1 };
11
+ export function clampCrop(crop) {
12
+ return {
13
+ x: Math.min(100, Math.max(0, crop.x)),
14
+ y: Math.min(100, Math.max(0, crop.y)),
15
+ z: Math.min(MAX_CROP_ZOOM, Math.max(1, crop.z))
16
+ };
17
+ }
18
+ /**
19
+ * The styles an `<img>` inside an `overflow: hidden` frame takes. The image box is `z` times
20
+ * the frame with `object-fit: cover`, offset so the pinned point stays put: at `z` 1 the offset
21
+ * is zero and `object-position` alone pans the cover overflow.
22
+ */
23
+ export function cropImageStyle(crop) {
24
+ const c = clampCrop(crop);
25
+ const pct = (n) => `${Math.round(n * 1000) / 1000}%`;
26
+ return {
27
+ width: pct(c.z * 100),
28
+ height: pct(c.z * 100),
29
+ left: pct(-(c.z - 1) * c.x),
30
+ top: pct(-(c.z - 1) * c.y),
31
+ objectPosition: `${pct(c.x)} ${pct(c.y)}`
32
+ };
33
+ }
34
+ /** The painted image size at this crop: cover-scaled to the frame, then times `z`. */
35
+ export function paintedSize(frame, natural, z) {
36
+ if (natural.width <= 0 || natural.height <= 0)
37
+ return { width: frame.width * z, height: frame.height * z };
38
+ const scale = Math.max(frame.width / natural.width, frame.height / natural.height) * z;
39
+ return { width: natural.width * scale, height: natural.height * scale };
40
+ }
41
+ /**
42
+ * A drag of (`dx`, `dy`) px moves the window the other way, by the share of the overflow the
43
+ * pointer covered; an axis with nothing to pan on ignores its component.
44
+ */
45
+ export function panCrop(crop, dx, dy, frame, natural) {
46
+ const painted = paintedSize(frame, natural, crop.z);
47
+ const overX = painted.width - frame.width;
48
+ const overY = painted.height - frame.height;
49
+ return clampCrop({
50
+ x: overX > 0.5 ? crop.x - (dx / overX) * 100 : crop.x,
51
+ y: overY > 0.5 ? crop.y - (dy / overY) * 100 : crop.y,
52
+ z: crop.z
53
+ });
54
+ }
55
+ /** A wheel turn zooms about the pinned point; positive `deltaY` (scrolling down) zooms out. */
56
+ export function zoomCrop(crop, deltaY) {
57
+ return clampCrop({ ...crop, z: crop.z - deltaY * 0.002 });
58
+ }
59
+ export function isDefaultCrop(crop) {
60
+ return crop.x === 50 && crop.y === 50 && crop.z === 1;
61
+ }
62
+ /** Paint a crop onto a widget and its image, the way a committed one renders. */
63
+ export function applyCropToWidget(widget, img, frame, crop) {
64
+ widget.classList.add('md-image-cropped');
65
+ widget.style.width = `${Math.round(frame.width)}px`;
66
+ widget.style.aspectRatio = `${Math.round(frame.width)} / ${Math.round(frame.height)}`;
67
+ const style = cropImageStyle(crop);
68
+ img.style.width = style.width;
69
+ img.style.height = style.height;
70
+ img.style.left = style.left;
71
+ img.style.top = style.top;
72
+ img.style.objectPosition = style.objectPosition;
73
+ img.style.aspectRatio = '';
74
+ }
@@ -30,6 +30,7 @@ export interface ImageEditCommitter {
30
30
  * popover's dirty check compares against these. */
31
31
  buildEditBytes(target: WidgetTarget, newFields: ImageFields): string | null;
32
32
  commitImageResize(newWidth: number, newHeight: number | undefined): void;
33
+ removeImage(target: WidgetTarget): void;
33
34
  dismissImagePopover(): void;
34
35
  getEditorContentWidth(): number;
35
36
  attachWidgetSelectListener(): () => void;
@@ -2,7 +2,7 @@ import { resolvedInlineContent } from '../../core/inline/inline-cache';
2
2
  import { flattenInlineWidgets } from '../../core/inline/inline-widgets';
3
3
  import { createInlineRangeCommit } from '../../editor-actions/inline-range-commit';
4
4
  import { FALLBACK_CONTENT_WIDTH } from '../../cursor/typography-estimates';
5
- import { blockNodeAt } from '../../tree-operations/node-ops';
5
+ import { blockNodeAt } from '../../tree-operations/node-primitives';
6
6
  import { buildImageEditBytes } from './image-source-bytes';
7
7
  export function createImageEditCommitter(deps) {
8
8
  const { getDoc, getEditorEl, widgetSelection, controller, events } = deps;
@@ -71,15 +71,30 @@ export function createImageEditCommitter(deps) {
71
71
  const ctx = getSelectedImageFields();
72
72
  if (!ctx)
73
73
  return;
74
+ const { image } = ctx;
75
+ // A cropped frame keeps its shape through a resize: the height follows the width.
76
+ const framedHeight = image.crop && image.width !== undefined && image.height !== undefined
77
+ ? Math.round((image.height * newWidth) / image.width)
78
+ : newHeight;
74
79
  const newFields = {
75
- alt: ctx.image.alt ?? '',
76
- url: ctx.image.url ?? '',
77
- ...(ctx.image.title !== undefined ? { title: ctx.image.title } : {}),
80
+ alt: image.alt ?? '',
81
+ url: image.url ?? '',
82
+ ...(image.title !== undefined ? { title: image.title } : {}),
78
83
  width: newWidth,
79
- ...(newHeight !== undefined ? { height: newHeight } : {})
84
+ ...(framedHeight !== undefined ? { height: framedHeight } : {}),
85
+ ...(image.crop !== undefined && framedHeight !== undefined ? { crop: image.crop } : {})
80
86
  };
81
87
  commitImageEdit(sel, newFields);
82
88
  }
89
+ /** The whole `![...](...)` span goes; the caret lands where the image began. */
90
+ function removeImage(target) {
91
+ const paragraph = blockNodeAt(getDoc(), target.paragraphPath);
92
+ const image = paragraph && findImageInParagraph(paragraph, target.sourceStart);
93
+ if (!image)
94
+ return;
95
+ widgetSelection.clear();
96
+ void inlineRange.commitInlineRange(target.paragraphPath, image.start, image.end, '', 0);
97
+ }
83
98
  function dismissImagePopover() {
84
99
  widgetSelection.clear();
85
100
  }
@@ -151,6 +166,7 @@ export function createImageEditCommitter(deps) {
151
166
  commitImageEdit,
152
167
  buildEditBytes,
153
168
  commitImageResize,
169
+ removeImage,
154
170
  dismissImagePopover,
155
171
  getEditorContentWidth,
156
172
  attachWidgetSelectListener,
@@ -31,6 +31,7 @@ export function imageFieldsFromInline(image) {
31
31
  ...(image.title !== undefined ? { title: image.title } : {}),
32
32
  ...(image.width !== undefined ? { width: image.width } : {}),
33
33
  ...(image.height !== undefined ? { height: image.height } : {}),
34
+ ...(image.crop !== undefined ? { crop: image.crop } : {}),
34
35
  ...(image.label !== undefined ? { label: image.label } : {})
35
36
  };
36
37
  }
@@ -38,7 +39,7 @@ export function imageFieldsFromInline(image) {
38
39
  /** The built-in grammar's inverse. Reach it through `buildImageEditBytes`, the only
39
40
  * caller entitled to decide these bytes are GFM's to write. */
40
41
  export function buildImageSourceBytes(fields) {
41
- const dimSuffix = buildDimSuffix(fields.width, fields.height);
42
+ const dimSuffix = buildDimSuffix(fields.width, fields.height, fields.crop);
42
43
  const altSegment = escapeAlt(fields.alt) + dimSuffix;
43
44
  // Reference form: the dimension hint rides in the alt, and url/title are not
44
45
  // written — they belong to the LRD.
@@ -48,12 +49,18 @@ export function buildImageSourceBytes(fields) {
48
49
  const titleSegment = fields.title !== undefined ? ` "${escapeTitle(fields.title)}"` : '';
49
50
  return `![${altSegment}](${encodeDestination(fields.url)}${titleSegment})`;
50
51
  }
51
- function buildDimSuffix(width, height) {
52
+ function buildDimSuffix(width, height, crop) {
52
53
  if (width === undefined)
53
54
  return '';
54
55
  if (height === undefined)
55
56
  return `|${width}`;
56
- return `|${width}x${height}`;
57
+ return `|${width}x${height}${crop ? buildCropTail(crop) : ''}`;
58
+ }
59
+ /** Whole percents; the zoom only when it is one worth writing, trimmed to what it needs. */
60
+ function buildCropTail(crop) {
61
+ const z = Math.round(crop.z * 100) / 100;
62
+ const zoom = z === 1 ? '' : `,${String(z)}`;
63
+ return `@${Math.round(crop.x)},${Math.round(crop.y)}${zoom}`;
57
64
  }
58
65
  // Alt sits inside `[...]`, where an unescaped bracket closes the scan early. Unlike
59
66
  // `title` and `url` it carries RAW label bytes, so a blanket escape doubles every
@@ -28,6 +28,7 @@ export function imageWidgetOnSelectedKey(e, ctx) {
28
28
  ...(inline.height !== undefined
29
29
  ? { height: Math.round((newWidth / currentWidth) * inline.height) }
30
30
  : {}),
31
+ ...(inline.crop !== undefined ? { crop: inline.crop } : {}),
31
32
  ...(inline.label !== undefined ? { label: inline.label } : {})
32
33
  };
33
34
  const newBytes = buildImageEditBytes(inline, node.raw, newFields);
@@ -4,6 +4,7 @@
4
4
  import { isAllowedImageSrcScheme } from '../../core/url-policy';
5
5
  import { findSurfacePathForElement } from '../../selection/path-lookup';
6
6
  import { devWarn } from '../../dev-warn';
7
+ import { applyCropToWidget } from './image-crop';
7
8
  export function buildImageWidget(node, _raw, opts) {
8
9
  const widget = document.createElement('span');
9
10
  widget.className = 'md-image-widget';
@@ -63,9 +64,12 @@ export function buildImageWidget(node, _raw, opts) {
63
64
  img.setAttribute('height', String(node.height));
64
65
  // A declared `|WxH` box belongs to the author, so it both reserves space before the bytes
65
66
  // arrive and survives the decode: the attribute pair alone loses to the natural ratio the
66
- // moment `height: auto` has one to read.
67
+ // moment `height: auto` has one to read. With a crop the box is a frame the image pans in.
67
68
  if (node.width !== undefined && node.height !== undefined) {
68
69
  img.style.aspectRatio = `${node.width} / ${node.height}`;
70
+ if (node.crop) {
71
+ applyCropToWidget(widget, img, { width: node.width, height: node.height }, node.crop);
72
+ }
69
73
  }
70
74
  const markBroken = () => {
71
75
  opts.brokenUrlCache.add(resolvedUrl);
@@ -5,7 +5,7 @@
5
5
  */
6
6
  import { wireOverlayRemeasure } from '../../cursor/overlay-remeasure';
7
7
  import { createInlineRangeCommit } from '../../editor-actions/inline-range-commit';
8
- import { isBlockNode, nodeAt } from '../../tree-operations/node-ops';
8
+ import { isBlockNode, nodeAt } from '../../tree-operations/node-primitives';
9
9
  import { linkConstructAt } from '../blocks/text/link-at-point';
10
10
  import { buildLinkEditBytes, buildLinkUnwrapBytes, buildLinkWrapBytes, linkFieldsFromInline } from '../blocks/text/link-source-bytes';
11
11
  export function createLinkCardCommitter(deps) {
@@ -1,4 +1,4 @@
1
- import { nodeAt } from '../tree-operations/node-ops';
1
+ import { nodeAt } from '../tree-operations/node-primitives';
2
2
  /**
3
3
  * Whether a commit could change the LRD set, keeping the O(nodes) map rebuild off the keystroke
4
4
  * hot path. Any op that is not a kind-stable `input`/`metadataUpdate` could add or remove a