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.
Files changed (43) hide show
  1. package/asset/language/en.json +4 -0
  2. package/asset/language/zh.json +4 -0
  3. package/dist/core/document/document.d.ts +41 -2
  4. package/dist/core/document/document.js +113 -36
  5. package/dist/core/{file-tree-model.d.ts → io/file-tree-model.d.ts} +12 -3
  6. package/dist/core/{file-tree-model.js → io/file-tree-model.js} +23 -12
  7. package/dist/core/{session.d.ts → io/session.d.ts} +1 -1
  8. package/dist/core/{session.js → io/session.js} +1 -1
  9. package/dist/index.js +4 -4
  10. package/dist/{view → utils/view}/click-mapping.d.ts +1 -1
  11. package/dist/{view → utils/view}/frame-clamp.d.ts +4 -0
  12. package/dist/{view → utils/view}/frame-clamp.js +8 -0
  13. package/dist/{i18n-resources.js → utils/view/i18n-resources.js} +1 -1
  14. package/dist/view/{command-bar.d.ts → editor/command-bar.d.ts} +1 -1
  15. package/dist/view/{editor-setting.js → editor/editor-setting.js} +2 -2
  16. package/dist/view/{file-tree.d.ts → editor/file-tree.d.ts} +5 -1
  17. package/dist/view/{file-tree.js → editor/file-tree.js} +46 -28
  18. package/dist/view/{information-bar.d.ts → editor/information-bar.d.ts} +1 -1
  19. package/dist/view/editor/tool-bar.d.ts +36 -0
  20. package/dist/view/editor/tool-bar.js +134 -0
  21. package/dist/view/{toolbar-store.d.ts → event/subscription/toolbar-store.d.ts} +5 -6
  22. package/dist/view/event/subscription/tree-store.d.ts +10 -0
  23. package/dist/view/{tree-width-store.js → event/subscription/tree-store.js} +9 -12
  24. package/dist/view/{editor.js → page/editor.js} +12 -12
  25. package/dist/view/{main-menu.js → page/main-menu.js} +1 -1
  26. package/dist/view/{settings.js → page/settings.js} +3 -3
  27. package/dist/view/{modal-frame.js → utils/modal-frame.js} +1 -1
  28. package/package.json +1 -1
  29. package/dist/view/tool-bar.d.ts +0 -62
  30. package/dist/view/tool-bar.js +0 -148
  31. package/dist/view/tree-width-store.d.ts +0 -6
  32. /package/dist/{view → utils/view}/click-mapping.js +0 -0
  33. /package/dist/{i18n-resources.d.ts → utils/view/i18n-resources.d.ts} +0 -0
  34. /package/dist/view/{command-bar.js → editor/command-bar.js} +0 -0
  35. /package/dist/view/{editor-setting.d.ts → editor/editor-setting.d.ts} +0 -0
  36. /package/dist/view/{information-bar.js → editor/information-bar.js} +0 -0
  37. /package/dist/view/{toolbar-store.js → event/subscription/toolbar-store.js} +0 -0
  38. /package/dist/view/{editor.d.ts → page/editor.d.ts} +0 -0
  39. /package/dist/view/{main-menu.d.ts → page/main-menu.d.ts} +0 -0
  40. /package/dist/view/{settings.d.ts → page/settings.d.ts} +0 -0
  41. /package/dist/view/{modal-frame.d.ts → utils/modal-frame.d.ts} +0 -0
  42. /package/dist/view/{sensitivity-bar.d.ts → utils/sensitivity-bar.d.ts} +0 -0
  43. /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 "../core/file-tree-model.js";
7
- import { useSettings } from "../core/settings/useSettings.js";
8
- import { ModalFrame } from "./modal-frame.js";
9
- import { setTreeWidth } from "./tree-width-store.js";
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 = useMemo(() => {
31
- // scanTick is a dependency only to force a re-scan on refresh.
32
- void scanTick;
33
- const resolved = resolveFileTreeRoot(settings.fileTree, process.cwd());
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
- }, [root]);
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
- setTreeWidth(treeWidth);
92
- }, [treeWidth]);
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 ? (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: () => {
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
- } })))) : (React.createElement(Box, { paddingLeft: 1 },
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: "Unsaved changes",
224
+ title,
207
225
  onSave: () => {
208
226
  const saved = session.save();
209
227
  if (saved.ok) {
@@ -1,5 +1,5 @@
1
1
  import React from "react";
2
- import type { EditorSession } from "../core/session.js";
2
+ import type { EditorSession } from "../../core/io/session.js";
3
3
  export type InformationBarProps = {
4
4
  mode: string;
5
5
  cursor?: {
@@ -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
- * The toolbar element is erased and re-applied whenever its props change
5
- * (the layer system treats re-applies of an existing element id as no-ops),
6
- * which would otherwise reset the component's internal position state. An
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 width = 32;
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
- /** Imperative read for drag handlers. */
17
- export function getTreeWidth() {
18
- return width;
16
+ export function getTreePos() {
17
+ return pos;
19
18
  }
20
- /** Update the reported pane width (no-op when unchanged). */
21
- export function setTreeWidth(next) {
22
- if (next === width) {
23
- return;
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 useTreeWidth() {
30
- return useSyncExternalStore(subscribe, getTreeWidth, getTreeWidth);
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 "../core/session.js";
5
- import { useSettings } from "../core/settings/useSettings.js";
6
- import { clickToPosition } from "./click-mapping.js";
7
- import { CommandBar } from "./command-bar.js";
8
- import { EditorSetting } from "./editor-setting.js";
9
- import { FileTree } from "./file-tree.js";
10
- import { InformationBar } from "./information-bar.js";
11
- import { ToolBar } from "./tool-bar.js";
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
- fileTreeOpen,
106
- onToggleFileTree: () => setFileTreeOpen((open) => !open),
107
- onOpenSettings: () => setSettingsOpen((open) => !open),
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 "./logo.js";
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 "../core/settings/useSettings.js";
5
- import { ModalFrame } from "./modal-frame.js";
6
- import { SensitivityBar, snapSensitivity } from "./sensitivity-bar.js";
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 "./frame-clamp.js";
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "blots-editor",
3
- "version": "0.0.14",
3
+ "version": "0.0.16",
4
4
  "description": "A lightweight Markdown editor for the terminal, built on ink-cartridge",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -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;