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.
@@ -69,32 +69,35 @@ function useKeybindingRegistry(focus) {
69
69
  }
70
70
 
71
71
  // src/core/focus/useFocusNode.ts
72
- import { useContext as useContext2, useEffect as useEffect2, useId as useId2, useMemo, useSyncExternalStore as useSyncExternalStore2 } from "react";
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
- useEffect2(() => {
81
- store.registerNode(id, parentId);
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, parentId, store]);
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 useEffect3, useRef as useRef2 } from "react";
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
- useEffect3(() => {
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 useEffect4, useId as useId3, useMemo as useMemo2, useSyncExternalStore as useSyncExternalStore3 } from "react";
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
- useEffect4(() => {
133
- store.registerNode(id, parentId);
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, parentId, store]);
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 resolvedBindings = typeof (options == null ? void 0 : options.keybindings) === "function" ? options.keybindings({ next, prev, nextShallow, prevShallow, escape, drillIn }) : (options == null ? void 0 : options.keybindings) ?? {};
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
- useEffect4(() => {
156
+ useEffect3(() => {
149
157
  return () => {
150
158
  store.unregisterKeybindings(id, keybindingRegistrationId);
151
159
  };
152
160
  }, [id, keybindingRegistrationId, store]);
153
- useEffect4(() => {
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 { id, hasFocus, isPassive, next, prev, nextShallow, prevShallow, escape, drillIn };
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 useEffect5 } from "react";
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
- useEffect5(() => {
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
- // Keybindings register synchronously during render; nodes register in useEffect.
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 node = { id, parentId, childrenIds: [] };
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
- this.focusNode(id);
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
- this.focusNode(id);
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
- for (const passiveId of this.passiveSet) {
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-TWXBZE5C.js";
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 { useEffect as useEffect2, useMemo, useRef as useRef2 } from "react";
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
- useEffect2(() => {
102
+ useLayoutEffect(() => {
103
103
  if (!wasTopRef.current && isTop) {
104
104
  const saved = restoreFocus ? lastFocusedChildRef.current : null;
105
105
  if (saved) {
@@ -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-TWXBZE5C.js";
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 { Fragment, jsx, jsxs } from "react/jsx-runtime";
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({ onClose, render }) {
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] = useState("");
55
- const [selectedIndex, setSelectedIndex] = useState(0);
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
- left: () => setSelectedIndex((i) => (i - 1 + filtered.length) % filtered.length),
73
- right: () => setSelectedIndex((i) => (i + 1) % filtered.length),
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__ */ jsx(Fragment, { children: render({ query, filtered, selectedIndex: clampedIndex, onSelect }) });
209
+ return /* @__PURE__ */ jsx3(Fragment, { children: render({ query, filtered, selectedIndex: clampedIndex, onSelect }) });
90
210
  }
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: " " })
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__ */ 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
- }) })
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__ */ 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: [
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__ */ jsx(Text, { color: theme.hintDimColor, children: " \u2022 " })
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__ */ jsx(HintsBar, {});
260
+ return /* @__PURE__ */ jsx3(HintsBar, {});
131
261
  }
132
- return /* @__PURE__ */ jsx(FocusTrap, { children: /* @__PURE__ */ jsx(Inner, { onClose: onClose ?? noop, render }) });
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 Text2 } from "ink";
138
- import { Fragment as Fragment2, jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
139
- function TextInput({ label, value, onChange, onSubmit, placeholder, render }) {
140
- const focus = useFocusNode();
141
- const cursorRef = useRef(value.length);
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__ */ jsx2(Fragment2, { children: render({ value, focused: focus.hasFocus, before, cursorChar, after, placeholder }) });
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__ */ jsxs2(Text2, { children: [
203
- label != null && /* @__PURE__ */ jsxs2(Text2, { bold: true, children: [
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__ */ jsx2(Text2, { inverse: true, children: activeCursorChar }),
209
- isPlaceholderVisible ? /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: activeAfter }) : activeAfter
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__ */ jsxs2(Text2, { dimColor: true, children: [
213
- label != null && /* @__PURE__ */ jsxs2(Text2, { children: [
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__ */ jsx2(Text2, { dimColor: true, children: displayValue }) : displayValue
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.6.2",
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"