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 available = all.filter((b) => {
51
- return (withinTrapSet ?? branchSet).has(b.nodeId);
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 = useSyncExternalStore(subscribe, () => store.isFocused(id));
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 useSyncExternalStore2 } from "react";
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 = useSyncExternalStore2(subscribe, () => store.isFocused(id));
125
- const isPassive = useSyncExternalStore2(subscribe, () => store.isPassive(id));
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
- capture: (options == null ? void 0 : options.capture) ?? false,
404
- onKeypress: options == null ? void 0 : options.onKeypress,
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 finalCapture = false;
426
- let finalOnKeypress;
427
- let finalPassthrough;
443
+ let finalFallback;
444
+ let finalBubble;
428
445
  for (const registration of nodeRegistrations.values()) {
429
- const isCaptureRegistration = registration.onKeypress !== void 0;
430
- const shouldIncludeBindings = !isCaptureRegistration || registration.capture;
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.capture) {
437
- finalCapture = true;
438
- finalOnKeypress = registration.onKeypress;
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
- capture: finalCapture,
445
- onKeypress: finalOnKeypress,
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, capture mode, and trap boundary — the full dispatch algorithm.
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. Capture mode (onKeypress) — deferred until after the full path walk,
474
- // so named bindings at any ancestor still fire before capture kicks in.
475
- // Keys in `passthrough` skip capture and bubble to the next ancestor.
476
- // 3. Trap boundary — stops the walk; capture inside the trap still fires.
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 pendingCapture;
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 (!pendingCapture && nodeBindings.capture && nodeBindings.onKeypress) {
494
- if ((_a = nodeBindings.passthrough) == null ? void 0 : _a.has(keyName)) {
504
+ if (!pendingFallback && nodeBindings.fallback) {
505
+ if ((_a = nodeBindings.bubble) == null ? void 0 : _a.has(keyName)) {
495
506
  continue;
496
507
  }
497
- pendingCapture = nodeBindings.onKeypress;
508
+ pendingFallback = nodeBindings.fallback;
498
509
  }
499
510
  }
500
511
  if (nodeId === trapNodeId) {
501
512
  break;
502
513
  }
503
514
  }
504
- pendingCapture == null ? void 0 : pendingCapture(input, key);
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-BODf7e7U.js';
6
- export { b as KeyHandler } from './types-BODf7e7U.js';
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-RO3ZD423.js";
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
- capture?: boolean;
13
- onKeypress?: (input: string, key: Key) => void;
14
- passthrough?: string[];
12
+ fallback?: (input: string, key: Key) => void;
13
+ bubble?: string[];
15
14
  };
16
15
  type RegisteredKeybinding = {
17
16
  nodeId: string;
@@ -1,5 +1,5 @@
1
1
  import * as react_jsx_runtime from 'react/jsx-runtime';
2
- import { R as RegisteredKeybinding } from '../types-BODf7e7U.js';
2
+ import { R as RegisteredKeybinding } from '../types-HR6Vak_5.js';
3
3
  import React$1 from 'react';
4
4
  import { BoxProps } from 'ink';
5
5
 
package/dist/ui/index.js CHANGED
@@ -4,7 +4,7 @@ import {
4
4
  useFocusNode,
5
5
  useKeybindingRegistry,
6
6
  useKeybindings
7
- } from "../chunk-RO3ZD423.js";
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
- capture: true,
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
- capture: true,
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
- capture: true,
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.5.3",
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
  },