giggles 0.7.0 → 0.7.1
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/dist/{chunk-74PBSWEK.js → chunk-HHDMTIXE.js} +101 -30
- package/dist/index.js +3 -3
- package/dist/ui/index.d.ts +2 -1
- package/dist/ui/index.js +176 -162
- package/package.json +1 -1
|
@@ -69,7 +69,7 @@ function useKeybindingRegistry(focus) {
|
|
|
69
69
|
}
|
|
70
70
|
|
|
71
71
|
// src/core/focus/useFocusNode.ts
|
|
72
|
-
import { useContext as useContext2,
|
|
72
|
+
import { useContext as useContext2, useId as useId2, useLayoutEffect, useMemo, useSyncExternalStore as useSyncExternalStore2 } from "react";
|
|
73
73
|
function useFocusNode(options) {
|
|
74
74
|
var _a;
|
|
75
75
|
const id = useId2();
|
|
@@ -78,24 +78,26 @@ function useFocusNode(options) {
|
|
|
78
78
|
const parentId = ((_a = options == null ? void 0 : options.parent) == null ? void 0 : _a.id) ?? contextParentId;
|
|
79
79
|
const focusKey = options == null ? void 0 : options.focusKey;
|
|
80
80
|
const subscribe = useMemo(() => store.subscribe.bind(store), [store]);
|
|
81
|
-
|
|
82
|
-
|
|
81
|
+
store.registerNode(id, parentId, focusKey, true);
|
|
82
|
+
useLayoutEffect(() => {
|
|
83
|
+
store.registerNode(id, parentId, focusKey, true);
|
|
84
|
+
store.flush();
|
|
83
85
|
return () => {
|
|
84
86
|
store.unregisterNode(id);
|
|
85
87
|
};
|
|
86
|
-
}, [id,
|
|
88
|
+
}, [id, store]);
|
|
87
89
|
const hasFocus = useSyncExternalStore2(subscribe, () => store.isFocused(id));
|
|
88
90
|
return { id, hasFocus };
|
|
89
91
|
}
|
|
90
92
|
|
|
91
93
|
// src/core/input/FocusTrap.tsx
|
|
92
|
-
import { useEffect as
|
|
94
|
+
import { useEffect as useEffect2, useRef as useRef2 } from "react";
|
|
93
95
|
import { jsx } from "react/jsx-runtime";
|
|
94
96
|
function FocusTrap({ children }) {
|
|
95
97
|
const { id } = useFocusNode();
|
|
96
98
|
const store = useStore();
|
|
97
99
|
const previousFocusRef = useRef2(store.getFocusedId());
|
|
98
|
-
|
|
100
|
+
useEffect2(() => {
|
|
99
101
|
const previousFocus = previousFocusRef.current;
|
|
100
102
|
store.setTrap(id);
|
|
101
103
|
store.focusFirstChild(id);
|
|
@@ -121,7 +123,7 @@ function InputRouter({ children }) {
|
|
|
121
123
|
}
|
|
122
124
|
|
|
123
125
|
// src/core/focus/useFocusScope.ts
|
|
124
|
-
import { useCallback, useContext as useContext3, useEffect as
|
|
126
|
+
import { useCallback, useContext as useContext3, useEffect as useEffect3, useId as useId3, useLayoutEffect as useLayoutEffect2, useMemo as useMemo2, useSyncExternalStore as useSyncExternalStore3 } from "react";
|
|
125
127
|
function useFocusScope(options) {
|
|
126
128
|
var _a;
|
|
127
129
|
const id = useId3();
|
|
@@ -131,12 +133,14 @@ function useFocusScope(options) {
|
|
|
131
133
|
const parentId = ((_a = options == null ? void 0 : options.parent) == null ? void 0 : _a.id) ?? contextParentId;
|
|
132
134
|
const focusKey = options == null ? void 0 : options.focusKey;
|
|
133
135
|
const subscribe = useMemo2(() => store.subscribe.bind(store), [store]);
|
|
134
|
-
|
|
135
|
-
|
|
136
|
+
store.registerNode(id, parentId, focusKey, true);
|
|
137
|
+
useLayoutEffect2(() => {
|
|
138
|
+
store.registerNode(id, parentId, focusKey, true);
|
|
139
|
+
store.flush();
|
|
136
140
|
return () => {
|
|
137
141
|
store.unregisterNode(id);
|
|
138
142
|
};
|
|
139
|
-
}, [id,
|
|
143
|
+
}, [id, store]);
|
|
140
144
|
const hasFocus = useSyncExternalStore3(subscribe, () => store.isFocused(id));
|
|
141
145
|
const isPassive = useSyncExternalStore3(subscribe, () => store.isPassive(id));
|
|
142
146
|
const next = useCallback(() => store.navigateSibling("next", true, id), [store, id]);
|
|
@@ -149,12 +153,12 @@ function useFocusScope(options) {
|
|
|
149
153
|
const focusChildShallow = useCallback((key) => store.focusChildByKey(id, key, true), [store, id]);
|
|
150
154
|
const resolvedBindings = typeof (options == null ? void 0 : options.keybindings) === "function" ? options.keybindings({ next, prev, nextShallow, prevShallow, escape, drillIn, focusChild, focusChildShallow }) : (options == null ? void 0 : options.keybindings) ?? {};
|
|
151
155
|
store.registerKeybindings(id, keybindingRegistrationId, resolvedBindings);
|
|
152
|
-
|
|
156
|
+
useEffect3(() => {
|
|
153
157
|
return () => {
|
|
154
158
|
store.unregisterKeybindings(id, keybindingRegistrationId);
|
|
155
159
|
};
|
|
156
160
|
}, [id, keybindingRegistrationId, store]);
|
|
157
|
-
|
|
161
|
+
useEffect3(() => {
|
|
158
162
|
if (!store.hasFocusScopeComponent(id)) {
|
|
159
163
|
throw new GigglesError(
|
|
160
164
|
"useFocusScope() was called but no <FocusScope handle={scope}> was rendered. Every useFocusScope() call requires a corresponding <FocusScope> in the render output \u2014 without it, child components register under the wrong parent scope and keyboard navigation silently breaks."
|
|
@@ -177,11 +181,11 @@ function useFocusScope(options) {
|
|
|
177
181
|
}
|
|
178
182
|
|
|
179
183
|
// src/core/focus/FocusScope.tsx
|
|
180
|
-
import { useEffect as
|
|
184
|
+
import { useEffect as useEffect4 } from "react";
|
|
181
185
|
import { jsx as jsx3 } from "react/jsx-runtime";
|
|
182
186
|
function FocusScope({ handle, children }) {
|
|
183
187
|
const store = useStore();
|
|
184
|
-
|
|
188
|
+
useEffect4(() => {
|
|
185
189
|
store.registerFocusScopeComponent(handle.id);
|
|
186
190
|
return () => store.unregisterFocusScopeComponent(handle.id);
|
|
187
191
|
}, [handle.id, store]);
|
|
@@ -224,7 +228,7 @@ var FocusStore = class {
|
|
|
224
228
|
listeners = /* @__PURE__ */ new Set();
|
|
225
229
|
version = 0;
|
|
226
230
|
// nodeId → registrationId → BindingRegistration
|
|
227
|
-
//
|
|
231
|
+
// Both keybindings and nodes register synchronously during render.
|
|
228
232
|
// A keybinding may exist for a node that has not yet appeared in the node tree —
|
|
229
233
|
// this is safe because dispatch only walks nodes in the active branch path.
|
|
230
234
|
keybindings = /* @__PURE__ */ new Map();
|
|
@@ -237,20 +241,43 @@ var FocusStore = class {
|
|
|
237
241
|
this.listeners.add(listener);
|
|
238
242
|
return () => this.listeners.delete(listener);
|
|
239
243
|
}
|
|
244
|
+
dirty = false;
|
|
240
245
|
notify() {
|
|
241
246
|
this.version++;
|
|
247
|
+
this.dirty = false;
|
|
242
248
|
for (const listener of this.listeners) {
|
|
243
249
|
listener();
|
|
244
250
|
}
|
|
245
251
|
}
|
|
252
|
+
// Mark the store as changed without notifying subscribers. Used when the
|
|
253
|
+
// store is mutated during React's render phase (e.g. silent node registration)
|
|
254
|
+
// where calling notify() would trigger subscription callbacks unsafely.
|
|
255
|
+
// Call flush() in a useLayoutEffect to deliver the notification before paint.
|
|
256
|
+
markDirty() {
|
|
257
|
+
this.dirty = true;
|
|
258
|
+
}
|
|
259
|
+
flush() {
|
|
260
|
+
if (this.dirty) {
|
|
261
|
+
this.notify();
|
|
262
|
+
}
|
|
263
|
+
}
|
|
246
264
|
getVersion() {
|
|
247
265
|
return this.version;
|
|
248
266
|
}
|
|
249
267
|
// ---------------------------------------------------------------------------
|
|
250
268
|
// Registration
|
|
251
269
|
// ---------------------------------------------------------------------------
|
|
252
|
-
registerNode(id, parentId, focusKey) {
|
|
253
|
-
const
|
|
270
|
+
registerNode(id, parentId, focusKey, silent = false) {
|
|
271
|
+
const existing = this.nodes.get(id);
|
|
272
|
+
if (existing && existing.parentId === parentId) {
|
|
273
|
+
if (focusKey !== existing.focusKey) {
|
|
274
|
+
this.updateFocusKey(id, parentId, existing.focusKey, focusKey);
|
|
275
|
+
existing.focusKey = focusKey;
|
|
276
|
+
this.markDirty();
|
|
277
|
+
}
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
const node = { id, parentId, childrenIds: [], focusKey };
|
|
254
281
|
this.nodes.set(id, node);
|
|
255
282
|
this.parentMap.set(id, parentId);
|
|
256
283
|
if (parentId) {
|
|
@@ -260,7 +287,11 @@ var FocusStore = class {
|
|
|
260
287
|
parent.childrenIds.push(id);
|
|
261
288
|
if (wasEmpty && this.pendingFocusFirstChild.has(parentId)) {
|
|
262
289
|
this.pendingFocusFirstChild.delete(parentId);
|
|
263
|
-
|
|
290
|
+
if (silent) {
|
|
291
|
+
this.setFocusedIdSilently(id);
|
|
292
|
+
} else {
|
|
293
|
+
this.focusNode(id);
|
|
294
|
+
}
|
|
264
295
|
}
|
|
265
296
|
}
|
|
266
297
|
}
|
|
@@ -276,9 +307,17 @@ var FocusStore = class {
|
|
|
276
307
|
this.keyIndex.get(parentId).set(focusKey, id);
|
|
277
308
|
}
|
|
278
309
|
if (this.nodes.size === 1) {
|
|
279
|
-
|
|
310
|
+
if (silent) {
|
|
311
|
+
this.setFocusedIdSilently(id);
|
|
312
|
+
} else {
|
|
313
|
+
this.focusNode(id);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
if (silent) {
|
|
317
|
+
this.markDirty();
|
|
318
|
+
} else {
|
|
319
|
+
this.notify();
|
|
280
320
|
}
|
|
281
|
-
this.notify();
|
|
282
321
|
}
|
|
283
322
|
unregisterNode(id) {
|
|
284
323
|
const node = this.nodes.get(id);
|
|
@@ -328,18 +367,18 @@ var FocusStore = class {
|
|
|
328
367
|
const oldFocusedId = this.focusedId;
|
|
329
368
|
if (oldFocusedId === id) return;
|
|
330
369
|
this.focusedId = id;
|
|
331
|
-
|
|
332
|
-
const wasAncestor = this.isAncestorOf(passiveId, oldFocusedId);
|
|
333
|
-
const isAncestor = this.isAncestorOf(passiveId, id);
|
|
334
|
-
if (wasAncestor && !isAncestor) {
|
|
335
|
-
this.passiveSet.delete(passiveId);
|
|
336
|
-
}
|
|
337
|
-
if (isAncestor && id !== passiveId) {
|
|
338
|
-
this.passiveSet.delete(passiveId);
|
|
339
|
-
}
|
|
340
|
-
}
|
|
370
|
+
this.clearPassiveOnFocusChange(oldFocusedId, id);
|
|
341
371
|
this.notify();
|
|
342
372
|
}
|
|
373
|
+
// Set focusedId without notifying subscribers. Safe to call during React's
|
|
374
|
+
// render phase. Clears passive flags the same way focusNode() does.
|
|
375
|
+
setFocusedIdSilently(id) {
|
|
376
|
+
const oldFocusedId = this.focusedId;
|
|
377
|
+
if (oldFocusedId === id) return;
|
|
378
|
+
this.focusedId = id;
|
|
379
|
+
this.clearPassiveOnFocusChange(oldFocusedId, id);
|
|
380
|
+
this.markDirty();
|
|
381
|
+
}
|
|
343
382
|
focusFirstChild(parentId) {
|
|
344
383
|
const parent = this.nodes.get(parentId);
|
|
345
384
|
if (parent && parent.childrenIds.length > 0) {
|
|
@@ -604,6 +643,38 @@ var FocusStore = class {
|
|
|
604
643
|
}
|
|
605
644
|
return false;
|
|
606
645
|
}
|
|
646
|
+
// Clear passive flags when focus transitions between nodes.
|
|
647
|
+
clearPassiveOnFocusChange(oldFocusedId, newFocusedId) {
|
|
648
|
+
for (const passiveId of this.passiveSet) {
|
|
649
|
+
const wasAncestor = this.isAncestorOf(passiveId, oldFocusedId);
|
|
650
|
+
const isAncestor = this.isAncestorOf(passiveId, newFocusedId);
|
|
651
|
+
if (wasAncestor && !isAncestor) {
|
|
652
|
+
this.passiveSet.delete(passiveId);
|
|
653
|
+
}
|
|
654
|
+
if (isAncestor && newFocusedId !== passiveId) {
|
|
655
|
+
this.passiveSet.delete(passiveId);
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
// Update the keyIndex when a node's focusKey changes during a re-render.
|
|
660
|
+
updateFocusKey(id, parentId, oldKey, newKey) {
|
|
661
|
+
if (!parentId) return;
|
|
662
|
+
if (oldKey) {
|
|
663
|
+
const parentKeys = this.keyIndex.get(parentId);
|
|
664
|
+
if (parentKeys) {
|
|
665
|
+
parentKeys.delete(oldKey);
|
|
666
|
+
if (parentKeys.size === 0) {
|
|
667
|
+
this.keyIndex.delete(parentId);
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
if (newKey) {
|
|
672
|
+
if (!this.keyIndex.has(parentId)) {
|
|
673
|
+
this.keyIndex.set(parentId, /* @__PURE__ */ new Map());
|
|
674
|
+
}
|
|
675
|
+
this.keyIndex.get(parentId).set(newKey, id);
|
|
676
|
+
}
|
|
677
|
+
}
|
|
607
678
|
};
|
|
608
679
|
|
|
609
680
|
export {
|
package/dist/index.js
CHANGED
|
@@ -14,7 +14,7 @@ import {
|
|
|
14
14
|
useKeybindingRegistry,
|
|
15
15
|
useKeybindings,
|
|
16
16
|
useStore
|
|
17
|
-
} from "./chunk-
|
|
17
|
+
} from "./chunk-HHDMTIXE.js";
|
|
18
18
|
import {
|
|
19
19
|
ThemeProvider,
|
|
20
20
|
useTheme
|
|
@@ -68,7 +68,7 @@ function Screen(_props) {
|
|
|
68
68
|
}
|
|
69
69
|
|
|
70
70
|
// src/core/router/ScreenEntry.tsx
|
|
71
|
-
import {
|
|
71
|
+
import { useLayoutEffect, useMemo, useRef as useRef2 } from "react";
|
|
72
72
|
import { Box as Box2 } from "ink";
|
|
73
73
|
|
|
74
74
|
// src/core/router/NavigationContext.tsx
|
|
@@ -99,7 +99,7 @@ function ScreenEntry({
|
|
|
99
99
|
const store = useStore();
|
|
100
100
|
const lastFocusedChildRef = useRef2(null);
|
|
101
101
|
const wasTopRef = useRef2(isTop);
|
|
102
|
-
|
|
102
|
+
useLayoutEffect(() => {
|
|
103
103
|
if (!wasTopRef.current && isTop) {
|
|
104
104
|
const saved = restoreFocus ? lastFocusedChildRef.current : null;
|
|
105
105
|
if (saved) {
|
package/dist/ui/index.d.ts
CHANGED
|
@@ -12,9 +12,10 @@ type CommandPaletteRenderProps = {
|
|
|
12
12
|
type CommandPaletteProps = {
|
|
13
13
|
onClose?: () => void;
|
|
14
14
|
interactive?: boolean;
|
|
15
|
+
maxVisible?: number;
|
|
15
16
|
render?: (props: CommandPaletteRenderProps) => React.ReactNode;
|
|
16
17
|
};
|
|
17
|
-
declare function CommandPalette({ onClose, interactive, render }: CommandPaletteProps): react_jsx_runtime.JSX.Element;
|
|
18
|
+
declare function CommandPalette({ onClose, interactive, maxVisible, render }: CommandPaletteProps): react_jsx_runtime.JSX.Element;
|
|
18
19
|
|
|
19
20
|
type TextInputRenderProps = {
|
|
20
21
|
value: string;
|
package/dist/ui/index.js
CHANGED
|
@@ -4,7 +4,7 @@ import {
|
|
|
4
4
|
useFocusNode,
|
|
5
5
|
useKeybindingRegistry,
|
|
6
6
|
useKeybindings
|
|
7
|
-
} from "../chunk-
|
|
7
|
+
} from "../chunk-HHDMTIXE.js";
|
|
8
8
|
import {
|
|
9
9
|
CodeBlock
|
|
10
10
|
} from "../chunk-SKSDNDQF.js";
|
|
@@ -13,9 +13,125 @@ import {
|
|
|
13
13
|
} from "../chunk-C77VBSPK.js";
|
|
14
14
|
|
|
15
15
|
// src/ui/CommandPalette.tsx
|
|
16
|
-
import { useState } from "react";
|
|
16
|
+
import { useState as useState2 } from "react";
|
|
17
|
+
import { Box as Box3, Text as Text2 } from "ink";
|
|
18
|
+
|
|
19
|
+
// src/ui/VirtualList.tsx
|
|
20
|
+
import React, { useEffect, useRef, useState } from "react";
|
|
21
|
+
import { Box as Box2 } from "ink";
|
|
22
|
+
|
|
23
|
+
// src/ui/Paginator.tsx
|
|
17
24
|
import { Box, Text } from "ink";
|
|
18
|
-
import {
|
|
25
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
26
|
+
function Paginator({ total, offset, visible, gap = 0, style = "arrows", position }) {
|
|
27
|
+
const theme = useTheme();
|
|
28
|
+
if (style === "none" || total <= visible) return null;
|
|
29
|
+
const hasAbove = offset > 0;
|
|
30
|
+
const hasBelow = offset + visible < total;
|
|
31
|
+
if (style === "arrows") {
|
|
32
|
+
if (position === "above" && hasAbove) return /* @__PURE__ */ jsx(Text, { color: theme.accentColor, children: "\u2191" });
|
|
33
|
+
if (position === "below" && hasBelow) return /* @__PURE__ */ jsx(Text, { color: theme.accentColor, children: "\u2193" });
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
if (style === "dots") {
|
|
37
|
+
if (position === "above") return null;
|
|
38
|
+
const totalPages = Math.ceil(total / visible);
|
|
39
|
+
const maxOffset = Math.max(1, total - visible);
|
|
40
|
+
const currentPage = Math.round(offset / maxOffset * (totalPages - 1));
|
|
41
|
+
return /* @__PURE__ */ jsx(Text, { children: Array.from({ length: totalPages }, (_, i) => /* @__PURE__ */ jsxs(Text, { dimColor: i !== currentPage, children: [
|
|
42
|
+
"\u25CF",
|
|
43
|
+
i < totalPages - 1 ? " " : ""
|
|
44
|
+
] }, i)) });
|
|
45
|
+
}
|
|
46
|
+
if (style === "counter") {
|
|
47
|
+
if (position === "above") return null;
|
|
48
|
+
return /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
|
|
49
|
+
offset + 1,
|
|
50
|
+
"\u2013",
|
|
51
|
+
Math.min(offset + visible, total),
|
|
52
|
+
" of ",
|
|
53
|
+
total
|
|
54
|
+
] });
|
|
55
|
+
}
|
|
56
|
+
const totalLines = visible + gap * (visible - 1);
|
|
57
|
+
const thumbSize = Math.max(1, Math.round(visible / total * totalLines));
|
|
58
|
+
const maxThumbOffset = totalLines - thumbSize;
|
|
59
|
+
const maxScrollOffset = total - visible;
|
|
60
|
+
const thumbOffset = maxScrollOffset === 0 ? 0 : Math.round(offset / maxScrollOffset * maxThumbOffset);
|
|
61
|
+
return /* @__PURE__ */ jsx(Box, { flexDirection: "column", children: Array.from({ length: totalLines }, (_, i) => {
|
|
62
|
+
const isThumb = i >= thumbOffset && i < thumbOffset + thumbSize;
|
|
63
|
+
return /* @__PURE__ */ jsx(Text, { color: isThumb ? theme.accentColor : void 0, dimColor: !isThumb, children: isThumb ? "\u2588" : "\u2502" }, i);
|
|
64
|
+
}) });
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// src/ui/VirtualList.tsx
|
|
68
|
+
import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
69
|
+
function VirtualList({
|
|
70
|
+
items,
|
|
71
|
+
highlightIndex,
|
|
72
|
+
scrollOffset: controlledOffset,
|
|
73
|
+
gap = 0,
|
|
74
|
+
maxVisible,
|
|
75
|
+
paginatorStyle = "dots",
|
|
76
|
+
render
|
|
77
|
+
}) {
|
|
78
|
+
const [internalOffset, setInternalOffset] = useState(0);
|
|
79
|
+
const offsetRef = useRef(0);
|
|
80
|
+
const windowed = maxVisible != null && items.length > maxVisible;
|
|
81
|
+
let offset = 0;
|
|
82
|
+
if (windowed) {
|
|
83
|
+
const maxOffset = Math.max(0, items.length - maxVisible);
|
|
84
|
+
if (controlledOffset != null) {
|
|
85
|
+
offset = Math.min(controlledOffset, maxOffset);
|
|
86
|
+
} else {
|
|
87
|
+
offset = Math.min(internalOffset, maxOffset);
|
|
88
|
+
if (highlightIndex != null && highlightIndex >= 0) {
|
|
89
|
+
if (highlightIndex < offset) {
|
|
90
|
+
offset = highlightIndex;
|
|
91
|
+
} else if (highlightIndex >= offset + maxVisible) {
|
|
92
|
+
offset = highlightIndex - maxVisible + 1;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
offsetRef.current = offset;
|
|
98
|
+
useEffect(() => {
|
|
99
|
+
if (windowed && offsetRef.current !== internalOffset) {
|
|
100
|
+
setInternalOffset(offsetRef.current);
|
|
101
|
+
}
|
|
102
|
+
}, [windowed, highlightIndex, controlledOffset, internalOffset]);
|
|
103
|
+
if (!windowed) {
|
|
104
|
+
return /* @__PURE__ */ jsx2(Box2, { flexDirection: "column", gap, children: items.map((item, index) => /* @__PURE__ */ jsx2(React.Fragment, { children: render({ item, index }) }, index)) });
|
|
105
|
+
}
|
|
106
|
+
const visible = items.slice(offset, offset + maxVisible);
|
|
107
|
+
const paginatorProps = {
|
|
108
|
+
total: items.length,
|
|
109
|
+
offset,
|
|
110
|
+
visible: maxVisible,
|
|
111
|
+
gap,
|
|
112
|
+
style: paginatorStyle
|
|
113
|
+
};
|
|
114
|
+
if (paginatorStyle === "scrollbar") {
|
|
115
|
+
return /* @__PURE__ */ jsxs2(Box2, { flexDirection: "row", gap: 1, children: [
|
|
116
|
+
/* @__PURE__ */ jsx2(Box2, { flexDirection: "column", gap, flexGrow: 1, children: visible.map((item, i) => {
|
|
117
|
+
const index = offset + i;
|
|
118
|
+
return /* @__PURE__ */ jsx2(React.Fragment, { children: render({ item, index }) }, index);
|
|
119
|
+
}) }),
|
|
120
|
+
/* @__PURE__ */ jsx2(Paginator, { ...paginatorProps })
|
|
121
|
+
] });
|
|
122
|
+
}
|
|
123
|
+
return /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", gap, children: [
|
|
124
|
+
/* @__PURE__ */ jsx2(Paginator, { ...paginatorProps, position: "above" }),
|
|
125
|
+
visible.map((item, i) => {
|
|
126
|
+
const index = offset + i;
|
|
127
|
+
return /* @__PURE__ */ jsx2(React.Fragment, { children: render({ item, index }) }, index);
|
|
128
|
+
}),
|
|
129
|
+
/* @__PURE__ */ jsx2(Paginator, { ...paginatorProps, position: "below" })
|
|
130
|
+
] });
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// src/ui/CommandPalette.tsx
|
|
134
|
+
import { Fragment, jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
|
|
19
135
|
var EMPTY_KEY = {
|
|
20
136
|
upArrow: false,
|
|
21
137
|
downArrow: false,
|
|
@@ -48,11 +164,15 @@ function fuzzyMatch(name, query) {
|
|
|
48
164
|
}
|
|
49
165
|
return qi === lowerQuery.length;
|
|
50
166
|
}
|
|
51
|
-
function Inner({
|
|
167
|
+
function Inner({
|
|
168
|
+
onClose,
|
|
169
|
+
maxVisible = 10,
|
|
170
|
+
render
|
|
171
|
+
}) {
|
|
52
172
|
const focus = useFocusNode();
|
|
53
173
|
const theme = useTheme();
|
|
54
|
-
const [query, setQuery] =
|
|
55
|
-
const [selectedIndex, setSelectedIndex] =
|
|
174
|
+
const [query, setQuery] = useState2("");
|
|
175
|
+
const [selectedIndex, setSelectedIndex] = useState2(0);
|
|
56
176
|
const registry = useKeybindingRegistry();
|
|
57
177
|
const named = registry.all.filter((cmd) => cmd.name != null);
|
|
58
178
|
const filtered = named.filter((cmd) => fuzzyMatch(cmd.name, query));
|
|
@@ -69,8 +189,8 @@ function Inner({ onClose, render }) {
|
|
|
69
189
|
const cmd = filtered[clampedIndex];
|
|
70
190
|
if (cmd) onSelect(cmd);
|
|
71
191
|
},
|
|
72
|
-
|
|
73
|
-
|
|
192
|
+
up: () => setSelectedIndex((i) => (i - 1 + filtered.length) % filtered.length),
|
|
193
|
+
down: () => setSelectedIndex((i) => (i + 1) % filtered.length),
|
|
74
194
|
backspace: () => {
|
|
75
195
|
setQuery((q) => q.slice(0, -1));
|
|
76
196
|
setSelectedIndex(0);
|
|
@@ -86,27 +206,37 @@ function Inner({ onClose, render }) {
|
|
|
86
206
|
}
|
|
87
207
|
);
|
|
88
208
|
if (render) {
|
|
89
|
-
return /* @__PURE__ */
|
|
209
|
+
return /* @__PURE__ */ jsx3(Fragment, { children: render({ query, filtered, selectedIndex: clampedIndex, onSelect }) });
|
|
90
210
|
}
|
|
91
|
-
return /* @__PURE__ */
|
|
92
|
-
|
|
93
|
-
/* @__PURE__ */
|
|
94
|
-
/* @__PURE__ */
|
|
95
|
-
/* @__PURE__ */
|
|
211
|
+
return /* @__PURE__ */ jsxs3(Box3, { flexDirection: "column", children: [
|
|
212
|
+
/* @__PURE__ */ jsxs3(Text2, { children: [
|
|
213
|
+
/* @__PURE__ */ jsx3(Text2, { dimColor: true, children: "> " }),
|
|
214
|
+
query.length > 0 ? /* @__PURE__ */ jsx3(Text2, { children: query }) : /* @__PURE__ */ jsx3(Text2, { dimColor: true, children: "Search commands\u2026" }),
|
|
215
|
+
/* @__PURE__ */ jsx3(Text2, { inverse: true, children: " " })
|
|
96
216
|
] }),
|
|
97
|
-
filtered.length === 0 ? /* @__PURE__ */
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
217
|
+
/* @__PURE__ */ jsx3(Box3, { flexDirection: "column", marginTop: 1, children: filtered.length === 0 ? /* @__PURE__ */ jsx3(Text2, { dimColor: true, children: "No commands found" }) : /* @__PURE__ */ jsx3(
|
|
218
|
+
VirtualList,
|
|
219
|
+
{
|
|
220
|
+
items: filtered,
|
|
221
|
+
highlightIndex: clampedIndex,
|
|
222
|
+
maxVisible,
|
|
223
|
+
paginatorStyle: "scrollbar",
|
|
224
|
+
render: ({ item: cmd, index }) => {
|
|
225
|
+
const highlighted = index === clampedIndex;
|
|
226
|
+
const keyColor = highlighted ? theme.hintHighlightColor : theme.hintColor;
|
|
227
|
+
const labelColor = highlighted ? theme.hintHighlightDimColor : theme.hintDimColor;
|
|
228
|
+
return /* @__PURE__ */ jsxs3(Text2, { children: [
|
|
229
|
+
/* @__PURE__ */ jsx3(Text2, { dimColor: true, children: highlighted ? theme.indicator + " " : " " }),
|
|
230
|
+
/* @__PURE__ */ jsx3(Text2, { color: keyColor, bold: true, children: cmd.key }),
|
|
231
|
+
/* @__PURE__ */ jsxs3(Text2, { color: labelColor, children: [
|
|
232
|
+
" ",
|
|
233
|
+
cmd.name
|
|
234
|
+
] })
|
|
235
|
+
] });
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
) }),
|
|
239
|
+
/* @__PURE__ */ jsx3(Box3, { marginTop: 1, children: /* @__PURE__ */ jsx3(Text2, { dimColor: true, children: "\u2191\u2193 navigate \xB7 \u21B5 run \xB7 esc close" }) })
|
|
110
240
|
] });
|
|
111
241
|
}
|
|
112
242
|
function HintsBar() {
|
|
@@ -114,31 +244,31 @@ function HintsBar() {
|
|
|
114
244
|
const theme = useTheme();
|
|
115
245
|
const commands = registry.available.filter((cmd) => cmd.name != null);
|
|
116
246
|
if (commands.length === 0) return null;
|
|
117
|
-
return /* @__PURE__ */
|
|
118
|
-
/* @__PURE__ */
|
|
119
|
-
/* @__PURE__ */
|
|
247
|
+
return /* @__PURE__ */ jsx3(Box3, { flexWrap: "wrap", children: commands.map((cmd, index) => /* @__PURE__ */ jsxs3(Text2, { children: [
|
|
248
|
+
/* @__PURE__ */ jsx3(Text2, { color: theme.hintColor, bold: true, children: cmd.key }),
|
|
249
|
+
/* @__PURE__ */ jsxs3(Text2, { color: theme.hintDimColor, children: [
|
|
120
250
|
" ",
|
|
121
251
|
cmd.name
|
|
122
252
|
] }),
|
|
123
|
-
index < commands.length - 1 && /* @__PURE__ */
|
|
253
|
+
index < commands.length - 1 && /* @__PURE__ */ jsx3(Text2, { color: theme.hintDimColor, children: " \u2022 " })
|
|
124
254
|
] }, `${cmd.nodeId}-${cmd.key}`)) });
|
|
125
255
|
}
|
|
126
256
|
var noop = () => {
|
|
127
257
|
};
|
|
128
|
-
function CommandPalette({ onClose, interactive = true, render }) {
|
|
258
|
+
function CommandPalette({ onClose, interactive = true, maxVisible, render }) {
|
|
129
259
|
if (!interactive) {
|
|
130
|
-
return /* @__PURE__ */
|
|
260
|
+
return /* @__PURE__ */ jsx3(HintsBar, {});
|
|
131
261
|
}
|
|
132
|
-
return /* @__PURE__ */
|
|
262
|
+
return /* @__PURE__ */ jsx3(FocusTrap, { children: /* @__PURE__ */ jsx3(Inner, { onClose: onClose ?? noop, maxVisible, render }) });
|
|
133
263
|
}
|
|
134
264
|
|
|
135
265
|
// src/ui/TextInput.tsx
|
|
136
|
-
import { useReducer, useRef } from "react";
|
|
137
|
-
import { Text as
|
|
138
|
-
import { Fragment as Fragment2, jsx as
|
|
266
|
+
import { useReducer, useRef as useRef2 } from "react";
|
|
267
|
+
import { Text as Text3 } from "ink";
|
|
268
|
+
import { Fragment as Fragment2, jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
|
|
139
269
|
function TextInput({ label, value, onChange, onSubmit, placeholder, render, focusKey }) {
|
|
140
270
|
const focus = useFocusNode({ focusKey });
|
|
141
|
-
const cursorRef =
|
|
271
|
+
const cursorRef = useRef2(value.length);
|
|
142
272
|
const [, forceRender] = useReducer((c) => c + 1, 0);
|
|
143
273
|
const cursor = Math.min(cursorRef.current, value.length);
|
|
144
274
|
cursorRef.current = cursor;
|
|
@@ -191,7 +321,7 @@ function TextInput({ label, value, onChange, onSubmit, placeholder, render, focu
|
|
|
191
321
|
const cursorChar = value[cursor] ?? " ";
|
|
192
322
|
const after = value.slice(cursor + 1);
|
|
193
323
|
if (render) {
|
|
194
|
-
return /* @__PURE__ */
|
|
324
|
+
return /* @__PURE__ */ jsx4(Fragment2, { children: render({ value, focused: focus.hasFocus, before, cursorChar, after, placeholder }) });
|
|
195
325
|
}
|
|
196
326
|
const displayValue = value.length > 0 ? value : placeholder ?? "";
|
|
197
327
|
const isPlaceholder = value.length === 0;
|
|
@@ -199,144 +329,28 @@ function TextInput({ label, value, onChange, onSubmit, placeholder, render, focu
|
|
|
199
329
|
const isPlaceholderVisible = value.length === 0 && placeholder != null;
|
|
200
330
|
const activeCursorChar = isPlaceholderVisible ? placeholder[0] ?? " " : cursorChar;
|
|
201
331
|
const activeAfter = isPlaceholderVisible ? placeholder.slice(1) : after;
|
|
202
|
-
return /* @__PURE__ */
|
|
203
|
-
label != null && /* @__PURE__ */
|
|
332
|
+
return /* @__PURE__ */ jsxs4(Text3, { children: [
|
|
333
|
+
label != null && /* @__PURE__ */ jsxs4(Text3, { bold: true, children: [
|
|
204
334
|
label,
|
|
205
335
|
" "
|
|
206
336
|
] }),
|
|
207
337
|
before,
|
|
208
|
-
/* @__PURE__ */
|
|
209
|
-
isPlaceholderVisible ? /* @__PURE__ */
|
|
338
|
+
/* @__PURE__ */ jsx4(Text3, { inverse: true, children: activeCursorChar }),
|
|
339
|
+
isPlaceholderVisible ? /* @__PURE__ */ jsx4(Text3, { dimColor: true, children: activeAfter }) : activeAfter
|
|
210
340
|
] });
|
|
211
341
|
}
|
|
212
|
-
return /* @__PURE__ */
|
|
213
|
-
label != null && /* @__PURE__ */
|
|
342
|
+
return /* @__PURE__ */ jsxs4(Text3, { dimColor: true, children: [
|
|
343
|
+
label != null && /* @__PURE__ */ jsxs4(Text3, { children: [
|
|
214
344
|
label,
|
|
215
345
|
" "
|
|
216
346
|
] }),
|
|
217
|
-
isPlaceholder ? /* @__PURE__ */
|
|
347
|
+
isPlaceholder ? /* @__PURE__ */ jsx4(Text3, { dimColor: true, children: displayValue }) : displayValue
|
|
218
348
|
] });
|
|
219
349
|
}
|
|
220
350
|
|
|
221
351
|
// src/ui/Select.tsx
|
|
222
352
|
import React3, { useEffect as useEffect2, useState as useState3 } from "react";
|
|
223
353
|
import { Box as Box4, Text as Text4 } from "ink";
|
|
224
|
-
|
|
225
|
-
// src/ui/VirtualList.tsx
|
|
226
|
-
import React2, { useEffect, useRef as useRef2, useState as useState2 } from "react";
|
|
227
|
-
import { Box as Box3 } from "ink";
|
|
228
|
-
|
|
229
|
-
// src/ui/Paginator.tsx
|
|
230
|
-
import { Box as Box2, Text as Text3 } from "ink";
|
|
231
|
-
import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
|
|
232
|
-
function Paginator({ total, offset, visible, gap = 0, style = "arrows", position }) {
|
|
233
|
-
const theme = useTheme();
|
|
234
|
-
if (style === "none" || total <= visible) return null;
|
|
235
|
-
const hasAbove = offset > 0;
|
|
236
|
-
const hasBelow = offset + visible < total;
|
|
237
|
-
if (style === "arrows") {
|
|
238
|
-
if (position === "above" && hasAbove) return /* @__PURE__ */ jsx3(Text3, { color: theme.accentColor, children: "\u2191" });
|
|
239
|
-
if (position === "below" && hasBelow) return /* @__PURE__ */ jsx3(Text3, { color: theme.accentColor, children: "\u2193" });
|
|
240
|
-
return null;
|
|
241
|
-
}
|
|
242
|
-
if (style === "dots") {
|
|
243
|
-
if (position === "above") return null;
|
|
244
|
-
const totalPages = Math.ceil(total / visible);
|
|
245
|
-
const maxOffset = Math.max(1, total - visible);
|
|
246
|
-
const currentPage = Math.round(offset / maxOffset * (totalPages - 1));
|
|
247
|
-
return /* @__PURE__ */ jsx3(Text3, { children: Array.from({ length: totalPages }, (_, i) => /* @__PURE__ */ jsxs3(Text3, { dimColor: i !== currentPage, children: [
|
|
248
|
-
"\u25CF",
|
|
249
|
-
i < totalPages - 1 ? " " : ""
|
|
250
|
-
] }, i)) });
|
|
251
|
-
}
|
|
252
|
-
if (style === "counter") {
|
|
253
|
-
if (position === "above") return null;
|
|
254
|
-
return /* @__PURE__ */ jsxs3(Text3, { dimColor: true, children: [
|
|
255
|
-
offset + 1,
|
|
256
|
-
"\u2013",
|
|
257
|
-
Math.min(offset + visible, total),
|
|
258
|
-
" of ",
|
|
259
|
-
total
|
|
260
|
-
] });
|
|
261
|
-
}
|
|
262
|
-
const totalLines = visible + gap * (visible - 1);
|
|
263
|
-
const thumbSize = Math.max(1, Math.round(visible / total * totalLines));
|
|
264
|
-
const maxThumbOffset = totalLines - thumbSize;
|
|
265
|
-
const maxScrollOffset = total - visible;
|
|
266
|
-
const thumbOffset = maxScrollOffset === 0 ? 0 : Math.round(offset / maxScrollOffset * maxThumbOffset);
|
|
267
|
-
return /* @__PURE__ */ jsx3(Box2, { flexDirection: "column", children: Array.from({ length: totalLines }, (_, i) => {
|
|
268
|
-
const isThumb = i >= thumbOffset && i < thumbOffset + thumbSize;
|
|
269
|
-
return /* @__PURE__ */ jsx3(Text3, { color: isThumb ? theme.accentColor : void 0, dimColor: !isThumb, children: isThumb ? "\u2588" : "\u2502" }, i);
|
|
270
|
-
}) });
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
// src/ui/VirtualList.tsx
|
|
274
|
-
import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
|
|
275
|
-
function VirtualList({
|
|
276
|
-
items,
|
|
277
|
-
highlightIndex,
|
|
278
|
-
scrollOffset: controlledOffset,
|
|
279
|
-
gap = 0,
|
|
280
|
-
maxVisible,
|
|
281
|
-
paginatorStyle = "dots",
|
|
282
|
-
render
|
|
283
|
-
}) {
|
|
284
|
-
const [internalOffset, setInternalOffset] = useState2(0);
|
|
285
|
-
const offsetRef = useRef2(0);
|
|
286
|
-
const windowed = maxVisible != null && items.length > maxVisible;
|
|
287
|
-
let offset = 0;
|
|
288
|
-
if (windowed) {
|
|
289
|
-
const maxOffset = Math.max(0, items.length - maxVisible);
|
|
290
|
-
if (controlledOffset != null) {
|
|
291
|
-
offset = Math.min(controlledOffset, maxOffset);
|
|
292
|
-
} else {
|
|
293
|
-
offset = Math.min(internalOffset, maxOffset);
|
|
294
|
-
if (highlightIndex != null && highlightIndex >= 0) {
|
|
295
|
-
if (highlightIndex < offset) {
|
|
296
|
-
offset = highlightIndex;
|
|
297
|
-
} else if (highlightIndex >= offset + maxVisible) {
|
|
298
|
-
offset = highlightIndex - maxVisible + 1;
|
|
299
|
-
}
|
|
300
|
-
}
|
|
301
|
-
}
|
|
302
|
-
}
|
|
303
|
-
offsetRef.current = offset;
|
|
304
|
-
useEffect(() => {
|
|
305
|
-
if (windowed && offsetRef.current !== internalOffset) {
|
|
306
|
-
setInternalOffset(offsetRef.current);
|
|
307
|
-
}
|
|
308
|
-
}, [windowed, highlightIndex, controlledOffset, internalOffset]);
|
|
309
|
-
if (!windowed) {
|
|
310
|
-
return /* @__PURE__ */ jsx4(Box3, { flexDirection: "column", gap, children: items.map((item, index) => /* @__PURE__ */ jsx4(React2.Fragment, { children: render({ item, index }) }, index)) });
|
|
311
|
-
}
|
|
312
|
-
const visible = items.slice(offset, offset + maxVisible);
|
|
313
|
-
const paginatorProps = {
|
|
314
|
-
total: items.length,
|
|
315
|
-
offset,
|
|
316
|
-
visible: maxVisible,
|
|
317
|
-
gap,
|
|
318
|
-
style: paginatorStyle
|
|
319
|
-
};
|
|
320
|
-
if (paginatorStyle === "scrollbar") {
|
|
321
|
-
return /* @__PURE__ */ jsxs4(Box3, { flexDirection: "row", gap: 1, children: [
|
|
322
|
-
/* @__PURE__ */ jsx4(Box3, { flexDirection: "column", gap, flexGrow: 1, children: visible.map((item, i) => {
|
|
323
|
-
const index = offset + i;
|
|
324
|
-
return /* @__PURE__ */ jsx4(React2.Fragment, { children: render({ item, index }) }, index);
|
|
325
|
-
}) }),
|
|
326
|
-
/* @__PURE__ */ jsx4(Paginator, { ...paginatorProps })
|
|
327
|
-
] });
|
|
328
|
-
}
|
|
329
|
-
return /* @__PURE__ */ jsxs4(Box3, { flexDirection: "column", gap, children: [
|
|
330
|
-
/* @__PURE__ */ jsx4(Paginator, { ...paginatorProps, position: "above" }),
|
|
331
|
-
visible.map((item, i) => {
|
|
332
|
-
const index = offset + i;
|
|
333
|
-
return /* @__PURE__ */ jsx4(React2.Fragment, { children: render({ item, index }) }, index);
|
|
334
|
-
}),
|
|
335
|
-
/* @__PURE__ */ jsx4(Paginator, { ...paginatorProps, position: "below" })
|
|
336
|
-
] });
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
// src/ui/Select.tsx
|
|
340
354
|
import { jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
|
|
341
355
|
function Select({
|
|
342
356
|
options,
|