@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
@@ -5,6 +5,7 @@
5
5
  import {
6
6
  EDITOR_POLICIES_KEY,
7
7
  EDITOR_SERVICES_KEY,
8
+ type CodeRunRequest,
8
9
  type EditorPolicies,
9
10
  type EditorServices
10
11
  } from '../../../editor-keys';
@@ -21,11 +22,12 @@
21
22
  import {
22
23
  createEditableSurface,
23
24
  createClipboardHandlers,
24
- consumePendingRestore
25
+ consumePendingRestore,
26
+ withKeydownVerdict
25
27
  } from '../editable-surface';
26
28
  import { wireSurfaceContexts, useParkFocusOnUnmount } from '../surface-wiring.svelte';
27
29
  import { createContentOffsetBackend, anchorTrailingNewline } from '../plain-text-backend';
28
- import { renderCodeBlock } from './code-renderer';
30
+ import { renderCodeBlock, sliceFencedCode } from './code-renderer';
29
31
  import {
30
32
  getLineLeadingWhitespace,
31
33
  isBetweenEmptyPair,
@@ -40,8 +42,8 @@
40
42
  writeFenceInfo
41
43
  } from '../../../schema/fenced-code-raw';
42
44
  import { hidesMarkers } from '../../../presentation-mode';
43
- import CodeLanguageChip from './CodeLanguageChip.svelte';
44
- import { computeFenceExit } from './code-fence-exit';
45
+ import CodeBlockRail from './CodeBlockRail.svelte';
46
+ import { computeFenceExit, computeTypedFenceExit } from './code-fence-exit';
45
47
  import {
46
48
  classifyFenceBoundary,
47
49
  clampCaretToBody,
@@ -80,15 +82,16 @@
80
82
  events: editorEvents
81
83
  } = wiring.deps;
82
84
  const { reorder } = getContext<EditorServices>(EDITOR_SERVICES_KEY);
83
- const { presentationMode: getPresentationMode, onPasteImage } =
84
- getContext<EditorPolicies>(EDITOR_POLICIES_KEY);
85
+ const {
86
+ presentationMode: getPresentationMode,
87
+ onPasteImage,
88
+ onRunCode,
89
+ codeMenuItems
90
+ } = getContext<EditorPolicies>(EDITOR_POLICIES_KEY);
85
91
  const presentationMode = $derived(getPresentationMode?.() ?? 'source');
86
92
  const readOnly = $derived(presentationMode === 'reading');
87
93
  let el: HTMLDivElement | undefined = $state();
88
94
  let composing = $state(false);
89
- // The walk container's own stamp, kept as state rather than re-derived: it is what
90
- // decides whether this block paints its fence chrome, and so whether it needs a chip.
91
- let contentEmpty = $state(false);
92
95
  let pendingCursorOffset = $state<number | null>(null);
93
96
  let pendingSelection = $state<{ start: number; end: number } | null>(null);
94
97
  let lastRenderedRaw = '';
@@ -192,9 +195,10 @@
192
195
 
193
196
  el.replaceChildren(renderCodeBlock(node));
194
197
  anchorTrailingNewline(el);
195
- const chromeOnly = holdsOnlyMarkerChrome(el);
196
- el.toggleAttribute(CONTENT_EMPTY_ATTR, chromeOnly);
197
- contentEmpty = chromeOnly;
198
+ // The walk container's own stamp, and the only surviving consumer of the emptiness
199
+ // read: both the marker-hiding CSS and the caret walk key off this attribute. The
200
+ // rail no longer gates on it — a content-empty fence gets one either way.
201
+ el.toggleAttribute(CONTENT_EMPTY_ATTR, holdsOnlyMarkerChrome(el));
198
202
  lastRenderedRaw = node.raw;
199
203
 
200
204
  // Restore only while this block still holds focus: an edit reparsing to multiple
@@ -224,13 +228,99 @@
224
228
 
225
229
  useParkFocusOnUnmount(() => el ?? null, getEditorRoot);
226
230
 
227
- // ── The language chip ─────────────────────────────────────────────────────
231
+ // ── The rail ──────────────────────────────────────────────────────────────
228
232
 
229
- // The chip stands in for fence chrome the mode paints nothing for, so it shows exactly
230
- // where that chrome is missing: never in source, never on a block painting its own.
231
- const showChip = $derived(hidesMarkers(presentationMode) && !contentEmpty);
233
+ // The rail stands in for fence chrome the mode paints nothing for: never in source, which
234
+ // paints its own always. A content-empty block DOES paint dimmed markers of its own and
235
+ // still gets the rail — the picker is the authoring path now, and a fence with no language
236
+ // is exactly where it is wanted. The two overlapping is the same accepted redundancy as a
237
+ // focused preview block showing fence and rail together.
238
+ const showRail = $derived(hidesMarkers(presentationMode));
232
239
  const infoString = $derived(metadataOf(node, 'fencedCode').info);
