giggles 0.5.3 → 0.6.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.
|
@@ -36,8 +36,13 @@ function useKeybindings(focus, bindings, options) {
|
|
|
36
36
|
}
|
|
37
37
|
|
|
38
38
|
// src/core/input/useKeybindingRegistry.ts
|
|
39
|
+
import { useSyncExternalStore } from "react";
|
|
39
40
|
function useKeybindingRegistry(focus) {
|
|
40
41
|
const store = useStore();
|
|
42
|
+
useSyncExternalStore(
|
|
43
|
+
(cb) => store.subscribe(cb),
|
|
44
|
+
() => store.getVersion()
|
|
45
|
+
);
|
|
41
46
|
const all = store.getAllBindings().filter((b) => b.name != null);
|
|
42
47
|
const branchPath = store.getActiveBranchPath();
|
|
43
48
|
const branchSet = new Set(branchPath);
|
|
@@ -47,15 +52,24 @@ function useKeybindingRegistry(focus) {
|
|
|
47
52
|
const trapIndex = branchPath.indexOf(trapNodeId);
|
|
48
53
|
return trapIndex >= 0 ? new Set(branchPath.slice(0, trapIndex + 1)) : null;
|
|
49
54
|
})();
|
|
50
|
-
const
|
|
51
|
-
|
|
52
|
-
|
|
55
|
+
const availableSet = withinTrapSet ?? branchSet;
|
|
56
|
+
const seenKeys = /* @__PURE__ */ new Set();
|
|
57
|
+
const available = [];
|
|
58
|
+
for (const nodeId of branchPath) {
|
|
59
|
+
if (!availableSet.has(nodeId)) continue;
|
|
60
|
+
for (const b of all) {
|
|
61
|
+
if (b.nodeId === nodeId && !seenKeys.has(b.key)) {
|
|
62
|
+
seenKeys.add(b.key);
|
|
63
|
+
available.push(b);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
53
67
|
const local = focus ? all.filter((b) => b.nodeId === focus.id) : [];
|
|
54
68
|
return { all, available, local };
|
|
55
69
|
}
|
|
56
70
|
|
|
57
71
|
// src/core/focus/useFocusNode.ts
|
|
58
|
-
import { useContext as useContext2, useEffect as useEffect2, useId as useId2, useMemo, useSyncExternalStore } from "react";
|
|
72
|
+
import { useContext as useContext2, useEffect as useEffect2, useId as useId2, useMemo, useSyncExternalStore as useSyncExternalStore2 } from "react";
|
|
59
73
|
function useFocusNode(options) {
|
|
60
74
|
var _a;
|
|
61
75
|
const id = useId2();
|
|
@@ -69,7 +83,7 @@ function useFocusNode(options) {
|
|
|
69
83
|
store.unregisterNode(id);
|
|
70
84
|
};
|
|
71
85
|
}, [id, parentId, store]);
|
|
72
|
-
const hasFocus =
|
|
86
|
+
const hasFocus = useSyncExternalStore2(subscribe, () => store.isFocused(id));
|
|
73
87
|
return { id, hasFocus };
|
|
74
88
|
}
|
|
75
89
|
|
|
@@ -106,7 +120,7 @@ function InputRouter({ children }) {
|
|
|
106
120
|
}
|
|
107
121
|
|
|
108
122
|
// src/core/focus/useFocusScope.ts
|
|
109
|
-
import { useCallback, useContext as useContext3, useEffect as useEffect4, useId as useId3, useMemo as useMemo2, useSyncExternalStore as
|
|
123
|
+
import { useCallback, useContext as useContext3, useEffect as useEffect4, useId as useId3, useMemo as useMemo2, useSyncExternalStore as useSyncExternalStore3 } from "react";
|
|
110
124
|
function useFocusScope(options) {
|
|
111
125
|
var _a;
|
|
112
126
|
const id = useId3();
|
|
@@ -121,8 +135,8 @@ function useFocusScope(options) {
|
|
|
121
135
|
store.unregisterNode(id);
|
|
122
136
|
};
|
|
123
137
|
}, [id, parentId, store]);
|
|
124
|
-
const hasFocus =
|
|
125
|
-
const isPassive =
|
|
138
|
+
const hasFocus = useSyncExternalStore3(subscribe, () => store.isFocused(id));
|
|
139
|
+
const isPassive = useSyncExternalStore3(subscribe, () => store.isPassive(id));
|
|
126
140
|
const next = useCallback(() => store.navigateSibling("next", true, id), [store, id]);
|
|
127
141
|
const prev = useCallback(() => store.navigateSibling("prev", true, id), [store, id]);
|
|
128
142
|
const nextShallow = useCallback(() => store.navigateSibling("next", true, id, true), [store, id]);
|
|
@@ -178,6 +192,7 @@ var FocusStore = class {
|
|
|
178
192
|
pendingFocusFirstChild = /* @__PURE__ */ new Set();
|
|
179
193
|
trapNodeId = null;
|
|
180
194
|
listeners = /* @__PURE__ */ new Set();
|
|
195
|
+
version = 0;
|
|
181
196
|
// nodeId → registrationId → BindingRegistration
|
|
182
197
|
// Keybindings register synchronously during render; nodes register in useEffect.
|
|
183
198
|
// A keybinding may exist for a node that has not yet appeared in the node tree —
|
|
@@ -191,10 +206,14 @@ var FocusStore = class {
|
|
|
191
206
|
return () => this.listeners.delete(listener);
|
|
192
207
|
}
|
|
193
208
|
notify() {
|
|
209
|
+
this.version++;
|
|
194
210
|
for (const listener of this.listeners) {
|
|
195
211
|
listener();
|
|
196
212
|
}
|
|
197
213
|
}
|
|
214
|
+
getVersion() {
|
|
215
|
+
return this.version;
|
|
216
|
+
}
|
|
198
217
|
// ---------------------------------------------------------------------------
|
|
199
218
|
// Registration
|
|
200
219
|
// ---------------------------------------------------------------------------
|
|
@@ -400,9 +419,8 @@ var FocusStore = class {
|
|
|
400
419
|
});
|
|
401
420
|
const registration = {
|
|
402
421
|
bindings: new Map(entries),
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
passthrough: (options == null ? void 0 : options.passthrough) ? new Set(options.passthrough) : void 0
|
|
422
|
+
fallback: options == null ? void 0 : options.fallback,
|
|
423
|
+
bubble: (options == null ? void 0 : options.bubble) ? new Set(options.bubble) : void 0
|
|
406
424
|
};
|
|
407
425
|
if (!this.keybindings.has(nodeId)) {
|
|
408
426
|
this.keybindings.set(nodeId, /* @__PURE__ */ new Map());
|
|
@@ -422,28 +440,21 @@ var FocusStore = class {
|
|
|
422
440
|
const nodeRegistrations = this.keybindings.get(nodeId);
|
|
423
441
|
if (!nodeRegistrations || nodeRegistrations.size === 0) return void 0;
|
|
424
442
|
const mergedBindings = /* @__PURE__ */ new Map();
|
|
425
|
-
let
|
|
426
|
-
let
|
|
427
|
-
let finalPassthrough;
|
|
443
|
+
let finalFallback;
|
|
444
|
+
let finalBubble;
|
|
428
445
|
for (const registration of nodeRegistrations.values()) {
|
|
429
|
-
const
|
|
430
|
-
|
|
431
|
-
if (shouldIncludeBindings) {
|
|
432
|
-
for (const [key, entry] of registration.bindings) {
|
|
433
|
-
mergedBindings.set(key, entry);
|
|
434
|
-
}
|
|
446
|
+
for (const [key, entry] of registration.bindings) {
|
|
447
|
+
mergedBindings.set(key, entry);
|
|
435
448
|
}
|
|
436
|
-
if (registration.
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
finalPassthrough = registration.passthrough;
|
|
449
|
+
if (registration.fallback) {
|
|
450
|
+
finalFallback = registration.fallback;
|
|
451
|
+
finalBubble = registration.bubble;
|
|
440
452
|
}
|
|
441
453
|
}
|
|
442
454
|
return {
|
|
443
455
|
bindings: mergedBindings,
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
passthrough: finalPassthrough
|
|
456
|
+
fallback: finalFallback,
|
|
457
|
+
bubble: finalBubble
|
|
447
458
|
};
|
|
448
459
|
}
|
|
449
460
|
getAllBindings() {
|
|
@@ -466,21 +477,21 @@ var FocusStore = class {
|
|
|
466
477
|
// Input dispatch
|
|
467
478
|
// ---------------------------------------------------------------------------
|
|
468
479
|
// Bridge target for InputRouter. Walks the active branch path with passive-scope
|
|
469
|
-
// skipping,
|
|
480
|
+
// skipping, fallback handlers, and trap boundary — the full dispatch algorithm.
|
|
470
481
|
//
|
|
471
482
|
// Priority order (per node, walking focused → root):
|
|
472
483
|
// 1. Named bindings — always checked first
|
|
473
|
-
// 2.
|
|
474
|
-
//
|
|
475
|
-
// Keys in `
|
|
476
|
-
// 3. Trap boundary — stops the walk;
|
|
484
|
+
// 2. Fallback handler — deferred until after the full path walk, so named
|
|
485
|
+
// bindings at any ancestor still fire before the fallback kicks in.
|
|
486
|
+
// Keys in `bubble` skip the fallback and continue to the next ancestor.
|
|
487
|
+
// 3. Trap boundary — stops the walk; fallback inside the trap still fires.
|
|
477
488
|
dispatch(input, key) {
|
|
478
489
|
var _a;
|
|
479
490
|
const keyName = normalizeKey(input, key);
|
|
480
491
|
if (!keyName) return;
|
|
481
492
|
const path = this.getActiveBranchPath();
|
|
482
493
|
const trapNodeId = this.trapNodeId;
|
|
483
|
-
let
|
|
494
|
+
let pendingFallback;
|
|
484
495
|
for (const nodeId of path) {
|
|
485
496
|
if (this.passiveSet.has(nodeId)) continue;
|
|
486
497
|
const nodeBindings = this.getNodeBindings(nodeId);
|
|
@@ -490,18 +501,18 @@ var FocusStore = class {
|
|
|
490
501
|
entry.handler(input, key);
|
|
491
502
|
return;
|
|
492
503
|
}
|
|
493
|
-
if (!
|
|
494
|
-
if ((_a = nodeBindings.
|
|
504
|
+
if (!pendingFallback && nodeBindings.fallback) {
|
|
505
|
+
if ((_a = nodeBindings.bubble) == null ? void 0 : _a.has(keyName)) {
|
|
495
506
|
continue;
|
|
496
507
|
}
|
|
497
|
-
|
|
508
|
+
pendingFallback = nodeBindings.fallback;
|
|
498
509
|
}
|
|
499
510
|
}
|
|
500
511
|
if (nodeId === trapNodeId) {
|
|
501
512
|
break;
|
|
502
513
|
}
|
|
503
514
|
}
|
|
504
|
-
|
|
515
|
+
pendingFallback == null ? void 0 : pendingFallback(input, key);
|
|
505
516
|
}
|
|
506
517
|
// ---------------------------------------------------------------------------
|
|
507
518
|
// Private helpers
|
package/dist/index.d.ts
CHANGED
|
@@ -2,8 +2,8 @@ import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
|
2
2
|
import React from 'react';
|
|
3
3
|
import { BoxProps } from 'ink';
|
|
4
4
|
export { Key } from 'ink';
|
|
5
|
-
import { K as Keybindings, a as KeybindingOptions, R as RegisteredKeybinding } from './types-
|
|
6
|
-
export { b as KeyHandler } from './types-
|
|
5
|
+
import { K as Keybindings, a as KeybindingOptions, R as RegisteredKeybinding } from './types-HR6Vak_5.js';
|
|
6
|
+
export { b as KeyHandler } from './types-HR6Vak_5.js';
|
|
7
7
|
|
|
8
8
|
declare class GigglesError extends Error {
|
|
9
9
|
constructor(message: string);
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,3 @@
|
|
|
1
|
-
import {
|
|
2
|
-
useTerminalSize
|
|
3
|
-
} from "./chunk-WNGBTD67.js";
|
|
4
1
|
import {
|
|
5
2
|
FocusScope,
|
|
6
3
|
FocusStore,
|
|
@@ -14,11 +11,14 @@ import {
|
|
|
14
11
|
useKeybindingRegistry,
|
|
15
12
|
useKeybindings,
|
|
16
13
|
useStore
|
|
17
|
-
} from "./chunk-
|
|
14
|
+
} from "./chunk-A7BRQXWE.js";
|
|
18
15
|
import {
|
|
19
16
|
ThemeProvider,
|
|
20
17
|
useTheme
|
|
21
18
|
} 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";
|
|
@@ -9,9 +9,8 @@ type KeybindingDefinition = KeyHandler | {
|
|
|
9
9
|
};
|
|
10
10
|
type Keybindings = Partial<Record<KeyName, KeybindingDefinition>>;
|
|
11
11
|
type KeybindingOptions = {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
passthrough?: string[];
|
|
12
|
+
fallback?: (input: string, key: Key) => void;
|
|
13
|
+
bubble?: string[];
|
|
15
14
|
};
|
|
16
15
|
type RegisteredKeybinding = {
|
|
17
16
|
nodeId: string;
|
package/dist/ui/index.d.ts
CHANGED
package/dist/ui/index.js
CHANGED
|
@@ -4,7 +4,7 @@ import {
|
|
|
4
4
|
useFocusNode,
|
|
5
5
|
useKeybindingRegistry,
|
|
6
6
|
useKeybindings
|
|
7
|
-
} from "../chunk-
|
|
7
|
+
} from "../chunk-A7BRQXWE.js";
|
|
8
8
|
import {
|
|
9
9
|
CodeBlock
|
|
10
10
|
} from "../chunk-SKSDNDQF.js";
|
|
@@ -77,8 +77,7 @@ function Inner({ onClose, render }) {
|
|
|
77
77
|
}
|
|
78
78
|
},
|
|
79
79
|
{
|
|
80
|
-
|
|
81
|
-
onKeypress: (input, key) => {
|
|
80
|
+
fallback: (input, key) => {
|
|
82
81
|
if (input.length === 1 && !key.ctrl) {
|
|
83
82
|
setQuery((q) => q + input);
|
|
84
83
|
setSelectedIndex(0);
|
|
@@ -178,15 +177,14 @@ function TextInput({ label, value, onChange, onSubmit, placeholder, render }) {
|
|
|
178
177
|
...onSubmit && { enter: () => onSubmit(value) }
|
|
179
178
|
},
|
|
180
179
|
{
|
|
181
|
-
|
|
182
|
-
passthrough: ["tab", "shift+tab", "enter", "escape", "backspace", "delete", "left", "right", "home", "end"],
|
|
183
|
-
onKeypress: (input, key) => {
|
|
180
|
+
fallback: (input, key) => {
|
|
184
181
|
if (input.length === 1 && !key.ctrl && !key.return && !key.escape && !key.tab) {
|
|
185
182
|
const c = cursorRef.current;
|
|
186
183
|
cursorRef.current = c + 1;
|
|
187
184
|
onChange(value.slice(0, c) + input + value.slice(c));
|
|
188
185
|
}
|
|
189
|
-
}
|
|
186
|
+
},
|
|
187
|
+
bubble: ["tab", "shift+tab", "enter", "escape", "backspace", "delete", "left", "right", "home", "end"]
|
|
190
188
|
}
|
|
191
189
|
);
|
|
192
190
|
const before = value.slice(0, cursor);
|
|
@@ -642,15 +640,14 @@ function Autocomplete({
|
|
|
642
640
|
}
|
|
643
641
|
},
|
|
644
642
|
{
|
|
645
|
-
|
|
646
|
-
passthrough: ["tab", "shift+tab", "escape"],
|
|
647
|
-
onKeypress: (input, key) => {
|
|
643
|
+
fallback: (input, key) => {
|
|
648
644
|
if (input.length === 1 && !key.ctrl && !key.return && !key.escape && !key.tab) {
|
|
649
645
|
const c = cursorRef.current;
|
|
650
646
|
cursorRef.current = c + 1;
|
|
651
647
|
updateQuery(query.slice(0, c) + input + query.slice(c));
|
|
652
648
|
}
|
|
653
|
-
}
|
|
649
|
+
},
|
|
650
|
+
bubble: ["tab", "shift+tab", "escape"]
|
|
654
651
|
}
|
|
655
652
|
);
|
|
656
653
|
const before = query.slice(0, cursor);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "giggles",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.1",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -41,6 +41,7 @@
|
|
|
41
41
|
"build": "tsup",
|
|
42
42
|
"build:watch": "nodemon --watch src --ext ts,tsx --exec tsup",
|
|
43
43
|
"play": "tsx --watch",
|
|
44
|
+
"record": "vhs",
|
|
44
45
|
"dev:docs": "pnpm build && concurrently --kill-others \"pnpm build:watch\" \"pnpm --filter documentation dev\"",
|
|
45
46
|
"lint": "prettier --write . && eslint . --fix"
|
|
46
47
|
},
|