@voithos-labs/aragonite 0.10.2 → 0.10.3

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 (255) hide show
  1. package/README.md +6 -21
  2. package/THIRD-PARTY-NOTICES.md +25 -0
  3. package/dist/a11y-strings.d.ts +18 -0
  4. package/dist/a11y-strings.js +18 -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 +35 -24
  9. package/dist/components/BlockHost.svelte +16 -9
  10. package/dist/components/Editor.svelte +381 -198
  11. package/dist/components/Editor.svelte.d.ts +1 -1
  12. package/dist/components/SelectionOverlay.svelte +17 -3
  13. package/dist/components/TailInsert.svelte +107 -0
  14. package/dist/components/TailInsert.svelte.d.ts +17 -0
  15. package/dist/components/block-content-selector.d.ts +6 -2
  16. package/dist/components/block-content-selector.js +6 -2
  17. package/dist/components/blocks/ThematicBreakBlock.svelte +19 -6
  18. package/dist/components/blocks/ThematicBreakBlock.svelte.d.ts +1 -0
  19. package/dist/components/blocks/code/CodeBlock.svelte +211 -30
  20. package/dist/components/blocks/code/CodeBlockRail.svelte +686 -0
  21. package/dist/components/blocks/code/CodeBlockRail.svelte.d.ts +26 -0
  22. package/dist/components/blocks/code/code-bootstrap.js +4 -0
  23. package/dist/components/blocks/code/code-context-actions.d.ts +1 -0
  24. package/dist/components/blocks/code/code-context-actions.js +24 -0
  25. package/dist/components/blocks/code/code-fence-exit.d.ts +15 -0
  26. package/dist/components/blocks/code/code-fence-exit.js +26 -0
  27. package/dist/components/blocks/code/code-languages.d.ts +6 -0
  28. package/dist/components/blocks/code/code-languages.js +11 -0
  29. package/dist/components/blocks/code/code-renderer.js +11 -0
  30. package/dist/components/blocks/directive/DirectiveContainerBlock.svelte +1 -1
  31. package/dist/components/blocks/editable-leaf.d.ts +37 -6
  32. package/dist/components/blocks/editable-leaf.js +243 -29
  33. package/dist/components/blocks/editable-surface.d.ts +9 -0
  34. package/dist/components/blocks/editable-surface.js +22 -3
  35. package/dist/components/blocks/list/ListItemBlock.svelte +3 -3
  36. package/dist/components/blocks/list/task-checkbox.d.ts +2 -0
  37. package/dist/components/blocks/list/task-checkbox.js +11 -2
  38. package/dist/components/blocks/surface-wiring.svelte.d.ts +4 -0
  39. package/dist/components/blocks/surface-wiring.svelte.js +8 -1
  40. package/dist/components/blocks/table/TableActionMenu.svelte +210 -86
  41. package/dist/components/blocks/table/TableActionMenu.svelte.d.ts +5 -0
  42. package/dist/components/blocks/table/TableBlock.svelte +205 -186
  43. package/dist/components/blocks/table/TableBlock.svelte.d.ts +1 -0
  44. package/dist/components/blocks/table/TableCellBlock.svelte +87 -22
  45. package/dist/components/blocks/table/TableRowBlock.svelte +4 -18
  46. package/dist/components/blocks/table/TableRowBlock.svelte.d.ts +0 -5
  47. package/dist/components/blocks/table/cell-clipboard.d.ts +10 -0
  48. package/dist/components/blocks/table/cell-clipboard.js +34 -1
  49. package/dist/components/blocks/table/cell-keydown-plan.d.ts +1 -1
  50. package/dist/components/blocks/table/cell-keydown-plan.js +3 -1
  51. package/dist/components/blocks/table/table-cell-paste.js +2 -1
  52. package/dist/components/blocks/table/table-menu-model.d.ts +35 -8
  53. package/dist/components/blocks/table/table-menu-model.js +36 -17
  54. package/dist/components/blocks/text/TextEditableBlock.svelte +58 -11
  55. package/dist/components/blocks/text/delimiter-autopair.d.ts +71 -0
  56. package/dist/components/blocks/text/delimiter-autopair.js +216 -0
  57. package/dist/components/blocks/text/edge-policy-dispatch.d.ts +4 -0
  58. package/dist/components/blocks/text/edge-policy-dispatch.js +56 -2
  59. package/dist/components/blocks/text/live-selection-edit.js +27 -0
  60. package/dist/components/blocks/text/text-keydown.d.ts +7 -1
  61. package/dist/components/blocks/text/text-keydown.js +11 -1
  62. package/dist/components/blocks/text/text-render.d.ts +1 -1
  63. package/dist/components/blocks/text/text-render.js +3 -1
  64. package/dist/components/blocks/text/widget-interaction.d.ts +3 -0
  65. package/dist/components/blocks/text/widget-interaction.js +144 -22
  66. package/dist/components/drag-handle.d.ts +35 -0
  67. package/dist/components/drag-handle.js +126 -0
  68. package/dist/components/editor-root-focus.d.ts +19 -0
  69. package/dist/components/editor-root-focus.js +67 -0
  70. package/dist/components/editor-root-geometry.d.ts +39 -0
  71. package/dist/components/editor-root-geometry.js +91 -0
  72. package/dist/components/editor-root-keydown.d.ts +1 -1
  73. package/dist/components/editor-root-keydown.js +12 -3
  74. package/dist/components/editor-root-listeners.d.ts +10 -6
  75. package/dist/components/editor-root-listeners.js +23 -22
  76. package/dist/components/editor-root-mode-flip.d.ts +36 -0
  77. package/dist/components/editor-root-mode-flip.js +92 -0
  78. package/dist/components/image/ImageOverlayHost.svelte +14 -6
  79. package/dist/components/image/ImageProperties.svelte +512 -70
  80. package/dist/components/image/ImageProperties.svelte.d.ts +6 -1
  81. package/dist/components/image/ImageResizeHandles.svelte +59 -34
  82. package/dist/components/image/image-crop.d.ts +39 -0
  83. package/dist/components/image/image-crop.js +74 -0
  84. package/dist/components/image/image-edit-commit.d.ts +1 -0
  85. package/dist/components/image/image-edit-commit.js +21 -5
  86. package/dist/components/image/image-source-bytes.js +10 -3
  87. package/dist/components/image/image-widget-editing.js +1 -0
  88. package/dist/components/image/widget-dom.js +5 -1
  89. package/dist/components/link-card/link-card-commit.js +1 -1
  90. package/dist/components/lrd-map-gate.js +1 -1
  91. package/dist/components/menu/BlockMenu.svelte +315 -0
  92. package/dist/components/menu/BlockMenu.svelte.d.ts +34 -0
  93. package/dist/components/menu/MenuIcon.svelte +153 -0
  94. package/dist/components/menu/MenuIcon.svelte.d.ts +51 -0
  95. package/dist/components/menu/clipboard-actions.d.ts +12 -0
  96. package/dist/components/menu/clipboard-actions.js +42 -0
  97. package/dist/components/menu/default-context-actions.d.ts +15 -0
  98. package/dist/components/menu/default-context-actions.js +76 -0
  99. package/dist/components/menu/flyout-placement.d.ts +6 -0
  100. package/dist/components/menu/flyout-placement.js +25 -0
  101. package/dist/core/inline/format-toggle.d.ts +12 -4
  102. package/dist/core/inline/format-toggle.js +94 -40
  103. package/dist/core/inline/image-dimensions.d.ts +3 -0
  104. package/dist/core/inline/image-dimensions.js +46 -10
  105. package/dist/core/inline/inline-widgets.d.ts +11 -0
  106. package/dist/core/inline/scan/brackets.js +1 -0
  107. package/dist/core/inline/scan/plugin-syntax.d.ts +8 -0
  108. package/dist/core/inline/scan/plugin-syntax.js +15 -1
  109. package/dist/core/inline-render.d.ts +6 -0
  110. package/dist/core/inline-render.js +32 -0
  111. package/dist/core/nodes.d.ts +14 -0
  112. package/dist/cursor/edge-affinity.js +2 -1
  113. package/dist/cursor/overlay-remeasure.js +8 -0
  114. package/dist/cursor/reveal-source.js +7 -2
  115. package/dist/cursor/widget-offset.d.ts +6 -0
  116. package/dist/cursor/widget-offset.js +61 -4
  117. package/dist/debug/interaction-trace.d.ts +4 -0
  118. package/dist/debug/interaction-trace.js +15 -0
  119. package/dist/decorations/decoration-state.svelte.js +1 -1
  120. package/dist/editor-actions/ancestry-folds.d.ts +2 -2
  121. package/dist/editor-actions/ancestry-folds.js +1 -1
  122. package/dist/editor-actions/block-edit-scope.js +1 -1
  123. package/dist/editor-actions/commit/text-batch.d.ts +3 -2
  124. package/dist/editor-actions/commit/text-batch.js +1 -1
  125. package/dist/editor-actions/commit/undo-controller.js +4 -2
  126. package/dist/editor-actions/container-edit.js +2 -1
  127. package/dist/editor-actions/enter-completion.d.ts +2 -0
  128. package/dist/editor-actions/enter-completion.js +23 -2
  129. package/dist/editor-actions/inline-range-commit.js +1 -1
  130. package/dist/editor-actions/reorder-action.js +19 -10
  131. package/dist/editor-actions/reorder-drag.js +29 -1
  132. package/dist/editor-actions/replacement-focus.d.ts +1 -1
  133. package/dist/editor-actions/replacement-focus.js +1 -1
  134. package/dist/editor-actions/search-replace.js +1 -1
  135. package/dist/editor-actions/table-context.d.ts +4 -1
  136. package/dist/editor-actions/table-context.js +57 -1
  137. package/dist/editor-events.d.ts +3 -0
  138. package/dist/editor-keys.d.ts +33 -0
  139. package/dist/editor-props.d.ts +21 -12
  140. package/dist/index.d.ts +1 -1
  141. package/dist/plugin.d.ts +6 -0
  142. package/dist/plugin.js +11 -0
  143. package/dist/plugins/latex/BlockMath.svelte +264 -29
  144. package/dist/plugins/latex/BlockMath.svelte.d.ts +2 -0
  145. package/dist/plugins/latex/index.d.ts +2 -1
  146. package/dist/plugins/latex/latex-kind.js +36 -3
  147. package/dist/plugins/latex/math-completion.js +4 -1
  148. package/dist/plugins/latex/math-layout.d.ts +15 -0
  149. package/dist/plugins/latex/math-layout.js +13 -0
  150. package/dist/plugins/latex/math-source.d.ts +19 -0
  151. package/dist/plugins/latex/math-source.js +97 -0
  152. package/dist/plugins/latex/register.d.ts +11 -2
  153. package/dist/plugins/latex/register.js +3 -1
  154. package/dist/plugins/latex/renderer.d.ts +3 -3
  155. package/dist/plugins/latex/renderer.js +13 -6
  156. package/dist/reactivity/list-windowing.svelte.d.ts +8 -8
  157. package/dist/reactivity/list-windowing.svelte.js +49 -24
  158. package/dist/schema/block-completions.d.ts +8 -0
  159. package/dist/schema/block-completions.js +11 -0
  160. package/dist/schema/context-actions.d.ts +31 -0
  161. package/dist/schema/context-actions.js +23 -0
  162. package/dist/schema/fenced-code-raw.js +31 -1
  163. package/dist/schema/operations.d.ts +8 -1
  164. package/dist/schema/reserved-chords.js +25 -4
  165. package/dist/schema/table-cell-raw.d.ts +1 -1
  166. package/dist/schema/table-cell-raw.js +1 -1
  167. package/dist/selection/block-hit-test.js +3 -2
  168. package/dist/selection/char-endpoint-snap.js +1 -1
  169. package/dist/selection/clipboard-text.js +6 -1
  170. package/dist/selection/covered-block.d.ts +10 -0
  171. package/dist/selection/covered-block.js +24 -0
  172. package/dist/selection/cross-block/dispatch.d.ts +3 -0
  173. package/dist/selection/cross-block/dispatch.js +13 -1
  174. package/dist/selection/cross-block/format-range.d.ts +1 -1
  175. package/dist/selection/cross-block/format-range.js +4 -15
  176. package/dist/selection/cross-block/format-toggle.js +1 -1
  177. package/dist/selection/cross-block/keydown.js +1 -1
  178. package/dist/selection/cross-block/ops.js +1 -1
  179. package/dist/selection/cross-block/paste.js +32 -30
  180. package/dist/selection/cross-block/type-replace.d.ts +3 -2
  181. package/dist/selection/cross-block/type-replace.js +62 -13
  182. package/dist/selection/dead-space-caret.d.ts +10 -0
  183. package/dist/selection/dead-space-caret.js +47 -1
  184. package/dist/selection/double-click-trim.d.ts +17 -0
  185. package/dist/selection/double-click-trim.js +57 -0
  186. package/dist/selection/drag-pointer.d.ts +7 -2
  187. package/dist/selection/drag-pointer.js +61 -2
  188. package/dist/selection/gap-caret.js +1 -1
  189. package/dist/selection/keyboard-extend.js +1 -1
  190. package/dist/selection/path-lookup.js +1 -1
  191. package/dist/selection/range-delete-ceremony.js +4 -2
  192. package/dist/selection/range-delete-chrome.js +2 -1
  193. package/dist/selection/range-delete-table-coverage.js +2 -1
  194. package/dist/selection/range-delete-table.js +3 -2
  195. package/dist/selection/range-delete.js +30 -2
  196. package/dist/selection/selection-restore.js +1 -1
  197. package/dist/selection/selection-state.svelte.d.ts +6 -0
  198. package/dist/selection/selection-state.svelte.js +36 -1
  199. package/dist/selection/table-endpoint-snap.js +1 -1
  200. package/dist/selection/table-rect-extend.js +1 -1
  201. package/dist/styles/editor-theme.css +57 -28
  202. package/dist/styles/editor.css +217 -18
  203. package/dist/testing/container-conformance.js +2 -2
  204. package/dist/testing/inline-conformance.js +2 -1
  205. package/dist/tree-operations/blockquote.js +1 -1
  206. package/dist/tree-operations/chain-rebuild.d.ts +63 -0
  207. package/dist/tree-operations/chain-rebuild.js +142 -0
  208. package/dist/tree-operations/children.d.ts +1 -1
  209. package/dist/tree-operations/children.js +1 -1
  210. package/dist/tree-operations/cleanup.js +1 -1
  211. package/dist/tree-operations/content-write.d.ts +50 -0
  212. package/dist/tree-operations/content-write.js +263 -0
  213. package/dist/tree-operations/index.d.ts +8 -3
  214. package/dist/tree-operations/index.js +6 -2
  215. package/dist/tree-operations/list/exit-replacement.js +1 -1
  216. package/dist/tree-operations/list/unwrap-merge.js +3 -3
  217. package/dist/tree-operations/node-ops.d.ts +17 -234
  218. package/dist/tree-operations/node-ops.js +47 -1113
  219. package/dist/tree-operations/node-primitives.d.ts +75 -0
  220. package/dist/tree-operations/node-primitives.js +117 -0
  221. package/dist/tree-operations/paste/apply.js +1 -1
  222. package/dist/tree-operations/paste/body-write.d.ts +1 -1
  223. package/dist/tree-operations/paste/body-write.js +2 -2
  224. package/dist/tree-operations/paste/container-match.js +4 -2
  225. package/dist/tree-operations/paste/dispatch.js +2 -1
  226. package/dist/tree-operations/paste/find-enclosing-list.js +1 -1
  227. package/dist/tree-operations/paste/focus-target.d.ts +1 -1
  228. package/dist/tree-operations/paste/list-absorb.js +1 -1
  229. package/dist/tree-operations/paste/list-break-out.js +1 -1
  230. package/dist/tree-operations/paste/parent-scope.js +1 -1
  231. package/dist/tree-operations/paste/paste-replacement.js +1 -1
  232. package/dist/tree-operations/paste/replace-block-at-parent.js +1 -1
  233. package/dist/tree-operations/path-mutate.d.ts +1 -1
  234. package/dist/tree-operations/path-mutate.js +2 -1
  235. package/dist/tree-operations/reorder-unit.js +1 -1
  236. package/dist/tree-operations/reorder.d.ts +5 -2
  237. package/dist/tree-operations/reorder.js +55 -2
  238. package/dist/tree-operations/settle.d.ts +105 -0
  239. package/dist/tree-operations/settle.js +660 -0
  240. package/dist/tree-operations/table-grid-clipboard.d.ts +21 -0
  241. package/dist/tree-operations/table-grid-clipboard.js +90 -0
  242. package/dist/tree-operations/unshare.d.ts +15 -77
  243. package/dist/tree-operations/unshare.js +15 -166
  244. package/docs/guide/consumer-guide.md +126 -96
  245. package/docs/guide/plugin-api.md +31 -3
  246. package/docs/guide/plugin-guide.md +26 -3
  247. package/package.json +4 -2
  248. package/dist/components/blocks/code/CodeLanguageChip.svelte +0 -127
  249. package/dist/components/blocks/code/CodeLanguageChip.svelte.d.ts +0 -14
  250. package/dist/components/blocks/table/TableGrip.svelte +0 -91
  251. package/dist/components/blocks/table/TableGrip.svelte.d.ts +0 -8
  252. package/dist/components/blocks/table/table-drop-target.d.ts +0 -1
  253. package/dist/components/blocks/table/table-drop-target.js +0 -16
  254. package/dist/components/blocks/table/table-reorder-drag.d.ts +0 -78
  255. package/dist/components/blocks/table/table-reorder-drag.js +0 -97
