giggles 0.7.0 → 0.7.2
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 +3 -1
- package/dist/ui/index.js +158 -156
- 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
|
@@ -5,6 +5,7 @@ import { BoxProps } from 'ink';
|
|
|
5
5
|
|
|
6
6
|
type CommandPaletteRenderProps = {
|
|
7
7
|
query: string;
|
|
8
|
+
onChange: (query: string) => void;
|
|
8
9
|
filtered: RegisteredKeybinding[];
|
|
9
10
|
selectedIndex: number;
|
|
10
11
|
onSelect: (cmd: RegisteredKeybinding) => void;
|
|
@@ -12,9 +13,10 @@ type CommandPaletteRenderProps = {
|
|
|
12
13
|
type CommandPaletteProps = {
|
|
13
14
|
onClose?: () => void;
|
|
14
15
|
interactive?: boolean;
|
|
16
|
+
maxVisible?: number;
|
|
15
17
|
render?: (props: CommandPaletteRenderProps) => React.ReactNode;
|
|
16
18
|
};
|
|
17
|
-
declare function CommandPalette({ onClose, interactive, render }: CommandPaletteProps): react_jsx_runtime.JSX.Element;
|
|
19
|
+
declare function CommandPalette({ onClose, interactive, maxVisible, render }: CommandPaletteProps): react_jsx_runtime.JSX.Element;
|
|
18
20
|
|
|
19
21
|
type TextInputRenderProps = {
|
|
20
22
|
value: string;
|
package/dist/ui/index.js
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import {
|
|
2
|
+
FocusScope,
|
|
2
3
|
FocusTrap,
|
|
3
4
|
GigglesError,
|
|
4
5
|
useFocusNode,
|
|
6
|
+
useFocusScope,
|
|
5
7
|
useKeybindingRegistry,
|
|
6
8
|
useKeybindings
|
|
7
|
-
} from "../chunk-
|
|
9
|
+
} from "../chunk-HHDMTIXE.js";
|
|
8
10
|
import {
|
|
9
11
|
CodeBlock
|
|
10
12
|
} from "../chunk-SKSDNDQF.js";
|
|
@@ -13,129 +15,13 @@ import {
|
|
|
13
15
|
} from "../chunk-C77VBSPK.js";
|
|
14
16
|
|
|
15
17
|
// src/ui/CommandPalette.tsx
|
|
16
|
-
import { useState } from "react";
|
|
17
|
-
import { Box, Text } from "ink";
|
|
18
|
-
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
19
|
-
var EMPTY_KEY = {
|
|
20
|
-
upArrow: false,
|
|
21
|
-
downArrow: false,
|
|
22
|
-
leftArrow: false,
|
|
23
|
-
rightArrow: false,
|
|
24
|
-
pageDown: false,
|
|
25
|
-
pageUp: false,
|
|
26
|
-
home: false,
|
|
27
|
-
end: false,
|
|
28
|
-
return: false,
|
|
29
|
-
escape: false,
|
|
30
|
-
ctrl: false,
|
|
31
|
-
shift: false,
|
|
32
|
-
tab: false,
|
|
33
|
-
backspace: false,
|
|
34
|
-
delete: false,
|
|
35
|
-
meta: false,
|
|
36
|
-
super: false,
|
|
37
|
-
hyper: false,
|
|
38
|
-
capsLock: false,
|
|
39
|
-
numLock: false
|
|
40
|
-
};
|
|
41
|
-
function fuzzyMatch(name, query) {
|
|
42
|
-
if (!query) return true;
|
|
43
|
-
const lowerName = name.toLowerCase();
|
|
44
|
-
const lowerQuery = query.toLowerCase();
|
|
45
|
-
let qi = 0;
|
|
46
|
-
for (let i = 0; i < lowerName.length && qi < lowerQuery.length; i++) {
|
|
47
|
-
if (lowerName[i] === lowerQuery[qi]) qi++;
|
|
48
|
-
}
|
|
49
|
-
return qi === lowerQuery.length;
|
|
50
|
-
}
|
|
51
|
-
function Inner({ onClose, render }) {
|
|
52
|
-
const focus = useFocusNode();
|
|
53
|
-
const theme = useTheme();
|
|
54
|
-
const [query, setQuery] = useState("");
|
|
55
|
-
const [selectedIndex, setSelectedIndex] = useState(0);
|
|
56
|
-
const registry = useKeybindingRegistry();
|
|
57
|
-
const named = registry.all.filter((cmd) => cmd.name != null);
|
|
58
|
-
const filtered = named.filter((cmd) => fuzzyMatch(cmd.name, query));
|
|
59
|
-
const clampedIndex = Math.min(selectedIndex, Math.max(0, filtered.length - 1));
|
|
60
|
-
const onSelect = (cmd) => {
|
|
61
|
-
cmd.handler("", EMPTY_KEY);
|
|
62
|
-
onClose();
|
|
63
|
-
};
|
|
64
|
-
useKeybindings(
|
|
65
|
-
focus,
|
|
66
|
-
{
|
|
67
|
-
escape: onClose,
|
|
68
|
-
enter: () => {
|
|
69
|
-
const cmd = filtered[clampedIndex];
|
|
70
|
-
if (cmd) onSelect(cmd);
|
|
71
|
-
},
|
|
72
|
-
left: () => setSelectedIndex((i) => (i - 1 + filtered.length) % filtered.length),
|
|
73
|
-
right: () => setSelectedIndex((i) => (i + 1) % filtered.length),
|
|
74
|
-
backspace: () => {
|
|
75
|
-
setQuery((q) => q.slice(0, -1));
|
|
76
|
-
setSelectedIndex(0);
|
|
77
|
-
}
|
|
78
|
-
},
|
|
79
|
-
{
|
|
80
|
-
fallback: (input, key) => {
|
|
81
|
-
if (input.length === 1 && !key.ctrl) {
|
|
82
|
-
setQuery((q) => q + input);
|
|
83
|
-
setSelectedIndex(0);
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
);
|
|
88
|
-
if (render) {
|
|
89
|
-
return /* @__PURE__ */ jsx(Fragment, { children: render({ query, filtered, selectedIndex: clampedIndex, onSelect }) });
|
|
90
|
-
}
|
|
91
|
-
return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", children: [
|
|
92
|
-
query.length > 0 && /* @__PURE__ */ jsxs(Text, { children: [
|
|
93
|
-
/* @__PURE__ */ jsx(Text, { dimColor: true, children: "> " }),
|
|
94
|
-
/* @__PURE__ */ jsx(Text, { children: query }),
|
|
95
|
-
/* @__PURE__ */ jsx(Text, { inverse: true, children: " " })
|
|
96
|
-
] }),
|
|
97
|
-
filtered.length === 0 ? /* @__PURE__ */ jsx(Text, { dimColor: true, children: "No commands found" }) : /* @__PURE__ */ jsx(Box, { flexWrap: "wrap", children: filtered.map((cmd, index) => {
|
|
98
|
-
const highlighted = index === clampedIndex;
|
|
99
|
-
const keyColor = highlighted ? theme.hintHighlightColor : theme.hintColor;
|
|
100
|
-
const labelColor = highlighted ? theme.hintHighlightDimColor : theme.hintDimColor;
|
|
101
|
-
return /* @__PURE__ */ jsxs(Text, { children: [
|
|
102
|
-
/* @__PURE__ */ jsx(Text, { color: keyColor, bold: true, children: cmd.key }),
|
|
103
|
-
/* @__PURE__ */ jsxs(Text, { color: labelColor, children: [
|
|
104
|
-
" ",
|
|
105
|
-
cmd.name
|
|
106
|
-
] }),
|
|
107
|
-
index < filtered.length - 1 && /* @__PURE__ */ jsx(Text, { color: theme.hintDimColor, children: " \u2022 " })
|
|
108
|
-
] }, `${cmd.nodeId}-${cmd.key}`);
|
|
109
|
-
}) })
|
|
110
|
-
] });
|
|
111
|
-
}
|
|
112
|
-
function HintsBar() {
|
|
113
|
-
const registry = useKeybindingRegistry();
|
|
114
|
-
const theme = useTheme();
|
|
115
|
-
const commands = registry.available.filter((cmd) => cmd.name != null);
|
|
116
|
-
if (commands.length === 0) return null;
|
|
117
|
-
return /* @__PURE__ */ jsx(Box, { flexWrap: "wrap", children: commands.map((cmd, index) => /* @__PURE__ */ jsxs(Text, { children: [
|
|
118
|
-
/* @__PURE__ */ jsx(Text, { color: theme.hintColor, bold: true, children: cmd.key }),
|
|
119
|
-
/* @__PURE__ */ jsxs(Text, { color: theme.hintDimColor, children: [
|
|
120
|
-
" ",
|
|
121
|
-
cmd.name
|
|
122
|
-
] }),
|
|
123
|
-
index < commands.length - 1 && /* @__PURE__ */ jsx(Text, { color: theme.hintDimColor, children: " \u2022 " })
|
|
124
|
-
] }, `${cmd.nodeId}-${cmd.key}`)) });
|
|
125
|
-
}
|
|
126
|
-
var noop = () => {
|
|
127
|
-
};
|
|
128
|
-
function CommandPalette({ onClose, interactive = true, render }) {
|
|
129
|
-
if (!interactive) {
|
|
130
|
-
return /* @__PURE__ */ jsx(HintsBar, {});
|
|
131
|
-
}
|
|
132
|
-
return /* @__PURE__ */ jsx(FocusTrap, { children: /* @__PURE__ */ jsx(Inner, { onClose: onClose ?? noop, render }) });
|
|
133
|
-
}
|
|
18
|
+
import { useState as useState2 } from "react";
|
|
19
|
+
import { Box as Box3, Text as Text3 } from "ink";
|
|
134
20
|
|
|
135
21
|
// src/ui/TextInput.tsx
|
|
136
22
|
import { useReducer, useRef } from "react";
|
|
137
|
-
import { Text
|
|
138
|
-
import { Fragment
|
|
23
|
+
import { Text } from "ink";
|
|
24
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
139
25
|
function TextInput({ label, value, onChange, onSubmit, placeholder, render, focusKey }) {
|
|
140
26
|
const focus = useFocusNode({ focusKey });
|
|
141
27
|
const cursorRef = useRef(value.length);
|
|
@@ -191,7 +77,7 @@ function TextInput({ label, value, onChange, onSubmit, placeholder, render, focu
|
|
|
191
77
|
const cursorChar = value[cursor] ?? " ";
|
|
192
78
|
const after = value.slice(cursor + 1);
|
|
193
79
|
if (render) {
|
|
194
|
-
return /* @__PURE__ */
|
|
80
|
+
return /* @__PURE__ */ jsx(Fragment, { children: render({ value, focused: focus.hasFocus, before, cursorChar, after, placeholder }) });
|
|
195
81
|
}
|
|
196
82
|
const displayValue = value.length > 0 ? value : placeholder ?? "";
|
|
197
83
|
const isPlaceholder = value.length === 0;
|
|
@@ -199,44 +85,40 @@ function TextInput({ label, value, onChange, onSubmit, placeholder, render, focu
|
|
|
199
85
|
const isPlaceholderVisible = value.length === 0 && placeholder != null;
|
|
200
86
|
const activeCursorChar = isPlaceholderVisible ? placeholder[0] ?? " " : cursorChar;
|
|
201
87
|
const activeAfter = isPlaceholderVisible ? placeholder.slice(1) : after;
|
|
202
|
-
return /* @__PURE__ */
|
|
203
|
-
label != null && /* @__PURE__ */
|
|
88
|
+
return /* @__PURE__ */ jsxs(Text, { children: [
|
|
89
|
+
label != null && /* @__PURE__ */ jsxs(Text, { bold: true, children: [
|
|
204
90
|
label,
|
|
205
91
|
" "
|
|
206
92
|
] }),
|
|
207
93
|
before,
|
|
208
|
-
/* @__PURE__ */
|
|
209
|
-
isPlaceholderVisible ? /* @__PURE__ */
|
|
94
|
+
/* @__PURE__ */ jsx(Text, { inverse: true, children: activeCursorChar }),
|
|
95
|
+
isPlaceholderVisible ? /* @__PURE__ */ jsx(Text, { dimColor: true, children: activeAfter }) : activeAfter
|
|
210
96
|
] });
|
|
211
97
|
}
|
|
212
|
-
return /* @__PURE__ */
|
|
213
|
-
label != null && /* @__PURE__ */
|
|
98
|
+
return /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
|
|
99
|
+
label != null && /* @__PURE__ */ jsxs(Text, { children: [
|
|
214
100
|
label,
|
|
215
101
|
" "
|
|
216
102
|
] }),
|
|
217
|
-
isPlaceholder ? /* @__PURE__ */
|
|
103
|
+
isPlaceholder ? /* @__PURE__ */ jsx(Text, { dimColor: true, children: displayValue }) : displayValue
|
|
218
104
|
] });
|
|
219
105
|
}
|
|
220
106
|
|
|
221
|
-
// src/ui/Select.tsx
|
|
222
|
-
import React3, { useEffect as useEffect2, useState as useState3 } from "react";
|
|
223
|
-
import { Box as Box4, Text as Text4 } from "ink";
|
|
224
|
-
|
|
225
107
|
// src/ui/VirtualList.tsx
|
|
226
|
-
import React2, { useEffect, useRef as useRef2, useState
|
|
227
|
-
import { Box as
|
|
108
|
+
import React2, { useEffect, useRef as useRef2, useState } from "react";
|
|
109
|
+
import { Box as Box2 } from "ink";
|
|
228
110
|
|
|
229
111
|
// src/ui/Paginator.tsx
|
|
230
|
-
import { Box
|
|
231
|
-
import { jsx as
|
|
112
|
+
import { Box, Text as Text2 } from "ink";
|
|
113
|
+
import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
232
114
|
function Paginator({ total, offset, visible, gap = 0, style = "arrows", position }) {
|
|
233
115
|
const theme = useTheme();
|
|
234
116
|
if (style === "none" || total <= visible) return null;
|
|
235
117
|
const hasAbove = offset > 0;
|
|
236
118
|
const hasBelow = offset + visible < total;
|
|
237
119
|
if (style === "arrows") {
|
|
238
|
-
if (position === "above" && hasAbove) return /* @__PURE__ */
|
|
239
|
-
if (position === "below" && hasBelow) return /* @__PURE__ */
|
|
120
|
+
if (position === "above" && hasAbove) return /* @__PURE__ */ jsx2(Text2, { color: theme.accentColor, children: "\u2191" });
|
|
121
|
+
if (position === "below" && hasBelow) return /* @__PURE__ */ jsx2(Text2, { color: theme.accentColor, children: "\u2193" });
|
|
240
122
|
return null;
|
|
241
123
|
}
|
|
242
124
|
if (style === "dots") {
|
|
@@ -244,14 +126,14 @@ function Paginator({ total, offset, visible, gap = 0, style = "arrows", position
|
|
|
244
126
|
const totalPages = Math.ceil(total / visible);
|
|
245
127
|
const maxOffset = Math.max(1, total - visible);
|
|
246
128
|
const currentPage = Math.round(offset / maxOffset * (totalPages - 1));
|
|
247
|
-
return /* @__PURE__ */
|
|
129
|
+
return /* @__PURE__ */ jsx2(Text2, { children: Array.from({ length: totalPages }, (_, i) => /* @__PURE__ */ jsxs2(Text2, { dimColor: i !== currentPage, children: [
|
|
248
130
|
"\u25CF",
|
|
249
131
|
i < totalPages - 1 ? " " : ""
|
|
250
132
|
] }, i)) });
|
|
251
133
|
}
|
|
252
134
|
if (style === "counter") {
|
|
253
135
|
if (position === "above") return null;
|
|
254
|
-
return /* @__PURE__ */
|
|
136
|
+
return /* @__PURE__ */ jsxs2(Text2, { dimColor: true, children: [
|
|
255
137
|
offset + 1,
|
|
256
138
|
"\u2013",
|
|
257
139
|
Math.min(offset + visible, total),
|
|
@@ -264,14 +146,14 @@ function Paginator({ total, offset, visible, gap = 0, style = "arrows", position
|
|
|
264
146
|
const maxThumbOffset = totalLines - thumbSize;
|
|
265
147
|
const maxScrollOffset = total - visible;
|
|
266
148
|
const thumbOffset = maxScrollOffset === 0 ? 0 : Math.round(offset / maxScrollOffset * maxThumbOffset);
|
|
267
|
-
return /* @__PURE__ */
|
|
149
|
+
return /* @__PURE__ */ jsx2(Box, { flexDirection: "column", children: Array.from({ length: totalLines }, (_, i) => {
|
|
268
150
|
const isThumb = i >= thumbOffset && i < thumbOffset + thumbSize;
|
|
269
|
-
return /* @__PURE__ */
|
|
151
|
+
return /* @__PURE__ */ jsx2(Text2, { color: isThumb ? theme.accentColor : void 0, dimColor: !isThumb, children: isThumb ? "\u2588" : "\u2502" }, i);
|
|
270
152
|
}) });
|
|
271
153
|
}
|
|
272
154
|
|
|
273
155
|
// src/ui/VirtualList.tsx
|
|
274
|
-
import { jsx as
|
|
156
|
+
import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
|
|
275
157
|
function VirtualList({
|
|
276
158
|
items,
|
|
277
159
|
highlightIndex,
|
|
@@ -281,7 +163,7 @@ function VirtualList({
|
|
|
281
163
|
paginatorStyle = "dots",
|
|
282
164
|
render
|
|
283
165
|
}) {
|
|
284
|
-
const [internalOffset, setInternalOffset] =
|
|
166
|
+
const [internalOffset, setInternalOffset] = useState(0);
|
|
285
167
|
const offsetRef = useRef2(0);
|
|
286
168
|
const windowed = maxVisible != null && items.length > maxVisible;
|
|
287
169
|
let offset = 0;
|
|
@@ -307,7 +189,7 @@ function VirtualList({
|
|
|
307
189
|
}
|
|
308
190
|
}, [windowed, highlightIndex, controlledOffset, internalOffset]);
|
|
309
191
|
if (!windowed) {
|
|
310
|
-
return /* @__PURE__ */
|
|
192
|
+
return /* @__PURE__ */ jsx3(Box2, { flexDirection: "column", gap, children: items.map((item, index) => /* @__PURE__ */ jsx3(React2.Fragment, { children: render({ item, index }) }, index)) });
|
|
311
193
|
}
|
|
312
194
|
const visible = items.slice(offset, offset + maxVisible);
|
|
313
195
|
const paginatorProps = {
|
|
@@ -318,25 +200,145 @@ function VirtualList({
|
|
|
318
200
|
style: paginatorStyle
|
|
319
201
|
};
|
|
320
202
|
if (paginatorStyle === "scrollbar") {
|
|
321
|
-
return /* @__PURE__ */
|
|
322
|
-
/* @__PURE__ */
|
|
203
|
+
return /* @__PURE__ */ jsxs3(Box2, { flexDirection: "row", gap: 1, children: [
|
|
204
|
+
/* @__PURE__ */ jsx3(Box2, { flexDirection: "column", gap, flexGrow: 1, children: visible.map((item, i) => {
|
|
323
205
|
const index = offset + i;
|
|
324
|
-
return /* @__PURE__ */
|
|
206
|
+
return /* @__PURE__ */ jsx3(React2.Fragment, { children: render({ item, index }) }, index);
|
|
325
207
|
}) }),
|
|
326
|
-
/* @__PURE__ */
|
|
208
|
+
/* @__PURE__ */ jsx3(Paginator, { ...paginatorProps })
|
|
327
209
|
] });
|
|
328
210
|
}
|
|
329
|
-
return /* @__PURE__ */
|
|
330
|
-
/* @__PURE__ */
|
|
211
|
+
return /* @__PURE__ */ jsxs3(Box2, { flexDirection: "column", gap, children: [
|
|
212
|
+
/* @__PURE__ */ jsx3(Paginator, { ...paginatorProps, position: "above" }),
|
|
331
213
|
visible.map((item, i) => {
|
|
332
214
|
const index = offset + i;
|
|
333
|
-
return /* @__PURE__ */
|
|
215
|
+
return /* @__PURE__ */ jsx3(React2.Fragment, { children: render({ item, index }) }, index);
|
|
334
216
|
}),
|
|
335
|
-
/* @__PURE__ */
|
|
217
|
+
/* @__PURE__ */ jsx3(Paginator, { ...paginatorProps, position: "below" })
|
|
336
218
|
] });
|
|
337
219
|
}
|
|
338
220
|
|
|
221
|
+
// src/ui/CommandPalette.tsx
|
|
222
|
+
import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
|
|
223
|
+
var EMPTY_KEY = {
|
|
224
|
+
upArrow: false,
|
|
225
|
+
downArrow: false,
|
|
226
|
+
leftArrow: false,
|
|
227
|
+
rightArrow: false,
|
|
228
|
+
pageDown: false,
|
|
229
|
+
pageUp: false,
|
|
230
|
+
home: false,
|
|
231
|
+
end: false,
|
|
232
|
+
return: false,
|
|
233
|
+
escape: false,
|
|
234
|
+
ctrl: false,
|
|
235
|
+
shift: false,
|
|
236
|
+
tab: false,
|
|
237
|
+
backspace: false,
|
|
238
|
+
delete: false,
|
|
239
|
+
meta: false,
|
|
240
|
+
super: false,
|
|
241
|
+
hyper: false,
|
|
242
|
+
capsLock: false,
|
|
243
|
+
numLock: false
|
|
244
|
+
};
|
|
245
|
+
function fuzzyMatch(name, query) {
|
|
246
|
+
if (!query) return true;
|
|
247
|
+
const lowerName = name.toLowerCase();
|
|
248
|
+
const lowerQuery = query.toLowerCase();
|
|
249
|
+
let qi = 0;
|
|
250
|
+
for (let i = 0; i < lowerName.length && qi < lowerQuery.length; i++) {
|
|
251
|
+
if (lowerName[i] === lowerQuery[qi]) qi++;
|
|
252
|
+
}
|
|
253
|
+
return qi === lowerQuery.length;
|
|
254
|
+
}
|
|
255
|
+
function Inner({
|
|
256
|
+
onClose,
|
|
257
|
+
maxVisible = 10,
|
|
258
|
+
render
|
|
259
|
+
}) {
|
|
260
|
+
const theme = useTheme();
|
|
261
|
+
const [query, setQuery] = useState2("");
|
|
262
|
+
const [selectedIndex, setSelectedIndex] = useState2(0);
|
|
263
|
+
const registry = useKeybindingRegistry();
|
|
264
|
+
const named = registry.all.filter((cmd) => cmd.name != null);
|
|
265
|
+
const filtered = named.filter((cmd) => fuzzyMatch(cmd.name, query));
|
|
266
|
+
const clampedIndex = Math.min(selectedIndex, Math.max(0, filtered.length - 1));
|
|
267
|
+
const onSelect = (cmd) => {
|
|
268
|
+
cmd.handler("", EMPTY_KEY);
|
|
269
|
+
onClose();
|
|
270
|
+
};
|
|
271
|
+
const handleChange = (value) => {
|
|
272
|
+
setQuery(value);
|
|
273
|
+
setSelectedIndex(0);
|
|
274
|
+
};
|
|
275
|
+
const scope = useFocusScope({
|
|
276
|
+
keybindings: {
|
|
277
|
+
escape: onClose,
|
|
278
|
+
enter: () => {
|
|
279
|
+
const cmd = filtered[clampedIndex];
|
|
280
|
+
if (cmd) onSelect(cmd);
|
|
281
|
+
},
|
|
282
|
+
up: () => setSelectedIndex((i) => (i - 1 + filtered.length) % filtered.length),
|
|
283
|
+
down: () => setSelectedIndex((i) => (i + 1) % filtered.length)
|
|
284
|
+
}
|
|
285
|
+
});
|
|
286
|
+
if (render) {
|
|
287
|
+
return /* @__PURE__ */ jsx4(FocusScope, { handle: scope, children: render({ query, onChange: handleChange, filtered, selectedIndex: clampedIndex, onSelect }) });
|
|
288
|
+
}
|
|
289
|
+
return /* @__PURE__ */ jsx4(FocusScope, { handle: scope, children: /* @__PURE__ */ jsxs4(Box3, { flexDirection: "column", children: [
|
|
290
|
+
/* @__PURE__ */ jsx4(TextInput, { label: ">", value: query, onChange: handleChange, placeholder: "Search commands\u2026" }),
|
|
291
|
+
/* @__PURE__ */ jsx4(Box3, { flexDirection: "column", marginTop: 1, children: filtered.length === 0 ? /* @__PURE__ */ jsx4(Text3, { dimColor: true, children: "No commands found" }) : /* @__PURE__ */ jsx4(
|
|
292
|
+
VirtualList,
|
|
293
|
+
{
|
|
294
|
+
items: filtered,
|
|
295
|
+
highlightIndex: clampedIndex,
|
|
296
|
+
maxVisible,
|
|
297
|
+
paginatorStyle: "scrollbar",
|
|
298
|
+
render: ({ item: cmd, index }) => {
|
|
299
|
+
const highlighted = index === clampedIndex;
|
|
300
|
+
const keyColor = highlighted ? theme.hintHighlightColor : theme.hintColor;
|
|
301
|
+
const labelColor = highlighted ? theme.hintHighlightDimColor : theme.hintDimColor;
|
|
302
|
+
return /* @__PURE__ */ jsxs4(Text3, { children: [
|
|
303
|
+
/* @__PURE__ */ jsx4(Text3, { dimColor: true, children: highlighted ? theme.indicator + " " : " " }),
|
|
304
|
+
/* @__PURE__ */ jsx4(Text3, { color: keyColor, bold: true, children: cmd.key }),
|
|
305
|
+
/* @__PURE__ */ jsxs4(Text3, { color: labelColor, children: [
|
|
306
|
+
" ",
|
|
307
|
+
cmd.name
|
|
308
|
+
] })
|
|
309
|
+
] });
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
) }),
|
|
313
|
+
/* @__PURE__ */ jsx4(Box3, { marginTop: 1, children: /* @__PURE__ */ jsx4(Text3, { dimColor: true, children: "\u2191\u2193 navigate \xB7 \u21B5 run \xB7 esc close" }) })
|
|
314
|
+
] }) });
|
|
315
|
+
}
|
|
316
|
+
function HintsBar() {
|
|
317
|
+
const registry = useKeybindingRegistry();
|
|
318
|
+
const theme = useTheme();
|
|
319
|
+
const commands = registry.available.filter((cmd) => cmd.name != null);
|
|
320
|
+
if (commands.length === 0) return null;
|
|
321
|
+
return /* @__PURE__ */ jsx4(Box3, { flexWrap: "wrap", children: commands.map((cmd, index) => /* @__PURE__ */ jsxs4(Text3, { children: [
|
|
322
|
+
/* @__PURE__ */ jsx4(Text3, { color: theme.hintColor, bold: true, children: cmd.key }),
|
|
323
|
+
/* @__PURE__ */ jsxs4(Text3, { color: theme.hintDimColor, children: [
|
|
324
|
+
" ",
|
|
325
|
+
cmd.name
|
|
326
|
+
] }),
|
|
327
|
+
index < commands.length - 1 && /* @__PURE__ */ jsx4(Text3, { color: theme.hintDimColor, children: " \u2022 " })
|
|
328
|
+
] }, `${cmd.nodeId}-${cmd.key}`)) });
|
|
329
|
+
}
|
|
330
|
+
var noop = () => {
|
|
331
|
+
};
|
|
332
|
+
function CommandPalette({ onClose, interactive = true, maxVisible, render }) {
|
|
333
|
+
if (!interactive) {
|
|
334
|
+
return /* @__PURE__ */ jsx4(HintsBar, {});
|
|
335
|
+
}
|
|
336
|
+
return /* @__PURE__ */ jsx4(FocusTrap, { children: /* @__PURE__ */ jsx4(Inner, { onClose: onClose ?? noop, maxVisible, render }) });
|
|
337
|
+
}
|
|
338
|
+
|
|
339
339
|
// src/ui/Select.tsx
|
|
340
|
+
import React3, { useEffect as useEffect2, useState as useState3 } from "react";
|
|
341
|
+
import { Box as Box4, Text as Text4 } from "ink";
|
|
340
342
|
import { jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
|
|
341
343
|
function Select({
|
|
342
344
|
options,
|
|
@@ -877,7 +879,7 @@ function Badge({ children, color, background, variant = "round" }) {
|
|
|
877
879
|
// src/ui/Panel.tsx
|
|
878
880
|
import { useState as useState7 } from "react";
|
|
879
881
|
import { Box as Box9, Text as Text10, measureElement as measureElement2 } from "ink";
|
|
880
|
-
import { Fragment as
|
|
882
|
+
import { Fragment as Fragment2, jsx as jsx11, jsxs as jsxs12 } from "react/jsx-runtime";
|
|
881
883
|
function Panel({ children, title, width, borderColor, footer, ...boxProps }) {
|
|
882
884
|
const theme = useTheme();
|
|
883
885
|
const color = borderColor ?? theme.borderColor;
|
|
@@ -930,7 +932,7 @@ function Panel({ children, title, width, borderColor, footer, ...boxProps }) {
|
|
|
930
932
|
borderTop: false,
|
|
931
933
|
borderColor: color,
|
|
932
934
|
paddingX: 1,
|
|
933
|
-
children: footer ? /* @__PURE__ */ jsxs12(
|
|
935
|
+
children: footer ? /* @__PURE__ */ jsxs12(Fragment2, { children: [
|
|
934
936
|
/* @__PURE__ */ jsx11(Box9, { flexDirection: "column", flexGrow: 1, children }),
|
|
935
937
|
footer
|
|
936
938
|
] }) : children
|