blots-editor 0.0.13 → 0.0.15
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 +19 -1
- package/asset/language/zh.json +19 -1
- package/dist/core/document/document.d.ts +14 -0
- package/dist/core/document/document.js +24 -2
- package/dist/core/document/text-line.d.ts +16 -1
- package/dist/core/document/text-line.js +36 -3
- package/dist/core/io/file-tree-model.d.ts +49 -0
- package/dist/core/io/file-tree-model.js +129 -0
- package/dist/core/io/session.d.ts +58 -0
- package/dist/core/io/session.js +116 -0
- package/dist/core/settings/schema.d.ts +13 -0
- package/dist/core/settings/schema.js +11 -0
- package/dist/core/settings/useSettings.d.ts +2 -0
- package/dist/core/settings/useSettings.js +17 -12
- 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/editor/command-bar.d.ts +12 -0
- package/dist/view/{command-bar.js → editor/command-bar.js} +11 -2
- package/dist/view/editor/editor-setting.d.ts +8 -0
- package/dist/view/editor/editor-setting.js +69 -0
- package/dist/view/editor/file-tree.d.ts +19 -0
- package/dist/view/editor/file-tree.js +235 -0
- package/dist/view/editor/information-bar.d.ts +16 -0
- package/dist/view/editor/information-bar.js +35 -0
- package/dist/view/editor/tool-bar.d.ts +36 -0
- package/dist/view/editor/tool-bar.js +134 -0
- package/dist/view/event/subscription/toolbar-store.d.ts +18 -0
- package/dist/view/event/subscription/toolbar-store.js +25 -0
- package/dist/view/event/subscription/tree-store.d.ts +10 -0
- package/dist/view/event/subscription/tree-store.js +28 -0
- package/dist/view/{editor.js → page/editor.js} +94 -18
- package/dist/view/{main-menu.js → page/main-menu.js} +1 -1
- package/dist/view/page/settings.d.ts +13 -0
- package/dist/view/{settings.js → page/settings.js} +117 -10
- package/dist/view/utils/modal-frame.js +75 -0
- package/package.json +1 -1
- package/dist/view/command-bar.d.ts +0 -7
- package/dist/view/information-bar.d.ts +0 -10
- package/dist/view/information-bar.js +0 -13
- package/dist/view/modal-frame.js +0 -43
- package/dist/view/settings.d.ts +0 -2
- package/dist/view/tool-bar.d.ts +0 -51
- package/dist/view/tool-bar.js +0 -92
- /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/{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/{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
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { Box, Text, useWindowSize } from "ink";
|
|
2
|
+
import { useMouseRegion } from "ink-cartridge";
|
|
3
|
+
import React, { useEffect, useRef, useState } from "react";
|
|
4
|
+
import { clampFrame } from "../../utils/view/frame-clamp.js";
|
|
5
|
+
/**
|
|
6
|
+
* Shared draggable modal frame used by the language and sensitivity pickers.
|
|
7
|
+
*
|
|
8
|
+
* Centered by flex layout until the user grabs it, then switches to absolute
|
|
9
|
+
* positioning and follows the cursor (clamped to the terminal). Border turns
|
|
10
|
+
* green while dragging. Callers provide the title, body and optional footer.
|
|
11
|
+
*/
|
|
12
|
+
export function ModalFrame({ title, children, footer, width = 36, }) {
|
|
13
|
+
const { columns, rows } = useWindowSize();
|
|
14
|
+
// null = centered by flex layout; set once dragging starts so the frame
|
|
15
|
+
// keeps following the cursor via absolute positioning (0-based coords).
|
|
16
|
+
const [pos, setPos] = useState(null);
|
|
17
|
+
const [dragging, setDragging] = useState(false);
|
|
18
|
+
// Where inside the frame the cursor was grabbed, so the frame doesn't
|
|
19
|
+
// jump so its top-left lands under the cursor on the first drag move.
|
|
20
|
+
const grabOffsetRef = useRef({ dx: 0, dy: 0 });
|
|
21
|
+
// The frame's size, captured at grab time — used by the resize clamp
|
|
22
|
+
// below (a non-null position implies a drag happened, so this is set).
|
|
23
|
+
const frameSizeRef = useRef({ width: 0, height: 0 });
|
|
24
|
+
const frameRef = useMouseRegion({
|
|
25
|
+
onDragStart: (event, rect) => {
|
|
26
|
+
grabOffsetRef.current = {
|
|
27
|
+
dx: event.x - rect.x,
|
|
28
|
+
dy: event.y - rect.y,
|
|
29
|
+
};
|
|
30
|
+
frameSizeRef.current = { width: rect.width, height: rect.height };
|
|
31
|
+
// Switch from flex centering to absolute positioning at the
|
|
32
|
+
// current rect — seamless, no jump on the first frame.
|
|
33
|
+
setPos({ top: rect.y - 1, left: rect.x - 1 });
|
|
34
|
+
setDragging(true);
|
|
35
|
+
},
|
|
36
|
+
onDragMove: (event, rect) => {
|
|
37
|
+
const { dx, dy } = grabOffsetRef.current;
|
|
38
|
+
const clamped = clampFrame(event.x - dx - 1, event.y - dy - 1, columns, rows, rect.width, rect.height);
|
|
39
|
+
// The information bar occupies the top row — keep the frame below
|
|
40
|
+
// it, same as the toolbar.
|
|
41
|
+
setPos({ top: Math.max(1, clamped.top), left: clamped.left });
|
|
42
|
+
},
|
|
43
|
+
onDragEnd: () => setDragging(false),
|
|
44
|
+
});
|
|
45
|
+
// The drag clamp only runs while dragging — a terminal resize can shrink
|
|
46
|
+
// the viewport under a dragged frame afterwards. Re-clamp on every size
|
|
47
|
+
// change so the frame never extends past the edge (top stays ≥ 1 so it
|
|
48
|
+
// cannot overlap the information bar). The centered state (pos null) is
|
|
49
|
+
// flex-laid-out and follows the terminal automatically; a non-null pos
|
|
50
|
+
// implies a drag happened, so frameSizeRef is populated.
|
|
51
|
+
useEffect(() => {
|
|
52
|
+
const { width, height } = frameSizeRef.current;
|
|
53
|
+
if (!width || !height) {
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
setPos((current) => {
|
|
57
|
+
if (!current) {
|
|
58
|
+
return current;
|
|
59
|
+
}
|
|
60
|
+
const clamped = clampFrame(current.left, current.top, columns, rows, width, height);
|
|
61
|
+
const next = { top: Math.max(1, clamped.top), left: clamped.left };
|
|
62
|
+
return next.left === current.left && next.top === current.top ? current : next;
|
|
63
|
+
});
|
|
64
|
+
}, [columns, rows]);
|
|
65
|
+
return (
|
|
66
|
+
// paddingTop keeps the centered frame below the information bar even
|
|
67
|
+
// in short terminals where centering would push it into row 1.
|
|
68
|
+
React.createElement(Box, { width: "100%", height: "100%", paddingTop: 1, justifyContent: "center", alignItems: "center" },
|
|
69
|
+
React.createElement(Box, { ref: frameRef, position: pos ? "absolute" : undefined, top: pos?.top, left: pos?.left, flexDirection: "column", borderStyle: "bold", borderColor: dragging ? "green" : "white",
|
|
70
|
+
// Match the editor surface so dialogs read as part of it.
|
|
71
|
+
backgroundColor: "#1e1e1e", borderBackgroundColor: "#1e1e1e", width: width, alignItems: "center", paddingX: 4, paddingY: 1, gap: 1 },
|
|
72
|
+
React.createElement(Text, { bold: true }, title),
|
|
73
|
+
children,
|
|
74
|
+
footer)));
|
|
75
|
+
}
|
package/package.json
CHANGED
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
import React from "react";
|
|
2
|
-
/**
|
|
3
|
-
* Modal command bar (like Helix's `:` prompt). Rendered as a modal layer
|
|
4
|
-
* element, so it exclusively owns the keyboard while open — the editor's
|
|
5
|
-
* bindings (including the insert-mode `*` wildcard) stay dormant.
|
|
6
|
-
*/
|
|
7
|
-
export declare function CommandBar(): React.JSX.Element;
|
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
import React from "react";
|
|
2
|
-
export type InformationBarProps = {
|
|
3
|
-
mode: string;
|
|
4
|
-
cursor?: {
|
|
5
|
-
line: number;
|
|
6
|
-
column: number;
|
|
7
|
-
};
|
|
8
|
-
};
|
|
9
|
-
/** Status bar: current mode (live) plus the cursor's 1-based position. */
|
|
10
|
-
export declare function InformationBar({ mode, cursor }: InformationBarProps): React.JSX.Element;
|
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
import { Box, Text } from "ink";
|
|
2
|
-
import React from "react";
|
|
3
|
-
/** Status bar: current mode (live) plus the cursor's 1-based position. */
|
|
4
|
-
export function InformationBar({ mode, cursor }) {
|
|
5
|
-
return (React.createElement(Box, { width: "100%", height: 1, backgroundColor: "blue", flexDirection: "row", paddingLeft: 2 },
|
|
6
|
-
React.createElement(Text, { color: "white", bold: true }, mode),
|
|
7
|
-
cursor ? (React.createElement(Text, { color: "white" },
|
|
8
|
-
" ",
|
|
9
|
-
"Ln ",
|
|
10
|
-
cursor.line + 1,
|
|
11
|
-
", Col ",
|
|
12
|
-
cursor.column + 1)) : null));
|
|
13
|
-
}
|
package/dist/view/modal-frame.js
DELETED
|
@@ -1,43 +0,0 @@
|
|
|
1
|
-
import { Box, Text, useWindowSize } from "ink";
|
|
2
|
-
import { useMouseRegion } from "ink-cartridge";
|
|
3
|
-
import React, { useRef, useState } from "react";
|
|
4
|
-
import { clampFrame } from "./frame-clamp.js";
|
|
5
|
-
/**
|
|
6
|
-
* Shared draggable modal frame used by the language and sensitivity pickers.
|
|
7
|
-
*
|
|
8
|
-
* Centered by flex layout until the user grabs it, then switches to absolute
|
|
9
|
-
* positioning and follows the cursor (clamped to the terminal). Border turns
|
|
10
|
-
* green while dragging. Callers provide the title, body and optional footer.
|
|
11
|
-
*/
|
|
12
|
-
export function ModalFrame({ title, children, footer, width = 36, }) {
|
|
13
|
-
const { columns, rows } = useWindowSize();
|
|
14
|
-
// null = centered by flex layout; set once dragging starts so the frame
|
|
15
|
-
// keeps following the cursor via absolute positioning (0-based coords).
|
|
16
|
-
const [pos, setPos] = useState(null);
|
|
17
|
-
const [dragging, setDragging] = useState(false);
|
|
18
|
-
// Where inside the frame the cursor was grabbed, so the frame doesn't
|
|
19
|
-
// jump so its top-left lands under the cursor on the first drag move.
|
|
20
|
-
const grabOffsetRef = useRef({ dx: 0, dy: 0 });
|
|
21
|
-
const frameRef = useMouseRegion({
|
|
22
|
-
onDragStart: (event, rect) => {
|
|
23
|
-
grabOffsetRef.current = {
|
|
24
|
-
dx: event.x - rect.x,
|
|
25
|
-
dy: event.y - rect.y,
|
|
26
|
-
};
|
|
27
|
-
// Switch from flex centering to absolute positioning at the
|
|
28
|
-
// current rect — seamless, no jump on the first frame.
|
|
29
|
-
setPos({ top: rect.y - 1, left: rect.x - 1 });
|
|
30
|
-
setDragging(true);
|
|
31
|
-
},
|
|
32
|
-
onDragMove: (event, rect) => {
|
|
33
|
-
const { dx, dy } = grabOffsetRef.current;
|
|
34
|
-
setPos(clampFrame(event.x - dx - 1, event.y - dy - 1, columns, rows, rect.width, rect.height));
|
|
35
|
-
},
|
|
36
|
-
onDragEnd: () => setDragging(false),
|
|
37
|
-
});
|
|
38
|
-
return (React.createElement(Box, { width: "100%", height: "100%", justifyContent: "center", alignItems: "center" },
|
|
39
|
-
React.createElement(Box, { ref: frameRef, position: pos ? "absolute" : undefined, top: pos?.top, left: pos?.left, flexDirection: "column", borderStyle: "bold", borderColor: dragging ? "green" : "white", backgroundColor: "black", width: width, alignItems: "center", paddingX: 4, paddingY: 1, gap: 1 },
|
|
40
|
-
React.createElement(Text, { bold: true }, title),
|
|
41
|
-
children,
|
|
42
|
-
footer)));
|
|
43
|
-
}
|
package/dist/view/settings.d.ts
DELETED
package/dist/view/tool-bar.d.ts
DELETED
|
@@ -1,51 +0,0 @@
|
|
|
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;
|
package/dist/view/tool-bar.js
DELETED
|
@@ -1,92 +0,0 @@
|
|
|
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
|
-
}
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|