blots-editor 0.0.12 → 0.0.13
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/asset/language/en.json +2 -1
- package/asset/language/zh.json +2 -1
- package/dist/view/editor.js +37 -1
- package/dist/view/tool-bar.d.ts +51 -0
- package/dist/view/tool-bar.js +92 -0
- package/package.json +1 -1
package/asset/language/en.json
CHANGED
package/asset/language/zh.json
CHANGED
package/dist/view/editor.js
CHANGED
|
@@ -1,11 +1,16 @@
|
|
|
1
1
|
import { Box, Text, useBoxMetrics, useCursor } from "ink";
|
|
2
|
-
import { applyElementToModalLayer, openModalLayer, useI18n, useKeyboard, useMouseRegion, } from "ink-cartridge";
|
|
2
|
+
import { applyElement, applyElementToModalLayer, closeLayer, openLayer, openModalLayer, useI18n, useKeyboard, useMouseRegion, } from "ink-cartridge";
|
|
3
3
|
import React, { useCallback, useEffect, useRef, useState } from "react";
|
|
4
4
|
import { EditorController } from "../core/editor-controller.js";
|
|
5
5
|
import { useSettings } from "../core/settings/useSettings.js";
|
|
6
6
|
import { clickToPosition } from "./click-mapping.js";
|
|
7
7
|
import { CommandBar } from "./command-bar.js";
|
|
8
8
|
import { InformationBar } from "./information-bar.js";
|
|
9
|
+
import { ToolBar } from "./tool-bar.js";
|
|
10
|
+
/** Layer hosting the floating toolbar; below modal layers so it never
|
|
11
|
+
* intercepts keys meant for a modal (e.g. the command bar). */
|
|
12
|
+
const TOOLBAR_LAYER_ID = "toolbar";
|
|
13
|
+
const TOOLBAR_Z_INDEX = 1;
|
|
9
14
|
/**
|
|
10
15
|
* Main editor view: renders the status bar + line numbers + text, routes
|
|
11
16
|
* every keystroke to the controller as a named command, and splits
|
|
@@ -42,6 +47,37 @@ export function Editor({ value: initialText = "", lineNumberRightSpacing = 1, cu
|
|
|
42
47
|
element: CommandBar,
|
|
43
48
|
});
|
|
44
49
|
}, []);
|
|
50
|
+
// The toolbar lives in a regular layer (below modal layers) so it floats
|
|
51
|
+
// above the document without taking over the keyboard. The editor owns the
|
|
52
|
+
// layer lifecycle and the ctrl+tab toggle because the layer's element
|
|
53
|
+
// unmounts when it closes — a binding inside it could never re-open it.
|
|
54
|
+
const [toolbarOpen, setToolbarOpen] = useState(true);
|
|
55
|
+
useEffect(() => {
|
|
56
|
+
return () => closeLayer(TOOLBAR_LAYER_ID);
|
|
57
|
+
}, []);
|
|
58
|
+
// Opening must wait one tick: passive effects run child-first on mount, so
|
|
59
|
+
// when the editor IS the initial screen this effect races the provider's
|
|
60
|
+
// dispatcher registration and openLayer would throw. ctrl+tab toggles hit
|
|
61
|
+
// the same deferred path — the one-tick delay is imperceptible. Closing is
|
|
62
|
+
// safe immediately because the dispatcher is registered by then.
|
|
63
|
+
useEffect(() => {
|
|
64
|
+
if (!toolbarOpen) {
|
|
65
|
+
closeLayer(TOOLBAR_LAYER_ID);
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
const timer = setTimeout(() => {
|
|
69
|
+
openLayer(TOOLBAR_LAYER_ID, TOOLBAR_Z_INDEX);
|
|
70
|
+
applyElement(TOOLBAR_LAYER_ID, {
|
|
71
|
+
elementId: "toolbar",
|
|
72
|
+
element: ToolBar,
|
|
73
|
+
props: { mode },
|
|
74
|
+
});
|
|
75
|
+
}, 0);
|
|
76
|
+
return () => clearTimeout(timer);
|
|
77
|
+
}, [toolbarOpen, mode]);
|
|
78
|
+
useEffect(() => {
|
|
79
|
+
return boundKeyboard(["ctrl+tab"], () => setToolbarOpen((open) => !open));
|
|
80
|
+
}, [boundKeyboard]);
|
|
45
81
|
useEffect(() => {
|
|
46
82
|
const removeWildcard = enableWildcardPriority();
|
|
47
83
|
const unbinds = [];
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import type { XtermMouseEvent } from "@cartridge-engine/keyboard-engine";
|
|
3
|
+
/**
|
|
4
|
+
* Props for a single tool button.
|
|
5
|
+
*
|
|
6
|
+
* @prop priority - Hit-test priority over whatever sits underneath (the editor
|
|
7
|
+
* surface). Buttons must win so clicking them does not also place the cursor.
|
|
8
|
+
* @prop onClick - Called on mouse click and on ctrl+enter selection; the event
|
|
9
|
+
* is absent for the keyboard path.
|
|
10
|
+
* @prop text - The text to display on the button.
|
|
11
|
+
* @prop focused - Keyboard focus (normal-mode navigation); overrides hover.
|
|
12
|
+
* @prop onDragStart/onDragMove/onDragEnd - Forwarded from the toolbar. The
|
|
13
|
+
* engine captures a drag on the pressed region only, so buttons must carry
|
|
14
|
+
* the drag handlers or dragging from a button would not move the bar.
|
|
15
|
+
* @prop dragging - The bar is being dragged; buttons tint green like hover.
|
|
16
|
+
*/
|
|
17
|
+
export type ToolButtonProps = {
|
|
18
|
+
priority: number;
|
|
19
|
+
onClick: (event?: XtermMouseEvent) => void;
|
|
20
|
+
text: string;
|
|
21
|
+
focused?: boolean;
|
|
22
|
+
onDragStart?: (event: XtermMouseEvent) => void;
|
|
23
|
+
onDragMove?: (event: XtermMouseEvent) => void;
|
|
24
|
+
onDragEnd?: (event: XtermMouseEvent) => void;
|
|
25
|
+
dragging?: boolean;
|
|
26
|
+
};
|
|
27
|
+
/** One tool entry; the list is the extension point for future tools. */
|
|
28
|
+
export type Tool = {
|
|
29
|
+
id: string;
|
|
30
|
+
/** i18n key for the button label. */
|
|
31
|
+
labelKey: string;
|
|
32
|
+
/** Placeholder until the file-tree feature lands. */
|
|
33
|
+
onClick: (event?: XtermMouseEvent) => void;
|
|
34
|
+
};
|
|
35
|
+
export type ToolBarProps = {
|
|
36
|
+
/** Current keyboard mode, passed down by the editor; drives focus display. */
|
|
37
|
+
mode?: string | null;
|
|
38
|
+
/** Tools to render, centered in the bar. Defaults to the file-tree entry. */
|
|
39
|
+
tools?: Tool[];
|
|
40
|
+
};
|
|
41
|
+
/**
|
|
42
|
+
* Floating toolbar, bottom-center of the terminal until the user drags it.
|
|
43
|
+
* Dragging switches it to absolute positioning and clamps it inside the
|
|
44
|
+
* terminal view (clampFrame). Purely presentational for now: one centered
|
|
45
|
+
* button with hover/focus states. The editor owns the layer that hosts this
|
|
46
|
+
* bar (and the ctrl+tab toggle); this component only adds the normal-mode
|
|
47
|
+
* keyboard navigation (ctrl+left/ctrl+right move the focus ring, ctrl+enter
|
|
48
|
+
* selects). Insert mode stays mouse-only because those bindings are
|
|
49
|
+
* mode-restricted and never fire there.
|
|
50
|
+
*/
|
|
51
|
+
export declare function ToolBar({ mode, tools }: ToolBarProps): React.JSX.Element;
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { Box, Text, measureElement, useWindowSize } from "ink";
|
|
2
|
+
import { useI18n, useKeyboard, useMouseRegion } from "ink-cartridge";
|
|
3
|
+
import React, { useCallback, useEffect, useRef, useState } from "react";
|
|
4
|
+
import { clampFrame } from "./frame-clamp.js";
|
|
5
|
+
function ToolButton({ priority, onClick, text, focused = false, onDragStart, onDragMove, onDragEnd, dragging = false, }) {
|
|
6
|
+
const [hover, setHover] = useState(false);
|
|
7
|
+
const ref = useMouseRegion({
|
|
8
|
+
onClick: (event) => onClick(event),
|
|
9
|
+
onEnter: () => setHover(true),
|
|
10
|
+
onLeave: () => setHover(false),
|
|
11
|
+
onDragStart,
|
|
12
|
+
onDragMove,
|
|
13
|
+
onDragEnd,
|
|
14
|
+
}, { priority });
|
|
15
|
+
// Focus is the keyboard state, so it outranks the transient hover color.
|
|
16
|
+
const borderColor = focused ? "blue" : hover || dragging ? "green" : "white";
|
|
17
|
+
return (React.createElement(Box, { ref: ref, borderColor: borderColor, borderStyle: "bold",
|
|
18
|
+
// Match the editor's surface so the floating bar reads as part of it.
|
|
19
|
+
backgroundColor: "#1e1e1e", height: "100%", justifyContent: "center", alignItems: "center", paddingLeft: 2, paddingRight: 2 },
|
|
20
|
+
React.createElement(Text, null, text)));
|
|
21
|
+
}
|
|
22
|
+
const DEFAULT_TOOLS = [
|
|
23
|
+
{
|
|
24
|
+
id: "file-tree",
|
|
25
|
+
labelKey: "toolbar.fileTree",
|
|
26
|
+
onClick: () => {
|
|
27
|
+
// Business logic (opening the file tree) lands here later.
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
];
|
|
31
|
+
/**
|
|
32
|
+
* Floating toolbar, bottom-center of the terminal until the user drags it.
|
|
33
|
+
* Dragging switches it to absolute positioning and clamps it inside the
|
|
34
|
+
* terminal view (clampFrame). Purely presentational for now: one centered
|
|
35
|
+
* button with hover/focus states. The editor owns the layer that hosts this
|
|
36
|
+
* bar (and the ctrl+tab toggle); this component only adds the normal-mode
|
|
37
|
+
* keyboard navigation (ctrl+left/ctrl+right move the focus ring, ctrl+enter
|
|
38
|
+
* selects). Insert mode stays mouse-only because those bindings are
|
|
39
|
+
* mode-restricted and never fire there.
|
|
40
|
+
*/
|
|
41
|
+
export function ToolBar({ mode = null, tools = DEFAULT_TOOLS }) {
|
|
42
|
+
const { rows, columns } = useWindowSize();
|
|
43
|
+
const { t } = useI18n();
|
|
44
|
+
const { boundKeyboard } = useKeyboard();
|
|
45
|
+
const [activeIndex, setActiveIndex] = useState(0);
|
|
46
|
+
// null = bar sits at the bottom-center via flex layout; once the user
|
|
47
|
+
// grabs it, absolute positioning takes over so it follows the cursor.
|
|
48
|
+
const [pos, setPos] = useState(null);
|
|
49
|
+
const [dragging, setDragging] = useState(false);
|
|
50
|
+
const barRef = useRef(null);
|
|
51
|
+
// Grab offset (where inside the bar the cursor went down) plus the bar's
|
|
52
|
+
// size at grab time, kept in a ref so drag moves need no re-measurement.
|
|
53
|
+
const grabRef = useRef({ dx: 0, dy: 0, width: 0, height: 0 });
|
|
54
|
+
const startDrag = useCallback((event) => {
|
|
55
|
+
const node = barRef.current;
|
|
56
|
+
if (!node) {
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
// Measure the bar itself, not the hit region — a button may be the
|
|
60
|
+
// captured target, and its rect would put the bar's corner at the
|
|
61
|
+
// button's corner.
|
|
62
|
+
const m = measureElement(node);
|
|
63
|
+
grabRef.current = {
|
|
64
|
+
dx: event.x - m.x - 1,
|
|
65
|
+
dy: event.y - m.y - 1,
|
|
66
|
+
width: m.width,
|
|
67
|
+
height: m.height,
|
|
68
|
+
};
|
|
69
|
+
// Switch from flex centering to absolute positioning at the current
|
|
70
|
+
// rect — seamless, no jump on the first frame.
|
|
71
|
+
setPos({ top: m.y, left: m.x });
|
|
72
|
+
setDragging(true);
|
|
73
|
+
}, []);
|
|
74
|
+
const moveDrag = useCallback((event) => {
|
|
75
|
+
const { dx, dy, width, height } = grabRef.current;
|
|
76
|
+
setPos(clampFrame(event.x - dx - 1, event.y - dy - 1, columns, rows, width, height));
|
|
77
|
+
}, [columns, rows]);
|
|
78
|
+
const endDrag = useCallback(() => setDragging(false), []);
|
|
79
|
+
const showFocus = mode === "normal";
|
|
80
|
+
useEffect(() => {
|
|
81
|
+
const unbinds = [
|
|
82
|
+
boundKeyboard(["ctrl+left"], () => setActiveIndex((i) => (i - 1 + tools.length) % tools.length), { mode: "normal" }),
|
|
83
|
+
boundKeyboard(["ctrl+right"], () => setActiveIndex((i) => (i + 1) % tools.length), { mode: "normal" }),
|
|
84
|
+
boundKeyboard(["ctrl+return"], () => tools[activeIndex]?.onClick(), {
|
|
85
|
+
mode: "normal",
|
|
86
|
+
}),
|
|
87
|
+
];
|
|
88
|
+
return () => unbinds.forEach((fn) => fn());
|
|
89
|
+
}, [boundKeyboard, tools, activeIndex]);
|
|
90
|
+
return (React.createElement(Box, { width: "100%", height: "100%", justifyContent: "center", alignItems: "flex-end" },
|
|
91
|
+
React.createElement(Box, { ref: barRef, position: pos ? "absolute" : undefined, top: pos?.top, left: pos?.left, height: 4, flexDirection: "row", justifyContent: "center", alignItems: "center" }, tools.map((tool, index) => (React.createElement(ToolButton, { key: tool.id, text: t(tool.labelKey), priority: 1, focused: showFocus && index === activeIndex, onClick: tool.onClick, dragging: dragging, onDragStart: startDrag, onDragMove: moveDrag, onDragEnd: endDrag }))))));
|
|
92
|
+
}
|