giggles 0.7.0 → 0.7.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -69,7 +69,7 @@ function useKeybindingRegistry(focus) {
69
69
  }
70
70
 
71
71
  // src/core/focus/useFocusNode.ts
72
- import { useContext as useContext2, useEffect as useEffect2, useId as useId2, useMemo, useSyncExternalStore as useSyncExternalStore2 } from "react";
72
+ import { useContext as useContext2, useId as useId2, useLayoutEffect, useMemo, useSyncExternalStore as useSyncExternalStore2 } from "react";
73
73
  function useFocusNode(options) {
74
74
  var _a;
75
75
  const id = useId2();
@@ -78,24 +78,26 @@ function useFocusNode(options) {
78
78
  const parentId = ((_a = options == null ? void 0 : options.parent) == null ? void 0 : _a.id) ?? contextParentId;
79
79
  const focusKey = options == null ? void 0 : options.focusKey;
80
80
  const subscribe = useMemo(() => store.subscribe.bind(store), [store]);
81
- useEffect2(() => {
82
- store.registerNode(id, parentId, focusKey);
81
+ store.registerNode(id, parentId, focusKey, true);
82
+ useLayoutEffect(() => {
83
+ store.registerNode(id, parentId, focusKey, true);
84
+ store.flush();
83
85
  return () => {
84
86
  store.unregisterNode(id);
85
87
  };
86
- }, [id, parentId, focusKey, store]);
88
+ }, [id, store]);
87
89
  const hasFocus = useSyncExternalStore2(subscribe, () => store.isFocused(id));
88
90
  return { id, hasFocus };
89
91
  }
90
92
 
91
93
  // src/core/input/FocusTrap.tsx
92
- import { useEffect as useEffect3, useRef as useRef2 } from "react";
94
+ import { useEffect as useEffect2, useRef as useRef2 } from "react";
93
95
  import { jsx } from "react/jsx-runtime";
94
96
  function FocusTrap({ children }) {
95
97
  const { id } = useFocusNode();
96
98
  const store = useStore();
97
99
  const previousFocusRef = useRef2(store.getFocusedId());
98
- useEffect3(() => {
100
+ useEffect2(() => {
99
101
  const previousFocus = previousFocusRef.current;
100
102
  store.setTrap(id);
101
103
  store.focusFirstChild(id);
@@ -121,7 +123,7 @@ function InputRouter({ children }) {
121
123
  }
122
124
 
123
125
  // src/core/focus/useFocusScope.ts
124
- import { useCallback, useContext as useContext3, useEffect as useEffect4, useId as useId3, useMemo as useMemo2, useSyncExternalStore as useSyncExternalStore3 } from "react";
126
+ import { useCallback, useContext as useContext3, useEffect as useEffect3, useId as useId3, useLayoutEffect as useLayoutEffect2, useMemo as useMemo2, useSyncExternalStore as useSyncExternalStore3 } from "react";
125
127
  function useFocusScope(options) {
126
128
  var _a;
127
129
  const id = useId3();
@@ -131,12 +133,14 @@ function useFocusScope(options) {
131
133
  const parentId = ((_a = options == null ? void 0 : options.parent) == null ? void 0 : _a.id) ?? contextParentId;
132
134
  const focusKey = options == null ? void 0 : options.focusKey;
133
135
  const subscribe = useMemo2(() => store.subscribe.bind(store), [store]);
134
- useEffect4(() => {
135
- store.registerNode(id, parentId, focusKey);
136
+ store.registerNode(id, parentId, focusKey, true);
137
+ useLayoutEffect2(() => {
138
+ store.registerNode(id, parentId, focusKey, true);
139
+ store.flush();
136
140
  return () => {
137
141
  store.unregisterNode(id);
138
142
  };
139
- }, [id, parentId, focusKey, store]);
143
+ }, [id, store]);
140
144
  const hasFocus = useSyncExternalStore3(subscribe, () => store.isFocused(id));
141
145
  const isPassive = useSyncExternalStore3(subscribe, () => store.isPassive(id));
142
146
  const next = useCallback(() => store.navigateSibling("next", true, id), [store, id]);
@@ -149,12 +153,12 @@ function useFocusScope(options) {
149
153
  const focusChildShallow = useCallback((key) => store.focusChildByKey(id, key, true), [store, id]);
150
154
  const resolvedBindings = typeof (options == null ? void 0 : options.keybindings) === "function" ? options.keybindings({ next, prev, nextShallow, prevShallow, escape, drillIn, focusChild, focusChildShallow }) : (options == null ? void 0 : options.keybindings) ?? {};
151
155
  store.registerKeybindings(id, keybindingRegistrationId, resolvedBindings);
152
- useEffect4(() => {
156
+ useEffect3(() => {
153
157
  return () => {
154
158
  store.unregisterKeybindings(id, keybindingRegistrationId);
155
159
  };
156
160
  }, [id, keybindingRegistrationId, store]);
157
- useEffect4(() => {
161
+ useEffect3(() => {
158
162
  if (!store.hasFocusScopeComponent(id)) {
159
163
  throw new GigglesError(
160
164
  "useFocusScope() was called but no <FocusScope handle={scope}> was rendered. Every useFocusScope() call requires a corresponding <FocusScope> in the render output \u2014 without it, child components register under the wrong parent scope and keyboard navigation silently breaks."
@@ -177,11 +181,11 @@ function useFocusScope(options) {
177
181
  }
178
182
 
179
183
  // src/core/focus/FocusScope.tsx
180
- import { useEffect as useEffect5 } from "react";
184
+ import { useEffect as useEffect4 } from "react";
181
185
  import { jsx as jsx3 } from "react/jsx-runtime";
182
186
  function FocusScope({ handle, children }) {
183
187
  const store = useStore();
184
- useEffect5(() => {
188
+ useEffect4(() => {
185
189
  store.registerFocusScopeComponent(handle.id);
186
190
  return () => store.unregisterFocusScopeComponent(handle.id);
187
191
  }, [handle.id, store]);
@@ -224,7 +228,7 @@ var FocusStore = class {
224
228
  listeners = /* @__PURE__ */ new Set();
225
229
  version = 0;
226
230
  // nodeId → registrationId → BindingRegistration
227
- // Keybindings register synchronously during render; nodes register in useEffect.
231
+ // Both keybindings and nodes register synchronously during render.
228
232
  // A keybinding may exist for a node that has not yet appeared in the node tree —
229
233
  // this is safe because dispatch only walks nodes in the active branch path.
230
234
  keybindings = /* @__PURE__ */ new Map();
@@ -237,20 +241,43 @@ var FocusStore = class {
237
241
  this.listeners.add(listener);
238
242
  return () => this.listeners.delete(listener);
239
243
  }
244
+ dirty = false;
240
245
  notify() {
241
246
  this.version++;
247
+ this.dirty = false;
242
248
  for (const listener of this.listeners) {
243
249
  listener();
244
250
  }
245
251
  }
252
+ // Mark the store as changed without notifying subscribers. Used when the
253
+ // store is mutated during React's render phase (e.g. silent node registration)
254
+ // where calling notify() would trigger subscription callbacks unsafely.
255
+ // Call flush() in a useLayoutEffect to deliver the notification before paint.
256
+ markDirty() {
257
+ this.dirty = true;
258
+ }
259
+ flush() {
260
+ if (this.dirty) {
261
+ this.notify();
262
+ }
263
+ }
246
264
  getVersion() {
247
265
  return this.version;
248
266
  }
249
267
  // ---------------------------------------------------------------------------
250
268
  // Registration
251
269
  // ---------------------------------------------------------------------------
252
- registerNode(id, parentId, focusKey) {
253
- const node = { id, parentId, childrenIds: [] };
270
+ registerNode(id, parentId, focusKey, silent = false) {
271
+ const existing = this.nodes.get(id);
272
+ if (existing && existing.parentId === parentId) {
273
+ if (focusKey !== existing.focusKey) {
274
+ this.updateFocusKey(id, parentId, existing.focusKey, focusKey);
275
+ existing.focusKey = focusKey;
276
+ this.markDirty();
277
+ }
278
+ return;
279
+ }
280
+ const node = { id, parentId, childrenIds: [], focusKey };
254
281
  this.nodes.set(id, node);
255
282
  this.parentMap.set(id, parentId);
256
283
  if (parentId) {
@@ -260,7 +287,11 @@ var FocusStore = class {
260
287
  parent.childrenIds.push(id);
261
288
  if (wasEmpty && this.pendingFocusFirstChild.has(parentId)) {
262
289
  this.pendingFocusFirstChild.delete(parentId);
263
- this.focusNode(id);
290
+ if (silent) {
291
+ this.setFocusedIdSilently(id);
292
+ } else {
293
+ this.focusNode(id);
294
+ }
264
295
  }
265
296
  }
266
297
  }
@@ -276,9 +307,17 @@ var FocusStore = class {
276
307
  this.keyIndex.get(parentId).set(focusKey, id);
277
308
  }
278
309
  if (this.nodes.size === 1) {
279
- this.focusNode(id);
310
+ if (silent) {
311
+ this.setFocusedIdSilently(id);
312
+ } else {
313
+ this.focusNode(id);
314
+ }
315
+ }
316
+ if (silent) {
317
+ this.markDirty();
318
+ } else {
319
+ this.notify();
280
320
  }
281
- this.notify();
282
321
  }
283
322
  unregisterNode(id) {
284
323
  const node = this.nodes.get(id);
@@ -328,18 +367,18 @@ var FocusStore = class {
328
367
  const oldFocusedId = this.focusedId;
329
368
  if (oldFocusedId === id) return;
330
369
  this.focusedId = id;
331
- for (const passiveId of this.passiveSet) {
332
- const wasAncestor = this.isAncestorOf(passiveId, oldFocusedId);
333
- const isAncestor = this.isAncestorOf(passiveId, id);
334
- if (wasAncestor && !isAncestor) {
335
- this.passiveSet.delete(passiveId);
336
- }
337
- if (isAncestor && id !== passiveId) {
338
- this.passiveSet.delete(passiveId);
339
- }
340
- }
370
+ this.clearPassiveOnFocusChange(oldFocusedId, id);
341
371
  this.notify();
342
372
  }
373
+ // Set focusedId without notifying subscribers. Safe to call during React's
374
+ // render phase. Clears passive flags the same way focusNode() does.
375
+ setFocusedIdSilently(id) {
376
+ const oldFocusedId = this.focusedId;
377
+ if (oldFocusedId === id) return;
378
+ this.focusedId = id;
379
+ this.clearPassiveOnFocusChange(oldFocusedId, id);
380
+ this.markDirty();
381
+ }
343
382
  focusFirstChild(parentId) {
344
383
  const parent = this.nodes.get(parentId);
345
384
  if (parent && parent.childrenIds.length > 0) {
@@ -604,6 +643,38 @@ var FocusStore = class {
604
643
  }
605
644
  return false;
606
645
  }
646
+ // Clear passive flags when focus transitions between nodes.
647
+ clearPassiveOnFocusChange(oldFocusedId, newFocusedId) {
648
+ for (const passiveId of this.passiveSet) {
649
+ const wasAncestor = this.isAncestorOf(passiveId, oldFocusedId);
650
+ const isAncestor = this.isAncestorOf(passiveId, newFocusedId);
651
+ if (wasAncestor && !isAncestor) {
652
+ this.passiveSet.delete(passiveId);
653
+ }
654
+ if (isAncestor && newFocusedId !== passiveId) {
655
+ this.passiveSet.delete(passiveId);
656
+ }
657
+ }
658
+ }
659
+ // Update the keyIndex when a node's focusKey changes during a re-render.
660
+ updateFocusKey(id, parentId, oldKey, newKey) {
661
+ if (!parentId) return;
662
+ if (oldKey) {
663
+ const parentKeys = this.keyIndex.get(parentId);
664
+ if (parentKeys) {
665
+ parentKeys.delete(oldKey);
666
+ if (parentKeys.size === 0) {
667
+ this.keyIndex.delete(parentId);
668
+ }
669
+ }
670
+ }
671
+ if (newKey) {
672
+ if (!this.keyIndex.has(parentId)) {
673
+ this.keyIndex.set(parentId, /* @__PURE__ */ new Map());
674
+ }
675
+ this.keyIndex.get(parentId).set(newKey, id);
676
+ }
677
+ }
607
678
  };
608
679
 
609
680
  export {
package/dist/index.js CHANGED
@@ -14,7 +14,7 @@ import {
14
14
  useKeybindingRegistry,
15
15
  useKeybindings,
16
16
  useStore
17
- } from "./chunk-74PBSWEK.js";
17
+ } from "./chunk-HHDMTIXE.js";
18
18
  import {
19
19
  ThemeProvider,
20
20
  useTheme
@@ -68,7 +68,7 @@ function Screen(_props) {
68
68
  }
69
69
 
70
70
  // src/core/router/ScreenEntry.tsx
71
- import { useEffect as useEffect2, useMemo, useRef as useRef2 } from "react";
71
+ import { useLayoutEffect, useMemo, useRef as useRef2 } from "react";
72
72
  import { Box as Box2 } from "ink";
73
73
 
74
74
  // src/core/router/NavigationContext.tsx
@@ -99,7 +99,7 @@ function ScreenEntry({
99
99
  const store = useStore();
100
100
  const lastFocusedChildRef = useRef2(null);
101
101
  const wasTopRef = useRef2(isTop);
102
- useEffect2(() => {
102
+ useLayoutEffect(() => {
103
103
  if (!wasTopRef.current && isTop) {
104
104
  const saved = restoreFocus ? lastFocusedChildRef.current : null;
105
105
  if (saved) {
@@ -5,6 +5,7 @@ import { BoxProps } from 'ink';
5
5
 
6
6
  type CommandPaletteRenderProps = {
7
7
  query: string;
8
+ onChange: (query: string) => void;
8
9
  filtered: RegisteredKeybinding[];
9
10
  selectedIndex: number;
10
11
  onSelect: (cmd: RegisteredKeybinding) => void;
@@ -12,9 +13,10 @@ type CommandPaletteRenderProps = {
12
13
  type CommandPaletteProps = {
13
14
  onClose?: () => void;
14
15
  interactive?: boolean;
16
+ maxVisible?: number;
15
17
  render?: (props: CommandPaletteRenderProps) => React.ReactNode;
16
18
  };
17
- declare function CommandPalette({ onClose, interactive, render }: CommandPaletteProps): react_jsx_runtime.JSX.Element;
19
+ declare function CommandPalette({ onClose, interactive, maxVisible, render }: CommandPaletteProps): react_jsx_runtime.JSX.Element;
18
20
 
19
21
  type TextInputRenderProps = {
20
22
  value: string;
package/dist/ui/index.js CHANGED
@@ -1,10 +1,12 @@
1
1
  import {
2
+ FocusScope,
2
3
  FocusTrap,
3
4
  GigglesError,
4
5
  useFocusNode,
6
+ useFocusScope,
5
7
  useKeybindingRegistry,
6
8
  useKeybindings
7
- } from "../chunk-74PBSWEK.js";
9
+ } from "../chunk-HHDMTIXE.js";
8
10
  import {
9
11
  CodeBlock
10
12
  } from "../chunk-SKSDNDQF.js";
@@ -13,129 +15,13 @@ import {
13
15
  } from "../chunk-C77VBSPK.js";
14
16
 
15
17
  // src/ui/CommandPalette.tsx
16
- import { useState } from "react";
17
- import { Box, Text } from "ink";
18
- import { Fragment, jsx, jsxs } from "react/jsx-runtime";
19
- var EMPTY_KEY = {
20
- upArrow: false,
21
- downArrow: false,
22
- leftArrow: false,
23
- rightArrow: false,
24
- pageDown: false,
25
- pageUp: false,
26
- home: false,
27
- end: false,
28
- return: false,
29
- escape: false,
30
- ctrl: false,
31
- shift: false,
32
- tab: false,
33
- backspace: false,
34
- delete: false,
35
- meta: false,
36
- super: false,
37
- hyper: false,
38
- capsLock: false,
39
- numLock: false
40
- };
41
- function fuzzyMatch(name, query) {
42
- if (!query) return true;
43
- const lowerName = name.toLowerCase();
44
- const lowerQuery = query.toLowerCase();
45
- let qi = 0;
46
- for (let i = 0; i < lowerName.length && qi < lowerQuery.length; i++) {
47
- if (lowerName[i] === lowerQuery[qi]) qi++;
48
- }
49
- return qi === lowerQuery.length;
50
- }
51
- function Inner({ onClose, render }) {
52
- const focus = useFocusNode();
53
- const theme = useTheme();
54
- const [query, setQuery] = useState("");
55
- const [selectedIndex, setSelectedIndex] = useState(0);
56
- const registry = useKeybindingRegistry();
57
- const named = registry.all.filter((cmd) => cmd.name != null);
58
- const filtered = named.filter((cmd) => fuzzyMatch(cmd.name, query));
59
- const clampedIndex = Math.min(selectedIndex, Math.max(0, filtered.length - 1));
60
- const onSelect = (cmd) => {
61
- cmd.handler("", EMPTY_KEY);
62
- onClose();
63
- };
64
- useKeybindings(
65
- focus,
66
- {
67
- escape: onClose,
68
- enter: () => {
69
- const cmd = filtered[clampedIndex];
70
- if (cmd) onSelect(cmd);
71
- },
72
- left: () => setSelectedIndex((i) => (i - 1 + filtered.length) % filtered.length),
73
- right: () => setSelectedIndex((i) => (i + 1) % filtered.length),
74
- backspace: () => {
75
- setQuery((q) => q.slice(0, -1));
76
- setSelectedIndex(0);
77
- }
78
- },
79
- {
80
- fallback: (input, key) => {
81
- if (input.length === 1 && !key.ctrl) {
82
- setQuery((q) => q + input);
83
- setSelectedIndex(0);
84
- }
85
- }
86
- }
87
- );
88
- if (render) {
89
- return /* @__PURE__ */ jsx(Fragment, { children: render({ query, filtered, selectedIndex: clampedIndex, onSelect }) });
90
- }
91
- return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", children: [
92
- query.length > 0 && /* @__PURE__ */ jsxs(Text, { children: [
93
- /* @__PURE__ */ jsx(Text, { dimColor: true, children: "> " }),
94
- /* @__PURE__ */ jsx(Text, { children: query }),
95
- /* @__PURE__ */ jsx(Text, { inverse: true, children: " " })
96
- ] }),
97
- filtered.length === 0 ? /* @__PURE__ */ jsx(Text, { dimColor: true, children: "No commands found" }) : /* @__PURE__ */ jsx(Box, { flexWrap: "wrap", children: filtered.map((cmd, index) => {
98
- const highlighted = index === clampedIndex;
99
- const keyColor = highlighted ? theme.hintHighlightColor : theme.hintColor;
100
- const labelColor = highlighted ? theme.hintHighlightDimColor : theme.hintDimColor;
101
- return /* @__PURE__ */ jsxs(Text, { children: [
102
- /* @__PURE__ */ jsx(Text, { color: keyColor, bold: true, children: cmd.key }),
103
- /* @__PURE__ */ jsxs(Text, { color: labelColor, children: [
104
- " ",
105
- cmd.name
106
- ] }),
107
- index < filtered.length - 1 && /* @__PURE__ */ jsx(Text, { color: theme.hintDimColor, children: " \u2022 " })
108
- ] }, `${cmd.nodeId}-${cmd.key}`);
109
- }) })
110
- ] });
111
- }
112
- function HintsBar() {
113
- const registry = useKeybindingRegistry();
114
- const theme = useTheme();
115
- const commands = registry.available.filter((cmd) => cmd.name != null);
116
- if (commands.length === 0) return null;
117
- return /* @__PURE__ */ jsx(Box, { flexWrap: "wrap", children: commands.map((cmd, index) => /* @__PURE__ */ jsxs(Text, { children: [
118
- /* @__PURE__ */ jsx(Text, { color: theme.hintColor, bold: true, children: cmd.key }),
119
- /* @__PURE__ */ jsxs(Text, { color: theme.hintDimColor, children: [
120
- " ",
121
- cmd.name
122
- ] }),
123
- index < commands.length - 1 && /* @__PURE__ */ jsx(Text, { color: theme.hintDimColor, children: " \u2022 " })
124
- ] }, `${cmd.nodeId}-${cmd.key}`)) });
125
- }
126
- var noop = () => {
127
- };
128
- function CommandPalette({ onClose, interactive = true, render }) {
129
- if (!interactive) {
130
- return /* @__PURE__ */ jsx(HintsBar, {});
131
- }
132
- return /* @__PURE__ */ jsx(FocusTrap, { children: /* @__PURE__ */ jsx(Inner, { onClose: onClose ?? noop, render }) });
133
- }
18
+ import { useState as useState2 } from "react";
19
+ import { Box as Box3, Text as Text3 } from "ink";
134
20
 
135
21
  // src/ui/TextInput.tsx
136
22
  import { useReducer, useRef } from "react";
137
- import { Text as Text2 } from "ink";
138
- import { Fragment as Fragment2, jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
23
+ import { Text } from "ink";
24
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
139
25
  function TextInput({ label, value, onChange, onSubmit, placeholder, render, focusKey }) {
140
26
  const focus = useFocusNode({ focusKey });
141
27
  const cursorRef = useRef(value.length);
@@ -191,7 +77,7 @@ function TextInput({ label, value, onChange, onSubmit, placeholder, render, focu
191
77
  const cursorChar = value[cursor] ?? " ";
192
78
  const after = value.slice(cursor + 1);
193
79
  if (render) {
194
- return /* @__PURE__ */ jsx2(Fragment2, { children: render({ value, focused: focus.hasFocus, before, cursorChar, after, placeholder }) });
80
+ return /* @__PURE__ */ jsx(Fragment, { children: render({ value, focused: focus.hasFocus, before, cursorChar, after, placeholder }) });
195
81
  }
196
82
  const displayValue = value.length > 0 ? value : placeholder ?? "";
197
83
  const isPlaceholder = value.length === 0;
@@ -199,44 +85,40 @@ function TextInput({ label, value, onChange, onSubmit, placeholder, render, focu
199
85
  const isPlaceholderVisible = value.length === 0 && placeholder != null;
200
86
  const activeCursorChar = isPlaceholderVisible ? placeholder[0] ?? " " : cursorChar;
201
87
  const activeAfter = isPlaceholderVisible ? placeholder.slice(1) : after;
202
- return /* @__PURE__ */ jsxs2(Text2, { children: [
203
- label != null && /* @__PURE__ */ jsxs2(Text2, { bold: true, children: [
88
+ return /* @__PURE__ */ jsxs(Text, { children: [
89
+ label != null && /* @__PURE__ */ jsxs(Text, { bold: true, children: [
204
90
  label,
205
91
  " "
206
92
  ] }),
207
93
  before,
208
- /* @__PURE__ */ jsx2(Text2, { inverse: true, children: activeCursorChar }),
209
- isPlaceholderVisible ? /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: activeAfter }) : activeAfter
94
+ /* @__PURE__ */ jsx(Text, { inverse: true, children: activeCursorChar }),
95
+ isPlaceholderVisible ? /* @__PURE__ */ jsx(Text, { dimColor: true, children: activeAfter }) : activeAfter
210
96
  ] });
211
97
  }
212
- return /* @__PURE__ */ jsxs2(Text2, { dimColor: true, children: [
213
- label != null && /* @__PURE__ */ jsxs2(Text2, { children: [
98
+ return /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
99
+ label != null && /* @__PURE__ */ jsxs(Text, { children: [
214
100
  label,
215
101
  " "
216
102
  ] }),
217
- isPlaceholder ? /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: displayValue }) : displayValue
103
+ isPlaceholder ? /* @__PURE__ */ jsx(Text, { dimColor: true, children: displayValue }) : displayValue
218
104
  ] });
219
105
  }
220
106
 
221
- // src/ui/Select.tsx
222
- import React3, { useEffect as useEffect2, useState as useState3 } from "react";
223
- import { Box as Box4, Text as Text4 } from "ink";
224
-
225
107
  // src/ui/VirtualList.tsx
226
- import React2, { useEffect, useRef as useRef2, useState as useState2 } from "react";
227
- import { Box as Box3 } from "ink";
108
+ import React2, { useEffect, useRef as useRef2, useState } from "react";
109
+ import { Box as Box2 } from "ink";
228
110
 
229
111
  // src/ui/Paginator.tsx
230
- import { Box as Box2, Text as Text3 } from "ink";
231
- import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
112
+ import { Box, Text as Text2 } from "ink";
113
+ import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
232
114
  function Paginator({ total, offset, visible, gap = 0, style = "arrows", position }) {
233
115
  const theme = useTheme();
234
116
  if (style === "none" || total <= visible) return null;
235
117
  const hasAbove = offset > 0;
236
118
  const hasBelow = offset + visible < total;
237
119
  if (style === "arrows") {
238
- if (position === "above" && hasAbove) return /* @__PURE__ */ jsx3(Text3, { color: theme.accentColor, children: "\u2191" });
239
- if (position === "below" && hasBelow) return /* @__PURE__ */ jsx3(Text3, { color: theme.accentColor, children: "\u2193" });
120
+ if (position === "above" && hasAbove) return /* @__PURE__ */ jsx2(Text2, { color: theme.accentColor, children: "\u2191" });
121
+ if (position === "below" && hasBelow) return /* @__PURE__ */ jsx2(Text2, { color: theme.accentColor, children: "\u2193" });
240
122
  return null;
241
123
  }
242
124
  if (style === "dots") {
@@ -244,14 +126,14 @@ function Paginator({ total, offset, visible, gap = 0, style = "arrows", position
244
126
  const totalPages = Math.ceil(total / visible);
245
127
  const maxOffset = Math.max(1, total - visible);
246
128
  const currentPage = Math.round(offset / maxOffset * (totalPages - 1));
247
- return /* @__PURE__ */ jsx3(Text3, { children: Array.from({ length: totalPages }, (_, i) => /* @__PURE__ */ jsxs3(Text3, { dimColor: i !== currentPage, children: [
129
+ return /* @__PURE__ */ jsx2(Text2, { children: Array.from({ length: totalPages }, (_, i) => /* @__PURE__ */ jsxs2(Text2, { dimColor: i !== currentPage, children: [
248
130
  "\u25CF",
249
131
  i < totalPages - 1 ? " " : ""
250
132
  ] }, i)) });
251
133
  }
252
134
  if (style === "counter") {
253
135
  if (position === "above") return null;
254
- return /* @__PURE__ */ jsxs3(Text3, { dimColor: true, children: [
136
+ return /* @__PURE__ */ jsxs2(Text2, { dimColor: true, children: [
255
137
  offset + 1,
256
138
  "\u2013",
257
139
  Math.min(offset + visible, total),
@@ -264,14 +146,14 @@ function Paginator({ total, offset, visible, gap = 0, style = "arrows", position
264
146
  const maxThumbOffset = totalLines - thumbSize;
265
147
  const maxScrollOffset = total - visible;
266
148
  const thumbOffset = maxScrollOffset === 0 ? 0 : Math.round(offset / maxScrollOffset * maxThumbOffset);
267
- return /* @__PURE__ */ jsx3(Box2, { flexDirection: "column", children: Array.from({ length: totalLines }, (_, i) => {
149
+ return /* @__PURE__ */ jsx2(Box, { flexDirection: "column", children: Array.from({ length: totalLines }, (_, i) => {
268
150
  const isThumb = i >= thumbOffset && i < thumbOffset + thumbSize;
269
- return /* @__PURE__ */ jsx3(Text3, { color: isThumb ? theme.accentColor : void 0, dimColor: !isThumb, children: isThumb ? "\u2588" : "\u2502" }, i);
151
+ return /* @__PURE__ */ jsx2(Text2, { color: isThumb ? theme.accentColor : void 0, dimColor: !isThumb, children: isThumb ? "\u2588" : "\u2502" }, i);
270
152
  }) });
271
153
  }
272
154
 
273
155
  // src/ui/VirtualList.tsx
274
- import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
156
+ import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
275
157
  function VirtualList({
276
158
  items,
277
159
  highlightIndex,
@@ -281,7 +163,7 @@ function VirtualList({
281
163
  paginatorStyle = "dots",
282
164
  render
283
165
  }) {
284
- const [internalOffset, setInternalOffset] = useState2(0);
166
+ const [internalOffset, setInternalOffset] = useState(0);
285
167
  const offsetRef = useRef2(0);
286
168
  const windowed = maxVisible != null && items.length > maxVisible;
287
169
  let offset = 0;
@@ -307,7 +189,7 @@ function VirtualList({
307
189
  }
308
190
  }, [windowed, highlightIndex, controlledOffset, internalOffset]);
309
191
  if (!windowed) {
310
- return /* @__PURE__ */ jsx4(Box3, { flexDirection: "column", gap, children: items.map((item, index) => /* @__PURE__ */ jsx4(React2.Fragment, { children: render({ item, index }) }, index)) });
192
+ return /* @__PURE__ */ jsx3(Box2, { flexDirection: "column", gap, children: items.map((item, index) => /* @__PURE__ */ jsx3(React2.Fragment, { children: render({ item, index }) }, index)) });
311
193
  }
312
194
  const visible = items.slice(offset, offset + maxVisible);
313
195
  const paginatorProps = {
@@ -318,25 +200,145 @@ function VirtualList({
318
200
  style: paginatorStyle
319
201
  };
320
202
  if (paginatorStyle === "scrollbar") {
321
- return /* @__PURE__ */ jsxs4(Box3, { flexDirection: "row", gap: 1, children: [
322
- /* @__PURE__ */ jsx4(Box3, { flexDirection: "column", gap, flexGrow: 1, children: visible.map((item, i) => {
203
+ return /* @__PURE__ */ jsxs3(Box2, { flexDirection: "row", gap: 1, children: [
204
+ /* @__PURE__ */ jsx3(Box2, { flexDirection: "column", gap, flexGrow: 1, children: visible.map((item, i) => {
323
205
  const index = offset + i;
324
- return /* @__PURE__ */ jsx4(React2.Fragment, { children: render({ item, index }) }, index);
206
+ return /* @__PURE__ */ jsx3(React2.Fragment, { children: render({ item, index }) }, index);
325
207
  }) }),
326
- /* @__PURE__ */ jsx4(Paginator, { ...paginatorProps })
208
+ /* @__PURE__ */ jsx3(Paginator, { ...paginatorProps })
327
209
  ] });
328
210
  }
329
- return /* @__PURE__ */ jsxs4(Box3, { flexDirection: "column", gap, children: [
330
- /* @__PURE__ */ jsx4(Paginator, { ...paginatorProps, position: "above" }),
211
+ return /* @__PURE__ */ jsxs3(Box2, { flexDirection: "column", gap, children: [
212
+ /* @__PURE__ */ jsx3(Paginator, { ...paginatorProps, position: "above" }),
331
213
  visible.map((item, i) => {
332
214
  const index = offset + i;
333
- return /* @__PURE__ */ jsx4(React2.Fragment, { children: render({ item, index }) }, index);
215
+ return /* @__PURE__ */ jsx3(React2.Fragment, { children: render({ item, index }) }, index);
334
216
  }),
335
- /* @__PURE__ */ jsx4(Paginator, { ...paginatorProps, position: "below" })
217
+ /* @__PURE__ */ jsx3(Paginator, { ...paginatorProps, position: "below" })
336
218
  ] });
337
219
  }
338
220
 
221
+ // src/ui/CommandPalette.tsx
222
+ import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
223
+ var EMPTY_KEY = {
224
+ upArrow: false,
225
+ downArrow: false,
226
+ leftArrow: false,
227
+ rightArrow: false,
228
+ pageDown: false,
229
+ pageUp: false,
230
+ home: false,
231
+ end: false,
232
+ return: false,
233
+ escape: false,
234
+ ctrl: false,
235
+ shift: false,
236
+ tab: false,
237
+ backspace: false,
238
+ delete: false,
239
+ meta: false,
240
+ super: false,
241
+ hyper: false,
242
+ capsLock: false,
243
+ numLock: false
244
+ };
245
+ function fuzzyMatch(name, query) {
246
+ if (!query) return true;
247
+ const lowerName = name.toLowerCase();
248
+ const lowerQuery = query.toLowerCase();
249
+ let qi = 0;
250
+ for (let i = 0; i < lowerName.length && qi < lowerQuery.length; i++) {
251
+ if (lowerName[i] === lowerQuery[qi]) qi++;
252
+ }
253
+ return qi === lowerQuery.length;
254
+ }
255
+ function Inner({
256
+ onClose,
257
+ maxVisible = 10,
258
+ render
259
+ }) {
260
+ const theme = useTheme();
261
+ const [query, setQuery] = useState2("");
262
+ const [selectedIndex, setSelectedIndex] = useState2(0);
263
+ const registry = useKeybindingRegistry();
264
+ const named = registry.all.filter((cmd) => cmd.name != null);
265
+ const filtered = named.filter((cmd) => fuzzyMatch(cmd.name, query));
266
+ const clampedIndex = Math.min(selectedIndex, Math.max(0, filtered.length - 1));
267
+ const onSelect = (cmd) => {
268
+ cmd.handler("", EMPTY_KEY);
269
+ onClose();
270
+ };
271
+ const handleChange = (value) => {
272
+ setQuery(value);
273
+ setSelectedIndex(0);
274
+ };
275
+ const scope = useFocusScope({
276
+ keybindings: {
277
+ escape: onClose,
278
+ enter: () => {
279
+ const cmd = filtered[clampedIndex];
280
+ if (cmd) onSelect(cmd);
281
+ },
282
+ up: () => setSelectedIndex((i) => (i - 1 + filtered.length) % filtered.length),
283
+ down: () => setSelectedIndex((i) => (i + 1) % filtered.length)
284
+ }
285
+ });
286
+ if (render) {
287
+ return /* @__PURE__ */ jsx4(FocusScope, { handle: scope, children: render({ query, onChange: handleChange, filtered, selectedIndex: clampedIndex, onSelect }) });
288
+ }
289
+ return /* @__PURE__ */ jsx4(FocusScope, { handle: scope, children: /* @__PURE__ */ jsxs4(Box3, { flexDirection: "column", children: [
290
+ /* @__PURE__ */ jsx4(TextInput, { label: ">", value: query, onChange: handleChange, placeholder: "Search commands\u2026" }),
291
+ /* @__PURE__ */ jsx4(Box3, { flexDirection: "column", marginTop: 1, children: filtered.length === 0 ? /* @__PURE__ */ jsx4(Text3, { dimColor: true, children: "No commands found" }) : /* @__PURE__ */ jsx4(
292
+ VirtualList,
293
+ {
294
+ items: filtered,
295
+ highlightIndex: clampedIndex,
296
+ maxVisible,
297
+ paginatorStyle: "scrollbar",
298
+ render: ({ item: cmd, index }) => {
299
+ const highlighted = index === clampedIndex;
300
+ const keyColor = highlighted ? theme.hintHighlightColor : theme.hintColor;
301
+ const labelColor = highlighted ? theme.hintHighlightDimColor : theme.hintDimColor;
302
+ return /* @__PURE__ */ jsxs4(Text3, { children: [
303
+ /* @__PURE__ */ jsx4(Text3, { dimColor: true, children: highlighted ? theme.indicator + " " : " " }),
304
+ /* @__PURE__ */ jsx4(Text3, { color: keyColor, bold: true, children: cmd.key }),
305
+ /* @__PURE__ */ jsxs4(Text3, { color: labelColor, children: [
306
+ " ",
307
+ cmd.name
308
+ ] })
309
+ ] });
310
+ }
311
+ }
312
+ ) }),
313
+ /* @__PURE__ */ jsx4(Box3, { marginTop: 1, children: /* @__PURE__ */ jsx4(Text3, { dimColor: true, children: "\u2191\u2193 navigate \xB7 \u21B5 run \xB7 esc close" }) })
314
+ ] }) });
315
+ }
316
+ function HintsBar() {
317
+ const registry = useKeybindingRegistry();
318
+ const theme = useTheme();
319
+ const commands = registry.available.filter((cmd) => cmd.name != null);
320
+ if (commands.length === 0) return null;
321
+ return /* @__PURE__ */ jsx4(Box3, { flexWrap: "wrap", children: commands.map((cmd, index) => /* @__PURE__ */ jsxs4(Text3, { children: [
322
+ /* @__PURE__ */ jsx4(Text3, { color: theme.hintColor, bold: true, children: cmd.key }),
323
+ /* @__PURE__ */ jsxs4(Text3, { color: theme.hintDimColor, children: [
324
+ " ",
325
+ cmd.name
326
+ ] }),
327
+ index < commands.length - 1 && /* @__PURE__ */ jsx4(Text3, { color: theme.hintDimColor, children: " \u2022 " })
328
+ ] }, `${cmd.nodeId}-${cmd.key}`)) });
329
+ }
330
+ var noop = () => {
331
+ };
332
+ function CommandPalette({ onClose, interactive = true, maxVisible, render }) {
333
+ if (!interactive) {
334
+ return /* @__PURE__ */ jsx4(HintsBar, {});
335
+ }
336
+ return /* @__PURE__ */ jsx4(FocusTrap, { children: /* @__PURE__ */ jsx4(Inner, { onClose: onClose ?? noop, maxVisible, render }) });
337
+ }
338
+
339
339
  // src/ui/Select.tsx
340
+ import React3, { useEffect as useEffect2, useState as useState3 } from "react";
341
+ import { Box as Box4, Text as Text4 } from "ink";
340
342
  import { jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
341
343
  function Select({
342
344
  options,
@@ -877,7 +879,7 @@ function Badge({ children, color, background, variant = "round" }) {
877
879
  // src/ui/Panel.tsx
878
880
  import { useState as useState7 } from "react";
879
881
  import { Box as Box9, Text as Text10, measureElement as measureElement2 } from "ink";
880
- import { Fragment as Fragment3, jsx as jsx11, jsxs as jsxs12 } from "react/jsx-runtime";
882
+ import { Fragment as Fragment2, jsx as jsx11, jsxs as jsxs12 } from "react/jsx-runtime";
881
883
  function Panel({ children, title, width, borderColor, footer, ...boxProps }) {
882
884
  const theme = useTheme();
883
885
  const color = borderColor ?? theme.borderColor;
@@ -930,7 +932,7 @@ function Panel({ children, title, width, borderColor, footer, ...boxProps }) {
930
932
  borderTop: false,
931
933
  borderColor: color,
932
934
  paddingX: 1,
933
- children: footer ? /* @__PURE__ */ jsxs12(Fragment3, { children: [
935
+ children: footer ? /* @__PURE__ */ jsxs12(Fragment2, { children: [
934
936
  /* @__PURE__ */ jsx11(Box9, { flexDirection: "column", flexGrow: 1, children }),
935
937
  footer
936
938
  ] }) : children
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "giggles",
3
- "version": "0.7.0",
3
+ "version": "0.7.2",
4
4
  "license": "MIT",
5
5
  "repository": {
6
6
  "type": "git",