233
240
 
241
+ /** The fence body alone, which is what a host executes or a copy writes — never the
242
+ * opener and closer lines, which are this block's syntax rather than its content. */
243
+ function bodyText(): string {
244
+ return sliceFencedCode(node).body;
245
+ }
246
+
247
+ function runRequest(): CodeRunRequest {
248
+ return { code: bodyText(), info: infoString, path: myPath };
249
+ }
250
+
251
+ /**
252
+ * A fence with no language and no body, the moment it takes focus. Gated on FIRST focus
253
+ * per mount, not on the creating edit: a block made by an insert command mounts AFTER the
254
+ * commit that made it and never hears that edit, so the caret arriving is the only signal
255
+ * every creation path shares. Loading a document focuses nothing and opens no pickers.
256
+ */
257
+ let autoOpenLanguage = $state(false);
258
+ let languageOffered = false;
259
+ function onSurfaceFocus(): void {
260
+ if (languageOffered || readOnly || !showRail) return;
261
+ languageOffered = true;
262
+ // A caret that STEPPED into an existing fence is passing through, and a picker taking
263
+ // focus there traps the walk. A typed opener is still open, and a click or an insert
264
+ // command seats the caret with no arrival key noted: those are the authoring moments.
265
+ const steppedIn = metadataOf(node, 'fencedCode').closed && edgeAffinity.get() !== null;
266
+ const offerLanguage = infoString === '' && bodyText().trim() === '' && !steppedIn;
267
+ // Deferred past the commit's OWN focus work. Creating a fence focuses this surface
268
+ // twice — once as it mounts, once when the commit seats its caret — and opening on the
269
+ // first would put the field up only for the second to blur it straight back down. A
270
+ // fence that is still bare is completed first (language or not), so the picker opens
271
+ // over a real block.
272
+ void tick().then(() => {
273
+ const completed = completeBareFence();
274
+ if (!offerLanguage) return;
275
+ if (completed) {
276
+ void tick().then(() => {
277
+ autoOpenLanguage = true;
278
+ });
279
+ } else {
280
+ autoOpenLanguage = true;
281
+ }
282
+ });
283
+ }
284
+
285
+ /**
286
+ * A fence with no body line — a just-typed opener, or an opener glued to its closer — is a
287
+ * block whose only bytes are its own chrome: nowhere for a caret, so the markers must paint.
288
+ * Writes opener, one empty body line, closer, with the caret on that line. Block math gets
289
+ * this from the Enter-completion seam, which a fence cannot use: ``` parses as an (unclosed)
290
+ * fence the moment it is typed, so no paragraph is left for that seam to claim.
291
+ */
292
+ function completeBareFence(): boolean {
293
+ if (!el) return false;
294
+ const meta = metadataOf(node, 'fencedCode');
295
+ const slice = sliceFencedCode(node);
296
+ const text = getDisplayText();
297
+ const ending = trailingLineEnding(node.raw);
298
+ const offset = backend.getRaw() ?? 0;
299
+ if (!meta.closed) {
300
+ if (slice.body.trim() !== '') return false;
301
+ const closer = meta.fenceMarker.repeat(meta.fenceLength);
302
+ const completed = text + ending + ending + closer;
303
+ pendingCursorOffset = commitDisplay(completed, offset, text.length + ending.length);
304
+ return true;
305
+ }
306
+ if (slice.body !== '') return false;
307
+ const openerLength = slice.openerLine.length;
308
+ const completed = text.slice(0, openerLength) + ending + text.slice(openerLength);
309
+ pendingCursorOffset = commitDisplay(completed, offset, openerLength);
310
+ return true;
311
+ }
312
+
313
+ async function copyBody(): Promise<boolean> {
314
+ try {
315
+ await navigator.clipboard.writeText(bodyText());
316
+ return true;
317
+ } catch {
318
+ // A denied or absent clipboard is the host's business, not an editor error: the
319
+ // affordance simply reports nothing copied.
320
+ return false;
321
+ }
322
+ }
323
+
234
324
  // The chip's write: the opener's info span, through the display funnel so the fence rule
235
325
  // runs over it, isolated so no typing burst on either side joins its undo entry.
236
326
  function commitLanguage(info: string): void {
@@ -249,10 +339,26 @@
249
339
  returnCaretToBody();
250
340
  }
251
341
 
