giggles 0.6.2 → 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-TWXBZE5C.js → chunk-HHDMTIXE.js} +157 -32
- package/dist/index.d.ts +5 -1
- package/dist/index.js +3 -3
- package/dist/ui/index.d.ts +13 -6
- package/dist/ui/index.js +191 -174
- package/package.json +2 -2
|
@@ -69,32 +69,35 @@ 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();
|
|
76
76
|
const store = useStore();
|
|
77
77
|
const contextParentId = useContext2(ScopeIdContext);
|
|
78
78
|
const parentId = ((_a = options == null ? void 0 : options.parent) == null ? void 0 : _a.id) ?? contextParentId;
|
|
79
|
+
const focusKey = options == null ? void 0 : options.focusKey;
|
|
79
80
|
const subscribe = useMemo(() => store.subscribe.bind(store), [store]);
|
|
80
|
-
|
|
81
|
-
|
|
81
|
+
store.registerNode(id, parentId, focusKey, true);
|
|
82
|
+
useLayoutEffect(() => {
|
|
83
|
+
store.registerNode(id, parentId, focusKey, true);
|
|
84
|
+
store.flush();
|
|
82
85
|
return () => {
|
|
83
86
|
store.unregisterNode(id);
|
|
84
87
|
};
|
|
85
|
-
}, [id,
|
|
88
|
+
}, [id, store]);
|
|
86
89
|
const hasFocus = useSyncExternalStore2(subscribe, () => store.isFocused(id));
|
|
87
90
|
return { id, hasFocus };
|
|
88
91
|
}
|
|
89
92
|
|
|
90
93
|
// src/core/input/FocusTrap.tsx
|
|
91
|
-
import { useEffect as
|
|
94
|
+
import { useEffect as useEffect2, useRef as useRef2 } from "react";
|
|
92
95
|
import { jsx } from "react/jsx-runtime";
|
|
93
96
|
function FocusTrap({ children }) {
|
|
94
97
|
const { id } = useFocusNode();
|
|
95
98
|
const store = useStore();
|
|
96
99
|
const previousFocusRef = useRef2(store.getFocusedId());
|
|
97
|
-
|
|
100
|
+
useEffect2(() => {
|
|
98
101
|
const previousFocus = previousFocusRef.current;
|
|
99
102
|
store.setTrap(id);
|
|
100
103
|
store.focusFirstChild(id);
|
|
@@ -120,7 +123,7 @@ function InputRouter({ children }) {
|
|
|
120
123
|
}
|
|
121
124
|
|
|
122
125
|
// src/core/focus/useFocusScope.ts
|
|
123
|
-
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";
|
|
124
127
|
function useFocusScope(options) {
|
|
125
128
|
var _a;
|
|
126
129
|
const id = useId3();
|
|
@@ -128,13 +131,16 @@ function useFocusScope(options) {
|
|
|
128
131
|
const store = useStore();
|
|
129
132
|
const contextParentId = useContext3(ScopeIdContext);
|
|
130
133
|
const parentId = ((_a = options == null ? void 0 : options.parent) == null ? void 0 : _a.id) ?? contextParentId;
|
|
134
|
+
const focusKey = options == null ? void 0 : options.focusKey;
|
|
131
135
|
const subscribe = useMemo2(() => store.subscribe.bind(store), [store]);
|
|
132
|
-
|
|
133
|
-
|
|
136
|
+
store.registerNode(id, parentId, focusKey, true);
|
|
137
|
+
useLayoutEffect2(() => {
|
|
138
|
+
store.registerNode(id, parentId, focusKey, true);
|
|
139
|
+
store.flush();
|
|
134
140
|
return () => {
|
|
135
141
|
store.unregisterNode(id);
|
|
136
142
|
};
|
|
137
|
-
}, [id,
|
|
143
|
+
}, [id, store]);
|
|
138
144
|
const hasFocus = useSyncExternalStore3(subscribe, () => store.isFocused(id));
|
|
139
145
|
const isPassive = useSyncExternalStore3(subscribe, () => store.isPassive(id));
|
|
140
146
|
const next = useCallback(() => store.navigateSibling("next", true, id), [store, id]);
|
|
@@ -143,29 +149,43 @@ function useFocusScope(options) {
|
|
|
143
149
|
const prevShallow = useCallback(() => store.navigateSibling("prev", true, id, true), [store, id]);
|
|
144
150
|
const escape = useCallback(() => store.makePassive(id), [store, id]);
|
|
145
151
|
const drillIn = useCallback(() => store.focusFirstChild(id), [store, id]);
|
|
146
|
-
const
|
|
152
|
+
const focusChild = useCallback((key) => store.focusChildByKey(id, key, false), [store, id]);
|
|
153
|
+
const focusChildShallow = useCallback((key) => store.focusChildByKey(id, key, true), [store, id]);
|
|
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) ?? {};
|
|
147
155
|
store.registerKeybindings(id, keybindingRegistrationId, resolvedBindings);
|
|
148
|
-
|
|
156
|
+
useEffect3(() => {
|
|
149
157
|
return () => {
|
|
150
158
|
store.unregisterKeybindings(id, keybindingRegistrationId);
|
|
151
159
|
};
|
|
152
160
|
}, [id, keybindingRegistrationId, store]);
|
|
153
|
-
|
|
161
|
+
useEffect3(() => {
|
|
154
162
|
if (!store.hasFocusScopeComponent(id)) {
|
|
155
163
|
throw new GigglesError(
|
|
156
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."
|
|
157
165
|
);
|
|
158
166
|
}
|
|
159
167
|
}, [id, store]);
|
|
160
|
-
return {
|
|
168
|
+
return {
|
|
169
|
+
id,
|
|
170
|
+
hasFocus,
|
|
171
|
+
isPassive,
|
|
172
|
+
next,
|
|
173
|
+
prev,
|
|
174
|
+
nextShallow,
|
|
175
|
+
prevShallow,
|
|
176
|
+
escape,
|
|
177
|
+
drillIn,
|
|
178
|
+
focusChild,
|
|
179
|
+
focusChildShallow
|
|
180
|
+
};
|
|
161
181
|
}
|
|
162
182
|
|
|
163
183
|
// src/core/focus/FocusScope.tsx
|
|
164
|
-
import { useEffect as
|
|
184
|
+
import { useEffect as useEffect4 } from "react";
|
|
165
185
|
import { jsx as jsx3 } from "react/jsx-runtime";
|
|
166
186
|
function FocusScope({ handle, children }) {
|
|
167
187
|
const store = useStore();
|
|
168
|
-
|
|
188
|
+
useEffect4(() => {
|
|
169
189
|
store.registerFocusScopeComponent(handle.id);
|
|
170
190
|
return () => store.unregisterFocusScopeComponent(handle.id);
|
|
171
191
|
}, [handle.id, store]);
|
|
@@ -208,10 +228,12 @@ var FocusStore = class {
|
|
|
208
228
|
listeners = /* @__PURE__ */ new Set();
|
|
209
229
|
version = 0;
|
|
210
230
|
// nodeId → registrationId → BindingRegistration
|
|
211
|
-
//
|
|
231
|
+
// Both keybindings and nodes register synchronously during render.
|
|
212
232
|
// A keybinding may exist for a node that has not yet appeared in the node tree —
|
|
213
233
|
// this is safe because dispatch only walks nodes in the active branch path.
|
|
214
234
|
keybindings = /* @__PURE__ */ new Map();
|
|
235
|
+
// parentId → focusKey → childId
|
|
236
|
+
keyIndex = /* @__PURE__ */ new Map();
|
|
215
237
|
// ---------------------------------------------------------------------------
|
|
216
238
|
// Subscription
|
|
217
239
|
// ---------------------------------------------------------------------------
|
|
@@ -219,20 +241,43 @@ var FocusStore = class {
|
|
|
219
241
|
this.listeners.add(listener);
|
|
220
242
|
return () => this.listeners.delete(listener);
|
|
221
243
|
}
|
|
244
|
+
dirty = false;
|
|
222
245
|
notify() {
|
|
223
246
|
this.version++;
|
|
247
|
+
this.dirty = false;
|
|
224
248
|
for (const listener of this.listeners) {
|
|
225
249
|
listener();
|
|
226
250
|
}
|
|
227
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
|
+
}
|
|
228
264
|
getVersion() {
|
|
229
265
|
return this.version;
|
|
230
266
|
}
|
|
231
267
|
// ---------------------------------------------------------------------------
|
|
232
268
|
// Registration
|
|
233
269
|
// ---------------------------------------------------------------------------
|
|
234
|
-
registerNode(id, parentId) {
|
|
235
|
-
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 };
|
|
236
281
|
this.nodes.set(id, node);
|
|
237
282
|
this.parentMap.set(id, parentId);
|
|
238
283
|
if (parentId) {
|
|
@@ -242,7 +287,11 @@ var FocusStore = class {
|
|
|
242
287
|
parent.childrenIds.push(id);
|
|
243
288
|
if (wasEmpty && this.pendingFocusFirstChild.has(parentId)) {
|
|
244
289
|
this.pendingFocusFirstChild.delete(parentId);
|
|
245
|
-
|
|
290
|
+
if (silent) {
|
|
291
|
+
this.setFocusedIdSilently(id);
|
|
292
|
+
} else {
|
|
293
|
+
this.focusNode(id);
|
|
294
|
+
}
|
|
246
295
|
}
|
|
247
296
|
}
|
|
248
297
|
}
|
|
@@ -251,10 +300,24 @@ var FocusStore = class {
|
|
|
251
300
|
node.childrenIds.push(existingId);
|
|
252
301
|
}
|
|
253
302
|
}
|
|
303
|
+
if (focusKey && parentId) {
|
|
304
|
+
if (!this.keyIndex.has(parentId)) {
|
|
305
|
+
this.keyIndex.set(parentId, /* @__PURE__ */ new Map());
|
|
306
|
+
}
|
|
307
|
+
this.keyIndex.get(parentId).set(focusKey, id);
|
|
308
|
+
}
|
|
254
309
|
if (this.nodes.size === 1) {
|
|
255
|
-
|
|
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();
|
|
256
320
|
}
|
|
257
|
-
this.notify();
|
|
258
321
|
}
|
|
259
322
|
unregisterNode(id) {
|
|
260
323
|
const node = this.nodes.get(id);
|
|
@@ -268,6 +331,21 @@ var FocusStore = class {
|
|
|
268
331
|
this.nodes.delete(id);
|
|
269
332
|
this.passiveSet.delete(id);
|
|
270
333
|
this.pendingFocusFirstChild.delete(id);
|
|
334
|
+
if (node.parentId) {
|
|
335
|
+
const parentKeys = this.keyIndex.get(node.parentId);
|
|
336
|
+
if (parentKeys) {
|
|
337
|
+
for (const [key, childId] of parentKeys) {
|
|
338
|
+
if (childId === id) {
|
|
339
|
+
parentKeys.delete(key);
|
|
340
|
+
break;
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
if (parentKeys.size === 0) {
|
|
344
|
+
this.keyIndex.delete(node.parentId);
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
this.keyIndex.delete(id);
|
|
271
349
|
if (this.focusedId === id) {
|
|
272
350
|
let candidate = node.parentId;
|
|
273
351
|
while (candidate !== null) {
|
|
@@ -289,18 +367,18 @@ var FocusStore = class {
|
|
|
289
367
|
const oldFocusedId = this.focusedId;
|
|
290
368
|
if (oldFocusedId === id) return;
|
|
291
369
|
this.focusedId = id;
|
|
292
|
-
|
|
293
|
-
const wasAncestor = this.isAncestorOf(passiveId, oldFocusedId);
|
|
294
|
-
const isAncestor = this.isAncestorOf(passiveId, id);
|
|
295
|
-
if (wasAncestor && !isAncestor) {
|
|
296
|
-
this.passiveSet.delete(passiveId);
|
|
297
|
-
}
|
|
298
|
-
if (isAncestor && id !== passiveId) {
|
|
299
|
-
this.passiveSet.delete(passiveId);
|
|
300
|
-
}
|
|
301
|
-
}
|
|
370
|
+
this.clearPassiveOnFocusChange(oldFocusedId, id);
|
|
302
371
|
this.notify();
|
|
303
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
|
+
}
|
|
304
382
|
focusFirstChild(parentId) {
|
|
305
383
|
const parent = this.nodes.get(parentId);
|
|
306
384
|
if (parent && parent.childrenIds.length > 0) {
|
|
@@ -315,6 +393,21 @@ var FocusStore = class {
|
|
|
315
393
|
this.pendingFocusFirstChild.add(parentId);
|
|
316
394
|
}
|
|
317
395
|
}
|
|
396
|
+
focusChildByKey(parentId, key, shallow) {
|
|
397
|
+
var _a;
|
|
398
|
+
const childId = (_a = this.keyIndex.get(parentId)) == null ? void 0 : _a.get(key);
|
|
399
|
+
if (!childId || !this.nodes.has(childId)) return;
|
|
400
|
+
if (shallow) {
|
|
401
|
+
this.focusNode(childId);
|
|
402
|
+
} else {
|
|
403
|
+
const child = this.nodes.get(childId);
|
|
404
|
+
if (child.childrenIds.length > 0) {
|
|
405
|
+
this.focusFirstChild(childId);
|
|
406
|
+
} else {
|
|
407
|
+
this.focusNode(childId);
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
}
|
|
318
411
|
// ---------------------------------------------------------------------------
|
|
319
412
|
// Navigation
|
|
320
413
|
// ---------------------------------------------------------------------------
|
|
@@ -550,6 +643,38 @@ var FocusStore = class {
|
|
|
550
643
|
}
|
|
551
644
|
return false;
|
|
552
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
|
+
}
|
|
553
678
|
};
|
|
554
679
|
|
|
555
680
|
export {
|
package/dist/index.d.ts
CHANGED
|
@@ -47,10 +47,13 @@ type FocusScopeHandle = {
|
|
|
47
47
|
prevShallow: () => void;
|
|
48
48
|
escape: () => void;
|
|
49
49
|
drillIn: () => void;
|
|
50
|
+
focusChild: (key: string) => void;
|
|
51
|
+
focusChildShallow: (key: string) => void;
|
|
50
52
|
};
|
|
51
|
-
type FocusScopeHelpers = Pick<FocusScopeHandle, 'next' | 'prev' | 'nextShallow' | 'prevShallow' | 'escape' | 'drillIn'>;
|
|
53
|
+
type FocusScopeHelpers = Pick<FocusScopeHandle, 'next' | 'prev' | 'nextShallow' | 'prevShallow' | 'escape' | 'drillIn' | 'focusChild' | 'focusChildShallow'>;
|
|
52
54
|
type FocusScopeOptions = {
|
|
53
55
|
parent?: FocusScopeHandle;
|
|
56
|
+
focusKey?: string;
|
|
54
57
|
keybindings?: Keybindings | ((helpers: FocusScopeHelpers) => Keybindings);
|
|
55
58
|
};
|
|
56
59
|
declare function useFocusScope(options?: FocusScopeOptions): FocusScopeHandle;
|
|
@@ -67,6 +70,7 @@ type FocusNodeHandle = {
|
|
|
67
70
|
};
|
|
68
71
|
type FocusNodeOptions = {
|
|
69
72
|
parent?: FocusScopeHandle;
|
|
73
|
+
focusKey?: string;
|
|
70
74
|
};
|
|
71
75
|
declare function useFocusNode(options?: FocusNodeOptions): FocusNodeHandle;
|
|
72
76
|
|
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;
|
|
@@ -31,8 +32,9 @@ type TextInputProps = {
|
|
|
31
32
|
onSubmit?: (value: string) => void;
|
|
32
33
|
placeholder?: string;
|
|
33
34
|
render?: (props: TextInputRenderProps) => React$1.ReactNode;
|
|
35
|
+
focusKey?: string;
|
|
34
36
|
};
|
|
35
|
-
declare function TextInput({ label, value, onChange, onSubmit, placeholder, render }: TextInputProps): react_jsx_runtime.JSX.Element;
|
|
37
|
+
declare function TextInput({ label, value, onChange, onSubmit, placeholder, render, focusKey }: TextInputProps): react_jsx_runtime.JSX.Element;
|
|
36
38
|
|
|
37
39
|
type PaginatorStyle = 'dots' | 'arrows' | 'scrollbar' | 'counter' | 'none';
|
|
38
40
|
type PaginatorProps = {
|
|
@@ -70,8 +72,9 @@ type SelectProps<T> = {
|
|
|
70
72
|
paginatorStyle?: PaginatorStyle;
|
|
71
73
|
wrap?: boolean;
|
|
72
74
|
render?: (props: SelectRenderProps<T>) => React$1.ReactNode;
|
|
75
|
+
focusKey?: string;
|
|
73
76
|
};
|
|
74
|
-
declare function Select<T>({ options, value, onChange, onSubmit, onHighlight, label, immediate, direction, gap, maxVisible, paginatorStyle, wrap, render }: SelectProps<T>): react_jsx_runtime.JSX.Element;
|
|
77
|
+
declare function Select<T>({ options, value, onChange, onSubmit, onHighlight, label, immediate, direction, gap, maxVisible, paginatorStyle, wrap, render, focusKey }: SelectProps<T>): react_jsx_runtime.JSX.Element;
|
|
75
78
|
|
|
76
79
|
type MultiSelectRenderProps<T> = {
|
|
77
80
|
option: SelectOption<T>;
|
|
@@ -92,15 +95,17 @@ type MultiSelectProps<T> = {
|
|
|
92
95
|
paginatorStyle?: PaginatorStyle;
|
|
93
96
|
wrap?: boolean;
|
|
94
97
|
render?: (props: MultiSelectRenderProps<T>) => React$1.ReactNode;
|
|
98
|
+
focusKey?: string;
|
|
95
99
|
};
|
|
96
|
-
declare function MultiSelect<T>({ options, value, onChange, onSubmit, onHighlight, label, direction, gap, maxVisible, paginatorStyle, wrap, render }: MultiSelectProps<T>): react_jsx_runtime.JSX.Element;
|
|
100
|
+
declare function MultiSelect<T>({ options, value, onChange, onSubmit, onHighlight, label, direction, gap, maxVisible, paginatorStyle, wrap, render, focusKey }: MultiSelectProps<T>): react_jsx_runtime.JSX.Element;
|
|
97
101
|
|
|
98
102
|
type ConfirmProps = {
|
|
99
103
|
message: string;
|
|
100
104
|
defaultValue?: boolean;
|
|
101
105
|
onSubmit: (value: boolean) => void;
|
|
106
|
+
focusKey?: string;
|
|
102
107
|
};
|
|
103
|
-
declare function Confirm({ message, defaultValue, onSubmit }: ConfirmProps): react_jsx_runtime.JSX.Element;
|
|
108
|
+
declare function Confirm({ message, defaultValue, onSubmit, focusKey }: ConfirmProps): react_jsx_runtime.JSX.Element;
|
|
104
109
|
|
|
105
110
|
type AutocompleteRenderProps<T> = {
|
|
106
111
|
option: SelectOption<T>;
|
|
@@ -122,8 +127,9 @@ type AutocompleteProps<T> = {
|
|
|
122
127
|
paginatorStyle?: PaginatorStyle;
|
|
123
128
|
wrap?: boolean;
|
|
124
129
|
render?: (props: AutocompleteRenderProps<T>) => React$1.ReactNode;
|
|
130
|
+
focusKey?: string;
|
|
125
131
|
};
|
|
126
|
-
declare function Autocomplete<T>({ options, value, onChange, onSubmit, onHighlight, label, placeholder, filter, gap, maxVisible, paginatorStyle, wrap, render }: AutocompleteProps<T>): react_jsx_runtime.JSX.Element;
|
|
132
|
+
declare function Autocomplete<T>({ options, value, onChange, onSubmit, onHighlight, label, placeholder, filter, gap, maxVisible, paginatorStyle, wrap, render, focusKey }: AutocompleteProps<T>): react_jsx_runtime.JSX.Element;
|
|
127
133
|
|
|
128
134
|
type VirtualListRenderProps<T> = {
|
|
129
135
|
item: T;
|
|
@@ -160,6 +166,7 @@ declare const Viewport: React$1.ForwardRefExoticComponent<Omit<BoxProps, "flexDi
|
|
|
160
166
|
height: number;
|
|
161
167
|
keybindings?: boolean;
|
|
162
168
|
footer?: React$1.ReactNode;
|
|
169
|
+
focusKey?: string;
|
|
163
170
|
} & React$1.RefAttributes<ViewportRef>>;
|
|
164
171
|
|
|
165
172
|
type ModalProps = Omit<BoxProps, 'children'> & {
|
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
|
|
139
|
-
function TextInput({ label, value, onChange, onSubmit, placeholder, render }) {
|
|
140
|
-
const focus = useFocusNode();
|
|
141
|
-
const cursorRef =
|
|
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";
|
|
269
|
+
function TextInput({ label, value, onChange, onSubmit, placeholder, render, focusKey }) {
|
|
270
|
+
const focus = useFocusNode({ focusKey });
|
|
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 }) {
|
|
|
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 }) {
|
|
|
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,
|
|
@@ -351,7 +365,8 @@ function Select({
|
|
|
351
365
|
maxVisible,
|
|
352
366
|
paginatorStyle,
|
|
353
367
|
wrap = true,
|
|
354
|
-
render
|
|
368
|
+
render,
|
|
369
|
+
focusKey
|
|
355
370
|
}) {
|
|
356
371
|
const seen = /* @__PURE__ */ new Set();
|
|
357
372
|
for (const opt of options) {
|
|
@@ -361,7 +376,7 @@ function Select({
|
|
|
361
376
|
}
|
|
362
377
|
seen.add(key);
|
|
363
378
|
}
|
|
364
|
-
const focus = useFocusNode();
|
|
379
|
+
const focus = useFocusNode({ focusKey });
|
|
365
380
|
const theme = useTheme();
|
|
366
381
|
const [highlightIndex, setHighlightIndex] = useState3(0);
|
|
367
382
|
const safeIndex = options.length === 0 ? -1 : Math.min(highlightIndex, options.length - 1);
|
|
@@ -438,7 +453,8 @@ function MultiSelect({
|
|
|
438
453
|
maxVisible,
|
|
439
454
|
paginatorStyle,
|
|
440
455
|
wrap = true,
|
|
441
|
-
render
|
|
456
|
+
render,
|
|
457
|
+
focusKey
|
|
442
458
|
}) {
|
|
443
459
|
const seen = /* @__PURE__ */ new Set();
|
|
444
460
|
for (const opt of options) {
|
|
@@ -448,7 +464,7 @@ function MultiSelect({
|
|
|
448
464
|
}
|
|
449
465
|
seen.add(key);
|
|
450
466
|
}
|
|
451
|
-
const focus = useFocusNode();
|
|
467
|
+
const focus = useFocusNode({ focusKey });
|
|
452
468
|
const theme = useTheme();
|
|
453
469
|
const [highlightIndex, setHighlightIndex] = useState4(0);
|
|
454
470
|
const [internalSelected, setInternalSelected] = useState4([]);
|
|
@@ -515,8 +531,8 @@ function MultiSelect({
|
|
|
515
531
|
// src/ui/Confirm.tsx
|
|
516
532
|
import { Text as Text6 } from "ink";
|
|
517
533
|
import { jsxs as jsxs7 } from "react/jsx-runtime";
|
|
518
|
-
function Confirm({ message, defaultValue = true, onSubmit }) {
|
|
519
|
-
const focus = useFocusNode();
|
|
534
|
+
function Confirm({ message, defaultValue = true, onSubmit, focusKey }) {
|
|
535
|
+
const focus = useFocusNode({ focusKey });
|
|
520
536
|
useKeybindings(focus, {
|
|
521
537
|
y: () => onSubmit(true),
|
|
522
538
|
n: () => onSubmit(false),
|
|
@@ -558,7 +574,8 @@ function Autocomplete({
|
|
|
558
574
|
maxVisible,
|
|
559
575
|
paginatorStyle,
|
|
560
576
|
wrap = true,
|
|
561
|
-
render
|
|
577
|
+
render,
|
|
578
|
+
focusKey
|
|
562
579
|
}) {
|
|
563
580
|
const seen = /* @__PURE__ */ new Set();
|
|
564
581
|
for (const opt of options) {
|
|
@@ -568,7 +585,7 @@ function Autocomplete({
|
|
|
568
585
|
}
|
|
569
586
|
seen.add(key);
|
|
570
587
|
}
|
|
571
|
-
const focus = useFocusNode();
|
|
588
|
+
const focus = useFocusNode({ focusKey });
|
|
572
589
|
const theme = useTheme();
|
|
573
590
|
const [query, setQuery] = useState5("");
|
|
574
591
|
const [highlightIndex, setHighlightIndex] = useState5(0);
|
|
@@ -718,8 +735,8 @@ function MeasurableItem({
|
|
|
718
735
|
}, [index, onMeasure, children]);
|
|
719
736
|
return /* @__PURE__ */ jsx8(Box7, { ref, flexShrink: 0, width: "100%", flexDirection: "column", children });
|
|
720
737
|
}
|
|
721
|
-
var Viewport = forwardRef(function Viewport2({ children, height, keybindings: enableKeybindings = true, footer, ...boxProps }, ref) {
|
|
722
|
-
const focus = useFocusNode();
|
|
738
|
+
var Viewport = forwardRef(function Viewport2({ children, height, keybindings: enableKeybindings = true, footer, focusKey, ...boxProps }, ref) {
|
|
739
|
+
const focus = useFocusNode({ focusKey });
|
|
723
740
|
const [scrollOffset, setScrollOffset] = useState6(0);
|
|
724
741
|
const contentHeightRef = useRef4(0);
|
|
725
742
|
const itemHeightsRef = useRef4({});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "giggles",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.1",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -43,7 +43,7 @@
|
|
|
43
43
|
"play": "f() { tsx --watch playground/examples/$1.tsx; }; f",
|
|
44
44
|
"record": "f() { vhs playground/tapes/$1.tape; }; f",
|
|
45
45
|
"dev:docs": "pnpm build && concurrently --kill-others \"pnpm build:watch\" \"pnpm --filter documentation dev\"",
|
|
46
|
-
"lint": "prettier --write . && eslint . --fix"
|
|
46
|
+
"lint": "prettier --write --loglevel warn . && eslint . --fix --quiet"
|
|
47
47
|
},
|
|
48
48
|
"files": [
|
|
49
49
|
"dist"
|