@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.
- package/README.md +6 -21
- package/THIRD-PARTY-NOTICES.md +25 -0
- package/dist/a11y-strings.d.ts +18 -0
- package/dist/a11y-strings.js +18 -0
- package/dist/action-contracts.d.ts +7 -1
- package/dist/ambient/ambient-dom.js +5 -1
- package/dist/block-component.d.ts +14 -0
- package/dist/components/BlockDragHandle.svelte +35 -24
- package/dist/components/BlockHost.svelte +16 -9
- package/dist/components/Editor.svelte +381 -198
- package/dist/components/Editor.svelte.d.ts +1 -1
- package/dist/components/SelectionOverlay.svelte +17 -3
- package/dist/components/TailInsert.svelte +107 -0
- package/dist/components/TailInsert.svelte.d.ts +17 -0
- package/dist/components/block-content-selector.d.ts +6 -2
- package/dist/components/block-content-selector.js +6 -2
- package/dist/components/blocks/ThematicBreakBlock.svelte +19 -6
- package/dist/components/blocks/ThematicBreakBlock.svelte.d.ts +1 -0
- package/dist/components/blocks/code/CodeBlock.svelte +211 -30
- package/dist/components/blocks/code/CodeBlockRail.svelte +686 -0
- package/dist/components/blocks/code/CodeBlockRail.svelte.d.ts +26 -0
- package/dist/components/blocks/code/code-bootstrap.js +4 -0
- package/dist/components/blocks/code/code-context-actions.d.ts +1 -0
- package/dist/components/blocks/code/code-context-actions.js +24 -0
- package/dist/components/blocks/code/code-fence-exit.d.ts +15 -0
- package/dist/components/blocks/code/code-fence-exit.js +26 -0
- package/dist/components/blocks/code/code-languages.d.ts +6 -0
- package/dist/components/blocks/code/code-languages.js +11 -0
- package/dist/components/blocks/code/code-renderer.js +11 -0
- package/dist/components/blocks/directive/DirectiveContainerBlock.svelte +1 -1
- package/dist/components/blocks/editable-leaf.d.ts +37 -6
- package/dist/components/blocks/editable-leaf.js +243 -29
- package/dist/components/blocks/editable-surface.d.ts +9 -0
- package/dist/components/blocks/editable-surface.js +22 -3
- package/dist/components/blocks/list/ListItemBlock.svelte +3 -3
- package/dist/components/blocks/list/task-checkbox.d.ts +2 -0
- package/dist/components/blocks/list/task-checkbox.js +11 -2
- package/dist/components/blocks/surface-wiring.svelte.d.ts +4 -0
- package/dist/components/blocks/surface-wiring.svelte.js +8 -1
- package/dist/components/blocks/table/TableActionMenu.svelte +210 -86
- package/dist/components/blocks/table/TableActionMenu.svelte.d.ts +5 -0
- package/dist/components/blocks/table/TableBlock.svelte +205 -186
- package/dist/components/blocks/table/TableBlock.svelte.d.ts +1 -0
- package/dist/components/blocks/table/TableCellBlock.svelte +87 -22
- package/dist/components/blocks/table/TableRowBlock.svelte +4 -18
- package/dist/components/blocks/table/TableRowBlock.svelte.d.ts +0 -5
- package/dist/components/blocks/table/cell-clipboard.d.ts +10 -0
- package/dist/components/blocks/table/cell-clipboard.js +34 -1
- package/dist/components/blocks/table/cell-keydown-plan.d.ts +1 -1
- package/dist/components/blocks/table/cell-keydown-plan.js +3 -1
- package/dist/components/blocks/table/table-cell-paste.js +2 -1
- package/dist/components/blocks/table/table-menu-model.d.ts +35 -8
- package/dist/components/blocks/table/table-menu-model.js +36 -17
- package/dist/components/blocks/text/TextEditableBlock.svelte +58 -11
- package/dist/components/blocks/text/delimiter-autopair.d.ts +71 -0
- package/dist/components/blocks/text/delimiter-autopair.js +216 -0
- package/dist/components/blocks/text/edge-policy-dispatch.d.ts +4 -0
- package/dist/components/blocks/text/edge-policy-dispatch.js +56 -2
- package/dist/components/blocks/text/live-selection-edit.js +27 -0
- package/dist/components/blocks/text/text-keydown.d.ts +7 -1
- package/dist/components/blocks/text/text-keydown.js +11 -1
- package/dist/components/blocks/text/text-render.d.ts +1 -1
- package/dist/components/blocks/text/text-render.js +3 -1
- package/dist/components/blocks/text/widget-interaction.d.ts +3 -0
- package/dist/components/blocks/text/widget-interaction.js +144 -22
- package/dist/components/drag-handle.d.ts +35 -0
- package/dist/components/drag-handle.js +126 -0
- package/dist/components/editor-root-focus.d.ts +19 -0
- package/dist/components/editor-root-focus.js +67 -0
- package/dist/components/editor-root-geometry.d.ts +39 -0
- package/dist/components/editor-root-geometry.js +91 -0
- package/dist/components/editor-root-keydown.d.ts +1 -1
- package/dist/components/editor-root-keydown.js +12 -3
- package/dist/components/editor-root-listeners.d.ts +10 -6
- package/dist/components/editor-root-listeners.js +23 -22
- package/dist/components/editor-root-mode-flip.d.ts +36 -0
- package/dist/components/editor-root-mode-flip.js +92 -0
- package/dist/components/image/ImageOverlayHost.svelte +14 -6
- package/dist/components/image/ImageProperties.svelte +512 -70
- package/dist/components/image/ImageProperties.svelte.d.ts +6 -1
- package/dist/components/image/ImageResizeHandles.svelte +59 -34
- package/dist/components/image/image-crop.d.ts +39 -0
- package/dist/components/image/image-crop.js +74 -0
- package/dist/components/image/image-edit-commit.d.ts +1 -0
- package/dist/components/image/image-edit-commit.js +21 -5
- package/dist/components/image/image-source-bytes.js +10 -3
- package/dist/components/image/image-widget-editing.js +1 -0
- package/dist/components/image/widget-dom.js +5 -1
- package/dist/components/link-card/link-card-commit.js +1 -1
- package/dist/components/lrd-map-gate.js +1 -1
- package/dist/components/menu/BlockMenu.svelte +315 -0
- package/dist/components/menu/BlockMenu.svelte.d.ts +34 -0
- package/dist/components/menu/MenuIcon.svelte +153 -0
- package/dist/components/menu/MenuIcon.svelte.d.ts +51 -0
- package/dist/components/menu/clipboard-actions.d.ts +12 -0
- package/dist/components/menu/clipboard-actions.js +42 -0
- package/dist/components/menu/default-context-actions.d.ts +15 -0
- package/dist/components/menu/default-context-actions.js +76 -0
- package/dist/components/menu/flyout-placement.d.ts +6 -0
- package/dist/components/menu/flyout-placement.js +25 -0
- package/dist/core/inline/format-toggle.d.ts +12 -4
- package/dist/core/inline/format-toggle.js +94 -40
- package/dist/core/inline/image-dimensions.d.ts +3 -0
- package/dist/core/inline/image-dimensions.js +46 -10
- package/dist/core/inline/inline-widgets.d.ts +11 -0
- package/dist/core/inline/scan/brackets.js +1 -0
- package/dist/core/inline/scan/plugin-syntax.d.ts +8 -0
- package/dist/core/inline/scan/plugin-syntax.js +15 -1
- package/dist/core/inline-render.d.ts +6 -0
- package/dist/core/inline-render.js +32 -0
- package/dist/core/nodes.d.ts +14 -0
- package/dist/cursor/edge-affinity.js +2 -1
- package/dist/cursor/overlay-remeasure.js +8 -0
- package/dist/cursor/reveal-source.js +7 -2
- package/dist/cursor/widget-offset.d.ts +6 -0
- package/dist/cursor/widget-offset.js +61 -4
- package/dist/debug/interaction-trace.d.ts +4 -0
- package/dist/debug/interaction-trace.js +15 -0
- package/dist/decorations/decoration-state.svelte.js +1 -1
- package/dist/editor-actions/ancestry-folds.d.ts +2 -2
- package/dist/editor-actions/ancestry-folds.js +1 -1
- package/dist/editor-actions/block-edit-scope.js +1 -1
- package/dist/editor-actions/commit/text-batch.d.ts +3 -2
- package/dist/editor-actions/commit/text-batch.js +1 -1
- package/dist/editor-actions/commit/undo-controller.js +4 -2
- package/dist/editor-actions/container-edit.js +2 -1
- package/dist/editor-actions/enter-completion.d.ts +2 -0
- package/dist/editor-actions/enter-completion.js +23 -2
- package/dist/editor-actions/inline-range-commit.js +1 -1
- package/dist/editor-actions/reorder-action.js +19 -10
- package/dist/editor-actions/reorder-drag.js +29 -1
- package/dist/editor-actions/replacement-focus.d.ts +1 -1
- package/dist/editor-actions/replacement-focus.js +1 -1
- package/dist/editor-actions/search-replace.js +1 -1
- package/dist/editor-actions/table-context.d.ts +4 -1
- package/dist/editor-actions/table-context.js +57 -1
- package/dist/editor-events.d.ts +3 -0
- package/dist/editor-keys.d.ts +33 -0
- package/dist/editor-props.d.ts +21 -12
- package/dist/index.d.ts +1 -1
- package/dist/plugin.d.ts +6 -0
- package/dist/plugin.js +11 -0
- package/dist/plugins/latex/BlockMath.svelte +264 -29
- package/dist/plugins/latex/BlockMath.svelte.d.ts +2 -0
- package/dist/plugins/latex/index.d.ts +2 -1
- package/dist/plugins/latex/latex-kind.js +36 -3
- package/dist/plugins/latex/math-completion.js +4 -1
- package/dist/plugins/latex/math-layout.d.ts +15 -0
- package/dist/plugins/latex/math-layout.js +13 -0
- package/dist/plugins/latex/math-source.d.ts +19 -0
- package/dist/plugins/latex/math-source.js +97 -0
- package/dist/plugins/latex/register.d.ts +11 -2
- package/dist/plugins/latex/register.js +3 -1
- package/dist/plugins/latex/renderer.d.ts +3 -3
- package/dist/plugins/latex/renderer.js +13 -6
- package/dist/reactivity/list-windowing.svelte.d.ts +8 -8
- package/dist/reactivity/list-windowing.svelte.js +49 -24
- package/dist/schema/block-completions.d.ts +8 -0
- package/dist/schema/block-completions.js +11 -0
- package/dist/schema/context-actions.d.ts +31 -0
- package/dist/schema/context-actions.js +23 -0
- package/dist/schema/fenced-code-raw.js +31 -1
- package/dist/schema/operations.d.ts +8 -1
- package/dist/schema/reserved-chords.js +25 -4
- package/dist/schema/table-cell-raw.d.ts +1 -1
- package/dist/schema/table-cell-raw.js +1 -1
- package/dist/selection/block-hit-test.js +3 -2
- package/dist/selection/char-endpoint-snap.js +1 -1
- package/dist/selection/clipboard-text.js +6 -1
- package/dist/selection/covered-block.d.ts +10 -0
- package/dist/selection/covered-block.js +24 -0
- package/dist/selection/cross-block/dispatch.d.ts +3 -0
- package/dist/selection/cross-block/dispatch.js +13 -1
- package/dist/selection/cross-block/format-range.d.ts +1 -1
- package/dist/selection/cross-block/format-range.js +4 -15
- package/dist/selection/cross-block/format-toggle.js +1 -1
- package/dist/selection/cross-block/keydown.js +1 -1
- package/dist/selection/cross-block/ops.js +1 -1
- package/dist/selection/cross-block/paste.js +32 -30
- package/dist/selection/cross-block/type-replace.d.ts +3 -2
- package/dist/selection/cross-block/type-replace.js +62 -13
- package/dist/selection/dead-space-caret.d.ts +10 -0
- package/dist/selection/dead-space-caret.js +47 -1
- package/dist/selection/double-click-trim.d.ts +17 -0
- package/dist/selection/double-click-trim.js +57 -0
- package/dist/selection/drag-pointer.d.ts +7 -2
- package/dist/selection/drag-pointer.js +61 -2
- package/dist/selection/gap-caret.js +1 -1
- package/dist/selection/keyboard-extend.js +1 -1
- package/dist/selection/path-lookup.js +1 -1
- package/dist/selection/range-delete-ceremony.js +4 -2
- package/dist/selection/range-delete-chrome.js +2 -1
- package/dist/selection/range-delete-table-coverage.js +2 -1
- package/dist/selection/range-delete-table.js +3 -2
- package/dist/selection/range-delete.js +30 -2
- package/dist/selection/selection-restore.js +1 -1
- package/dist/selection/selection-state.svelte.d.ts +6 -0
- package/dist/selection/selection-state.svelte.js +36 -1
- package/dist/selection/table-endpoint-snap.js +1 -1
- package/dist/selection/table-rect-extend.js +1 -1
- package/dist/styles/editor-theme.css +57 -28
- package/dist/styles/editor.css +217 -18
- package/dist/testing/container-conformance.js +2 -2
- package/dist/testing/inline-conformance.js +2 -1
- package/dist/tree-operations/blockquote.js +1 -1
- package/dist/tree-operations/chain-rebuild.d.ts +63 -0
- package/dist/tree-operations/chain-rebuild.js +142 -0
- package/dist/tree-operations/children.d.ts +1 -1
- package/dist/tree-operations/children.js +1 -1
- package/dist/tree-operations/cleanup.js +1 -1
- package/dist/tree-operations/content-write.d.ts +50 -0
- package/dist/tree-operations/content-write.js +263 -0
- package/dist/tree-operations/index.d.ts +8 -3
- package/dist/tree-operations/index.js +6 -2
- package/dist/tree-operations/list/exit-replacement.js +1 -1
- package/dist/tree-operations/list/unwrap-merge.js +3 -3
- package/dist/tree-operations/node-ops.d.ts +17 -234
- package/dist/tree-operations/node-ops.js +47 -1113
- package/dist/tree-operations/node-primitives.d.ts +75 -0
- package/dist/tree-operations/node-primitives.js +117 -0
- package/dist/tree-operations/paste/apply.js +1 -1
- package/dist/tree-operations/paste/body-write.d.ts +1 -1
- package/dist/tree-operations/paste/body-write.js +2 -2
- package/dist/tree-operations/paste/container-match.js +4 -2
- package/dist/tree-operations/paste/dispatch.js +2 -1
- package/dist/tree-operations/paste/find-enclosing-list.js +1 -1
- package/dist/tree-operations/paste/focus-target.d.ts +1 -1
- package/dist/tree-operations/paste/list-absorb.js +1 -1
- package/dist/tree-operations/paste/list-break-out.js +1 -1
- package/dist/tree-operations/paste/parent-scope.js +1 -1
- package/dist/tree-operations/paste/paste-replacement.js +1 -1
- package/dist/tree-operations/paste/replace-block-at-parent.js +1 -1
- package/dist/tree-operations/path-mutate.d.ts +1 -1
- package/dist/tree-operations/path-mutate.js +2 -1
- package/dist/tree-operations/reorder-unit.js +1 -1
- package/dist/tree-operations/reorder.d.ts +5 -2
- package/dist/tree-operations/reorder.js +55 -2
- package/dist/tree-operations/settle.d.ts +105 -0
- package/dist/tree-operations/settle.js +660 -0
- package/dist/tree-operations/table-grid-clipboard.d.ts +21 -0
- package/dist/tree-operations/table-grid-clipboard.js +90 -0
- package/dist/tree-operations/unshare.d.ts +15 -77
- package/dist/tree-operations/unshare.js +15 -166
- package/docs/guide/consumer-guide.md +126 -96
- package/docs/guide/plugin-api.md +31 -3
- package/docs/guide/plugin-guide.md +26 -3
- package/package.json +4 -2
- package/dist/components/blocks/code/CodeLanguageChip.svelte +0 -127
- package/dist/components/blocks/code/CodeLanguageChip.svelte.d.ts +0 -14
- package/dist/components/blocks/table/TableGrip.svelte +0 -91
- package/dist/components/blocks/table/TableGrip.svelte.d.ts +0 -8
- package/dist/components/blocks/table/table-drop-target.d.ts +0 -1
- package/dist/components/blocks/table/table-drop-target.js +0 -16
- package/dist/components/blocks/table/table-reorder-drag.d.ts +0 -78
- 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
|
-
|
|
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
|
|
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
|
|
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-
|
|
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
|
|
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-
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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 {
|
|
84
|
-
|
|
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
|
-
|
|
196
|
-
|
|
197
|
-
|
|
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
|
|
231
|
+
// ── The rail ──────────────────────────────────────────────────────────────
|
|
228
232
|
|
|
229
|
-
// The
|
|
230
|
-
//
|
|
231
|
-
|
|
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
|
-
|
|
253
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
686
|
-
<
|
|
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:
|
|
699
|
-
font-family: var(--font-
|
|
867
|
+
padding: 14px 16px;
|
|
868
|
+
font-family: var(--font-code, ui-monospace, monospace);
|
|
700
869
|
font-size: 0.9em;
|
|
701
|
-
line-height: 1.
|
|
870
|
+
line-height: 1.55;
|
|
702
871
|
background: var(--color-bg-secondary, rgba(128, 128, 128, 0.12));
|
|
703
|
-
border: 1px solid
|
|
704
|
-
border-radius:
|
|
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
|
-
|
|
715
|
-
|
|
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) {
|