blots-editor 0.0.14 → 0.0.16
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 +4 -0
- package/asset/language/zh.json +4 -0
- package/dist/core/document/document.d.ts +41 -2
- package/dist/core/document/document.js +113 -36
- package/dist/core/{file-tree-model.d.ts → io/file-tree-model.d.ts} +12 -3
- package/dist/core/{file-tree-model.js → io/file-tree-model.js} +23 -12
- package/dist/core/{session.d.ts → io/session.d.ts} +1 -1
- package/dist/core/{session.js → io/session.js} +1 -1
- package/dist/index.js +4 -4
- package/dist/{view → utils/view}/click-mapping.d.ts +1 -1
- package/dist/{view → utils/view}/frame-clamp.d.ts +4 -0
- package/dist/{view → utils/view}/frame-clamp.js +8 -0
- package/dist/{i18n-resources.js → utils/view/i18n-resources.js} +1 -1
- package/dist/view/{command-bar.d.ts → editor/command-bar.d.ts} +1 -1
- package/dist/view/{editor-setting.js → editor/editor-setting.js} +2 -2
- package/dist/view/{file-tree.d.ts → editor/file-tree.d.ts} +5 -1
- package/dist/view/{file-tree.js → editor/file-tree.js} +46 -28
- package/dist/view/{information-bar.d.ts → editor/information-bar.d.ts} +1 -1
- package/dist/view/editor/tool-bar.d.ts +36 -0
- package/dist/view/editor/tool-bar.js +134 -0
- package/dist/view/{toolbar-store.d.ts → event/subscription/toolbar-store.d.ts} +5 -6
- package/dist/view/event/subscription/tree-store.d.ts +10 -0
- package/dist/view/{tree-width-store.js → event/subscription/tree-store.js} +9 -12
- package/dist/view/{editor.js → page/editor.js} +12 -12
- package/dist/view/{main-menu.js → page/main-menu.js} +1 -1
- package/dist/view/{settings.js → page/settings.js} +3 -3
- package/dist/view/{modal-frame.js → utils/modal-frame.js} +1 -1
- package/package.json +1 -1
- package/dist/view/tool-bar.d.ts +0 -62
- package/dist/view/tool-bar.js +0 -148
- package/dist/view/tree-width-store.d.ts +0 -6
- /package/dist/{view → utils/view}/click-mapping.js +0 -0
- /package/dist/{i18n-resources.d.ts → utils/view/i18n-resources.d.ts} +0 -0
- /package/dist/view/{command-bar.js → editor/command-bar.js} +0 -0
- /package/dist/view/{editor-setting.d.ts → editor/editor-setting.d.ts} +0 -0
- /package/dist/view/{information-bar.js → editor/information-bar.js} +0 -0
- /package/dist/view/{toolbar-store.js → event/subscription/toolbar-store.js} +0 -0
- /package/dist/view/{editor.d.ts → page/editor.d.ts} +0 -0
- /package/dist/view/{main-menu.d.ts → page/main-menu.d.ts} +0 -0
- /package/dist/view/{settings.d.ts → page/settings.d.ts} +0 -0
- /package/dist/view/{modal-frame.d.ts → utils/modal-frame.d.ts} +0 -0
- /package/dist/view/{sensitivity-bar.d.ts → utils/sensitivity-bar.d.ts} +0 -0
- /package/dist/view/{sensitivity-bar.js → utils/sensitivity-bar.js} +0 -0
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import { Box, Text, useWindowSize } from "ink";
|
|
2
2
|
import { applyElementToModalLayer, ModalLayerElementContext, openModalLayer, useI18n, useKeyboard, useMouseRegion, useScreenSystem, } from "ink-cartridge";
|
|
3
|
-
import React, { useCallback, useContext, useEffect, useMemo, useState } from "react";
|
|
3
|
+
import React, { useCallback, useContext, useEffect, useMemo, useState, } from "react";
|
|
4
4
|
import { basename } from "node:path";
|
|
5
5
|
import stringWidth from "string-width";
|
|
6
|
-
import { clearFileTreeCache, flattenTree, resolveFileTreeRoot, scanDirectoryCached, toggleExpanded, } from "
|
|
7
|
-
import { useSettings } from "
|
|
8
|
-
import { ModalFrame } from "
|
|
9
|
-
import {
|
|
6
|
+
import { clearFileTreeCache, flattenTree, resolveFileTreeRoot, scanDirectoryCached, toggleExpanded, } from "../../core/io/file-tree-model.js";
|
|
7
|
+
import { useSettings } from "../../core/settings/useSettings.js";
|
|
8
|
+
import { ModalFrame } from "../utils/modal-frame.js";
|
|
9
|
+
import { setTreePos } from "../event/subscription/tree-store.js";
|
|
10
10
|
/** Narrowest the pane can be; wide enough for short names. */
|
|
11
11
|
const MIN_TREE_WIDTH = 24;
|
|
12
12
|
/** Widest the pane can be — the editor keeps at least 20 columns. */
|
|
@@ -22,23 +22,34 @@ const MAX_TREE_WIDTH = 60;
|
|
|
22
22
|
export function FileTree({ session }) {
|
|
23
23
|
const { rows, columns } = useWindowSize();
|
|
24
24
|
const { settings } = useSettings();
|
|
25
|
+
const { t } = useI18n();
|
|
25
26
|
const [expanded, setExpanded] = useState(() => new Set());
|
|
26
27
|
const [scrollTop, setScrollTop] = useState(0);
|
|
27
28
|
const [hoveredPath, setHoveredPath] = useState(null);
|
|
28
29
|
// scanTick bumps to force a re-scan — the cache never expires on its own.
|
|
29
30
|
const [scanTick, setScanTick] = useState(0);
|
|
30
|
-
const root =
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
return resolved ? scanDirectoryCached(resolved) : null;
|
|
35
|
-
}, [settings.fileTree, scanTick]);
|
|
36
|
-
// A new scan (root path changed) starts collapsed at the top.
|
|
31
|
+
const [root, setRoot] = useState({
|
|
32
|
+
scanning: true,
|
|
33
|
+
});
|
|
34
|
+
const [prevRoot, setPrevRoot] = useState(root);
|
|
37
35
|
useEffect(() => {
|
|
36
|
+
setRoot({ scanning: true });
|
|
37
|
+
(async () => {
|
|
38
|
+
const resolved = resolveFileTreeRoot(settings.fileTree, process.cwd());
|
|
39
|
+
const tree = resolved
|
|
40
|
+
? await scanDirectoryCached(resolved)
|
|
41
|
+
: { noDir: true };
|
|
42
|
+
setRoot(tree);
|
|
43
|
+
})();
|
|
44
|
+
}, [settings.fileTree, scanTick]);
|
|
45
|
+
// A new scan (root changed) starts collapsed at the top — reset during
|
|
46
|
+
// render so the next frame already renders with the reset state.
|
|
47
|
+
if (prevRoot !== root) {
|
|
48
|
+
setPrevRoot(root);
|
|
38
49
|
setExpanded(new Set());
|
|
39
50
|
setScrollTop(0);
|
|
40
|
-
}
|
|
41
|
-
const visibleRows = useMemo(() => (root ? flattenTree(root, expanded) : []), [root, expanded]);
|
|
51
|
+
}
|
|
52
|
+
const visibleRows = useMemo(() => ('node' in root ? flattenTree(root.node, expanded) : []), [root, expanded]);
|
|
42
53
|
const handleWheel = useCallback((event) => {
|
|
43
54
|
if (event.button === "wheel-up") {
|
|
44
55
|
setScrollTop((t) => Math.max(0, t - 1));
|
|
@@ -49,12 +60,12 @@ export function FileTree({ session }) {
|
|
|
49
60
|
}, []);
|
|
50
61
|
const openFile = useCallback((path) => {
|
|
51
62
|
if (session.isDirty()) {
|
|
52
|
-
openUnsavedPrompt(session, path);
|
|
63
|
+
openUnsavedPrompt(session, path, t("fileTree.unsaved"));
|
|
53
64
|
}
|
|
54
65
|
else {
|
|
55
66
|
session.open(path);
|
|
56
67
|
}
|
|
57
|
-
}, [session]);
|
|
68
|
+
}, [session, t]);
|
|
58
69
|
const toggleDir = useCallback((path) => {
|
|
59
70
|
setExpanded((prev) => {
|
|
60
71
|
const next = new Set(prev);
|
|
@@ -80,7 +91,7 @@ export function FileTree({ session }) {
|
|
|
80
91
|
// long file names stay readable; capped so the editor keeps room.
|
|
81
92
|
const treeWidth = useMemo(() => {
|
|
82
93
|
const widest = visible.reduce((max, row) => Math.max(max, (row.depth - 1) * 2 + 2 + stringWidth(row.node.name)), 0);
|
|
83
|
-
const title = root ? stringWidth(basename(root.path)) : 0;
|
|
94
|
+
const title = "node" in root ? stringWidth(basename(root.path)) : 0;
|
|
84
95
|
// +3 = row padding (1) + the two border cells.
|
|
85
96
|
const content = Math.max(widest, title) + 3;
|
|
86
97
|
return Math.min(Math.max(MIN_TREE_WIDTH, content), MAX_TREE_WIDTH);
|
|
@@ -88,24 +99,31 @@ export function FileTree({ session }) {
|
|
|
88
99
|
// Publish the live width so the toolbar's drag clamp stays in sync
|
|
89
100
|
// (module store — the tree and toolbar are separate layer elements).
|
|
90
101
|
useEffect(() => {
|
|
91
|
-
|
|
92
|
-
|
|
102
|
+
setTreePos({
|
|
103
|
+
y: 1,
|
|
104
|
+
x: columns - treeWidth,
|
|
105
|
+
width: treeWidth,
|
|
106
|
+
height: rows - 1,
|
|
107
|
+
});
|
|
108
|
+
}, [treeWidth, columns, rows]);
|
|
93
109
|
return (React.createElement(Box, { ref: containerRef, position: "absolute", top: 1, left: columns - treeWidth, width: treeWidth, height: rows - 1, borderStyle: "bold", borderColor: "white",
|
|
94
110
|
// Match the editor surface so the pane reads as part of it.
|
|
95
111
|
backgroundColor: "#1e1e1e", borderBackgroundColor: "#1e1e1e", flexDirection: "column" },
|
|
96
112
|
React.createElement(Box, { paddingLeft: 1, paddingTop: 1, flexDirection: "row" },
|
|
97
|
-
React.createElement(Text, { bold: true }, root ? basename(root.path) : "-"),
|
|
113
|
+
React.createElement(Text, { bold: true }, "node" in root ? basename(root.path) : "-"),
|
|
98
114
|
React.createElement(Box, { ref: refreshRef, marginLeft: 1 },
|
|
99
115
|
React.createElement(Text, { color: refreshHovered ? "green" : "gray" }, "\u21BB"))),
|
|
100
|
-
root ? (
|
|
116
|
+
"scanning" in root ? (React.createElement(Box, { paddingLeft: 1 },
|
|
117
|
+
React.createElement(Text, { dimColor: true }, t("fileTree.scanning")))) : "noDir" in root ? (React.createElement(Box, { paddingLeft: 1 },
|
|
118
|
+
React.createElement(Text, { dimColor: true }, t("fileTree.noDir")))) : "fail" in root ? (React.createElement(Box, { paddingLeft: 1 },
|
|
119
|
+
React.createElement(Text, { dimColor: true }, t("fileTree.fail")))) : (visible.map((row) => (React.createElement(TreeRow, { key: row.node.path, node: row.node, depth: row.depth, expanded: expanded.has(row.node.path), hovered: hoveredPath === row.node.path, onEnter: () => setHoveredPath(row.node.path), onLeave: () => setHoveredPath((h) => (h === row.node.path ? null : h)), onWheel: handleWheel, onClick: () => {
|
|
101
120
|
if (row.node.isDir) {
|
|
102
121
|
toggleDir(row.node.path);
|
|
103
122
|
}
|
|
104
123
|
else {
|
|
105
124
|
openFile(row.node.path);
|
|
106
125
|
}
|
|
107
|
-
} }))))
|
|
108
|
-
React.createElement(Text, { dimColor: true }, "No directory")))));
|
|
126
|
+
} }))))));
|
|
109
127
|
}
|
|
110
128
|
/**
|
|
111
129
|
* One tree line: indent + expand arrow (dirs) + name. The arrow follows the
|
|
@@ -114,7 +132,7 @@ export function FileTree({ session }) {
|
|
|
114
132
|
* so scrolling works while the cursor is over a row — the engine delivers
|
|
115
133
|
* wheel to the top hit region only.
|
|
116
134
|
*/
|
|
117
|
-
function TreeRow({ node, depth, expanded, hovered, onEnter, onLeave, onWheel, onClick }) {
|
|
135
|
+
function TreeRow({ node, depth, expanded, hovered, onEnter, onLeave, onWheel, onClick, }) {
|
|
118
136
|
const ref = useMouseRegion({ onEnter, onLeave, onClick, onWheel }, { priority: 1 });
|
|
119
137
|
const hasChildren = (node.children?.length ?? 0) > 0;
|
|
120
138
|
const arrow = node.isDir
|
|
@@ -133,7 +151,7 @@ function TreeRow({ node, depth, expanded, hovered, onEnter, onLeave, onWheel, on
|
|
|
133
151
|
node.name)));
|
|
134
152
|
}
|
|
135
153
|
/** One button of the unsaved-changes prompt (mouse + keyboard hotkey). */
|
|
136
|
-
function PromptButton({ label, hotkey, active, onEnter, onLeave, onClick }) {
|
|
154
|
+
function PromptButton({ label, hotkey, active, onEnter, onLeave, onClick, }) {
|
|
137
155
|
const ref = useMouseRegion({ onEnter, onLeave, onClick }, { priority: 1 });
|
|
138
156
|
return (React.createElement(Box, { ref: ref, paddingLeft: 1, paddingRight: 1 },
|
|
139
157
|
React.createElement(Text, { inverse: active },
|
|
@@ -147,7 +165,7 @@ function PromptButton({ label, hotkey, active, onEnter, onLeave, onClick }) {
|
|
|
147
165
|
* Cancel), opened when a file-tree click would discard a dirty buffer.
|
|
148
166
|
* Keyboard: s / d / c (Esc also cancels); the buttons are mouse-clickable.
|
|
149
167
|
*/
|
|
150
|
-
function UnsavedPrompt({ title, onSave, onDiscard, onCancel }) {
|
|
168
|
+
function UnsavedPrompt({ title, onSave, onDiscard, onCancel, }) {
|
|
151
169
|
const ctx = useContext(ModalLayerElementContext);
|
|
152
170
|
const { t } = useI18n();
|
|
153
171
|
const { boundKeyboard } = useKeyboard();
|
|
@@ -197,13 +215,13 @@ function UnsavedPrompt({ title, onSave, onDiscard, onCancel }) {
|
|
|
197
215
|
* Open the unsaved-changes prompt; on Save the buffer is written first and
|
|
198
216
|
* only then is the target file loaded (a failed save keeps the editor put).
|
|
199
217
|
*/
|
|
200
|
-
function openUnsavedPrompt(session, targetPath) {
|
|
218
|
+
function openUnsavedPrompt(session, targetPath, title) {
|
|
201
219
|
openModalLayer("unsaved", 100);
|
|
202
220
|
applyElementToModalLayer("unsaved", {
|
|
203
221
|
elementId: "unsaved-prompt",
|
|
204
222
|
element: UnsavedPrompt,
|
|
205
223
|
props: {
|
|
206
|
-
title
|
|
224
|
+
title,
|
|
207
225
|
onSave: () => {
|
|
208
226
|
const saved = session.save();
|
|
209
227
|
if (saved.ok) {
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { XtermMouseEvent } from "@cartridge-engine/keyboard-engine";
|
|
2
|
+
import React from "react";
|
|
3
|
+
import { EditorSession } from "../../core/io/session.js";
|
|
4
|
+
/**
|
|
5
|
+
* Props for a single tool button.
|
|
6
|
+
*
|
|
7
|
+
* @prop priority - Hit-test priority over whatever sits underneath (the editor
|
|
8
|
+
* surface). Buttons must win so clicking them does not also place the cursor.
|
|
9
|
+
* @prop onClick - Called on mouse click and on Enter selection in normal
|
|
10
|
+
* mode; the event is absent for the keyboard path.
|
|
11
|
+
* @prop text - The text to display on the button.
|
|
12
|
+
* @prop focused - Keyboard focus (normal-mode navigation); overrides hover.
|
|
13
|
+
*/
|
|
14
|
+
export type ToolButtonProps = {
|
|
15
|
+
priority: number;
|
|
16
|
+
onClick: (event?: XtermMouseEvent) => void;
|
|
17
|
+
onDragStart: (event: XtermMouseEvent) => void;
|
|
18
|
+
onDragMove: (event: XtermMouseEvent) => void;
|
|
19
|
+
onDragEnd: (event: XtermMouseEvent) => void;
|
|
20
|
+
text: string;
|
|
21
|
+
focused?: boolean;
|
|
22
|
+
};
|
|
23
|
+
export type ToolBarProps = {
|
|
24
|
+
currentMode: string | null;
|
|
25
|
+
session: EditorSession;
|
|
26
|
+
openFileTree: () => void;
|
|
27
|
+
openSettings: () => void;
|
|
28
|
+
fileTreeOpen: boolean;
|
|
29
|
+
};
|
|
30
|
+
export type ToolButton = {
|
|
31
|
+
onClick: (event?: XtermMouseEvent) => void;
|
|
32
|
+
text: string;
|
|
33
|
+
priority: number;
|
|
34
|
+
id: string;
|
|
35
|
+
};
|
|
36
|
+
export declare function ToolBar({ currentMode, session, openFileTree, openSettings, fileTreeOpen, }: ToolBarProps): React.JSX.Element;
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { useI18n, useKeyboard, useMouseRegion } from "ink-cartridge";
|
|
2
|
+
import React, { useCallback, useEffect, useMemo, useRef } from "react";
|
|
3
|
+
import { useState } from "react";
|
|
4
|
+
import { Box, measureElement, Text, useWindowSize } from "ink";
|
|
5
|
+
import { useTree } from "../event/subscription/tree-store.js";
|
|
6
|
+
import { getBarPosition, setBarPosition, useBarPosition, } from "../event/subscription/toolbar-store.js";
|
|
7
|
+
import { clampBarPos } from "../../utils/view/frame-clamp.js";
|
|
8
|
+
function ToolButton({ priority, onClick, text, focused = false, onDragStart, onDragMove, onDragEnd, }) {
|
|
9
|
+
const [flush, setFlush] = useState(false);
|
|
10
|
+
const timerRef = React.useRef(null);
|
|
11
|
+
useEffect(() => {
|
|
12
|
+
return () => {
|
|
13
|
+
if (timerRef.current) {
|
|
14
|
+
clearTimeout(timerRef.current);
|
|
15
|
+
}
|
|
16
|
+
};
|
|
17
|
+
}, []);
|
|
18
|
+
const ref = useMouseRegion({
|
|
19
|
+
onClick: (event) => {
|
|
20
|
+
setFlush(true);
|
|
21
|
+
if (timerRef.current) {
|
|
22
|
+
clearTimeout(timerRef.current);
|
|
23
|
+
}
|
|
24
|
+
timerRef.current = setTimeout(() => {
|
|
25
|
+
setFlush(false);
|
|
26
|
+
}, 150);
|
|
27
|
+
onClick(event);
|
|
28
|
+
},
|
|
29
|
+
onDragEnd,
|
|
30
|
+
onDragMove,
|
|
31
|
+
onDragStart,
|
|
32
|
+
}, { priority });
|
|
33
|
+
return (React.createElement(Box, { ref: ref, borderColor: flush ? "whiteBright" : focused ? "blue" : "white", borderStyle: "bold",
|
|
34
|
+
// Match the editor's surface so the floating bar reads as part of it.
|
|
35
|
+
backgroundColor: "#1e1e1e", height: "100%", justifyContent: "center", alignItems: "center", borderBackgroundColor: "#1e1e1e", paddingLeft: 2, paddingRight: 2 },
|
|
36
|
+
React.createElement(Text, null, text)));
|
|
37
|
+
}
|
|
38
|
+
export function ToolBar({ currentMode, session, openFileTree, openSettings, fileTreeOpen, }) {
|
|
39
|
+
const { t } = useI18n();
|
|
40
|
+
const { boundKeyboard } = useKeyboard();
|
|
41
|
+
const [index, setIndex] = useState(0);
|
|
42
|
+
const items = useMemo(() => [
|
|
43
|
+
{
|
|
44
|
+
id: "save",
|
|
45
|
+
onClick: () => {
|
|
46
|
+
session.save();
|
|
47
|
+
},
|
|
48
|
+
text: t("toolbar.save"),
|
|
49
|
+
priority: 1,
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
id: "file-tree",
|
|
53
|
+
onClick: () => {
|
|
54
|
+
openFileTree();
|
|
55
|
+
},
|
|
56
|
+
text: t("toolbar.fileTree"),
|
|
57
|
+
priority: 1,
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
id: "settings",
|
|
61
|
+
onClick: () => {
|
|
62
|
+
openSettings();
|
|
63
|
+
},
|
|
64
|
+
text: t("toolbar.settings"),
|
|
65
|
+
priority: 1,
|
|
66
|
+
},
|
|
67
|
+
], [session, t, openFileTree, openSettings]);
|
|
68
|
+
const { rows, columns } = useWindowSize();
|
|
69
|
+
const pos = useBarPosition();
|
|
70
|
+
const treePos = useTree();
|
|
71
|
+
const barRef = useRef(null);
|
|
72
|
+
// Grab offset (where inside the bar the cursor went down) plus the bar's
|
|
73
|
+
// size at grab time, kept in a ref so drag moves need no re-measurement.
|
|
74
|
+
const grabRef = useRef({ dx: 0, dy: 0, width: 0, height: 0 });
|
|
75
|
+
const treeWidth = treePos.width;
|
|
76
|
+
const startDrag = useCallback((event) => {
|
|
77
|
+
const node = barRef.current;
|
|
78
|
+
if (!node) {
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
// Measure the bar itself, not the hit region — a button may be the
|
|
82
|
+
// captured target, and its rect would put the bar's corner at the
|
|
83
|
+
// button's corner.
|
|
84
|
+
const m = measureElement(node);
|
|
85
|
+
grabRef.current = {
|
|
86
|
+
dx: event.x - m.x - 1,
|
|
87
|
+
dy: event.y - m.y - 1,
|
|
88
|
+
width: m.width,
|
|
89
|
+
height: m.height,
|
|
90
|
+
};
|
|
91
|
+
setBarPosition({ top: m.y, left: m.x });
|
|
92
|
+
}, []);
|
|
93
|
+
const moveDrag = useCallback((event) => {
|
|
94
|
+
const { dx, dy, width, height } = grabRef.current;
|
|
95
|
+
setBarPosition(clampBarPos(event.x - dx - 1, event.y - dy - 1, width, height, columns, rows, fileTreeOpen, treeWidth));
|
|
96
|
+
}, [columns, rows, fileTreeOpen, treeWidth]);
|
|
97
|
+
const endDrag = useCallback(() => { }, []);
|
|
98
|
+
// Re-clamp when a layout input changes (resize, tree toggled): a bar
|
|
99
|
+
// positioned earlier can end up outside the allowed region.
|
|
100
|
+
useEffect(() => {
|
|
101
|
+
const node = barRef.current;
|
|
102
|
+
if (!node) {
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
const m = measureElement(node);
|
|
106
|
+
const current = getBarPosition();
|
|
107
|
+
if (!current) {
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
const next = clampBarPos(current.left, current.top, m.width, m.height, columns, rows, fileTreeOpen, treeWidth);
|
|
111
|
+
if (next.left !== current.left || next.top !== current.top) {
|
|
112
|
+
setBarPosition(next);
|
|
113
|
+
}
|
|
114
|
+
}, [columns, rows, fileTreeOpen, treeWidth]);
|
|
115
|
+
useEffect(() => {
|
|
116
|
+
const unbinds = [
|
|
117
|
+
boundKeyboard(["ctrl+left"], () => {
|
|
118
|
+
setIndex((prev) => (prev - 1 + items.length) % items.length);
|
|
119
|
+
}),
|
|
120
|
+
boundKeyboard(["ctrl+right"], () => {
|
|
121
|
+
setIndex((prev) => (prev + 1) % items.length);
|
|
122
|
+
}),
|
|
123
|
+
boundKeyboard(["return"], () => {
|
|
124
|
+
items[index].onClick();
|
|
125
|
+
}, { mode: "normal" }),
|
|
126
|
+
];
|
|
127
|
+
return () => {
|
|
128
|
+
unbinds.forEach((unbind) => unbind());
|
|
129
|
+
};
|
|
130
|
+
}, [boundKeyboard, index, items]);
|
|
131
|
+
const showFocus = currentMode === "normal";
|
|
132
|
+
return (React.createElement(Box, { width: "100%", height: "100%", justifyContent: "center", alignItems: "flex-end" },
|
|
133
|
+
React.createElement(Box, { ref: barRef, position: pos ? "absolute" : undefined, top: pos?.top, left: pos?.left, height: 3, flexDirection: "row", justifyContent: "center", alignItems: "center" }, items.map((item, itemIndex) => (React.createElement(ToolButton, { key: item.id, onClick: item.onClick, text: item.text, priority: item.priority, focused: showFocus && index === itemIndex, onDragStart: startDrag, onDragMove: moveDrag, onDragEnd: endDrag }))))));
|
|
134
|
+
}
|
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Module-level drag position of the floating toolbar.
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* external store survives those remounts, so a dragged bar keeps its spot.
|
|
2
|
+
* Module-level drag position of the floating toolbar. null = the bar sits
|
|
3
|
+
* at the bottom-center via flex layout; once the user grabs it, absolute
|
|
4
|
+
* positioning takes over so it follows the cursor. An external store
|
|
5
|
+
* survives the layer system's erase/re-apply remounts, so a dragged bar
|
|
6
|
+
* keeps its spot.
|
|
8
7
|
*/
|
|
9
8
|
type BarPosition = {
|
|
10
9
|
top: number;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export type Rect = {
|
|
2
|
+
x: number;
|
|
3
|
+
y: number;
|
|
4
|
+
width: number;
|
|
5
|
+
height: number;
|
|
6
|
+
};
|
|
7
|
+
export declare function getTreePos(): Rect;
|
|
8
|
+
export declare function setTreePos(newPos: Rect): void;
|
|
9
|
+
/** Reactive binding: re-renders whenever the pane width changes. */
|
|
10
|
+
export declare function useTree(): Rect;
|
|
@@ -5,7 +5,7 @@ import { useSyncExternalStore } from "react";
|
|
|
5
5
|
* The tree sizes itself to its content, so its width is dynamic; the toolbar
|
|
6
6
|
* must not overlap it, so it reads the live width instead of a constant.
|
|
7
7
|
*/
|
|
8
|
-
let
|
|
8
|
+
let pos = { x: 0, y: 0, width: 0, height: 0 };
|
|
9
9
|
const listeners = new Set();
|
|
10
10
|
function subscribe(listener) {
|
|
11
11
|
listeners.add(listener);
|
|
@@ -13,19 +13,16 @@ function subscribe(listener) {
|
|
|
13
13
|
listeners.delete(listener);
|
|
14
14
|
};
|
|
15
15
|
}
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
return width;
|
|
16
|
+
export function getTreePos() {
|
|
17
|
+
return pos;
|
|
19
18
|
}
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
19
|
+
export function setTreePos(newPos) {
|
|
20
|
+
pos = newPos;
|
|
21
|
+
for (const listener of listeners) {
|
|
22
|
+
listener();
|
|
24
23
|
}
|
|
25
|
-
width = next;
|
|
26
|
-
listeners.forEach((fn) => fn());
|
|
27
24
|
}
|
|
28
25
|
/** Reactive binding: re-renders whenever the pane width changes. */
|
|
29
|
-
export function
|
|
30
|
-
return useSyncExternalStore(subscribe,
|
|
26
|
+
export function useTree() {
|
|
27
|
+
return useSyncExternalStore(subscribe, getTreePos, getTreePos);
|
|
31
28
|
}
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
import { Box, Text, useBoxMetrics, useCursor } from "ink";
|
|
2
2
|
import { applyElement, applyElementToModalLayer, closeLayer, eraseElement, openLayer, openModalLayer, useI18n, useKeyboard, useMouseRegion, } from "ink-cartridge";
|
|
3
3
|
import React, { useCallback, useEffect, useRef, useState } from "react";
|
|
4
|
-
import { EditorSession } from "
|
|
5
|
-
import { useSettings } from "
|
|
6
|
-
import { clickToPosition } from "
|
|
7
|
-
import { CommandBar } from "
|
|
8
|
-
import { EditorSetting } from "
|
|
9
|
-
import { FileTree } from "
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
4
|
+
import { EditorSession } from "../../core/io/session.js";
|
|
5
|
+
import { useSettings } from "../../core/settings/useSettings.js";
|
|
6
|
+
import { clickToPosition } from "../../utils/view/click-mapping.js";
|
|
7
|
+
import { CommandBar } from "../editor/command-bar.js";
|
|
8
|
+
import { EditorSetting } from "../editor/editor-setting.js";
|
|
9
|
+
import { FileTree } from "../editor/file-tree.js";
|
|
10
|
+
import { ToolBar } from "../editor/tool-bar.js";
|
|
11
|
+
import { InformationBar } from "../editor/information-bar.js";
|
|
12
12
|
/** Layer hosting the floating toolbar; below modal layers so it never
|
|
13
13
|
* intercepts keys meant for a modal (e.g. the command bar). */
|
|
14
14
|
const TOOLBAR_LAYER_ID = "toolbar";
|
|
@@ -100,11 +100,11 @@ export function Editor({ value: initialText = "", lineNumberRightSpacing = 1, cu
|
|
|
100
100
|
elementId: "toolbar",
|
|
101
101
|
element: ToolBar,
|
|
102
102
|
props: {
|
|
103
|
-
mode,
|
|
103
|
+
currentMode: mode,
|
|
104
104
|
session,
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
105
|
+
openFileTree: () => setFileTreeOpen((open) => !open),
|
|
106
|
+
openSettings: () => setSettingsOpen((open) => !open),
|
|
107
|
+
fileTreeOpen
|
|
108
108
|
},
|
|
109
109
|
});
|
|
110
110
|
}, 0);
|
|
@@ -4,7 +4,7 @@ import React, { useEffect, useState } from "react";
|
|
|
4
4
|
import { Editor } from "./editor.js";
|
|
5
5
|
import { Settings } from "./settings.js";
|
|
6
6
|
import { gotoScreen } from "ink-cartridge";
|
|
7
|
-
import { getLogo } from "
|
|
7
|
+
import { getLogo } from "../logo.js";
|
|
8
8
|
/**
|
|
9
9
|
* A borderless text button: the plain Box provides the mouse hit area,
|
|
10
10
|
* hovering inverts the text as visual feedback, disabled buttons dim.
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { Box, Text } from "ink";
|
|
2
2
|
import { back, ModalLayerElementContext, useI18n, useKeyboard, useMouseRegion, useScreenSystem, } from "ink-cartridge";
|
|
3
3
|
import React, { useCallback, useContext, useEffect, useMemo, useRef, useState, } from "react";
|
|
4
|
-
import { useSettings } from "
|
|
5
|
-
import { ModalFrame } from "
|
|
6
|
-
import { SensitivityBar, snapSensitivity } from "
|
|
4
|
+
import { useSettings } from "../../core/settings/useSettings.js";
|
|
5
|
+
import { ModalFrame } from "../utils/modal-frame.js";
|
|
6
|
+
import { SensitivityBar, snapSensitivity } from "../utils/sensitivity-bar.js";
|
|
7
7
|
const LANGUAGES = [
|
|
8
8
|
{ code: "en", labelKey: "settings.language.en" },
|
|
9
9
|
{ code: "zh", labelKey: "settings.language.zh" },
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Box, Text, useWindowSize } from "ink";
|
|
2
2
|
import { useMouseRegion } from "ink-cartridge";
|
|
3
3
|
import React, { useEffect, useRef, useState } from "react";
|
|
4
|
-
import { clampFrame } from "
|
|
4
|
+
import { clampFrame } from "../../utils/view/frame-clamp.js";
|
|
5
5
|
/**
|
|
6
6
|
* Shared draggable modal frame used by the language and sensitivity pickers.
|
|
7
7
|
*
|
package/package.json
CHANGED
package/dist/view/tool-bar.d.ts
DELETED
|
@@ -1,62 +0,0 @@
|
|
|
1
|
-
import React from "react";
|
|
2
|
-
import type { XtermMouseEvent } from "@cartridge-engine/keyboard-engine";
|
|
3
|
-
import type { EditorSession } from "../core/session.js";
|
|
4
|
-
/**
|
|
5
|
-
* Props for a single tool button.
|
|
6
|
-
*
|
|
7
|
-
* @prop priority - Hit-test priority over whatever sits underneath (the editor
|
|
8
|
-
* surface). Buttons must win so clicking them does not also place the cursor.
|
|
9
|
-
* @prop onClick - Called on mouse click and on Enter selection in normal
|
|
10
|
-
* mode; the event is absent for the keyboard path.
|
|
11
|
-
* @prop text - The text to display on the button.
|
|
12
|
-
* @prop focused - Keyboard focus (normal-mode navigation); overrides hover.
|
|
13
|
-
* @prop onDragStart/onDragMove/onDragEnd - Forwarded from the toolbar. The
|
|
14
|
-
* engine captures a drag on the pressed region only, so buttons must carry
|
|
15
|
-
* the drag handlers or dragging from a button would not move the bar.
|
|
16
|
-
* @prop dragging - The bar is being dragged; buttons tint red.
|
|
17
|
-
*/
|
|
18
|
-
export type ToolButtonProps = {
|
|
19
|
-
priority: number;
|
|
20
|
-
onClick: (event?: XtermMouseEvent) => void;
|
|
21
|
-
text: string;
|
|
22
|
-
focused?: boolean;
|
|
23
|
-
onDragStart?: (event: XtermMouseEvent) => void;
|
|
24
|
-
onDragMove?: (event: XtermMouseEvent) => void;
|
|
25
|
-
onDragEnd?: (event: XtermMouseEvent) => void;
|
|
26
|
-
dragging?: boolean;
|
|
27
|
-
};
|
|
28
|
-
/** One tool entry; the list is the extension point for future tools. */
|
|
29
|
-
export type Tool = {
|
|
30
|
-
id: string;
|
|
31
|
-
/** i18n key for the button label. */
|
|
32
|
-
labelKey: string;
|
|
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 save + file tree. */
|
|
39
|
-
tools?: Tool[];
|
|
40
|
-
/** File session wired to the default Save tool. */
|
|
41
|
-
session?: EditorSession | null;
|
|
42
|
-
/** Toggles the file-tree pane (default File Tree tool). */
|
|
43
|
-
onToggleFileTree?: () => void;
|
|
44
|
-
/** Opens/closes the in-editor settings layer (default Settings tool). */
|
|
45
|
-
onOpenSettings?: () => void;
|
|
46
|
-
/** Whether the file-tree pane is open; the bar stays left of it. */
|
|
47
|
-
fileTreeOpen?: boolean;
|
|
48
|
-
};
|
|
49
|
-
/**
|
|
50
|
-
* Floating toolbar, bottom-center of the terminal until the user drags it.
|
|
51
|
-
* Dragging switches it to absolute positioning and clamps it inside the
|
|
52
|
-
* terminal view (clampFrame, top ≥ 1 to preserve the information bar).
|
|
53
|
-
* Purely presentational for now: one centered button with hover/focus states.
|
|
54
|
-
* The editor owns the layer that hosts this bar (and the ctrl+tab toggle);
|
|
55
|
-
* this component only adds the normal-mode keyboard navigation
|
|
56
|
-
* (ctrl+left/ctrl+right move the focus ring, enter selects). The activator
|
|
57
|
-
* is bare Enter, not ctrl+enter: terminals send the same byte (`\r`) for
|
|
58
|
-
* both, so a ctrl-modified Enter can never reach the engine. Insert mode
|
|
59
|
-
* stays mouse-only because these bindings are mode-restricted and never
|
|
60
|
-
* fire there.
|
|
61
|
-
*/
|
|
62
|
-
export declare function ToolBar({ mode, tools, session, onToggleFileTree, onOpenSettings, fileTreeOpen, }: ToolBarProps): React.JSX.Element;
|