@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
|
@@ -0,0 +1,686 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { tick } from 'svelte';
|
|
3
|
+
import {
|
|
4
|
+
CODE_COPY_LABEL,
|
|
5
|
+
CODE_COPIED_LABEL,
|
|
6
|
+
CODE_LANGUAGE_FIELD,
|
|
7
|
+
CODE_MENU_LABEL,
|
|
8
|
+
CODE_RAIL_LABEL,
|
|
9
|
+
CODE_RUN_LABEL,
|
|
10
|
+
codeLanguageLabel
|
|
11
|
+
} from '../../../a11y-strings';
|
|
12
|
+
import type { CodeMenuItem } from '../../../editor-keys';
|
|
13
|
+
import { listLanguages } from './code-languages';
|
|
14
|
+
|
|
15
|
+
// The chrome for the modes that paint no fence: the language door plus whatever action
|
|
16
|
+
// affordances the host has earned by installing a hook. Drafts live here and only a
|
|
17
|
+
// commit reaches the tree.
|
|
18
|
+
let {
|
|
19
|
+
info,
|
|
20
|
+
editable,
|
|
21
|
+
autoOpen = false,
|
|
22
|
+
onCommit,
|
|
23
|
+
onCancel,
|
|
24
|
+
onRun,
|
|
25
|
+
onCopy,
|
|
26
|
+
menuItems
|
|
27
|
+
}: {
|
|
28
|
+
/** The opener's full info string; the button shows its first token. */
|
|
29
|
+
info: string;
|
|
30
|
+
/** False in reading mode, which writes no bytes — the chip is then a label. */
|
|
31
|
+
editable: boolean;
|
|
32
|
+
/** Open the language field as soon as the rail mounts: a fence the user just created
|
|
33
|
+
* has no language yet, and asking is the whole reason the chip exists. */
|
|
34
|
+
autoOpen?: boolean;
|
|
35
|
+
/** Only Enter or a list pick calls this, and it owns the caret's landing afterwards. */
|
|
36
|
+
onCommit: (info: string) => void;
|
|
37
|
+
/** Nothing written. Escape asks for the caret back; a blur must not yank it from
|
|
38
|
+
* wherever the user just clicked, so it does not. */
|
|
39
|
+
onCancel: (returnCaret: boolean) => void;
|
|
40
|
+
/** Absent when the host installed no execution hook — no run affordance renders. */
|
|
41
|
+
onRun?: () => void;
|
|
42
|
+
/** Copies the fence body; resolves once the clipboard write settles. */
|
|
43
|
+
onCopy: () => Promise<boolean>;
|
|
44
|
+
/** Consulted on each open, so items read live state. Empty renders no affordance. */
|
|
45
|
+
menuItems?: () => readonly CodeMenuItem[];
|
|
46
|
+
} = $props();
|
|
47
|
+
|
|
48
|
+
const language = $derived(info.split(/\s+/)[0] || 'text');
|
|
49
|
+
|
|
50
|
+
let editing = $state(false);
|
|
51
|
+
let draft = $state('');
|
|
52
|
+
// The draft SEEDS with the current language, so filtering on it from the start would open
|
|
53
|
+
// the picker onto a list of one. Filtering waits for a keystroke; until then the full list
|
|
54
|
+
// shows with the current language highlighted, which is also what Enter would re-commit.
|
|
55
|
+
let filtering = $state(false);
|
|
56
|
+
// Whether the LIST is what Enter should take. Until the user types or arrows, it is not:
|
|
57
|
+
// the highlight is only a where-you-stand marker, and a language the registry does not
|
|
58
|
+
// carry (an unregistered grammar, or a bare mount that registered none) would otherwise
|
|
59
|
+
// have no entry to seat on, so a bare Enter would rewrite it to the list's first row.
|
|
60
|
+
let activeIndex = $state(0);
|
|
61
|
+
let copied = $state(false);
|
|
62
|
+
let menuOpen = $state(false);
|
|
63
|
+
let openMenuItems = $state<readonly CodeMenuItem[]>([]);
|
|
64
|
+
|
|
65
|
+
let railEl: HTMLElement | undefined = $state();
|
|
66
|
+
let chipEl: HTMLElement | undefined = $state();
|
|
67
|
+
let inputEl: HTMLInputElement | undefined = $state();
|
|
68
|
+
let listEl: HTMLElement | undefined = $state();
|
|
69
|
+
let pickerEl: HTMLElement | undefined = $state();
|
|
70
|
+
let menuEl: HTMLElement | undefined = $state();
|
|
71
|
+
let menuButtonEl: HTMLElement | undefined = $state();
|
|
72
|
+
|
|
73
|
+
// The picker offers what the renderer can actually tokenize, filtered as the draft is
|
|
74
|
+
// typed. `text` is always offered: it is how a user CLEARS a language, and no grammar
|
|
75
|
+
// registers under that name.
|
|
76
|
+
const suggestions = $derived.by(() => {
|
|
77
|
+
const all = ['text', ...listLanguages().filter((name) => name !== 'text')];
|
|
78
|
+
const needle = filtering ? draft.trim().toLowerCase() : '';
|
|
79
|
+
if (needle.length > 0) {
|
|
80
|
+
const starts = all.filter((name) => name.startsWith(needle));
|
|
81
|
+
const contains = all.filter((name) => !name.startsWith(needle) && name.includes(needle));
|
|
82
|
+
return [...starts, ...contains];
|
|
83
|
+
}
|
|
84
|
+
// Unfiltered, the block's own language leads. This menu is ours to order, so the row
|
|
85
|
+
// that matters goes where the eye already is rather than somewhere the list has to be
|
|
86
|
+
// scrolled to — which is why nothing here scrolls itself on open.
|
|
87
|
+
const current = language;
|
|
88
|
+
const rest = all.filter((name) => name !== current);
|
|
89
|
+
return all.includes(current) ? [current, ...rest] : [current, ...rest];
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
// The highlight cannot outrun a filter that shortened the list under it.
|
|
93
|
+
$effect(() => {
|
|
94
|
+
if (activeIndex >= suggestions.length) activeIndex = 0;
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
// Mount-time open for a freshly minted fence. Guarded on `editable` like every other
|
|
98
|
+
// write door, and fired once: re-arming it would reopen the field after every commit.
|
|
99
|
+
let autoOpened = false;
|
|
100
|
+
$effect(() => {
|
|
101
|
+
if (!autoOpen || autoOpened || !editable) return;
|
|
102
|
+
autoOpened = true;
|
|
103
|
+
open();
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
export function open(): void {
|
|
107
|
+
if (!editable) return;
|
|
108
|
+
draft = '';
|
|
109
|
+
filtering = false;
|
|
110
|
+
menuOpen = false;
|
|
111
|
+
editing = true;
|
|
112
|
+
// Row 0 is the block's own language (see `suggestions`), so the highlight starts where
|
|
113
|
+
// a bare Enter would re-commit what is already set.
|
|
114
|
+
activeIndex = 0;
|
|
115
|
+
// A provisional placement at the trigger, BEFORE the popout renders. Without it the
|
|
116
|
+
// popout mounts parked at the viewport's top-left corner while it waits to be measured,
|
|
117
|
+
// and focusing the field inside it drags the page up there — which is the scroll
|
|
118
|
+
// jumping to the top on creating a code block. The measure below refines it.
|
|
119
|
+
const anchor = chipEl?.getBoundingClientRect();
|
|
120
|
+
if (anchor) {
|
|
121
|
+
listAt = {
|
|
122
|
+
x: Math.max(EDGE_MARGIN, anchor.right - 200),
|
|
123
|
+
y: anchor.bottom + ANCHOR_GAP,
|
|
124
|
+
maxHeight: MAX_POPOUT_HEIGHT
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
void tick().then(() => {
|
|
128
|
+
// `preventScroll`: the rail is already on screen, and a scroll here would land as a
|
|
129
|
+
// genuine scroll event on the reposition path below.
|
|
130
|
+
inputEl?.focus({ preventScroll: true });
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function close(): void {
|
|
135
|
+
editing = false;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function commit(value: string): void {
|
|
139
|
+
close();
|
|
140
|
+
// `text` is the picker's spelling of "no language", and the info string's is empty.
|
|
141
|
+
onCommit(value.trim() === 'text' ? '' : value);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function onFieldKeyDown(e: KeyboardEvent): void {
|
|
145
|
+
// The field owns its keys while open: no Escape of this state may reach whatever else
|
|
146
|
+
// listens for one. A composing Enter/Escape is the IME's, never the field's.
|
|
147
|
+
e.stopPropagation();
|
|
148
|
+
if (e.isComposing) return;
|
|
149
|
+
if (e.key === 'Enter') {
|
|
150
|
+
e.preventDefault();
|
|
151
|
+
// The highlighted row is what Enter takes once the user has engaged the list; the
|
|
152
|
+
// raw draft stands otherwise, and whenever the filter matched nothing, so an
|
|
153
|
+
// unregistered language survives both opening the field and typing it in.
|
|
154
|
+
// The highlighted row wins; a typed string the list never matched still commits, so
|
|
155
|
+
// an unregistered language stays authorable.
|
|
156
|
+
commit(suggestions[activeIndex] ?? (draft.trim() === '' ? language : draft));
|
|
157
|
+
} else if (e.key === 'Escape') {
|
|
158
|
+
e.preventDefault();
|
|
159
|
+
close();
|
|
160
|
+
onCancel(true);
|
|
161
|
+
} else if (e.key === 'ArrowDown') {
|
|
162
|
+
e.preventDefault();
|
|
163
|
+
activeIndex = suggestions.length === 0 ? 0 : (activeIndex + 1) % suggestions.length;
|
|
164
|
+
scrollActiveIntoView();
|
|
165
|
+
} else if (e.key === 'ArrowUp') {
|
|
166
|
+
e.preventDefault();
|
|
167
|
+
activeIndex =
|
|
168
|
+
suggestions.length === 0 ? 0 : (activeIndex - 1 + suggestions.length) % suggestions.length;
|
|
169
|
+
scrollActiveIntoView();
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Only for keyboard travel: the highlight must stay on screen as it moves. Opening does
|
|
174
|
+
// NOT call this — the menu opens at row 0, which is already in view.
|
|
175
|
+
function scrollActiveIntoView(): void {
|
|
176
|
+
void tick().then(() => {
|
|
177
|
+
listEl?.querySelector('[data-active="true"]')?.scrollIntoView({ block: 'nearest' });
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function onFieldBlur(e: FocusEvent): void {
|
|
182
|
+
if (!editing) return;
|
|
183
|
+
// A pointer moving from the field to its own list is not a dismissal; the list's
|
|
184
|
+
// mousedown handler commits, and this must not tear the field down before it does.
|
|
185
|
+
const next = e.relatedTarget;
|
|
186
|
+
if (next instanceof Node && (railEl?.contains(next) || pickerEl?.contains(next))) return;
|
|
187
|
+
close();
|
|
188
|
+
onCancel(false);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
async function runCopy(): Promise<void> {
|
|
192
|
+
copied = await onCopy();
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// The confirmation lives exactly as long as the rail's own reveal: the pointer leaving
|
|
196
|
+
// the block (or focus leaving the rail) hides the rail, and the next reveal is a fresh
|
|
197
|
+
// gesture that should offer to copy again rather than still be reporting the last one.
|
|
198
|
+
// A wall-clock revert would be sequencing on a timer, which G4.4 rules out.
|
|
199
|
+
function clearCopied(): void {
|
|
200
|
+
copied = false;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function toggleMenu(): void {
|
|
204
|
+
if (menuOpen) {
|
|
205
|
+
menuOpen = false;
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
close();
|
|
209
|
+
openMenuItems = menuItems?.() ?? [];
|
|
210
|
+
menuOpen = openMenuItems.length > 0;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function activateMenuItem(item: CodeMenuItem): void {
|
|
214
|
+
if (item.disabled) return;
|
|
215
|
+
menuOpen = false;
|
|
216
|
+
item.run();
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// Escape rides the BUTTONS, not their container: the rail is a labelled `group`, and a
|
|
220
|
+
// non-interactive role may not carry key handlers. Focus sits on the ⋮ button while the
|
|
221
|
+
// menu is open, and on an item once the user tabs in — both are covered here.
|
|
222
|
+
function onMenuKeyDown(e: KeyboardEvent): void {
|
|
223
|
+
if (e.key !== 'Escape' || !menuOpen) return;
|
|
224
|
+
e.stopPropagation();
|
|
225
|
+
e.preventDefault();
|
|
226
|
+
menuOpen = false;
|
|
227
|
+
menuButtonEl?.focus();
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// The menu outlives the click that opened it, so it closes on the next press anywhere
|
|
231
|
+
// else. Scoped by containment, not target identity: a press on a disabled item is
|
|
232
|
+
// still inside.
|
|
233
|
+
function onRailFocusOut(e: FocusEvent): void {
|
|
234
|
+
const next = e.relatedTarget;
|
|
235
|
+
if (next instanceof Node && (railEl?.contains(next) || menuEl?.contains(next))) return;
|
|
236
|
+
menuOpen = false;
|
|
237
|
+
clearCopied();
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// ── Popout placement ──────────────────────────────────────────────────────
|
|
241
|
+
// Both popouts are `position: fixed`, like the table's action menu: an absolutely
|
|
242
|
+
// positioned one inside the block host would extend the editor's own scrollable area
|
|
243
|
+
// when it overhangs the last block, which is the surface growing under the reader.
|
|
244
|
+
// Fixed takes them out of flow entirely, so opening one can resize nothing.
|
|
245
|
+
|
|
246
|
+
interface Placement {
|
|
247
|
+
x: number;
|
|
248
|
+
y: number;
|
|
249
|
+
/** Space the popout may occupy on the side it was placed, so a long list scrolls
|
|
250
|
+
* inside the gap instead of growing past the viewport edge. */
|
|
251
|
+
maxHeight: number;
|
|
252
|
+
}
|
|
253
|
+
let listAt = $state<Placement | null>(null);
|
|
254
|
+
let menuAt = $state<Placement | null>(null);
|
|
255
|
+
|
|
256
|
+
const EDGE_MARGIN = 8;
|
|
257
|
+
const ANCHOR_GAP = 6;
|
|
258
|
+
/** A menu is a menu, not a column of the viewport: past this it scrolls. The room actually
|
|
259
|
+
* available still wins when it is smaller. */
|
|
260
|
+
const MAX_POPOUT_HEIGHT = 320;
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Place a popout beside its trigger WITHOUT ever covering it. Below is preferred; above is
|
|
264
|
+
* taken when below cannot hold it and above can; whichever side wins caps the popout's
|
|
265
|
+
* height to the room actually there. A plain viewport clamp — what this used to do — slides
|
|
266
|
+
* the box up over the button that opened it, which is exactly what must not happen.
|
|
267
|
+
*/
|
|
268
|
+
function placeAgainst(anchor: HTMLElement | undefined, popout: HTMLElement): Placement {
|
|
269
|
+
const a = (anchor ?? popout).getBoundingClientRect();
|
|
270
|
+
const size = popout.getBoundingClientRect();
|
|
271
|
+
// ALWAYS downward. Flipping above when the room below ran short made the menu appear
|
|
272
|
+
// on whichever side the block happened to sit, so the same gesture opened in two
|
|
273
|
+
// directions; a menu that is always under its trigger is the predictable one. When the
|
|
274
|
+
// room is short the popout scrolls inside what is there instead of moving.
|
|
275
|
+
const roomBelow = window.innerHeight - a.bottom - ANCHOR_GAP - EDGE_MARGIN;
|
|
276
|
+
const maxHeight = Math.min(MAX_POPOUT_HEIGHT, Math.max(120, roomBelow));
|
|
277
|
+
const y = a.bottom + ANCHOR_GAP;
|
|
278
|
+
// Right-aligned to the trigger, then held inside the viewport horizontally only.
|
|
279
|
+
const maxX = Math.max(EDGE_MARGIN, window.innerWidth - size.width - EDGE_MARGIN);
|
|
280
|
+
const x = Math.min(Math.max(EDGE_MARGIN, a.right - size.width), maxX);
|
|
281
|
+
return { x, y, maxHeight };
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
$effect(() => {
|
|
285
|
+
if (!editing || !pickerEl) {
|
|
286
|
+
listAt = null;
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
listAt = placeAgainst(chipEl, pickerEl);
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
$effect(() => {
|
|
293
|
+
if (!menuOpen || !menuEl) {
|
|
294
|
+
menuAt = null;
|
|
295
|
+
return;
|
|
296
|
+
}
|
|
297
|
+
menuAt = placeAgainst(menuButtonEl, menuEl);
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
// A fixed popout does not move with the rail it hangs off, so it is re-placed against
|
|
301
|
+
// its anchor whenever the page scrolls or resizes. Re-placing, not dismissing: a
|
|
302
|
+
// dismissal both threw away a menu the user had just opened and raced the scroll that
|
|
303
|
+
// focusing the field used to cause, closing the picker in the same tick it opened.
|
|
304
|
+
$effect(() => {
|
|
305
|
+
if (!editing && !menuOpen) return;
|
|
306
|
+
const reposition = (): void => {
|
|
307
|
+
if (editing && pickerEl) listAt = placeAgainst(chipEl, pickerEl);
|
|
308
|
+
if (menuOpen && menuEl) menuAt = placeAgainst(menuButtonEl, menuEl);
|
|
309
|
+
};
|
|
310
|
+
window.addEventListener('scroll', reposition, { capture: true, passive: true });
|
|
311
|
+
window.addEventListener('resize', reposition, { passive: true });
|
|
312
|
+
return () => {
|
|
313
|
+
window.removeEventListener('scroll', reposition, { capture: true });
|
|
314
|
+
window.removeEventListener('resize', reposition);
|
|
315
|
+
};
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
const popoutStyle = (at: Placement | null): string =>
|
|
319
|
+
at
|
|
320
|
+
? `left:${at.x}px;top:${at.y}px;max-height:${at.maxHeight}px`
|
|
321
|
+
: 'left:0;top:0;visibility:hidden';
|
|
322
|
+
|
|
323
|
+
// Presence of a hook is the whole gate — see `EditorPolicies.onRunCode`.
|
|
324
|
+
const showRun = $derived(editable && onRun !== undefined);
|
|
325
|
+
const showMenu = $derived(menuItems !== undefined);
|
|
326
|
+
</script>
|
|
327
|
+
|
|
328
|
+
<!-- Lucide (ISC) path data, inlined rather than depended on: the editor ships two runtime
|
|
329
|
+
dependencies and an icon package would be a third. Sized 14 / stroke 1.75 to match the
|
|
330
|
+
host app's menus. See THIRD-PARTY-NOTICES.md. -->
|
|
331
|
+
{#snippet icon(paths: string)}
|
|
332
|
+
<svg
|
|
333
|
+
viewBox="0 0 24 24"
|
|
334
|
+
width="14"
|
|
335
|
+
height="14"
|
|
336
|
+
fill="none"
|
|
337
|
+
stroke="currentColor"
|
|
338
|
+
stroke-width="1.75"
|
|
339
|
+
stroke-linecap="round"
|
|
340
|
+
stroke-linejoin="round"
|
|
341
|
+
aria-hidden="true"
|
|
342
|
+
>
|
|
343
|
+
<!-- eslint-disable-next-line svelte/no-at-html-tags -- static lucide path markup from this module's own constants -->
|
|
344
|
+
{@html paths}
|
|
345
|
+
</svg>
|
|
346
|
+
{/snippet}
|
|
347
|
+
|
|
348
|
+
<!-- `group`, not `toolbar`: these controls belong together, but a toolbar promises
|
|
349
|
+
arrow-key navigation between them, which this does not implement. -->
|
|
350
|
+
<div
|
|
351
|
+
bind:this={railEl}
|
|
352
|
+
class="code-rail"
|
|
353
|
+
class:code-rail-open={editing || menuOpen}
|
|
354
|
+
role="group"
|
|
355
|
+
aria-label={CODE_RAIL_LABEL}
|
|
356
|
+
onfocusout={onRailFocusOut}
|
|
357
|
+
>
|
|
358
|
+
<span bind:this={chipEl} class="code-lang-chip">
|
|
359
|
+
<button
|
|
360
|
+
type="button"
|
|
361
|
+
class="code-lang-button"
|
|
362
|
+
class:open={editing}
|
|
363
|
+
aria-haspopup="listbox"
|
|
364
|
+
aria-expanded={editing}
|
|
365
|
+
aria-label={codeLanguageLabel(language)}
|
|
366
|
+
onclick={() => (editing ? close() : open())}
|
|
367
|
+
>{language}{@render icon('<path d="m6 9 6 6 6-6"/>')}</button
|
|
368
|
+
>
|
|
369
|
+
</span>
|
|
370
|
+
|
|
371
|
+
{#if showRun}
|
|
372
|
+
<button type="button" class="code-rail-action" aria-label={CODE_RUN_LABEL} onclick={onRun}>
|
|
373
|
+
{@render icon(
|
|
374
|
+
'<path d="M5 5a2 2 0 0 1 3.008-1.728l11.997 6.998a2 2 0 0 1 .003 3.458l-12 7A2 2 0 0 1 5 19z"/>'
|
|
375
|
+
)}
|
|
376
|
+
</button>
|
|
377
|
+
{/if}
|
|
378
|
+
|
|
379
|
+
<button
|
|
380
|
+
type="button"
|
|
381
|
+
class="code-rail-action"
|
|
382
|
+
aria-label={copied ? CODE_COPIED_LABEL : CODE_COPY_LABEL}
|
|
383
|
+
onclick={runCopy}
|
|
384
|
+
onpointerleave={clearCopied}
|
|
385
|
+
>
|
|
386
|
+
{#if copied}
|
|
387
|
+
{@render icon('<path d="M20 6 9 17l-5-5"/>')}
|
|
388
|
+
{:else}
|
|
389
|
+
{@render icon(
|
|
390
|
+
'<rect width="14" height="14" x="8" y="8" rx="2" ry="2"/><path d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2"/>'
|
|
391
|
+
)}
|
|
392
|
+
{/if}
|
|
393
|
+
</button>
|
|
394
|
+
|
|
395
|
+
{#if showMenu}
|
|
396
|
+
<button
|
|
397
|
+
bind:this={menuButtonEl}
|
|
398
|
+
type="button"
|
|
399
|
+
class="code-rail-action"
|
|
400
|
+
aria-label={CODE_MENU_LABEL}
|
|
401
|
+
aria-haspopup="menu"
|
|
402
|
+
aria-expanded={menuOpen}
|
|
403
|
+
onclick={toggleMenu}
|
|
404
|
+
onkeydown={onMenuKeyDown}
|
|
405
|
+
>
|
|
406
|
+
{@render icon(
|
|
407
|
+
'<circle cx="12" cy="12" r="1"/><circle cx="12" cy="5" r="1"/><circle cx="12" cy="19" r="1"/>'
|
|
408
|
+
)}
|
|
409
|
+
</button>
|
|
410
|
+
{/if}
|
|
411
|
+
</div>
|
|
412
|
+
|
|
413
|
+
{#if editing}
|
|
414
|
+
<div
|
|
415
|
+
bind:this={pickerEl}
|
|
416
|
+
class="md-menu code-rail-popout code-lang-picker"
|
|
417
|
+
style={popoutStyle(listAt)}
|
|
418
|
+
>
|
|
419
|
+
<div class="code-lang-search">
|
|
420
|
+
{@render icon('<circle cx="11" cy="11" r="8"/><path d="m21 21-4.3-4.3"/>')}
|
|
421
|
+
<input
|
|
422
|
+
bind:this={inputEl}
|
|
423
|
+
bind:value={draft}
|
|
424
|
+
type="text"
|
|
425
|
+
spellcheck="false"
|
|
426
|
+
autocomplete="off"
|
|
427
|
+
role="combobox"
|
|
428
|
+
aria-expanded="true"
|
|
429
|
+
aria-controls="code-lang-list"
|
|
430
|
+
aria-label={CODE_LANGUAGE_FIELD}
|
|
431
|
+
placeholder="Search for a language…"
|
|
432
|
+
oninput={() => {
|
|
433
|
+
filtering = true;
|
|
434
|
+
activeIndex = 0;
|
|
435
|
+
}}
|
|
436
|
+
onkeydown={onFieldKeyDown}
|
|
437
|
+
onfocusout={onFieldBlur}
|
|
438
|
+
/>
|
|
439
|
+
</div>
|
|
440
|
+
{#if suggestions.length > 0}
|
|
441
|
+
<ul bind:this={listEl} id="code-lang-list" class="code-lang-list" role="listbox">
|
|
442
|
+
{#each suggestions as name, i (name)}
|
|
443
|
+
<li role="none">
|
|
444
|
+
<button
|
|
445
|
+
type="button"
|
|
446
|
+
role="option"
|
|
447
|
+
aria-selected={name === language}
|
|
448
|
+
data-active={i === activeIndex}
|
|
449
|
+
tabindex="-1"
|
|
450
|
+
onmouseenter={() => (activeIndex = i)}
|
|
451
|
+
onmousedown={(e) => {
|
|
452
|
+
// mousedown, not click: the field's blur would otherwise cancel the edit
|
|
453
|
+
// before a click ever landed.
|
|
454
|
+
e.preventDefault();
|
|
455
|
+
commit(name);
|
|
456
|
+
}}
|
|
457
|
+
>
|
|
458
|
+
<span class="code-lang-name">{name}</span>
|
|
459
|
+
{#if name === language}
|
|
460
|
+
<span class="code-lang-check">
|
|
461
|
+
{@render icon('<path d="M20 6 9 17l-5-5"/>')}
|
|
462
|
+
</span>
|
|
463
|
+
{/if}
|
|
464
|
+
</button>
|
|
465
|
+
</li>
|
|
466
|
+
{/each}
|
|
467
|
+
</ul>
|
|
468
|
+
{:else}
|
|
469
|
+
<p class="code-lang-empty">No language matches</p>
|
|
470
|
+
{/if}
|
|
471
|
+
</div>
|
|
472
|
+
{/if}
|
|
473
|
+
|
|
474
|
+
{#if menuOpen}
|
|
475
|
+
<ul
|
|
476
|
+
bind:this={menuEl}
|
|
477
|
+
class="md-menu code-rail-popout code-rail-menu"
|
|
478
|
+
role="menu"
|
|
479
|
+
aria-label={CODE_MENU_LABEL}
|
|
480
|
+
style={popoutStyle(menuAt)}
|
|
481
|
+
>
|
|
482
|
+
{#each openMenuItems as item (item.id)}
|
|
483
|
+
<li role="none">
|
|
484
|
+
<button
|
|
485
|
+
type="button"
|
|
486
|
+
role="menuitem"
|
|
487
|
+
disabled={item.disabled}
|
|
488
|
+
onclick={() => activateMenuItem(item)}
|
|
489
|
+
onkeydown={onMenuKeyDown}>{item.label}</button
|
|
490
|
+
>
|
|
491
|
+
</li>
|
|
492
|
+
{/each}
|
|
493
|
+
</ul>
|
|
494
|
+
{/if}
|
|
495
|
+
|
|
496
|
+
<style>
|
|
497
|
+
/* Bare controls over the code box's top-right — no container of their own, so the code
|
|
498
|
+
they sit above reads through between them. Positioned against the block host, whose
|
|
499
|
+
box the code box fills below the host's 6px stand-off (editor.css, fencedCode), and out
|
|
500
|
+
of the code box's own scroller so a horizontal scroll leaves them where they are. */
|
|
501
|
+
.code-rail {
|
|
502
|
+
position: absolute;
|
|
503
|
+
top: 12px;
|
|
504
|
+
right: 8px;
|
|
505
|
+
z-index: 1;
|
|
506
|
+
display: flex;
|
|
507
|
+
align-items: center;
|
|
508
|
+
gap: 2px;
|
|
509
|
+
font-size: 13px;
|
|
510
|
+
line-height: 1;
|
|
511
|
+
opacity: 0;
|
|
512
|
+
pointer-events: none;
|
|
513
|
+
transition: opacity 120ms ease-out;
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
/* Block hover or block focus, and nothing else — plus the two states that outlive both,
|
|
517
|
+
since a popout must not vanish from under the pointer that opened it. Child and sibling
|
|
518
|
+
combinators, so an outer container's hover never reveals a nested block's rail. */
|
|
519
|
+
:global(.block-host:hover) > .code-rail,
|
|
520
|
+
:global(.code-block:focus) ~ .code-rail,
|
|
521
|
+
.code-rail:focus-within,
|
|
522
|
+
.code-rail-open {
|
|
523
|
+
opacity: 1;
|
|
524
|
+
pointer-events: auto;
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
@media (prefers-reduced-motion: reduce) {
|
|
528
|
+
.code-rail {
|
|
529
|
+
transition: none;
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
.code-lang-chip {
|
|
534
|
+
display: flex;
|
|
535
|
+
align-items: center;
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
button {
|
|
539
|
+
display: flex;
|
|
540
|
+
align-items: center;
|
|
541
|
+
justify-content: center;
|
|
542
|
+
gap: 3px;
|
|
543
|
+
border: none;
|
|
544
|
+
border-radius: 5px;
|
|
545
|
+
background: transparent;
|
|
546
|
+
color: var(--color-ui-muted, #999999);
|
|
547
|
+
font: inherit;
|
|
548
|
+
cursor: pointer;
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
.code-lang-button {
|
|
552
|
+
padding: 4px 6px;
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
/* An open picker keeps its trigger lit, so the two read as one control. */
|
|
556
|
+
.code-lang-button.open {
|
|
557
|
+
background: var(--color-ui-faint, rgba(255, 255, 255, 0.07));
|
|
558
|
+
color: var(--color-text-secondary, #cfcfca);
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
.code-rail-action {
|
|
562
|
+
width: 24px;
|
|
563
|
+
height: 24px;
|
|
564
|
+
padding: 0;
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
button:hover:not(:disabled) {
|
|
568
|
+
background: var(--color-ui-faint, rgba(255, 255, 255, 0.07));
|
|
569
|
+
color: var(--color-text-secondary, #d6d9e0);
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
button:focus-visible {
|
|
573
|
+
outline: 1px solid var(--color-accent, #567b67);
|
|
574
|
+
outline-offset: -1px;
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
/* The host app's menu surface, so a code block's menus read as the app's own. */
|
|
578
|
+
/* Surface, hairline, shadow, face and colour come from the shared `.md-menu` (editor.css);
|
|
579
|
+
this adds only the picker's own layout. */
|
|
580
|
+
.code-rail-popout {
|
|
581
|
+
display: flex;
|
|
582
|
+
flex-direction: column;
|
|
583
|
+
min-width: 200px;
|
|
584
|
+
margin: 0;
|
|
585
|
+
list-style: none;
|
|
586
|
+
overflow: hidden;
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
/* Search sits INSIDE the menu rather than swapping the rail's chip for a field: the chip
|
|
590
|
+
is a fixed box that never changes width, so opening the picker moves nothing. */
|
|
591
|
+
.code-lang-search {
|
|
592
|
+
display: flex;
|
|
593
|
+
align-items: center;
|
|
594
|
+
gap: 8px;
|
|
595
|
+
flex: 0 0 auto;
|
|
596
|
+
margin: -4px -4px 4px;
|
|
597
|
+
padding: 8px 12px;
|
|
598
|
+
border-bottom: 1px solid var(--menu-search-divider, rgba(255, 255, 255, 0.08));
|
|
599
|
+
color: var(--color-ui-muted, #8f8f89);
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
.code-lang-search input {
|
|
603
|
+
flex: 1;
|
|
604
|
+
min-width: 0;
|
|
605
|
+
padding: 0;
|
|
606
|
+
border: 0;
|
|
607
|
+
background: transparent;
|
|
608
|
+
color: var(--color-text-primary, #e8e8e5);
|
|
609
|
+
font: inherit;
|
|
610
|
+
font-size: 13px;
|
|
611
|
+
outline: none;
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
.code-lang-search input::placeholder {
|
|
615
|
+
color: var(--color-ui-dulled, #a3a39d);
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
.code-lang-list {
|
|
619
|
+
flex: 1 1 auto;
|
|
620
|
+
margin: 0;
|
|
621
|
+
padding: 0;
|
|
622
|
+
list-style: none;
|
|
623
|
+
overflow-y: auto;
|
|
624
|
+
overscroll-behavior: contain;
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
/* A hairline thumb, as the host app's list menus use — the platform's default bar is
|
|
628
|
+
wider than the rows it sits beside. */
|
|
629
|
+
.code-lang-list::-webkit-scrollbar {
|
|
630
|
+
width: 1px;
|
|
631
|
+
}
|
|
632
|
+
.code-lang-list::-webkit-scrollbar-track {
|
|
633
|
+
background: transparent;
|
|
634
|
+
}
|
|
635
|
+
.code-lang-list::-webkit-scrollbar-thumb {
|
|
636
|
+
background: var(--menu-scrollbar-thumb, rgba(255, 255, 255, 0.18));
|
|
637
|
+
}
|
|
638
|
+
.code-lang-list {
|
|
639
|
+
scrollbar-width: thin;
|
|
640
|
+
scrollbar-color: var(--menu-scrollbar-thumb, rgba(255, 255, 255, 0.18)) transparent;
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
.code-rail-popout button {
|
|
644
|
+
gap: 9px;
|
|
645
|
+
width: 100%;
|
|
646
|
+
justify-content: flex-start;
|
|
647
|
+
padding: 7px 10px;
|
|
648
|
+
border-radius: 5px;
|
|
649
|
+
color: inherit;
|
|
650
|
+
font: inherit;
|
|
651
|
+
text-align: left;
|
|
652
|
+
white-space: nowrap;
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
.code-lang-name {
|
|
656
|
+
flex: 1;
|
|
657
|
+
min-width: 0;
|
|
658
|
+
overflow: hidden;
|
|
659
|
+
text-overflow: ellipsis;
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
/* The check marks what is set, inline on its own row, in the text colour: the host app
|
|
663
|
+
colours no text with its accent, and its menus are what these copy. */
|
|
664
|
+
.code-lang-check {
|
|
665
|
+
display: inline-flex;
|
|
666
|
+
flex-shrink: 0;
|
|
667
|
+
color: var(--color-text-primary, #e8e8e5);
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
.code-rail-popout button:hover:not(:disabled),
|
|
671
|
+
.code-lang-list button[data-active='true'] {
|
|
672
|
+
background: var(--menu-item-hover, rgba(255, 255, 255, 0.07));
|
|
673
|
+
color: inherit;
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
.code-rail-popout button:disabled {
|
|
677
|
+
opacity: 0.4;
|
|
678
|
+
cursor: default;
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
.code-lang-empty {
|
|
682
|
+
margin: 0;
|
|
683
|
+
padding: 7px 10px;
|
|
684
|
+
color: var(--color-ui-dulled, #a3a39d);
|
|
685
|
+
}
|
|
686
|
+
</style>
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { CodeMenuItem } from '../../../editor-keys';
|
|
2
|
+
type $$ComponentProps = {
|
|
3
|
+
/** The opener's full info string; the button shows its first token. */
|
|
4
|
+
info: string;
|
|
5
|
+
/** False in reading mode, which writes no bytes — the chip is then a label. */
|
|
6
|
+
editable: boolean;
|
|
7
|
+
/** Open the language field as soon as the rail mounts: a fence the user just created
|
|
8
|
+
* has no language yet, and asking is the whole reason the chip exists. */
|
|
9
|
+
autoOpen?: boolean;
|
|
10
|
+
/** Only Enter or a list pick calls this, and it owns the caret's landing afterwards. */
|
|
11
|
+
onCommit: (info: string) => void;
|
|
12
|
+
/** Nothing written. Escape asks for the caret back; a blur must not yank it from
|
|
13
|
+
* wherever the user just clicked, so it does not. */
|
|
14
|
+
onCancel: (returnCaret: boolean) => void;
|
|
15
|
+
/** Absent when the host installed no execution hook — no run affordance renders. */
|
|
16
|
+
onRun?: () => void;
|
|
17
|
+
/** Copies the fence body; resolves once the clipboard write settles. */
|
|
18
|
+
onCopy: () => Promise<boolean>;
|
|
19
|
+
/** Consulted on each open, so items read live state. Empty renders no affordance. */
|
|
20
|
+
menuItems?: () => readonly CodeMenuItem[];
|
|
21
|
+
};
|
|
22
|
+
declare const CodeBlockRail: import("svelte").Component<$$ComponentProps, {
|
|
23
|
+
open: () => void;
|
|
24
|
+
}, "">;
|
|
25
|
+
type CodeBlockRail = ReturnType<typeof CodeBlockRail>;
|
|
26
|
+
export default CodeBlockRail;
|