252
- // This block's own door, not `moveFocus`: the chip is chrome over one block, and a
253
- // traversal to it stops at the gap above instead. The seat waits for the commit's render.
342
+ /**
343
+ * This block's own door, not `moveFocus`: the rail is chrome over one block. Offset 0 is
344
+ * NOT the body — on an UNCLOSED fence the opener run counts as content by design (that is
345
+ * what lets a just-typed ` ``` ` be deleted back out), so `focus(0)` parks the caret BEFORE
346
+ * the backticks and the next keystroke rewrites the opener. `clampRangeToBody` clamps
347
+ * unconditionally, which is the "wherever the body starts" this needs.
348
+ */
254
349
  function returnCaretToBody(): void {
255
- void tick().then(() => focus(0));
350
+ // Focus first and SEAT through the pending-caret channel the render effect drains. A
351
+ // bare `focus()` after a tick raced the commit's own re-render, which replaces every
352
+ // child of the walk container and dropped the seat on the floor.
353
+ el?.focus({ preventScroll: true });
354
+ void tick().then(() => {
355
+ // Read the body AFTER the commit lands. Writing a language lengthens the opener, so
356
+ // an offset measured against the pre-commit node points into the info string that
357
+ // just grew — the caret arrived inside `js` and typing split it.
358
+ const at = clampRangeToBody(node, { start: 0, end: 0 }).start;
359
+ pendingCursorOffset = at;
360
+ focus(at);
361
+ });
256
362
  }
257
363
 
258
364
  // ── Event handlers ────────────────────────────────────────────────────────
@@ -299,6 +405,17 @@
299
405
  const offset = selOffsets ? selOffsets.start : (backend.getRaw() ?? 0);
300
406
 
301
407
  const meta = metadataOf(node, 'fencedCode');
