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 +1 -1
- package/dist/chunk-4LED4GXQ.js +460 -0
- package/dist/index.d.ts +10 -12
- package/dist/index.js +42 -432
- package/dist/types-ClgDW3fy.d.ts +26 -0
- package/dist/ui/index.d.ts +18 -0
- package/dist/ui/index.js +100 -0
- package/package.json +5 -1
package/README.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
[](https://github.com/zion-off/giggles/actions/workflows/giggles-ci.yml)
|
|
2
2
|
[](https://github.com/zion-off/giggles/actions/workflows/giggles-cd.yml)
|
|
3
3
|
[](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 {
|
|
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,
|
|
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
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
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
|
|
20
|
+
import { jsx } from "react/jsx-runtime";
|
|
412
21
|
function GigglesProvider({ children }) {
|
|
413
|
-
return /* @__PURE__ */
|
|
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
|
|
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
|
|
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
|
|
430
|
-
var NavigationContext =
|
|
38
|
+
import { createContext, useContext } from "react";
|
|
39
|
+
var NavigationContext = createContext(null);
|
|
431
40
|
var useNavigation = () => {
|
|
432
|
-
const context =
|
|
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
|
|
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 =
|
|
453
|
-
const parentId =
|
|
61
|
+
const screenNodeId = useId();
|
|
62
|
+
const parentId = React.useContext(FocusNodeContext);
|
|
454
63
|
const { registerNode, unregisterNode, focusFirstChild, focusNode, getFocusedId } = useFocusContext();
|
|
455
|
-
const lastFocusedChildRef =
|
|
456
|
-
const wasTopRef =
|
|
457
|
-
|
|
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
|
-
|
|
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 =
|
|
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__ */
|
|
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
|
|
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 =
|
|
501
|
-
const routes =
|
|
502
|
-
const screenNamesRef =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
135
|
+
const pop = useCallback(() => {
|
|
527
136
|
dispatch({ type: "pop" });
|
|
528
137
|
}, []);
|
|
529
|
-
const replace =
|
|
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 =
|
|
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__ */
|
|
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__ */
|
|
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 };
|
package/dist/ui/index.js
ADDED
|
@@ -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.
|
|
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": [
|