giggles 0.2.4 → 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
+ };
@@ -0,0 +1,23 @@
1
+ // src/terminal/components/AlternateScreen.tsx
2
+ import { useEffect, useState } from "react";
3
+ import { Fragment, jsx } from "react/jsx-runtime";
4
+ var _a;
5
+ var isTTY = typeof process !== "undefined" && ((_a = process.stdout) == null ? void 0 : _a.write);
6
+ function AlternateScreen({ children }) {
7
+ const [ready, setReady] = useState(!isTTY);
8
+ useEffect(() => {
9
+ if (!isTTY) return;
10
+ process.stdout.write("\x1B[?1049h");
11
+ process.stdout.write("\x1B[2J");
12
+ process.stdout.write("\x1B[H");
13
+ setReady(true);
14
+ return () => {
15
+ process.stdout.write("\x1B[?1049l");
16
+ };
17
+ }, []);
18
+ return ready ? /* @__PURE__ */ jsx(Fragment, { children }) : null;
19
+ }
20
+
21
+ export {
22
+ AlternateScreen
23
+ };
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,14 +14,29 @@ type GigglesProviderProps = {
13
14
  };
14
15
  declare function GigglesProvider({ children }: GigglesProviderProps): react_jsx_runtime.JSX.Element;
15
16
 
17
+ declare function useKeybindings(focus: FocusHandle, bindings: Keybindings, options?: KeybindingOptions): void;
18
+
19
+ type KeybindingRegistry = {
20
+ all: RegisteredKeybinding[];
21
+ available: RegisteredKeybinding[];
22
+ local: RegisteredKeybinding[];
23
+ };
24
+ declare function useKeybindingRegistry(focus?: FocusHandle): KeybindingRegistry;
25
+
26
+ type FocusTrapProps = {
27
+ children: React.ReactNode;
28
+ };
29
+ declare function FocusTrap({ children }: FocusTrapProps): react_jsx_runtime.JSX.Element;
30
+
16
31
  type FocusGroupProps = {
17
32
  children: React__default.ReactNode;
18
33
  direction?: 'vertical' | 'horizontal';
19
34
  value?: string;
20
35
  wrap?: boolean;
21
36
  navigable?: boolean;
37
+ keybindings?: Keybindings;
22
38
  };
23
- declare function FocusGroup({ children, direction, value, wrap, navigable }: FocusGroupProps): react_jsx_runtime.JSX.Element;
39
+ declare function FocusGroup({ children, direction, value, wrap, navigable, keybindings: customBindings }: FocusGroupProps): react_jsx_runtime.JSX.Element;
24
40
 
25
41
  type FocusHandle = {
26
42
  id: string;
@@ -32,23 +48,6 @@ declare const useFocus: (id?: string) => FocusHandle;
32
48
 
33
49
  declare function useFocusState<T extends string>(initial: T): readonly [T, React$1.Dispatch<React$1.SetStateAction<T>>];
34
50
 
35
- type KeyHandler = (input: string, key: Key) => void;
36
- type SpecialKey = 'up' | 'down' | 'left' | 'right' | 'enter' | 'escape' | 'tab' | 'backspace' | 'delete' | 'pageup' | 'pagedown' | 'home' | 'end';
37
- type KeyName = SpecialKey | (string & {});
38
- type Keybindings = Partial<Record<KeyName, KeyHandler>>;
39
- type KeybindingOptions = {
40
- capture?: boolean;
41
- onKeypress?: (input: string, key: Key) => void;
42
- layer?: string;
43
- };
44
-
45
- declare function useKeybindings(focus: FocusHandle, bindings: Keybindings, options?: KeybindingOptions): void;
46
-
47
- type FocusTrapProps = {
48
- children: React.ReactNode;
49
- };
50
- declare function FocusTrap({ children }: FocusTrapProps): react_jsx_runtime.JSX.Element;
51
-
52
51
  type RouterProps = {
53
52
  children: React__default.ReactNode;
54
53
  initialScreen: string;
@@ -80,4 +79,4 @@ type NavigationContextValue = {
80
79
 
81
80
  declare const useNavigation: () => NavigationContextValue;
82
81
 
83
- 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 };