giggles 0.6.1 → 0.7.0

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/README.md CHANGED
@@ -4,7 +4,7 @@
4
4
 
5
5
  # giggles
6
6
 
7
- ![giggles](https://github.com/user-attachments/assets/c5c7ef05-232f-4180-8b85-0160fb0f083a)
7
+ <img src="https://github.com/user-attachments/assets/c5c7ef05-232f-4180-8b85-0160fb0f083a" width="700" alt="giggles">
8
8
 
9
9
  giggles is a batteries-included react framework for building terminal apps. built on ink, it handles focus, input routing, screen navigation, and theming out of the box so you can skip the plumbing and build.
10
10
 
@@ -30,3 +30,83 @@ npx create-giggles-app
30
30
  ```
31
31
 
32
32
  see [giggles.zzzzion.com](https://giggles.zzzzion.com) for API documentation and live demos.
33
+
34
+ ## giggles/ui
35
+
36
+ ### [select](https://giggles.zzzzion.com/ui/select)
37
+
38
+ <img src="https://github.com/user-attachments/assets/8ce13f75-7a7b-4123-a973-f2992193bf84" width="500" alt="select">
39
+
40
+ ### [multi select](https://giggles.zzzzion.com/ui/multi-select)
41
+
42
+ <img src="https://github.com/user-attachments/assets/24f5d625-6e46-4cb1-8d22-42d40eb48f56" width="500" alt="multi-select">
43
+
44
+ ### [markdown](https://giggles.zzzzion.com/ui/markdown)
45
+
46
+ <img src="https://github.com/user-attachments/assets/1cdb6e84-c714-470a-8cf0-b6abf68b78a9" width="500" alt="markdown">
47
+
48
+ ### [text input](https://giggles.zzzzion.com/ui/text-input)
49
+
50
+ <img src="https://github.com/user-attachments/assets/b56056ca-97e2-4dc2-a1eb-2ee3f4d559e7" width="500" alt="text-input">
51
+
52
+ ### [viewport](https://giggles.zzzzion.com/ui/viewport)
53
+
54
+ <img src="https://github.com/user-attachments/assets/56c6cb6b-b2a7-4803-bed4-c6c6c34042c3" width="500" alt="viewport">
55
+
56
+ ### [code block](https://giggles.zzzzion.com/ui/codeblock)
57
+
58
+ <img src="https://github.com/user-attachments/assets/283dbd7e-326c-4acb-b8c0-d3608722952b" width="500" alt="codeblock">
59
+
60
+ ### [confirm](https://giggles.zzzzion.com/ui/confirm)
61
+
62
+ <img src="https://github.com/user-attachments/assets/b887da72-bf02-4084-b846-8b90cc3c3487" width="500" alt="confirm">
63
+
64
+ ### [spinner](https://giggles.zzzzion.com/ui/spinner)
65
+
66
+ <img src="https://github.com/user-attachments/assets/71aef7f8-e53b-4876-864f-b9b1a5100c5d" width="500" alt="spinner">
67
+
68
+ ### [modal](https://giggles.zzzzion.com/ui/modal)
69
+
70
+ <img src="https://github.com/user-attachments/assets/7415c554-927e-4b5c-91d7-7e3b1f4ea0ca" width="500" alt="modal">
71
+
72
+ ### [paginator](https://giggles.zzzzion.com/ui/paginator)
73
+
74
+ <img src="https://github.com/user-attachments/assets/b0780f46-848e-4881-822a-86d7db02d212" width="500" alt="paginator">
75
+
76
+ ### [autocomplete](https://giggles.zzzzion.com/ui/autocomplete)
77
+
78
+ <img src="https://github.com/user-attachments/assets/aa76dd7a-5357-4969-a979-95975b5ec578" width="500" alt="autocomplete">
79
+
80
+ ### [command palette](https://giggles.zzzzion.com/ui/command-palette)
81
+
82
+ <img src="https://github.com/user-attachments/assets/30886cb5-986f-4477-85c1-61cba4b499aa" width="500" alt="command-palette">
83
+
84
+ ### [virtual list](https://giggles.zzzzion.com/ui/virtual-list)
85
+
86
+ <img src="https://github.com/user-attachments/assets/d3ef1d92-813c-4546-8d60-2c38745ddbbc" width="500" alt="virtual-list">
87
+
88
+ ### [badge](https://giggles.zzzzion.com/ui/badge)
89
+
90
+ <img src="https://github.com/user-attachments/assets/b144c3f5-8b3b-4236-abf2-fc239d23f0c6" width="500" alt="badge">
91
+
92
+ ### [panel](https://giggles.zzzzion.com/ui/panel)
93
+
94
+ <img src="https://github.com/user-attachments/assets/9831d73a-baa9-410a-b933-e0dfd9433604" width="500" alt="panel">
95
+
96
+ ## giggles/terminal
97
+
98
+ ### [useShellOut](https://giggles.zzzzion.com/terminal#useshellout)
99
+
100
+ suspend the UI, hand off the terminal to an external program like `vim` or `less`, and resume cleanly when it exits
101
+
102
+ ### [useSpawn](https://giggles.zzzzion.com/terminal#usespawn)
103
+
104
+ spawn a child process and stream its stdout/stderr output into your UI — with support for colored output via a pty
105
+
106
+ ### [useTerminalSize](https://giggles.zzzzion.com/terminal#useterminalsize)
107
+
108
+ reactively track the terminal's current dimensions (rows and columns), updating on resize
109
+
110
+ ### [useTerminalFocus](https://giggles.zzzzion.com/terminal#useterminalfocus)
111
+
112
+ detect when the terminal window gains or loses focus
@@ -76,13 +76,14 @@ function useFocusNode(options) {
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
  useEffect2(() => {
81
- store.registerNode(id, parentId);
82
+ store.registerNode(id, parentId, focusKey);
82
83
  return () => {
83
84
  store.unregisterNode(id);
84
85
  };
85
- }, [id, parentId, store]);
86
+ }, [id, parentId, focusKey, store]);
86
87
  const hasFocus = useSyncExternalStore2(subscribe, () => store.isFocused(id));
87
88
  return { id, hasFocus };
88
89
  }
@@ -128,13 +129,14 @@ function useFocusScope(options) {
128
129
  const store = useStore();
129
130
  const contextParentId = useContext3(ScopeIdContext);
130
131
  const parentId = ((_a = options == null ? void 0 : options.parent) == null ? void 0 : _a.id) ?? contextParentId;
132
+ const focusKey = options == null ? void 0 : options.focusKey;
131
133
  const subscribe = useMemo2(() => store.subscribe.bind(store), [store]);
132
134
  useEffect4(() => {
133
- store.registerNode(id, parentId);
135
+ store.registerNode(id, parentId, focusKey);
134
136
  return () => {
135
137
  store.unregisterNode(id);
136
138
  };
137
- }, [id, parentId, store]);
139
+ }, [id, parentId, focusKey, store]);
138
140
  const hasFocus = useSyncExternalStore3(subscribe, () => store.isFocused(id));
139
141
  const isPassive = useSyncExternalStore3(subscribe, () => store.isPassive(id));
140
142
  const next = useCallback(() => store.navigateSibling("next", true, id), [store, id]);
@@ -143,19 +145,46 @@ function useFocusScope(options) {
143
145
  const prevShallow = useCallback(() => store.navigateSibling("prev", true, id, true), [store, id]);
144
146
  const escape = useCallback(() => store.makePassive(id), [store, id]);
145
147
  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) ?? {};
148
+ const focusChild = useCallback((key) => store.focusChildByKey(id, key, false), [store, id]);
149
+ const focusChildShallow = useCallback((key) => store.focusChildByKey(id, key, true), [store, id]);
150
+ 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
151
  store.registerKeybindings(id, keybindingRegistrationId, resolvedBindings);
148
152
  useEffect4(() => {
149
153
  return () => {
150
154
  store.unregisterKeybindings(id, keybindingRegistrationId);
151
155
  };
152
156
  }, [id, keybindingRegistrationId, store]);
153
- return { id, hasFocus, isPassive };
157
+ useEffect4(() => {
158
+ if (!store.hasFocusScopeComponent(id)) {
159
+ throw new GigglesError(
160
+ "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."
161
+ );
162
+ }
163
+ }, [id, store]);
164
+ return {
165
+ id,
166
+ hasFocus,
167
+ isPassive,
168
+ next,
169
+ prev,
170
+ nextShallow,
171
+ prevShallow,
172
+ escape,
173
+ drillIn,
174
+ focusChild,
175
+ focusChildShallow
176
+ };
154
177
  }
155
178
 
156
179
  // src/core/focus/FocusScope.tsx
180
+ import { useEffect as useEffect5 } from "react";
157
181
  import { jsx as jsx3 } from "react/jsx-runtime";
158
182
  function FocusScope({ handle, children }) {
183
+ const store = useStore();
184
+ useEffect5(() => {
185
+ store.registerFocusScopeComponent(handle.id);
186
+ return () => store.unregisterFocusScopeComponent(handle.id);
187
+ }, [handle.id, store]);
159
188
  return /* @__PURE__ */ jsx3(ScopeIdContext.Provider, { value: handle.id, children });
160
189
  }
161
190
 
@@ -191,6 +220,7 @@ var FocusStore = class {
191
220
  passiveSet = /* @__PURE__ */ new Set();
192
221
  pendingFocusFirstChild = /* @__PURE__ */ new Set();
193
222
  trapNodeId = null;
223
+ renderedScopes = /* @__PURE__ */ new Set();
194
224
  listeners = /* @__PURE__ */ new Set();
195
225
  version = 0;
196
226
  // nodeId → registrationId → BindingRegistration
@@ -198,6 +228,8 @@ var FocusStore = class {
198
228
  // A keybinding may exist for a node that has not yet appeared in the node tree —
199
229
  // this is safe because dispatch only walks nodes in the active branch path.
200
230
  keybindings = /* @__PURE__ */ new Map();
231
+ // parentId → focusKey → childId
232
+ keyIndex = /* @__PURE__ */ new Map();
201
233
  // ---------------------------------------------------------------------------
202
234
  // Subscription
203
235
  // ---------------------------------------------------------------------------
@@ -217,7 +249,7 @@ var FocusStore = class {
217
249
  // ---------------------------------------------------------------------------
218
250
  // Registration
219
251
  // ---------------------------------------------------------------------------
220
- registerNode(id, parentId) {
252
+ registerNode(id, parentId, focusKey) {
221
253
  const node = { id, parentId, childrenIds: [] };
222
254
  this.nodes.set(id, node);
223
255
  this.parentMap.set(id, parentId);
@@ -237,6 +269,12 @@ var FocusStore = class {
237
269
  node.childrenIds.push(existingId);
238
270
  }
239
271
  }
272
+ if (focusKey && parentId) {
273
+ if (!this.keyIndex.has(parentId)) {
274
+ this.keyIndex.set(parentId, /* @__PURE__ */ new Map());
275
+ }
276
+ this.keyIndex.get(parentId).set(focusKey, id);
277
+ }
240
278
  if (this.nodes.size === 1) {
241
279
  this.focusNode(id);
242
280
  }
@@ -254,6 +292,21 @@ var FocusStore = class {
254
292
  this.nodes.delete(id);
255
293
  this.passiveSet.delete(id);
256
294
  this.pendingFocusFirstChild.delete(id);
295
+ if (node.parentId) {
296
+ const parentKeys = this.keyIndex.get(node.parentId);
297
+ if (parentKeys) {
298
+ for (const [key, childId] of parentKeys) {
299
+ if (childId === id) {
300
+ parentKeys.delete(key);
301
+ break;
302
+ }
303
+ }
304
+ if (parentKeys.size === 0) {
305
+ this.keyIndex.delete(node.parentId);
306
+ }
307
+ }
308
+ }
309
+ this.keyIndex.delete(id);
257
310
  if (this.focusedId === id) {
258
311
  let candidate = node.parentId;
259
312
  while (candidate !== null) {
@@ -301,6 +354,21 @@ var FocusStore = class {
301
354
  this.pendingFocusFirstChild.add(parentId);
302
355
  }
303
356
  }
357
+ focusChildByKey(parentId, key, shallow) {
358
+ var _a;
359
+ const childId = (_a = this.keyIndex.get(parentId)) == null ? void 0 : _a.get(key);
360
+ if (!childId || !this.nodes.has(childId)) return;
361
+ if (shallow) {
362
+ this.focusNode(childId);
363
+ } else {
364
+ const child = this.nodes.get(childId);
365
+ if (child.childrenIds.length > 0) {
366
+ this.focusFirstChild(childId);
367
+ } else {
368
+ this.focusNode(childId);
369
+ }
370
+ }
371
+ }
304
372
  // ---------------------------------------------------------------------------
305
373
  // Navigation
306
374
  // ---------------------------------------------------------------------------
@@ -396,6 +464,15 @@ var FocusStore = class {
396
464
  // ---------------------------------------------------------------------------
397
465
  // Trap
398
466
  // ---------------------------------------------------------------------------
467
+ registerFocusScopeComponent(id) {
468
+ this.renderedScopes.add(id);
469
+ }
470
+ unregisterFocusScopeComponent(id) {
471
+ this.renderedScopes.delete(id);
472
+ }
473
+ hasFocusScopeComponent(id) {
474
+ return this.renderedScopes.has(id);
475
+ }
399
476
  setTrap(nodeId) {
400
477
  this.trapNodeId = nodeId;
401
478
  }
package/dist/index.d.ts CHANGED
@@ -41,17 +41,19 @@ type FocusScopeHandle = {
41
41
  id: string;
42
42
  hasFocus: boolean;
43
43
  isPassive: boolean;
44
- };
45
- type FocusScopeHelpers = {
46
44
  next: () => void;
47
45
  prev: () => void;
48
46
  nextShallow: () => void;
49
47
  prevShallow: () => void;
50
48
  escape: () => void;
51
49
  drillIn: () => void;
50
+ focusChild: (key: string) => void;
51
+ focusChildShallow: (key: string) => void;
52
52
  };
53
+ type FocusScopeHelpers = Pick<FocusScopeHandle, 'next' | 'prev' | 'nextShallow' | 'prevShallow' | 'escape' | 'drillIn' | 'focusChild' | 'focusChildShallow'>;
53
54
  type FocusScopeOptions = {
54
55
  parent?: FocusScopeHandle;
56
+ focusKey?: string;
55
57
  keybindings?: Keybindings | ((helpers: FocusScopeHelpers) => Keybindings);
56
58
  };
57
59
  declare function useFocusScope(options?: FocusScopeOptions): FocusScopeHandle;
@@ -68,6 +70,7 @@ type FocusNodeHandle = {
68
70
  };
69
71
  type FocusNodeOptions = {
70
72
  parent?: FocusScopeHandle;
73
+ focusKey?: string;
71
74
  };
72
75
  declare function useFocusNode(options?: FocusNodeOptions): FocusNodeHandle;
73
76
 
package/dist/index.js CHANGED
@@ -1,3 +1,6 @@
1
+ import {
2
+ useTerminalSize
3
+ } from "./chunk-WNGBTD67.js";
1
4
  import {
2
5
  FocusScope,
3
6
  FocusStore,
@@ -11,14 +14,11 @@ import {
11
14
  useKeybindingRegistry,
12
15
  useKeybindings,
13
16
  useStore
14
- } from "./chunk-A7BRQXWE.js";
17
+ } from "./chunk-74PBSWEK.js";
15
18
  import {
16
19
  ThemeProvider,
17
20
  useTheme
18
21
  } from "./chunk-C77VBSPK.js";
19
- import {
20
- useTerminalSize
21
- } from "./chunk-WNGBTD67.js";
22
22
 
23
23
  // src/core/GigglesProvider.tsx
24
24
  import { useRef } from "react";
@@ -4,6 +4,11 @@ type TerminalSize = {
4
4
  rows: number;
5
5
  columns: number;
6
6
  };
7
+ type ShellOutHandle = {
8
+ run: (command: string) => Promise<{
9
+ exitCode: number;
10
+ }>;
11
+ };
7
12
  type SpawnOptions = SpawnOptionsWithoutStdio & {
8
13
  /**
9
14
  * Inject FORCE_COLOR=1 and TERM=xterm-256color into the child process
@@ -28,12 +33,8 @@ declare function useTerminalSize(): TerminalSize;
28
33
 
29
34
  declare function useTerminalFocus(callback: (focused: boolean) => void): void;
30
35
 
31
- declare function useShellOut(): {
32
- run: (command: string) => Promise<{
33
- exitCode: number;
34
- }>;
35
- };
36
+ declare function useShellOut(): ShellOutHandle;
36
37
 
37
38
  declare function useSpawn(): SpawnHandle;
38
39
 
39
- export { type SpawnHandle, type SpawnOptions, type SpawnOutputLine, type TerminalSize, useShellOut, useSpawn, useTerminalFocus, useTerminalSize };
40
+ export { type ShellOutHandle, type SpawnHandle, type SpawnOptions, type SpawnOutputLine, type TerminalSize, useShellOut, useSpawn, useTerminalFocus, useTerminalSize };
@@ -31,8 +31,9 @@ type TextInputProps = {
31
31
  onSubmit?: (value: string) => void;
32
32
  placeholder?: string;
33
33
  render?: (props: TextInputRenderProps) => React$1.ReactNode;
34
+ focusKey?: string;
34
35
  };
35
- declare function TextInput({ label, value, onChange, onSubmit, placeholder, render }: TextInputProps): react_jsx_runtime.JSX.Element;
36
+ declare function TextInput({ label, value, onChange, onSubmit, placeholder, render, focusKey }: TextInputProps): react_jsx_runtime.JSX.Element;
36
37
 
37
38
  type PaginatorStyle = 'dots' | 'arrows' | 'scrollbar' | 'counter' | 'none';
38
39
  type PaginatorProps = {
@@ -70,8 +71,9 @@ type SelectProps<T> = {
70
71
  paginatorStyle?: PaginatorStyle;
71
72
  wrap?: boolean;
72
73
  render?: (props: SelectRenderProps<T>) => React$1.ReactNode;
74
+ focusKey?: string;
73
75
  };
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;
76
+ 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
77
 
76
78
  type MultiSelectRenderProps<T> = {
77
79
  option: SelectOption<T>;
@@ -92,15 +94,17 @@ type MultiSelectProps<T> = {
92
94
  paginatorStyle?: PaginatorStyle;
93
95
  wrap?: boolean;
94
96
  render?: (props: MultiSelectRenderProps<T>) => React$1.ReactNode;
97
+ focusKey?: string;
95
98
  };
96
- declare function MultiSelect<T>({ options, value, onChange, onSubmit, onHighlight, label, direction, gap, maxVisible, paginatorStyle, wrap, render }: MultiSelectProps<T>): react_jsx_runtime.JSX.Element;
99
+ 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
100
 
98
101
  type ConfirmProps = {
99
102
  message: string;
100
103
  defaultValue?: boolean;
101
104
  onSubmit: (value: boolean) => void;
105
+ focusKey?: string;
102
106
  };
103
- declare function Confirm({ message, defaultValue, onSubmit }: ConfirmProps): react_jsx_runtime.JSX.Element;
107
+ declare function Confirm({ message, defaultValue, onSubmit, focusKey }: ConfirmProps): react_jsx_runtime.JSX.Element;
104
108
 
105
109
  type AutocompleteRenderProps<T> = {
106
110
  option: SelectOption<T>;
@@ -122,8 +126,9 @@ type AutocompleteProps<T> = {
122
126
  paginatorStyle?: PaginatorStyle;
123
127
  wrap?: boolean;
124
128
  render?: (props: AutocompleteRenderProps<T>) => React$1.ReactNode;
129
+ focusKey?: string;
125
130
  };
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;
131
+ 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
132
 
128
133
  type VirtualListRenderProps<T> = {
129
134
  item: T;
@@ -160,6 +165,7 @@ declare const Viewport: React$1.ForwardRefExoticComponent<Omit<BoxProps, "flexDi
160
165
  height: number;
161
166
  keybindings?: boolean;
162
167
  footer?: React$1.ReactNode;
168
+ focusKey?: string;
163
169
  } & React$1.RefAttributes<ViewportRef>>;
164
170
 
165
171
  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-A7BRQXWE.js";
7
+ } from "../chunk-74PBSWEK.js";
8
8
  import {
9
9
  CodeBlock
10
10
  } from "../chunk-SKSDNDQF.js";
@@ -136,8 +136,8 @@ function CommandPalette({ onClose, interactive = true, render }) {
136
136
  import { useReducer, useRef } from "react";
137
137
  import { Text as Text2 } from "ink";
138
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();
139
+ function TextInput({ label, value, onChange, onSubmit, placeholder, render, focusKey }) {
140
+ const focus = useFocusNode({ focusKey });
141
141
  const cursorRef = useRef(value.length);
142
142
  const [, forceRender] = useReducer((c) => c + 1, 0);
143
143
  const cursor = Math.min(cursorRef.current, value.length);
@@ -351,7 +351,8 @@ function Select({
351
351
  maxVisible,
352
352
  paginatorStyle,
353
353
  wrap = true,
354
- render
354
+ render,
355
+ focusKey
355
356
  }) {
356
357
  const seen = /* @__PURE__ */ new Set();
357
358
  for (const opt of options) {
@@ -361,7 +362,7 @@ function Select({
361
362
  }
362
363
  seen.add(key);
363
364
  }
364
- const focus = useFocusNode();
365
+ const focus = useFocusNode({ focusKey });
365
366
  const theme = useTheme();
366
367
  const [highlightIndex, setHighlightIndex] = useState3(0);
367
368
  const safeIndex = options.length === 0 ? -1 : Math.min(highlightIndex, options.length - 1);
@@ -438,7 +439,8 @@ function MultiSelect({
438
439
  maxVisible,
439
440
  paginatorStyle,
440
441
  wrap = true,
441
- render
442
+ render,
443
+ focusKey
442
444
  }) {
443
445
  const seen = /* @__PURE__ */ new Set();
444
446
  for (const opt of options) {
@@ -448,7 +450,7 @@ function MultiSelect({
448
450
  }
449
451
  seen.add(key);
450
452
  }
451
- const focus = useFocusNode();
453
+ const focus = useFocusNode({ focusKey });
452
454
  const theme = useTheme();
453
455
  const [highlightIndex, setHighlightIndex] = useState4(0);
454
456
  const [internalSelected, setInternalSelected] = useState4([]);
@@ -515,8 +517,8 @@ function MultiSelect({
515
517
  // src/ui/Confirm.tsx
516
518
  import { Text as Text6 } from "ink";
517
519
  import { jsxs as jsxs7 } from "react/jsx-runtime";
518
- function Confirm({ message, defaultValue = true, onSubmit }) {
519
- const focus = useFocusNode();
520
+ function Confirm({ message, defaultValue = true, onSubmit, focusKey }) {
521
+ const focus = useFocusNode({ focusKey });
520
522
  useKeybindings(focus, {
521
523
  y: () => onSubmit(true),
522
524
  n: () => onSubmit(false),
@@ -558,7 +560,8 @@ function Autocomplete({
558
560
  maxVisible,
559
561
  paginatorStyle,
560
562
  wrap = true,
561
- render
563
+ render,
564
+ focusKey
562
565
  }) {
563
566
  const seen = /* @__PURE__ */ new Set();
564
567
  for (const opt of options) {
@@ -568,7 +571,7 @@ function Autocomplete({
568
571
  }
569
572
  seen.add(key);
570
573
  }
571
- const focus = useFocusNode();
574
+ const focus = useFocusNode({ focusKey });
572
575
  const theme = useTheme();
573
576
  const [query, setQuery] = useState5("");
574
577
  const [highlightIndex, setHighlightIndex] = useState5(0);
@@ -718,8 +721,8 @@ function MeasurableItem({
718
721
  }, [index, onMeasure, children]);
719
722
  return /* @__PURE__ */ jsx8(Box7, { ref, flexShrink: 0, width: "100%", flexDirection: "column", children });
720
723
  }
721
- var Viewport = forwardRef(function Viewport2({ children, height, keybindings: enableKeybindings = true, footer, ...boxProps }, ref) {
722
- const focus = useFocusNode();
724
+ var Viewport = forwardRef(function Viewport2({ children, height, keybindings: enableKeybindings = true, footer, focusKey, ...boxProps }, ref) {
725
+ const focus = useFocusNode({ focusKey });
723
726
  const [scrollOffset, setScrollOffset] = useState6(0);
724
727
  const contentHeightRef = useRef4(0);
725
728
  const itemHeightsRef = useRef4({});
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "giggles",
3
- "version": "0.6.1",
3
+ "version": "0.7.0",
4
4
  "license": "MIT",
5
5
  "repository": {
6
6
  "type": "git",
@@ -40,10 +40,10 @@
40
40
  "scripts": {
41
41
  "build": "tsup",
42
42
  "build:watch": "nodemon --watch src --ext ts,tsx --exec tsup",
43
- "play": "tsx --watch",
44
- "record": "vhs",
43
+ "play": "f() { tsx --watch playground/examples/$1.tsx; }; f",
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"