408
+ const collapsed = !selOffsets || selOffsets.start === selOffsets.end;
409
+ const typedExit = collapsed
410
+ ? computeTypedFenceExit({ text, offset, meta, typed: data })
411
+ : { kind: 'none' as const };
412
+ if (typedExit.kind === 'exitWithEdit') {
413
+ e.preventDefault();
414
+ commitDisplay(typedExit.newText, offset, offset);
415
+ exitDownward();
416
+ return;
417
+ }
418
+
302
419
  const result = computeAutoPair({
303
420
  text,
304
421
  selection: selOffsets ?? { start: offset, end: offset },
@@ -344,7 +461,8 @@
344
461
  function guardFenceRangedEdit(e: InputEvent): boolean {
345
462
  if (composing || !el) return false;
346
463
  const range = pendingEditRange(e, el);
347
- if (!range || !crossesFenceBoundary(node, range)) return false;
464
+ if (!range) return false;
465
+ if (!crossesFenceBoundary(node, range)) return guardHiddenFenceDelete(e, range);
348
466
 
349
467
  e.preventDefault();
350
468
  const insert = rangedEditInsertion(e, fenceEditSpan(node, range));
@@ -357,6 +475,27 @@
357
475
  return true;
358
476
  }
359
477
 
478
+ /**
479
+ * A delete while the fence lines hide is applied here, whatever range the engine reports:
480
+ * Chromium, deleting the last visible character of a line, also removes the unrendered nodes
481
+ * beside it — the opener's whole fence line — so a Backspace on the last body character
482
+ * left `\n\`\`\`` and reparsed the block into a fresh fence. The span is clamped to the body.
483
+ */
484
+ function guardHiddenFenceDelete(e: InputEvent, range: CodeRange): boolean {
485
+ if (!showRail || !/^delete(?!By)/.test(e.inputType)) return false;
486
+ e.preventDefault();
487
+ const span = clampRangeToBody(node, range);
488
+ if (span.end > span.start) {
489
+ const text = getDisplayText();
490
+ pendingCursorOffset = commitDisplay(
491
+ text.slice(0, span.start) + text.slice(span.end),
492
+ backend.getRaw() ?? 0,
493
+ span.start
494
+ );
495
+ }
496
+ return true;
497
+ }
498
+
360
499
  /**
361
500
  * What the pending edit will rewrite. `getTargetRanges()` is the authority — a word
362
501
  * delete at a collapsed caret reports the word, not the caret — and is
@@ -406,6 +545,8 @@
406
545
  if (wiring.dispatchChord(e, { kind: node.kind, runCommand })) return;
407
546
  }
408
547
 
548
+ const onKeyDownTraced = withKeydownVerdict(onKeyDown);
549
+
409
550
  // ── Commands ────────────────────────────────────────────────────────
410
551
 
411
552
  export function runCommand(id: CommandId): boolean {
@@ -443,6 +584,15 @@
443
584
  offset === 0 ||
444
585
  classifyFenceBoundary({ node, offset, forward: false }).kind === 'exitPrev'
445
586
  ) {
587
+ // At the top of an EMPTY fence the block is the only thing the press could mean:
588
+ // there is no text to delete and nothing to merge, and stepping the caret out
589
+ // leaves a block the user just asked to be rid of. A fence with a body keeps the
590
+ // step-out — one press must never take code with it.
591
+ if (bodyText().trim() === '') {
592
+ void blockEdit.deleteBlock(index);
593
+ focusActions.moveFocus(index - 1, 'end');
594
+ return true;
595
+ }
446
596
  focusActions.moveFocus(index - 1, 'end');
447
597
  return true;
448
598
  }
@@ -481,6 +631,18 @@
481
631
  const text = getDisplayText();
482
632
  const meta = metadataOf(node, 'fencedCode');
483
633
 
634
+ // Source mode paints the markers and never completes a bare fence on focus, so Enter is
635
+ // where it happens there; the marker-hiding rungs already did it as the caret arrived.
636
+ if (completeBareFence()) {
637
+ if (infoString === '' && !readOnly && !languageOffered) {
638
+ languageOffered = true;
639
+ void tick().then(() => {
640
+ autoOpenLanguage = true;
641
+ });
642
+ }
643
+ return true;
644
+ }
645
+
484
646
  const exit = computeFenceExit({ text, offset, meta });
485
647
  if (exit.kind === 'closeAndExit') {
486
648
  closeUnclosedFenceAndDescend(exit.newText);
@@ -671,7 +833,8 @@
671
833
  role="textbox"
672
834
  spellcheck="false"
673
835
  oninput={onInput}
674
- onkeydown={onKeyDown}
836
+ onfocus={onSurfaceFocus}
837
+ onkeydown={onKeyDownTraced}
675
838
  onbeforeinput={onBeforeInput}
676
839
  oncopy={onCopy}
677
840
  oncut={onCut}
@@ -682,26 +845,32 @@
682
845
  ></div>
683
846
  <!-- Beside the walk container, never inside it: the render effect replaces that element's
684
847
  children on every commit, and the offset walk counts everything that survives there. -->
685
- {#if showChip}
686
- <CodeLanguageChip
848
+ {#if showRail}
849
+ <CodeBlockRail
687
850
  info={infoString}
688
851
  editable={!readOnly}
852
+ autoOpen={autoOpenLanguage}
689
853
  onCommit={commitLanguage}
690
854
  onCancel={(returnCaret) => returnCaret && returnCaretToBody()}
855
+ onRun={onRunCode ? () => onRunCode(runRequest()) : undefined}
856
+ onCopy={copyBody}
857
+ menuItems={codeMenuItems ? () => codeMenuItems(runRequest()) : undefined}
691
858
  />
692
859
  {/if}
693
860
 
694
861
  <style>
862
+ /* A recessed slab rather than an outlined field: the fill carries the block and the
863
+ border only closes its edge, so a page of prose reads code as a surface it sits on. */
695
864
  .code-block {
696
865
  width: 100%;
697
866
  outline: none;
698
- padding: 12px;
699
- font-family: var(--font-editor, ui-monospace, monospace);
867
+ padding: 14px 16px;
868
+ font-family: var(--font-code, ui-monospace, monospace);
700
869
  font-size: 0.9em;
701
- line-height: 1.5;
870
+ line-height: 1.55;
702
871
  background: var(--color-bg-secondary, rgba(128, 128, 128, 0.12));
703
- border: 1px solid var(--color-ui-muted, #a4a4a4);
704
- border-radius: 4px;
872
+ border: 1px solid transparent;
873
+ border-radius: var(--radius-surface, 8px);
705
874
  color: inherit;
706
875
  white-space: pre;
707
876
  overflow-x: auto;
@@ -709,10 +878,22 @@
709
878
  tab-size: 4;
710
879
  box-sizing: border-box;
711
880
  min-height: 1.4em;
881
+ transition: border-color 120ms ease-out;
712
882
  }
713
883
 
714
- .code-block:focus {
715
- border-color: var(--color-accent, #567b67);
884
+ /* The caret is the primary focus signal inside the box; the border only warms, so a
885
+ click into code does not flash a heavy ring across the page. */
886
+ /* Focus lives in the rail's search field while the picker is open — outside this element —
887
+ so the block would otherwise drop its focused look mid-interaction. */
888
+ .code-block:focus,
889
+ .code-block:has(~ :global(.code-rail-open)) {
890
+ border-color: var(--color-border, #3d4047);
891
+ }
892
+
893
+ @media (prefers-reduced-motion: reduce) {
894
+ .code-block {
895
+ transition: none;
896
+ }
716
897
  }
717
898
 
718
899
  .code-block :global(.md-marker) {