@@ -18,7 +18,7 @@ declare const Editor: import("svelte").Component<$$ComponentProps, {
18
18
  setSelection: (selection: EditorSelection) => Promise<boolean>;
19
19
  placeCaretAtPoint: (x: number, y: number) => boolean;
20
20
  insertMarkdown: (md: string) => boolean;
21
- runCommand: (commandId: string) => boolean;
21
+ runCommand: (commandId: string, arg?: unknown) => boolean;
22
22
  canRunCommand: (commandId: string) => boolean;
23
23
  isCommandActive: (commandId: string) => boolean;
24
24
  getEvents: () => EditorEvents;
@@ -15,6 +15,7 @@
15
15
  type BlockSelectionClass
16
16
  } from '../selection/primitives';
17
17
  import { snapCrossBlockTableEndpoints } from '../selection/table-endpoint-snap';
18
+ import { pathsEqual } from '../selection/path-math';
18
19
  import { wireOverlayRemeasure } from '../cursor/overlay-remeasure';
19
20
 
20
21
  let {
@@ -52,6 +53,15 @@
52
53
  });
53
54
  });
54
55
 
56
+ // A whole unit (one surface-less block taken as the entire range) paints as a middle block
57
+ // does: its full box, no measuring.
58
+ const paintsWholeBox = $derived.by(() => {
59
+ if (classification === 'middle') return true;
60
+ if (classification !== 'single-block') return false;
61
+ const unit = selection?.wholeUnitPath ?? null;
62
+ return unit !== null && pathsEqual(unit, path);
63
+ });
64
+
55
65
  // The measuring effect and the template read this one predicate, so a rendered
