giggles 0.3.0 → 0.3.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/README.md CHANGED
@@ -1,4 +1,4 @@
1
- [![CI](https://github.com/zion-off/giggles/actions/workflows/giggles-lint.yml/badge.svg)](https://github.com/zion-off/giggles/actions/workflows/giggles-lint.yml)
1
+ [![CI](https://github.com/zion-off/giggles/actions/workflows/giggles-ci.yml/badge.svg)](https://github.com/zion-off/giggles/actions/workflows/giggles-ci.yml)
2
2
  [![CD](https://github.com/zion-off/giggles/actions/workflows/giggles-cd.yml/badge.svg)](https://github.com/zion-off/giggles/actions/workflows/giggles-cd.yml)
3
3
  [![docs](https://img.shields.io/badge/docs-giggles.zzzzion.com-blue)](https://giggles.zzzzion.com)
4
4
 
@@ -0,0 +1,460 @@
1
+ // src/core/GigglesError.ts
2
+ var GigglesError = class extends Error {
3
+ constructor(message) {
4
+ super(`[giggles] ${message}`);
5
+ this.name = "GigglesError";
6
+ }
7
+ };
8
+
9
+ // src/core/input/useKeybindings.tsx
10
+ import { useEffect } from "react";
11
+
12
+ // src/core/input/InputContext.tsx
13
+ import { createContext, useCallback, useContext, useRef } from "react";
14
+ import { jsx } from "react/jsx-runtime";
15
+ var InputContext = createContext(null);
16
+ var InputProvider = ({ children }) => {
17
+ const bindingsRef = useRef(/* @__PURE__ */ new Map());
18
+ const trapNodeIdRef = useRef(null);
19
+ const registerKeybindings = useCallback((nodeId, bindings, options) => {
20
+ const entries = Object.entries(bindings).filter((entry) => entry[1] != null).map(([key, def]) => {
21
+ if (typeof def === "function") {
22
+ return [key, { handler: def }];
23
+ }
24
+ return [key, { handler: def.action, name: def.name, when: def.when }];
25
+ });
26
+ const registration = {
27
+ bindings: new Map(entries),
28
+ capture: (options == null ? void 0 : options.capture) ?? false,
29
+ onKeypress: options == null ? void 0 : options.onKeypress,
30
+ layer: options == null ? void 0 : options.layer
31
+ };
32
+ bindingsRef.current.set(nodeId, registration);
33
+ }, []);
34
+ const unregisterKeybindings = useCallback((nodeId) => {
35
+ bindingsRef.current.delete(nodeId);
36
+ }, []);
37
+ const getNodeBindings = useCallback((nodeId) => {
38
+ return bindingsRef.current.get(nodeId);
39
+ }, []);
40
+ const setTrap = useCallback((nodeId) => {
41
+ trapNodeIdRef.current = nodeId;
42
+ }, []);
43
+ const clearTrap = useCallback((nodeId) => {
44
+ if (trapNodeIdRef.current === nodeId) {
45
+ trapNodeIdRef.current = null;
46
+ }
47
+ }, []);
48
+ const getTrapNodeId = useCallback(() => {
49
+ return trapNodeIdRef.current;
50
+ }, []);
51
+ const getAllBindings = useCallback(() => {
52
+ const allBindings = [];
53
+ bindingsRef.current.forEach((nodeBindings, nodeId) => {
54
+ nodeBindings.bindings.forEach((entry, key) => {
55
+ allBindings.push({
56
+ nodeId,
57
+ key,
58
+ handler: entry.handler,
59
+ name: entry.name,
60
+ when: entry.when,
61
+ layer: nodeBindings.layer
62
+ });
63
+ });
64
+ });
65
+ return allBindings;
66
+ }, []);
67
+ return /* @__PURE__ */ jsx(
68
+ InputContext.Provider,
69
+ {
70
+ value: {
71
+ registerKeybindings,
72
+ unregisterKeybindings,
73
+ getNodeBindings,
74
+ setTrap,
75
+ clearTrap,
76
+ getTrapNodeId,
77
+ getAllBindings
78
+ },
79
+ children
80
+ }
81
+ );
82
+ };
83
+ function useInputContext() {
84
+ const context = useContext(InputContext);
85
+ if (!context) {
86
+ throw new GigglesError("useInputContext must be used within an InputProvider");
87
+ }
88
+ return context;
89
+ }
90
+
91
+ // src/core/input/useKeybindings.tsx
92
+ function useKeybindings(focus, bindings, options) {
93
+ const { registerKeybindings, unregisterKeybindings } = useInputContext();
94
+ registerKeybindings(focus.id, bindings, options);
95
+ useEffect(() => {
96
+ return () => unregisterKeybindings(focus.id);
97
+ }, [focus.id, unregisterKeybindings]);
98
+ }
99
+
100
+ // src/core/focus/FocusContext.tsx
101
+ import { createContext as createContext2, useCallback as useCallback2, useContext as useContext2, useRef as useRef2, useState } from "react";
102
+ import { jsx as jsx2 } from "react/jsx-runtime";
103
+ var FocusContext = createContext2(null);
104
+ var FocusProvider = ({ children }) => {
105
+ const nodesRef = useRef2(/* @__PURE__ */ new Map());
106
+ const pendingFocusFirstChildRef = useRef2(/* @__PURE__ */ new Set());
107
+ const [focusedId, setFocusedId] = useState(null);
108
+ const focusNode = useCallback2((id) => {
109
+ const nodes = nodesRef.current;
110
+ if (!nodes.has(id)) return;
111
+ setFocusedId((current) => {
112
+ if (current === id) return current;
113
+ return id;
114
+ });
115
+ }, []);
116
+ const focusFirstChild = useCallback2(
117
+ (parentId) => {
118
+ const nodes = nodesRef.current;
119
+ const parent = nodes.get(parentId);
120
+ if (parent && parent.childrenIds.length > 0) {
121
+ focusNode(parent.childrenIds[0]);
122
+ } else {
123
+ pendingFocusFirstChildRef.current.add(parentId);
124
+ }
125
+ },
126
+ [focusNode]
127
+ );
128
+ const registerNode = useCallback2(
129
+ (id, parentId) => {
130
+ const nodes = nodesRef.current;
131
+ const node = {
132
+ id,
133
+ parentId,
134
+ childrenIds: []
135
+ };
136
+ nodes.set(id, node);
137
+ if (parentId) {
138
+ const parent = nodes.get(parentId);
139
+ if (parent && !parent.childrenIds.includes(id)) {
140
+ const wasEmpty = parent.childrenIds.length === 0;
141
+ parent.childrenIds.push(id);
142
+ if (wasEmpty && pendingFocusFirstChildRef.current.has(parentId)) {
143
+ pendingFocusFirstChildRef.current.delete(parentId);
144
+ focusNode(id);
145
+ }
146
+ }
147
+ }
148
+ nodes.forEach((existingNode) => {
149
+ if (existingNode.parentId === id && !node.childrenIds.includes(existingNode.id)) {
150
+ node.childrenIds.push(existingNode.id);
151
+ }
152
+ });
153
+ if (nodes.size === 1) {
154
+ focusNode(id);
155
+ }
156
+ },
157
+ [focusNode]
158
+ );
159
+ const unregisterNode = useCallback2((id) => {
160
+ const nodes = nodesRef.current;
161
+ const node = nodes.get(id);
162
+ if (!node) return;
163
+ if (node.parentId) {
164
+ const parent = nodes.get(node.parentId);
165
+ if (parent) {
166
+ parent.childrenIds = parent.childrenIds.filter((childId) => childId !== id);
167
+ }
168
+ }
169
+ nodes.delete(id);
170
+ pendingFocusFirstChildRef.current.delete(id);
171
+ setFocusedId((current) => {
172
+ if (current !== id) return current;
173
+ return node.parentId ?? null;
174
+ });
175
+ }, []);
176
+ const isFocused = useCallback2(
177
+ (id) => {
178
+ return id === focusedId;
179
+ },
180
+ [focusedId]
181
+ );
182
+ const getFocusedId = useCallback2(() => {
183
+ return focusedId;
184
+ }, [focusedId]);
185
+ const getActiveBranchPath = useCallback2(() => {
186
+ if (!focusedId) return [];
187
+ const nodes = nodesRef.current;
188
+ const pathArray = [];
189
+ let node = focusedId;
190
+ while (node) {
191
+ pathArray.push(node);
192
+ const n = nodes.get(node);
193
+ node = (n == null ? void 0 : n.parentId) ?? null;
194
+ }
195
+ return pathArray;
196
+ }, [focusedId]);
197
+ const navigateSibling = useCallback2(
198
+ (direction, wrap = true) => {
199
+ const currentId = focusedId;
200
+ if (!currentId) return;
201
+ const nodes = nodesRef.current;
202
+ const currentNode = nodes.get(currentId);
203
+ if (!(currentNode == null ? void 0 : currentNode.parentId)) return;
204
+ const parent = nodes.get(currentNode.parentId);
205
+ if (!parent || parent.childrenIds.length === 0) return;
206
+ const siblings = parent.childrenIds;
207
+ const currentIndex = siblings.indexOf(currentId);
208
+ if (currentIndex === -1) return;
209
+ let nextIndex;
210
+ if (wrap) {
211
+ nextIndex = direction === "next" ? (currentIndex + 1) % siblings.length : (currentIndex - 1 + siblings.length) % siblings.length;
212
+ } else {
213
+ nextIndex = direction === "next" ? Math.min(currentIndex + 1, siblings.length - 1) : Math.max(currentIndex - 1, 0);
214
+ }
215
+ focusNode(siblings[nextIndex]);
216
+ },
217
+ [focusedId, focusNode]
218
+ );
219
+ return /* @__PURE__ */ jsx2(
220
+ FocusContext.Provider,
221
+ {
222
+ value: {
223
+ registerNode,
224
+ unregisterNode,
225
+ focusNode,
226
+ focusFirstChild,
227
+ isFocused,
228
+ getFocusedId,
229
+ getActiveBranchPath,
230
+ navigateSibling
231
+ },
232
+ children
233
+ }
234
+ );
235
+ };
236
+ var FocusNodeContext = createContext2(null);
237
+ var useFocusContext = () => {
238
+ const context = useContext2(FocusContext);
239
+ if (!context) {
240
+ throw new GigglesError("useFocusContext must be used within a FocusProvider");
241
+ }
242
+ return context;
243
+ };
244
+
245
+ // src/core/input/useKeybindingRegistry.ts
246
+ function useKeybindingRegistry(focus) {
247
+ const { getAllBindings, getTrapNodeId } = useInputContext();
248
+ const { getActiveBranchPath } = useFocusContext();
249
+ const all = getAllBindings().filter((b) => b.name != null);
250
+ const branchPath = getActiveBranchPath();
251
+ const branchSet = new Set(branchPath);
252
+ const trapNodeId = getTrapNodeId();
253
+ const withinTrapSet = (() => {
254
+ if (!trapNodeId) return null;
255
+ const trapIndex = branchPath.indexOf(trapNodeId);
256
+ return trapIndex >= 0 ? new Set(branchPath.slice(0, trapIndex + 1)) : null;
257
+ })();
258
+ const available = all.filter((b) => {
259
+ if (b.when === "mounted") return withinTrapSet ? withinTrapSet.has(b.nodeId) : true;
260
+ return (withinTrapSet ?? branchSet).has(b.nodeId);
261
+ });
262
+ const local = focus ? all.filter((b) => b.nodeId === focus.id) : [];
263
+ return { all, available, local };
264
+ }
265
+
266
+ // src/core/input/FocusTrap.tsx
267
+ import { useEffect as useEffect4, useRef as useRef4 } from "react";
268
+
269
+ // src/core/focus/FocusGroup.tsx
270
+ import { useCallback as useCallback3, useEffect as useEffect3, useMemo, useRef as useRef3 } from "react";
271
+
272
+ // src/core/input/InputRouter.tsx
273
+ import { useInput } from "ink";
274
+
275
+ // src/core/input/normalizeKey.ts
276
+ function normalizeKey(input, key) {
277
+ if (input === "\x1B[I" || input === "\x1B[O") return "";
278
+ if (key.downArrow) return "down";
279
+ if (key.upArrow) return "up";
280
+ if (key.leftArrow) return "left";
281
+ if (key.rightArrow) return "right";
282
+ if (key.return) return "enter";
283
+ if (key.escape) return "escape";
284
+ if (key.tab) return "tab";
285
+ if (key.backspace || key.delete) return "backspace";
286
+ if (key.pageUp) return "pageup";
287
+ if (key.pageDown) return "pagedown";
288
+ if (key.home) return "home";
289
+ if (key.end) return "end";
290
+ if (key.ctrl && input.length === 1) {
291
+ return `ctrl+${input}`;
292
+ }
293
+ return input;
294
+ }
295
+
296
+ // src/core/input/InputRouter.tsx
297
+ import { Fragment, jsx as jsx3 } from "react/jsx-runtime";
298
+ function InputRouter({ children }) {
299
+ const { getFocusedId, getActiveBranchPath } = useFocusContext();
300
+ const { getNodeBindings, getTrapNodeId, getAllBindings } = useInputContext();
301
+ useInput((input, key) => {
302
+ const focusedId = getFocusedId();
303
+ if (!focusedId) return;
304
+ const path = getActiveBranchPath();
305
+ const trapNodeId = getTrapNodeId();
306
+ const keyName = normalizeKey(input, key);
307
+ if (!keyName) return;
308
+ for (const nodeId of path) {
309
+ const nodeBindings = getNodeBindings(nodeId);
310
+ if (!nodeBindings) continue;
311
+ const entry = nodeBindings.bindings.get(keyName);
312
+ if (entry && entry.when !== "mounted") {
313
+ entry.handler(input, key);
314
+ return;
315
+ }
316
+ if (nodeBindings.capture && nodeBindings.onKeypress) {
317
+ nodeBindings.onKeypress(input, key);
318
+ return;
319
+ }
320
+ if (nodeId === trapNodeId) {
321
+ return;
322
+ }
323
+ }
324
+ for (const binding of getAllBindings()) {
325
+ if (binding.key === keyName && binding.when === "mounted") {
326
+ binding.handler(input, key);
327
+ return;
328
+ }
329
+ }
330
+ });
331
+ return /* @__PURE__ */ jsx3(Fragment, { children });
332
+ }
333
+
334
+ // src/core/focus/FocusBindContext.tsx
335
+ import { createContext as createContext3 } from "react";
336
+ var FocusBindContext = createContext3(null);
337
+
338
+ // src/core/focus/useFocus.ts
339
+ import { useContext as useContext3, useEffect as useEffect2, useId } from "react";
340
+ var useFocus = (id) => {
341
+ const nodeId = useId();
342
+ const parentId = useContext3(FocusNodeContext);
343
+ const bindContext = useContext3(FocusBindContext);
344
+ const { focusNode, registerNode, unregisterNode, isFocused } = useFocusContext();
345
+ useEffect2(() => {
346
+ registerNode(nodeId, parentId);
347
+ if (id && bindContext) {
348
+ bindContext.register(id, nodeId);
349
+ }
350
+ return () => {
351
+ unregisterNode(nodeId);
352
+ if (id && bindContext) {
353
+ bindContext.unregister(id);
354
+ }
355
+ };
356
+ }, [nodeId, parentId, id, bindContext, registerNode, unregisterNode]);
357
+ return {
358
+ id: nodeId,
359
+ focused: isFocused(nodeId),
360
+ focus: () => focusNode(nodeId)
361
+ };
362
+ };
363
+
364
+ // src/core/focus/FocusGroup.tsx
365
+ import { jsx as jsx4 } from "react/jsx-runtime";
366
+ function FocusGroup({
367
+ children,
368
+ direction = "vertical",
369
+ value,
370
+ wrap = true,
371
+ navigable = true,
372
+ keybindings: customBindings
373
+ }) {
374
+ const focus = useFocus();
375
+ const { focusNode, navigateSibling } = useFocusContext();
376
+ const bindMapRef = useRef3(/* @__PURE__ */ new Map());
377
+ const register = useCallback3((logicalId, nodeId) => {
378
+ if (bindMapRef.current.has(logicalId)) {
379
+ throw new GigglesError(`FocusGroup: Duplicate id "${logicalId}". Each child must have a unique id.`);
380
+ }
381
+ bindMapRef.current.set(logicalId, nodeId);
382
+ }, []);
383
+ const unregister = useCallback3((logicalId) => {
384
+ bindMapRef.current.delete(logicalId);
385
+ }, []);
386
+ useEffect3(() => {
387
+ if (value) {
388
+ const nodeId = bindMapRef.current.get(value);
389
+ if (nodeId) {
390
+ focusNode(nodeId);
391
+ }
392
+ }
393
+ }, [value, focusNode]);
394
+ const bindContextValue = useMemo(() => value ? { register, unregister } : null, [value, register, unregister]);
395
+ const navigationKeys = useMemo(() => {
396
+ if (!navigable) return {};
397
+ const next = () => navigateSibling("next", wrap);
398
+ const prev = () => navigateSibling("prev", wrap);
399
+ return direction === "vertical" ? {
400
+ j: next,
401
+ k: prev,
402
+ down: next,
403
+ up: prev
404
+ } : {
405
+ l: next,
406
+ h: prev,
407
+ right: next,
408
+ left: prev
409
+ };
410
+ }, [navigable, direction, wrap, navigateSibling]);
411
+ const mergedBindings = useMemo(
412
+ () => ({ ...navigationKeys, ...customBindings }),
413
+ [navigationKeys, customBindings]
414
+ );
415
+ useKeybindings(focus, mergedBindings);
416
+ return /* @__PURE__ */ jsx4(FocusNodeContext.Provider, { value: focus.id, children: /* @__PURE__ */ jsx4(FocusBindContext.Provider, { value: bindContextValue, children }) });
417
+ }
418
+
419
+ // src/core/focus/useFocusState.ts
420
+ import { useState as useState2 } from "react";
421
+ function useFocusState(initial) {
422
+ const [focused, setFocused] = useState2(initial);
423
+ return [focused, setFocused];
424
+ }
425
+
426
+ // src/core/input/FocusTrap.tsx
427
+ import { jsx as jsx5 } from "react/jsx-runtime";
428
+ function FocusTrap({ children }) {
429
+ const { id } = useFocus();
430
+ const { setTrap, clearTrap } = useInputContext();
431
+ const { focusFirstChild, getFocusedId, focusNode } = useFocusContext();
432
+ const previousFocusRef = useRef4(getFocusedId());
433
+ useEffect4(() => {
434
+ const previousFocus = previousFocusRef.current;
435
+ setTrap(id);
436
+ focusFirstChild(id);
437
+ return () => {
438
+ clearTrap(id);
439
+ if (previousFocus) {
440
+ focusNode(previousFocus);
441
+ }
442
+ };
443
+ }, [id]);
444
+ return /* @__PURE__ */ jsx5(FocusNodeContext.Provider, { value: id, children });
445
+ }
446
+
447
+ export {
448
+ GigglesError,
449
+ FocusProvider,
450
+ FocusNodeContext,
451
+ useFocusContext,
452
+ InputProvider,
453
+ InputRouter,
454
+ useKeybindings,
455
+ useKeybindingRegistry,
456
+ FocusTrap,
457
+ useFocus,
458
+ FocusGroup,
459
+ useFocusState
460
+ };
package/dist/index.d.ts CHANGED
@@ -1,7 +1,8 @@
1
1
  import * as react_jsx_runtime from 'react/jsx-runtime';
2
2
  import * as React$1 from 'react';
3
3
  import React__default from 'react';
4
- import { Key } from 'ink';
4
+ import { K as Keybindings, a as KeybindingOptions, R as RegisteredKeybinding } from './types-ClgDW3fy.js';
5
+ export { b as KeyHandler } from './types-ClgDW3fy.js';
5
6
  export { Key } from 'ink';
6
7
 
7
8
  declare class GigglesError extends Error {
@@ -13,18 +14,15 @@ type GigglesProviderProps = {
13
14
  };
14
15
  declare function GigglesProvider({ children }: GigglesProviderProps): react_jsx_runtime.JSX.Element;
15
16
 
16
- type KeyHandler = (input: string, key: Key) => void;
17
- type SpecialKey = 'up' | 'down' | 'left' | 'right' | 'enter' | 'escape' | 'tab' | 'backspace' | 'delete' | 'pageup' | 'pagedown' | 'home' | 'end';
18
- type KeyName = SpecialKey | (string & {});
19
- type Keybindings = Partial<Record<KeyName, KeyHandler>>;
20
- type KeybindingOptions = {
21
- capture?: boolean;
22
- onKeypress?: (input: string, key: Key) => void;
23
- layer?: string;
24
- };
25
-
26
17
  declare function useKeybindings(focus: FocusHandle, bindings: Keybindings, options?: KeybindingOptions): void;
27
18
 
19
+ type KeybindingRegistry = {
20
+ all: RegisteredKeybinding[];
21
+ available: RegisteredKeybinding[];
22
+ local: RegisteredKeybinding[];
23
+ };
24
+ declare function useKeybindingRegistry(focus?: FocusHandle): KeybindingRegistry;
25
+
28
26
  type FocusTrapProps = {
29
27
  children: React.ReactNode;
30
28
  };
@@ -81,4 +79,4 @@ type NavigationContextValue = {
81
79
 
82
80
  declare const useNavigation: () => NavigationContextValue;
83
81
 
84
- export { FocusGroup, type FocusHandle, FocusTrap, GigglesError, GigglesProvider, type KeyHandler, type KeybindingOptions, type Keybindings, type NavigationContextValue, Router, Screen, useFocus, useFocusState, useKeybindings, useNavigation };
82
+ export { FocusGroup, type FocusHandle, FocusTrap, GigglesError, GigglesProvider, KeybindingOptions, type KeybindingRegistry, Keybindings, type NavigationContextValue, RegisteredKeybinding, Router, Screen, useFocus, useFocusState, useKeybindingRegistry, useKeybindings, useNavigation };
package/dist/index.js CHANGED
@@ -1,420 +1,29 @@
1
1
  import {
2
2
  AlternateScreen
3
3
  } from "./chunk-7PDVDYFB.js";
4
-
5
- // src/core/GigglesError.ts
6
- var GigglesError = class extends Error {
7
- constructor(message) {
8
- super(`[giggles] ${message}`);
9
- this.name = "GigglesError";
10
- }
11
- };
12
-
13
- // src/core/focus/FocusContext.tsx
14
- import { createContext, useCallback, useContext, useRef, useState } from "react";
15
- import { jsx } from "react/jsx-runtime";
16
- var FocusContext = createContext(null);
17
- var FocusProvider = ({ children }) => {
18
- const nodesRef = useRef(/* @__PURE__ */ new Map());
19
- const pendingFocusFirstChildRef = useRef(/* @__PURE__ */ new Set());
20
- const [focusedId, setFocusedId] = useState(null);
21
- const focusNode = useCallback((id) => {
22
- const nodes = nodesRef.current;
23
- if (!nodes.has(id)) return;
24
- setFocusedId((current) => {
25
- if (current === id) return current;
26
- return id;
27
- });
28
- }, []);
29
- const focusFirstChild = useCallback(
30
- (parentId) => {
31
- const nodes = nodesRef.current;
32
- const parent = nodes.get(parentId);
33
- if (parent && parent.childrenIds.length > 0) {
34
- focusNode(parent.childrenIds[0]);
35
- } else {
36
- pendingFocusFirstChildRef.current.add(parentId);
37
- }
38
- },
39
- [focusNode]
40
- );
41
- const registerNode = useCallback(
42
- (id, parentId) => {
43
- const nodes = nodesRef.current;
44
- const node = {
45
- id,
46
- parentId,
47
- childrenIds: []
48
- };
49
- nodes.set(id, node);
50
- if (parentId) {
51
- const parent = nodes.get(parentId);
52
- if (parent && !parent.childrenIds.includes(id)) {
53
- const wasEmpty = parent.childrenIds.length === 0;
54
- parent.childrenIds.push(id);
55
- if (wasEmpty && pendingFocusFirstChildRef.current.has(parentId)) {
56
- pendingFocusFirstChildRef.current.delete(parentId);
57
- focusNode(id);
58
- }
59
- }
60
- }
61
- nodes.forEach((existingNode) => {
62
- if (existingNode.parentId === id && !node.childrenIds.includes(existingNode.id)) {
63
- node.childrenIds.push(existingNode.id);
64
- }
65
- });
66
- if (nodes.size === 1) {
67
- focusNode(id);
68
- }
69
- },
70
- [focusNode]
71
- );
72
- const unregisterNode = useCallback((id) => {
73
- const nodes = nodesRef.current;
74
- const node = nodes.get(id);
75
- if (!node) return;
76
- if (node.parentId) {
77
- const parent = nodes.get(node.parentId);
78
- if (parent) {
79
- parent.childrenIds = parent.childrenIds.filter((childId) => childId !== id);
80
- }
81
- }
82
- nodes.delete(id);
83
- pendingFocusFirstChildRef.current.delete(id);
84
- setFocusedId((current) => {
85
- if (current !== id) return current;
86
- return node.parentId ?? null;
87
- });
88
- }, []);
89
- const isFocused = useCallback(
90
- (id) => {
91
- return id === focusedId;
92
- },
93
- [focusedId]
94
- );
95
- const getFocusedId = useCallback(() => {
96
- return focusedId;
97
- }, [focusedId]);
98
- const getActiveBranchPath = useCallback(() => {
99
- if (!focusedId) return [];
100
- const nodes = nodesRef.current;
101
- const pathArray = [];
102
- let node = focusedId;
103
- while (node) {
104
- pathArray.push(node);
105
- const n = nodes.get(node);
106
- node = (n == null ? void 0 : n.parentId) ?? null;
107
- }
108
- return pathArray;
109
- }, [focusedId]);
110
- const navigateSibling = useCallback(
111
- (direction, wrap = true) => {
112
- const currentId = focusedId;
113
- if (!currentId) return;
114
- const nodes = nodesRef.current;
115
- const currentNode = nodes.get(currentId);
116
- if (!(currentNode == null ? void 0 : currentNode.parentId)) return;
117
- const parent = nodes.get(currentNode.parentId);
118
- if (!parent || parent.childrenIds.length === 0) return;
119
- const siblings = parent.childrenIds;
120
- const currentIndex = siblings.indexOf(currentId);
121
- if (currentIndex === -1) return;
122
- let nextIndex;
123
- if (wrap) {
124
- nextIndex = direction === "next" ? (currentIndex + 1) % siblings.length : (currentIndex - 1 + siblings.length) % siblings.length;
125
- } else {
126
- nextIndex = direction === "next" ? Math.min(currentIndex + 1, siblings.length - 1) : Math.max(currentIndex - 1, 0);
127
- }
128
- focusNode(siblings[nextIndex]);
129
- },
130
- [focusedId, focusNode]
131
- );
132
- return /* @__PURE__ */ jsx(
133
- FocusContext.Provider,
134
- {
135
- value: {
136
- registerNode,
137
- unregisterNode,
138
- focusNode,
139
- focusFirstChild,
140
- isFocused,
141
- getFocusedId,
142
- getActiveBranchPath,
143
- navigateSibling
144
- },
145
- children
146
- }
147
- );
148
- };
149
- var FocusNodeContext = createContext(null);
150
- var useFocusContext = () => {
151
- const context = useContext(FocusContext);
152
- if (!context) {
153
- throw new GigglesError("useFocusContext must be used within a FocusProvider");
154
- }
155
- return context;
156
- };
157
-
158
- // src/core/focus/FocusGroup.tsx
159
- import { useCallback as useCallback3, useEffect as useEffect4, useMemo, useRef as useRef4 } from "react";
160
-
161
- // src/core/input/InputContext.tsx
162
- import { createContext as createContext2, useCallback as useCallback2, useContext as useContext2, useRef as useRef2 } from "react";
163
- import { jsx as jsx2 } from "react/jsx-runtime";
164
- var InputContext = createContext2(null);
165
- var InputProvider = ({ children }) => {
166
- const bindingsRef = useRef2(/* @__PURE__ */ new Map());
167
- const trapNodeIdRef = useRef2(null);
168
- const registerKeybindings = useCallback2((nodeId, bindings, options) => {
169
- const registration = {
170
- bindings: new Map(Object.entries(bindings).filter((entry) => entry[1] != null)),
171
- capture: (options == null ? void 0 : options.capture) ?? false,
172
- onKeypress: options == null ? void 0 : options.onKeypress,
173
- layer: options == null ? void 0 : options.layer
174
- };
175
- bindingsRef.current.set(nodeId, registration);
176
- }, []);
177
- const unregisterKeybindings = useCallback2((nodeId) => {
178
- bindingsRef.current.delete(nodeId);
179
- }, []);
180
- const getNodeBindings = useCallback2((nodeId) => {
181
- return bindingsRef.current.get(nodeId);
182
- }, []);
183
- const setTrap = useCallback2((nodeId) => {
184
- trapNodeIdRef.current = nodeId;
185
- }, []);
186
- const clearTrap = useCallback2((nodeId) => {
187
- if (trapNodeIdRef.current === nodeId) {
188
- trapNodeIdRef.current = null;
189
- }
190
- }, []);
191
- const getTrapNodeId = useCallback2(() => {
192
- return trapNodeIdRef.current;
193
- }, []);
194
- const getAllBindings = useCallback2(() => {
195
- const allBindings = [];
196
- bindingsRef.current.forEach((nodeBindings, nodeId) => {
197
- nodeBindings.bindings.forEach((handler, key) => {
198
- allBindings.push({
199
- nodeId,
200
- key,
201
- handler,
202
- layer: nodeBindings.layer
203
- });
204
- });
205
- });
206
- return allBindings;
207
- }, []);
208
- return /* @__PURE__ */ jsx2(
209
- InputContext.Provider,
210
- {
211
- value: {
212
- registerKeybindings,
213
- unregisterKeybindings,
214
- getNodeBindings,
215
- setTrap,
216
- clearTrap,
217
- getTrapNodeId,
218
- getAllBindings
219
- },
220
- children
221
- }
222
- );
223
- };
224
- function useInputContext() {
225
- const context = useContext2(InputContext);
226
- if (!context) {
227
- throw new GigglesError("useInputContext must be used within an InputProvider");
228
- }
229
- return context;
230
- }
231
-
232
- // src/core/input/InputRouter.tsx
233
- import { useInput } from "ink";
234
-
235
- // src/core/input/normalizeKey.ts
236
- function normalizeKey(input, key) {
237
- if (input === "\x1B[I" || input === "\x1B[O") return "";
238
- if (key.downArrow) return "down";
239
- if (key.upArrow) return "up";
240
- if (key.leftArrow) return "left";
241
- if (key.rightArrow) return "right";
242
- if (key.return) return "enter";
243
- if (key.escape) return "escape";
244
- if (key.tab) return "tab";
245
- if (key.backspace) return "backspace";
246
- if (key.delete) return "delete";
247
- if (key.pageUp) return "pageup";
248
- if (key.pageDown) return "pagedown";
249
- if (key.home) return "home";
250
- if (key.end) return "end";
251
- return input;
252
- }
253
-
254
- // src/core/input/InputRouter.tsx
255
- import { Fragment, jsx as jsx3 } from "react/jsx-runtime";
256
- function InputRouter({ children }) {
257
- const { getFocusedId, getActiveBranchPath } = useFocusContext();
258
- const { getNodeBindings, getTrapNodeId } = useInputContext();
259
- useInput((input, key) => {
260
- const focusedId = getFocusedId();
261
- if (!focusedId) return;
262
- const path = getActiveBranchPath();
263
- const trapNodeId = getTrapNodeId();
264
- const keyName = normalizeKey(input, key);
265
- if (!keyName) return;
266
- for (const nodeId of path) {
267
- const nodeBindings = getNodeBindings(nodeId);
268
- if (!nodeBindings) continue;
269
- const handler = nodeBindings.bindings.get(keyName);
270
- if (handler) {
271
- handler(input, key);
272
- return;
273
- }
274
- if (nodeBindings.capture && nodeBindings.onKeypress) {
275
- nodeBindings.onKeypress(input, key);
276
- return;
277
- }
278
- if (nodeId === trapNodeId) {
279
- return;
280
- }
281
- }
282
- });
283
- return /* @__PURE__ */ jsx3(Fragment, { children });
284
- }
285
-
286
- // src/core/input/useKeybindings.tsx
287
- import { useEffect } from "react";
288
- function useKeybindings(focus, bindings, options) {
289
- const { registerKeybindings, unregisterKeybindings } = useInputContext();
290
- registerKeybindings(focus.id, bindings, options);
291
- useEffect(() => {
292
- return () => unregisterKeybindings(focus.id);
293
- }, [focus.id, unregisterKeybindings]);
294
- }
295
-
296
- // src/core/input/FocusTrap.tsx
297
- import { useEffect as useEffect2, useRef as useRef3 } from "react";
298
- import { jsx as jsx4 } from "react/jsx-runtime";
299
- function FocusTrap({ children }) {
300
- const { id } = useFocus();
301
- const { setTrap, clearTrap } = useInputContext();
302
- const { focusFirstChild, getFocusedId, focusNode } = useFocusContext();
303
- const previousFocusRef = useRef3(getFocusedId());
304
- useEffect2(() => {
305
- const previousFocus = previousFocusRef.current;
306
- setTrap(id);
307
- focusFirstChild(id);
308
- return () => {
309
- clearTrap(id);
310
- if (previousFocus) {
311
- focusNode(previousFocus);
312
- }
313
- };
314
- }, [id]);
315
- return /* @__PURE__ */ jsx4(FocusNodeContext.Provider, { value: id, children });
316
- }
317
-
318
- // src/core/focus/FocusBindContext.tsx
319
- import { createContext as createContext3 } from "react";
320
- var FocusBindContext = createContext3(null);
321
-
322
- // src/core/focus/useFocus.ts
323
- import { useContext as useContext3, useEffect as useEffect3, useId } from "react";
324
- var useFocus = (id) => {
325
- const nodeId = useId();
326
- const parentId = useContext3(FocusNodeContext);
327
- const bindContext = useContext3(FocusBindContext);
328
- const { focusNode, registerNode, unregisterNode, isFocused } = useFocusContext();
329
- useEffect3(() => {
330
- registerNode(nodeId, parentId);
331
- if (id && bindContext) {
332
- bindContext.register(id, nodeId);
333
- }
334
- return () => {
335
- unregisterNode(nodeId);
336
- if (id && bindContext) {
337
- bindContext.unregister(id);
338
- }
339
- };
340
- }, [nodeId, parentId, id, bindContext, registerNode, unregisterNode]);
341
- return {
342
- id: nodeId,
343
- focused: isFocused(nodeId),
344
- focus: () => focusNode(nodeId)
345
- };
346
- };
347
-
348
- // src/core/focus/FocusGroup.tsx
349
- import { jsx as jsx5 } from "react/jsx-runtime";
350
- function FocusGroup({
351
- children,
352
- direction = "vertical",
353
- value,
354
- wrap = true,
355
- navigable = true,
356
- keybindings: customBindings
357
- }) {
358
- const focus = useFocus();
359
- const { focusNode, navigateSibling } = useFocusContext();
360
- const bindMapRef = useRef4(/* @__PURE__ */ new Map());
361
- const register = useCallback3((logicalId, nodeId) => {
362
- if (bindMapRef.current.has(logicalId)) {
363
- throw new GigglesError(`FocusGroup: Duplicate id "${logicalId}". Each child must have a unique id.`);
364
- }
365
- bindMapRef.current.set(logicalId, nodeId);
366
- }, []);
367
- const unregister = useCallback3((logicalId) => {
368
- bindMapRef.current.delete(logicalId);
369
- }, []);
370
- useEffect4(() => {
371
- if (value) {
372
- const nodeId = bindMapRef.current.get(value);
373
- if (nodeId) {
374
- focusNode(nodeId);
375
- }
376
- }
377
- }, [value, focusNode]);
378
- const bindContextValue = useMemo(() => value ? { register, unregister } : null, [value, register, unregister]);
379
- const navigationKeys = useMemo(() => {
380
- if (!navigable) return {};
381
- const next = () => navigateSibling("next", wrap);
382
- const prev = () => navigateSibling("prev", wrap);
383
- return direction === "vertical" ? {
384
- j: next,
385
- k: prev,
386
- down: next,
387
- up: prev
388
- } : {
389
- l: next,
390
- h: prev,
391
- right: next,
392
- left: prev
393
- };
394
- }, [navigable, direction, wrap, navigateSibling]);
395
- const mergedBindings = useMemo(
396
- () => ({ ...navigationKeys, ...customBindings }),
397
- [navigationKeys, customBindings]
398
- );
399
- useKeybindings(focus, mergedBindings);
400
- return /* @__PURE__ */ jsx5(FocusNodeContext.Provider, { value: focus.id, children: /* @__PURE__ */ jsx5(FocusBindContext.Provider, { value: bindContextValue, children }) });
401
- }
402
-
403
- // src/core/focus/useFocusState.ts
404
- import { useState as useState2 } from "react";
405
- function useFocusState(initial) {
406
- const [focused, setFocused] = useState2(initial);
407
- return [focused, setFocused];
408
- }
4
+ import {
5
+ FocusGroup,
6
+ FocusNodeContext,
7
+ FocusProvider,
8
+ FocusTrap,
9
+ GigglesError,
10
+ InputProvider,
11
+ InputRouter,
12
+ useFocus,
13
+ useFocusContext,
14
+ useFocusState,
15
+ useKeybindingRegistry,
16
+ useKeybindings
17
+ } from "./chunk-4LED4GXQ.js";
409
18
 
410
19
  // src/core/GigglesProvider.tsx
411
- import { jsx as jsx6 } from "react/jsx-runtime";
20
+ import { jsx } from "react/jsx-runtime";
412
21
  function GigglesProvider({ children }) {
413
- return /* @__PURE__ */ jsx6(AlternateScreen, { children: /* @__PURE__ */ jsx6(FocusProvider, { children: /* @__PURE__ */ jsx6(InputProvider, { children: /* @__PURE__ */ jsx6(InputRouter, { children }) }) }) });
22
+ return /* @__PURE__ */ jsx(AlternateScreen, { children: /* @__PURE__ */ jsx(FocusProvider, { children: /* @__PURE__ */ jsx(InputProvider, { children: /* @__PURE__ */ jsx(InputRouter, { children }) }) }) });
414
23
  }
415
24
 
416
25
  // src/core/router/Router.tsx
417
- import React4, { useCallback as useCallback4, useReducer, useRef as useRef6 } from "react";
26
+ import React2, { useCallback, useReducer, useRef as useRef2 } from "react";
418
27
 
419
28
  // src/core/router/Screen.tsx
420
29
  function Screen(_props) {
@@ -422,14 +31,14 @@ function Screen(_props) {
422
31
  }
423
32
 
424
33
  // src/core/router/ScreenEntry.tsx
425
- import React3, { useEffect as useEffect5, useId as useId2, useMemo as useMemo2, useRef as useRef5 } from "react";
34
+ import React, { useEffect, useId, useMemo, useRef } from "react";
426
35
  import { Box } from "ink";
427
36
 
428
37
  // src/core/router/NavigationContext.tsx
429
- import { createContext as createContext4, useContext as useContext4 } from "react";
430
- var NavigationContext = createContext4(null);
38
+ import { createContext, useContext } from "react";
39
+ var NavigationContext = createContext(null);
431
40
  var useNavigation = () => {
432
- const context = useContext4(NavigationContext);
41
+ const context = useContext(NavigationContext);
433
42
  if (!context) {
434
43
  throw new GigglesError("useNavigation must be used within a Router");
435
44
  }
@@ -437,7 +46,7 @@ var useNavigation = () => {
437
46
  };
438
47
 
439
48
  // src/core/router/ScreenEntry.tsx
440
- import { jsx as jsx7 } from "react/jsx-runtime";
49
+ import { jsx as jsx2 } from "react/jsx-runtime";
441
50
  function ScreenEntry({
442
51
  entry,
443
52
  isTop,
@@ -449,18 +58,18 @@ function ScreenEntry({
449
58
  replace,
450
59
  reset
451
60
  }) {
452
- const screenNodeId = useId2();
453
- const parentId = React3.useContext(FocusNodeContext);
61
+ const screenNodeId = useId();
62
+ const parentId = React.useContext(FocusNodeContext);
454
63
  const { registerNode, unregisterNode, focusFirstChild, focusNode, getFocusedId } = useFocusContext();
455
- const lastFocusedChildRef = useRef5(null);
456
- const wasTopRef = useRef5(isTop);
457
- useEffect5(() => {
64
+ const lastFocusedChildRef = useRef(null);
65
+ const wasTopRef = useRef(isTop);
66
+ useEffect(() => {
458
67
  registerNode(screenNodeId, parentId);
459
68
  return () => {
460
69
  unregisterNode(screenNodeId);
461
70
  };
462
71
  }, [screenNodeId, parentId, registerNode, unregisterNode]);
463
- useEffect5(() => {
72
+ useEffect(() => {
464
73
  if (!wasTopRef.current && isTop) {
465
74
  const saved = restoreFocus ? lastFocusedChildRef.current : null;
466
75
  if (saved) {
@@ -475,15 +84,15 @@ function ScreenEntry({
475
84
  }
476
85
  wasTopRef.current = isTop;
477
86
  }, [isTop, screenNodeId, restoreFocus, focusFirstChild, focusNode, getFocusedId]);
478
- const value = useMemo2(
87
+ const value = useMemo(
479
88
  () => ({ currentRoute: entry, active: isTop, canGoBack, push, pop, replace, reset }),
480
89
  [entry, isTop, canGoBack, push, pop, replace, reset]
481
90
  );
482
- return /* @__PURE__ */ jsx7(NavigationContext.Provider, { value, children: /* @__PURE__ */ jsx7(FocusNodeContext.Provider, { value: screenNodeId, children: /* @__PURE__ */ jsx7(Box, { display: isTop ? "flex" : "none", children: /* @__PURE__ */ jsx7(Component, { ...entry.params }) }) }) });
91
+ return /* @__PURE__ */ jsx2(NavigationContext.Provider, { value, children: /* @__PURE__ */ jsx2(FocusNodeContext.Provider, { value: screenNodeId, children: /* @__PURE__ */ jsx2(Box, { display: isTop ? "flex" : "none", children: /* @__PURE__ */ jsx2(Component, { ...entry.params }) }) }) });
483
92
  }
484
93
 
485
94
  // src/core/router/Router.tsx
486
- import { Fragment as Fragment2, jsx as jsx8 } from "react/jsx-runtime";
95
+ import { Fragment, jsx as jsx3 } from "react/jsx-runtime";
487
96
  function routerReducer(stack, action) {
488
97
  switch (action.type) {
489
98
  case "push":
@@ -497,11 +106,11 @@ function routerReducer(stack, action) {
497
106
  }
498
107
  }
499
108
  function Router({ children, initialScreen, initialParams, restoreFocus = true }) {
500
- const screenId = useRef6(0);
501
- const routes = React4.Children.toArray(children).filter((child) => React4.isValidElement(child) && child.type === Screen).map((child) => child.props);
502
- const screenNamesRef = useRef6(/* @__PURE__ */ new Set());
109
+ const screenId = useRef2(0);
110
+ const routes = React2.Children.toArray(children).filter((child) => React2.isValidElement(child) && child.type === Screen).map((child) => child.props);
111
+ const screenNamesRef = useRef2(/* @__PURE__ */ new Set());
503
112
  screenNamesRef.current = new Set(routes.map((r) => r.name));
504
- const assertScreen = useCallback4((name) => {
113
+ const assertScreen = useCallback((name) => {
505
114
  if (!screenNamesRef.current.has(name)) {
506
115
  throw new GigglesError(
507
116
  `Screen "${name}" is not registered. Available screens: ${[...screenNamesRef.current].join(", ")}`
@@ -516,24 +125,24 @@ function Router({ children, initialScreen, initialParams, restoreFocus = true })
516
125
  }
517
126
  return [{ id: screenId.current++, name, params: initialParams }];
518
127
  });
519
- const push = useCallback4(
128
+ const push = useCallback(
520
129
  (name, params) => {
521
130
  assertScreen(name);
522
131
  dispatch({ type: "push", route: { id: screenId.current++, name, params } });
523
132
  },
524
133
  [assertScreen]
525
134
  );
526
- const pop = useCallback4(() => {
135
+ const pop = useCallback(() => {
527
136
  dispatch({ type: "pop" });
528
137
  }, []);
529
- const replace = useCallback4(
138
+ const replace = useCallback(
530
139
  (name, params) => {
531
140
  assertScreen(name);
532
141
  dispatch({ type: "replace", route: { id: screenId.current++, name, params } });
533
142
  },
534
143
  [assertScreen]
535
144
  );
536
- const reset = useCallback4(
145
+ const reset = useCallback(
537
146
  (name, params) => {
538
147
  assertScreen(name);
539
148
  dispatch({ type: "reset", route: { id: screenId.current++, name, params } });
@@ -545,10 +154,10 @@ function Router({ children, initialScreen, initialParams, restoreFocus = true })
545
154
  components.set(route.name, route.component);
546
155
  }
547
156
  const canGoBack = stack.length > 1;
548
- return /* @__PURE__ */ jsx8(Fragment2, { children: stack.map((entry, i) => {
157
+ return /* @__PURE__ */ jsx3(Fragment, { children: stack.map((entry, i) => {
549
158
  const Component = components.get(entry.name);
550
159
  if (!Component) return null;
551
- return /* @__PURE__ */ jsx8(
160
+ return /* @__PURE__ */ jsx3(
552
161
  ScreenEntry,
553
162
  {
554
163
  entry,
@@ -574,6 +183,7 @@ export {
574
183
  Screen,
575
184
  useFocus,
576
185
  useFocusState,
186
+ useKeybindingRegistry,
577
187
  useKeybindings,
578
188
  useNavigation
579
189
  };
@@ -0,0 +1,26 @@
1
+ import { Key } from 'ink';
2
+
3
+ type KeyHandler = (input: string, key: Key) => void;
4
+ type SpecialKey = 'up' | 'down' | 'left' | 'right' | 'enter' | 'escape' | 'tab' | 'backspace' | 'pageup' | 'pagedown' | 'home' | 'end';
5
+ type KeyName = SpecialKey | (string & {});
6
+ type KeybindingDefinition = KeyHandler | {
7
+ action: KeyHandler;
8
+ name: string;
9
+ when?: 'focused' | 'mounted';
10
+ };
11
+ type Keybindings = Partial<Record<KeyName, KeybindingDefinition>>;
12
+ type KeybindingOptions = {
13
+ capture?: boolean;
14
+ onKeypress?: (input: string, key: Key) => void;
15
+ layer?: string;
16
+ };
17
+ type RegisteredKeybinding = {
18
+ nodeId: string;
19
+ key: string;
20
+ handler: KeyHandler;
21
+ name?: string;
22
+ when?: 'focused' | 'mounted';
23
+ layer?: string;
24
+ };
25
+
26
+ export type { Keybindings as K, RegisteredKeybinding as R, KeybindingOptions as a, KeyHandler as b };
@@ -0,0 +1,18 @@
1
+ import * as react_jsx_runtime from 'react/jsx-runtime';
2
+ import React__default from 'react';
3
+ import { R as RegisteredKeybinding } from '../types-ClgDW3fy.js';
4
+ import 'ink';
5
+
6
+ type CommandPaletteRenderProps = {
7
+ query: string;
8
+ filtered: RegisteredKeybinding[];
9
+ selectedIndex: number;
10
+ onSelect: (cmd: RegisteredKeybinding) => void;
11
+ };
12
+ type CommandPaletteProps = {
13
+ onClose: () => void;
14
+ render?: (props: CommandPaletteRenderProps) => React__default.ReactNode;
15
+ };
16
+ declare function CommandPalette({ onClose, render }: CommandPaletteProps): react_jsx_runtime.JSX.Element;
17
+
18
+ export { CommandPalette, type CommandPaletteRenderProps };
@@ -0,0 +1,100 @@
1
+ import {
2
+ FocusTrap,
3
+ useFocus,
4
+ useKeybindingRegistry,
5
+ useKeybindings
6
+ } from "../chunk-4LED4GXQ.js";
7
+
8
+ // src/ui/CommandPalette.tsx
9
+ import { useState } from "react";
10
+ import { Box, Text } from "ink";
11
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
12
+ var EMPTY_KEY = {
13
+ upArrow: false,
14
+ downArrow: false,
15
+ leftArrow: false,
16
+ rightArrow: false,
17
+ pageDown: false,
18
+ pageUp: false,
19
+ home: false,
20
+ end: false,
21
+ return: false,
22
+ escape: false,
23
+ ctrl: false,
24
+ shift: false,
25
+ tab: false,
26
+ backspace: false,
27
+ delete: false,
28
+ meta: false,
29
+ super: false,
30
+ hyper: false,
31
+ capsLock: false,
32
+ numLock: false
33
+ };
34
+ function fuzzyMatch(name, query) {
35
+ if (!query) return true;
36
+ const lowerName = name.toLowerCase();
37
+ const lowerQuery = query.toLowerCase();
38
+ let qi = 0;
39
+ for (let i = 0; i < lowerName.length && qi < lowerQuery.length; i++) {
40
+ if (lowerName[i] === lowerQuery[qi]) qi++;
41
+ }
42
+ return qi === lowerQuery.length;
43
+ }
44
+ function Inner({ onClose, render }) {
45
+ const focus = useFocus();
46
+ const [query, setQuery] = useState("");
47
+ const [selectedIndex, setSelectedIndex] = useState(0);
48
+ const registry = useKeybindingRegistry();
49
+ const filtered = registry.all.filter((cmd) => fuzzyMatch(cmd.name, query));
50
+ const clampedIndex = Math.min(selectedIndex, Math.max(0, filtered.length - 1));
51
+ const onSelect = (cmd) => {
52
+ cmd.handler("", EMPTY_KEY);
53
+ onClose();
54
+ };
55
+ useKeybindings(
56
+ focus,
57
+ {
58
+ escape: onClose,
59
+ enter: () => {
60
+ const cmd = filtered[clampedIndex];
61
+ if (cmd) onSelect(cmd);
62
+ },
63
+ up: () => setSelectedIndex((i) => Math.max(0, i - 1)),
64
+ down: () => setSelectedIndex((i) => Math.max(0, Math.min(filtered.length - 1, i + 1))),
65
+ backspace: () => {
66
+ setQuery((q) => q.slice(0, -1));
67
+ setSelectedIndex(0);
68
+ }
69
+ },
70
+ {
71
+ capture: true,
72
+ onKeypress: (input, key) => {
73
+ if (input.length === 1 && !key.ctrl) {
74
+ setQuery((q) => q + input);
75
+ setSelectedIndex(0);
76
+ }
77
+ }
78
+ }
79
+ );
80
+ if (render) {
81
+ return /* @__PURE__ */ jsx(Fragment, { children: render({ query, filtered, selectedIndex: clampedIndex, onSelect }) });
82
+ }
83
+ return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", borderStyle: "round", width: 40, children: [
84
+ /* @__PURE__ */ jsxs(Box, { paddingX: 1, children: [
85
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: "> " }),
86
+ /* @__PURE__ */ jsx(Text, { children: query }),
87
+ /* @__PURE__ */ jsx(Text, { inverse: true, children: " " })
88
+ ] }),
89
+ /* @__PURE__ */ jsx(Box, { flexDirection: "column", children: filtered.length === 0 ? /* @__PURE__ */ jsx(Box, { paddingX: 1, children: /* @__PURE__ */ jsx(Text, { dimColor: true, children: "No commands found" }) }) : filtered.map((cmd, i) => /* @__PURE__ */ jsxs(Box, { justifyContent: "space-between", paddingX: 1, children: [
90
+ /* @__PURE__ */ jsx(Text, { inverse: i === clampedIndex, children: cmd.name }),
91
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: cmd.key })
92
+ ] }, `${cmd.nodeId}-${cmd.key}`)) })
93
+ ] });
94
+ }
95
+ function CommandPalette({ onClose, render }) {
96
+ return /* @__PURE__ */ jsx(FocusTrap, { children: /* @__PURE__ */ jsx(Inner, { onClose, render }) });
97
+ }
98
+ export {
99
+ CommandPalette
100
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "giggles",
3
- "version": "0.3.0",
3
+ "version": "0.3.1",
4
4
  "license": "MIT",
5
5
  "repository": {
6
6
  "type": "git",
@@ -17,6 +17,10 @@
17
17
  "./terminal": {
18
18
  "import": "./dist/terminal/index.js",
19
19
  "types": "./dist/terminal/index.d.ts"
20
+ },
21
+ "./ui": {
22
+ "import": "./dist/ui/index.js",
23
+ "types": "./dist/ui/index.d.ts"
20
24
  }
21
25
  },
22
26
  "keywords": [