56
66
  // rect is always one the effect measured; two predicates render a stale box.
57
67
  const paintsEndpoints = $derived(
@@ -67,7 +77,9 @@
67
77
  height: number;
68
78
  }
69
79
 
70
- /** Merge rects on the same visual line into a single rect to prevent double-highlight. */
80
+ /** Merge rects on the same visual line into a single rect to prevent double-highlight. Same
81
+ * line means vertically overlapping, not equal tops: an inline widget (a KaTeX box) stands
82
+ * taller than the text beside it, and two rects painted over one span read twice as dark. */
71
83
  function mergeRectsPerLine(rects: LocalRect[]): LocalRect[] {
72
84
  if (rects.length <= 1) return rects;
73
85
  const sorted = [...rects].sort((a, b) => a.top - b.top);
@@ -76,7 +88,9 @@
76
88
 
77
89
  for (let i = 1; i < sorted.length; i++) {
78
90
  const r = sorted[i];
79
- if (Math.abs(r.top - current.top) < 2) {
91
+ const overlap =
92
+ Math.min(current.top + current.height, r.top + r.height) - Math.max(current.top, r.top);
93
+ if (overlap > Math.min(current.height, r.height) * 0.5) {
80
94
  const left = Math.min(current.left, r.left);
81
95
  const right = Math.max(current.left + current.width, r.left + r.width);
82
96
  const top = Math.min(current.top, r.top);
@@ -146,7 +160,7 @@
146
160
  });
147
161
  </script>
148
162
 
149
- {#if classification === 'middle'}
163
+ {#if paintsWholeBox}
150
164
  <div class="selection-overlay selection-overlay-middle" contenteditable="false"></div>
151
165
  {:else if paintsEndpoints}
152
166
  {#each endpointRects as rect, i (i)}
@@ -0,0 +1,107 @@
1
+ <script lang="ts">
2
+ /**
3
+ * The row below the last block: a full-width strip that adds an empty paragraph at the end
4
+ * of the document and lands the caret in it, with a `+` in the left gutter that does the
5
+ * same — so there is always a way to write below whatever the last block is (a table, a
6
+ * fence, an equation), and a place to add the next block from.
7
+ */
8
+ import type { BlockEditActions } from '../action-contracts';
9
+ import { TAIL_ADD_BLOCK, TAIL_ADD_ROW } from '../a11y-strings';
10
+ import MenuIcon from './menu/MenuIcon.svelte';
11
+
12
+ let {
13
+ blockEdit,
14
+ childCount,
15
+ readOnly,
16
+ onPlus
17
+ }: {
18
+ blockEdit: BlockEditActions | undefined;
19
+ childCount: number;
20
+ readOnly: boolean;
21
+ /** The `+`: the editor appends the paragraph and opens the block menu over it. */
22
+ onPlus: (button: HTMLElement) => void;
23
+ } = $props();
24
+
25
+ // The mint's own focus lands the caret; the mousedown is swallowed so nothing else seats one.
26
+ function append(): void {
27
+ if (readOnly) return;
28
+ void blockEdit?.insertParagraph(childCount, '');
29
+ }
30
+ </script>
31
+
32
+ {#if !readOnly}
33
+ <div class="editor-tail">
34
+ <button
35
+ type="button"
36
+ class="editor-tail-plus"
37
+ aria-label={TAIL_ADD_BLOCK}
38
+ title={TAIL_ADD_BLOCK}
39
+ onmousedown={(e) => e.preventDefault()}
40
+ onclick={(e) => onPlus(e.currentTarget)}><MenuIcon name="plus" size={14} /></button
41
+ >
42
+ <button
43
+ type="button"
44
+ class="editor-tail-row"
45
+ aria-label={TAIL_ADD_ROW}
46
+ onmousedown={(e) => e.preventDefault()}
47
+ onclick={append}
48
+ ></button>
49
+ </div>
50
+ {/if}
51
+
52
+ <style>
53
+ .editor-tail {
54
+ position: relative;
55
+ display: flex;
56
+ align-items: center;
57
+ height: 34px;
58
+ margin-top: 2px;
59
+ }
60
+ .editor-tail-row {
61
+ flex: 1;
62
+ height: 100%;
63
+ padding: 0;
64
+ border: 0;
65
+ background: transparent;
66
+ cursor: text;
67
+ }
68
+ .editor-tail-row:focus-visible {
69
+ outline: 1px solid var(--color-border, #3e3e3b);
70
+ outline-offset: -1px;
71
+ border-radius: 4px;
72
+ }
73
+ /* In the editor's gutter, exactly where a block's drag handle sits (`BlockDragHandle`): the
74
+ root's 1rem padding holds a 0.85rem slot, and this is the handle of the imaginary last line. */
75
+ .editor-tail-plus {
76
+ position: absolute;
77
+ left: -1rem;
78
+ top: 50%;
79
+ transform: translateY(-50%);
80
+ display: flex;
81
+ align-items: center;
82
+ justify-content: center;
83
+ width: 1rem;
84
+ height: 1rem;
85
+ padding: 0;
86
+ border: 0;
87
+ border-radius: 3px;
88
+ background: transparent;
89
+ color: var(--color-ui-muted, #8f8f89);
90
+ cursor: pointer;
91
+ opacity: 0;
92
+ transition: opacity 120ms ease-out;
93
+ }
94
+ .editor-tail:hover .editor-tail-plus,
95
+ .editor-tail-plus:focus-visible {
96
+ opacity: 1;
97
+ }
98
+ .editor-tail-plus:hover {
99
+ background: var(--color-ui-faint, rgba(255, 255, 255, 0.07));
100
+ color: var(--color-text-primary, #e8e8e5);
101
+ }
102
+ @media (prefers-reduced-motion: reduce) {
103
+ .editor-tail-plus {
104
+ transition: none;
105
+ }
106
+ }
107
+ </style>
@@ -0,0 +1,17 @@
1
+ /**
2
+ * The row below the last block: a full-width strip that adds an empty paragraph at the end
3
+ * of the document and lands the caret in it, with a `+` in the left gutter that does the
4
+ * same — so there is always a way to write below whatever the last block is (a table, a
5
+ * fence, an equation), and a place to add the next block from.
6
+ */
7
+ import type { BlockEditActions } from '../action-contracts';
8
+ type $$ComponentProps = {
9
+ blockEdit: BlockEditActions | undefined;
10
+ childCount: number;
11
+ readOnly: boolean;
12
+ /** The `+`: the editor appends the paragraph and opens the block menu over it. */
13
+ onPlus: (button: HTMLElement) => void;
14
+ };
15
+ declare const TailInsert: import("svelte").Component<$$ComponentProps, {}, "">;
16
+ type TailInsert = ReturnType<typeof TailInsert>;
17
+ export default TailInsert;
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * A block's content element is the first child of its `data-block-path` wrapper that is not chrome
3
- * wrapped around it (child order: badges, content, the code block's language chip, selection
3
+ * wrapped around it (child order: badges, content, the code block's rail, selection
4
4
  * overlay, decoration overlays, drag handle). One definition, shared by the runtime and the e2e
5
5
  * page-object.
6
6
  */
@@ -9,4 +9,8 @@
9
9
  export declare const BLOCK_CONTENT_SELECTOR = ":scope > :not(.selection-overlay):not(.decoration-badge)";
10
10
  /** For a Playwright `locator`, which enumerates ALL matches, so every chrome child
11
11
  * must be named — the decoration overlay is one per painted mark, not per block. */
12
- export declare const BLOCK_CONTENT_LOCATOR_SELECTOR = ":scope > *:not(.selection-overlay):not(.decoration-overlay):not(.block-drag-handle):not(.decoration-badge):not(.code-lang-chip)";
12
+ export declare const BLOCK_CONTENT_LOCATOR_SELECTOR = ":scope > *:not(.selection-overlay):not(.decoration-overlay):not(.block-drag-handle):not(.decoration-badge):not(.code-rail)";
13
+ /** Marks the element a block's drag grip centres on (`drag-handle.ts`); absent, the block's
14
+ * first line of text is it. Here rather than beside the handle so the ambient-prefix DOM
15
+ * builder can set it without reaching into the components layer. */
16
+ export declare const DRAG_ANCHOR_ATTR = "data-drag-anchor";
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * A block's content element is the first child of its `data-block-path` wrapper that is not chrome
3
- * wrapped around it (child order: badges, content, the code block's language chip, selection
3
+ * wrapped around it (child order: badges, content, the code block's rail, selection
4
4
  * overlay, decoration overlays, drag handle). One definition, shared by the runtime and the e2e
5
5
  * page-object.
6
6
  */
@@ -9,4 +9,8 @@
9
9
  export const BLOCK_CONTENT_SELECTOR = ':scope > :not(.selection-overlay):not(.decoration-badge)';
10
10
  /** For a Playwright `locator`, which enumerates ALL matches, so every chrome child
11
11
  * must be named — the decoration overlay is one per painted mark, not per block. */
12
- export const BLOCK_CONTENT_LOCATOR_SELECTOR = ':scope > *:not(.selection-overlay):not(.decoration-overlay):not(.block-drag-handle):not(.decoration-badge):not(.code-lang-chip)';
12
+ export const BLOCK_CONTENT_LOCATOR_SELECTOR = ':scope > *:not(.selection-overlay):not(.decoration-overlay):not(.block-drag-handle):not(.decoration-badge):not(.code-rail)';
13
+ /** Marks the element a block's drag grip centres on (`drag-handle.ts`); absent, the block's
14
+ * first line of text is it. Here rather than beside the handle so the ambient-prefix DOM
15
+ * builder can set it without reaching into the components layer. */
16
+ export const DRAG_ANCHOR_ATTR = 'data-drag-anchor';
@@ -72,13 +72,21 @@
72
72
  export function runCommand(id: CommandId): boolean {
73
73
  return reorderRunCommand(id, reorder, () => myPath);
74
74
  }
75
+
76
+ // The rule has no text to measure, so any non-empty range over it is its whole box — what a
77
+ // range ending on it, or the rule taken as a unit, paints.
78
+ export function measurePartialRects(startOffset: number, endOffset: number): DOMRect[] {
79
+ if (endOffset <= startOffset || !boxEl) return [];
80
+ return [boxEl.getBoundingClientRect()];
81
+ }
75
82
  void ({
76
83
  editable,
77
84
  focusable,
78
85
  focus,
79
86
  parkCaret,
80
87
  getCursorOffset,
81
- runCommand
88
+ runCommand,
89
+ measurePartialRects
82
90
  } satisfies BlockComponent);
83
91
 
84
92
  // ── Event Handlers ──────────────────────────────────────────────────
@@ -136,21 +144,26 @@
136
144
  position: relative;
137
145
  }
138
146
 
147
+ /* The rule is a hairline, so the SPACE around it is what reads as a division. Cramped
148
+ padding on a heavy line reads as a border instead. */
139
149
  .thematic-break-rule {
140
150
  outline: none;
141
- padding: 8px 0;
151
+ padding: 14px 0;
142
152
  }
143
153
 
144
- /* `:focus-within`: whole-block focus lands on the host, not the separator. */
154
+ /* `:focus-within`: whole-block focus lands on the host, not the separator. Painted as the
155
+ SAME wash the selection overlay uses, not an accent ring: a rule that showed focus one
156
+ way and selection another read as two different states of the same block. */
145
157
  .thematic-break-block:focus-within .thematic-break-rule {
146
- outline: 2px solid var(--color-accent, #567b67);
147
- outline-offset: 2px;
158
+ background: var(--selection-overlay-bg, rgba(100, 150, 255, 0.3));
148
159
  border-radius: 2px;
149
160
  }
150
161
 
151
162
  hr {
152
163
  border: none;
153
- border-top: 2px solid var(--color-ui-muted, #a4a4a4);
164
+ /* A hairline on the BORDER token, not the muted-UI one: a divider separates, and at
165
+ 2px of mid-grey it competed with the text it sits between. */
166
+ border-top: 1px solid var(--color-border, #3e3e3b);
154
167
  margin: 0;
155
168
  }
156
169
  </style>
@@ -12,6 +12,7 @@ declare const ThematicBreakBlock: import("svelte").Component<$$ComponentProps, {
12
12
  parkCaret: (_offset: number) => void;
13
13
  getCursorOffset: () => number | null;
14
14
  runCommand: (id: CommandId) => boolean;
15
+ measurePartialRects: (startOffset: number, endOffset: number) => DOMRect[];
15
16
  }, "">;
16
17
  type ThematicBreakBlock = ReturnType<typeof ThematicBreakBlock>;
17
18
  export default ThematicBreakBlock;
@@ -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) {