giggles 0.6.0 → 0.6.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.
- package/README.md +81 -1
- package/dist/{chunk-QV7GQTKO.js → chunk-TWXBZE5C.js} +51 -9
- package/dist/index.d.ts +1 -2
- package/dist/index.js +1 -1
- package/dist/terminal/index.d.ts +7 -6
- package/dist/ui/index.js +1 -1
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
# giggles
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
<img src="https://github.com/user-attachments/assets/c5c7ef05-232f-4180-8b85-0160fb0f083a" width="700" alt="giggles">
|
|
8
8
|
|
|
9
9
|
giggles is a batteries-included react framework for building terminal apps. built on ink, it handles focus, input routing, screen navigation, and theming out of the box so you can skip the plumbing and build.
|
|
10
10
|
|
|
@@ -30,3 +30,83 @@ npx create-giggles-app
|
|
|
30
30
|
```
|
|
31
31
|
|
|
32
32
|
see [giggles.zzzzion.com](https://giggles.zzzzion.com) for API documentation and live demos.
|
|
33
|
+
|
|
34
|
+
## giggles/ui
|
|
35
|
+
|
|
36
|
+
### [select](https://giggles.zzzzion.com/ui/select)
|
|
37
|
+
|
|
38
|
+
<img src="https://github.com/user-attachments/assets/8ce13f75-7a7b-4123-a973-f2992193bf84" width="500" alt="select">
|
|
39
|
+
|
|
40
|
+
### [multi select](https://giggles.zzzzion.com/ui/multi-select)
|
|
41
|
+
|
|
42
|
+
<img src="https://github.com/user-attachments/assets/24f5d625-6e46-4cb1-8d22-42d40eb48f56" width="500" alt="multi-select">
|
|
43
|
+
|
|
44
|
+
### [markdown](https://giggles.zzzzion.com/ui/markdown)
|
|
45
|
+
|
|
46
|
+
<img src="https://github.com/user-attachments/assets/1cdb6e84-c714-470a-8cf0-b6abf68b78a9" width="500" alt="markdown">
|
|
47
|
+
|
|
48
|
+
### [text input](https://giggles.zzzzion.com/ui/text-input)
|
|
49
|
+
|
|
50
|
+
<img src="https://github.com/user-attachments/assets/b56056ca-97e2-4dc2-a1eb-2ee3f4d559e7" width="500" alt="text-input">
|
|
51
|
+
|
|
52
|
+
### [viewport](https://giggles.zzzzion.com/ui/viewport)
|
|
53
|
+
|
|
54
|
+
<img src="https://github.com/user-attachments/assets/56c6cb6b-b2a7-4803-bed4-c6c6c34042c3" width="500" alt="viewport">
|
|
55
|
+
|
|
56
|
+
### [code block](https://giggles.zzzzion.com/ui/codeblock)
|
|
57
|
+
|
|
58
|
+
<img src="https://github.com/user-attachments/assets/283dbd7e-326c-4acb-b8c0-d3608722952b" width="500" alt="codeblock">
|
|
59
|
+
|
|
60
|
+
### [confirm](https://giggles.zzzzion.com/ui/confirm)
|
|
61
|
+
|
|
62
|
+
<img src="https://github.com/user-attachments/assets/b887da72-bf02-4084-b846-8b90cc3c3487" width="500" alt="confirm">
|
|
63
|
+
|
|
64
|
+
### [spinner](https://giggles.zzzzion.com/ui/spinner)
|
|
65
|
+
|
|
66
|
+
<img src="https://github.com/user-attachments/assets/71aef7f8-e53b-4876-864f-b9b1a5100c5d" width="500" alt="spinner">
|
|
67
|
+
|
|
68
|
+
### [modal](https://giggles.zzzzion.com/ui/modal)
|
|
69
|
+
|
|
70
|
+
<img src="https://github.com/user-attachments/assets/7415c554-927e-4b5c-91d7-7e3b1f4ea0ca" width="500" alt="modal">
|
|
71
|
+
|
|
72
|
+
### [paginator](https://giggles.zzzzion.com/ui/paginator)
|
|
73
|
+
|
|
74
|
+
<img src="https://github.com/user-attachments/assets/b0780f46-848e-4881-822a-86d7db02d212" width="500" alt="paginator">
|
|
75
|
+
|
|
76
|
+
### [autocomplete](https://giggles.zzzzion.com/ui/autocomplete)
|
|
77
|
+
|
|
78
|
+
<img src="https://github.com/user-attachments/assets/aa76dd7a-5357-4969-a979-95975b5ec578" width="500" alt="autocomplete">
|
|
79
|
+
|
|
80
|
+
### [command palette](https://giggles.zzzzion.com/ui/command-palette)
|
|
81
|
+
|
|
82
|
+
<img src="https://github.com/user-attachments/assets/30886cb5-986f-4477-85c1-61cba4b499aa" width="500" alt="command-palette">
|
|
83
|
+
|
|
84
|
+
### [virtual list](https://giggles.zzzzion.com/ui/virtual-list)
|
|
85
|
+
|
|
86
|
+
<img src="https://github.com/user-attachments/assets/d3ef1d92-813c-4546-8d60-2c38745ddbbc" width="500" alt="virtual-list">
|
|
87
|
+
|
|
88
|
+
### [badge](https://giggles.zzzzion.com/ui/badge)
|
|
89
|
+
|
|
90
|
+
<img src="https://github.com/user-attachments/assets/b144c3f5-8b3b-4236-abf2-fc239d23f0c6" width="500" alt="badge">
|
|
91
|
+
|
|
92
|
+
### [panel](https://giggles.zzzzion.com/ui/panel)
|
|
93
|
+
|
|
94
|
+
<img src="https://github.com/user-attachments/assets/9831d73a-baa9-410a-b933-e0dfd9433604" width="500" alt="panel">
|
|
95
|
+
|
|
96
|
+
## giggles/terminal
|
|
97
|
+
|
|
98
|
+
### [useShellOut](https://giggles.zzzzion.com/terminal#useshellout)
|
|
99
|
+
|
|
100
|
+
suspend the UI, hand off the terminal to an external program like `vim` or `less`, and resume cleanly when it exits
|
|
101
|
+
|
|
102
|
+
### [useSpawn](https://giggles.zzzzion.com/terminal#usespawn)
|
|
103
|
+
|
|
104
|
+
spawn a child process and stream its stdout/stderr output into your UI — with support for colored output via a pty
|
|
105
|
+
|
|
106
|
+
### [useTerminalSize](https://giggles.zzzzion.com/terminal#useterminalsize)
|
|
107
|
+
|
|
108
|
+
reactively track the terminal's current dimensions (rows and columns), updating on resize
|
|
109
|
+
|
|
110
|
+
### [useTerminalFocus](https://giggles.zzzzion.com/terminal#useterminalfocus)
|
|
111
|
+
|
|
112
|
+
detect when the terminal window gains or loses focus
|
|
@@ -36,8 +36,13 @@ function useKeybindings(focus, bindings, options) {
|
|
|
36
36
|
}
|
|
37
37
|
|
|
38
38
|
// src/core/input/useKeybindingRegistry.ts
|
|
39
|
+
import { useSyncExternalStore } from "react";
|
|
39
40
|
function useKeybindingRegistry(focus) {
|
|
40
41
|
const store = useStore();
|
|
42
|
+
useSyncExternalStore(
|
|
43
|
+
(cb) => store.subscribe(cb),
|
|
44
|
+
() => store.getVersion()
|
|
45
|
+
);
|
|
41
46
|
const all = store.getAllBindings().filter((b) => b.name != null);
|
|
42
47
|
const branchPath = store.getActiveBranchPath();
|
|
43
48
|
const branchSet = new Set(branchPath);
|
|
@@ -47,15 +52,24 @@ function useKeybindingRegistry(focus) {
|
|
|
47
52
|
const trapIndex = branchPath.indexOf(trapNodeId);
|
|
48
53
|
return trapIndex >= 0 ? new Set(branchPath.slice(0, trapIndex + 1)) : null;
|
|
49
54
|
})();
|
|
50
|
-
const
|
|
51
|
-
|
|
52
|
-
|
|
55
|
+
const availableSet = withinTrapSet ?? branchSet;
|
|
56
|
+
const seenKeys = /* @__PURE__ */ new Set();
|
|
57
|
+
const available = [];
|
|
58
|
+
for (const nodeId of branchPath) {
|
|
59
|
+
if (!availableSet.has(nodeId)) continue;
|
|
60
|
+
for (const b of all) {
|
|
61
|
+
if (b.nodeId === nodeId && !seenKeys.has(b.key)) {
|
|
62
|
+
seenKeys.add(b.key);
|
|
63
|
+
available.push(b);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
53
67
|
const local = focus ? all.filter((b) => b.nodeId === focus.id) : [];
|
|
54
68
|
return { all, available, local };
|
|
55
69
|
}
|
|
56
70
|
|
|
57
71
|
// src/core/focus/useFocusNode.ts
|
|
58
|
-
import { useContext as useContext2, useEffect as useEffect2, useId as useId2, useMemo, useSyncExternalStore } from "react";
|
|
72
|
+
import { useContext as useContext2, useEffect as useEffect2, useId as useId2, useMemo, useSyncExternalStore as useSyncExternalStore2 } from "react";
|
|
59
73
|
function useFocusNode(options) {
|
|
60
74
|
var _a;
|
|
61
75
|
const id = useId2();
|
|
@@ -69,7 +83,7 @@ function useFocusNode(options) {
|
|
|
69
83
|
store.unregisterNode(id);
|
|
70
84
|
};
|
|
71
85
|
}, [id, parentId, store]);
|
|
72
|
-
const hasFocus =
|
|
86
|
+
const hasFocus = useSyncExternalStore2(subscribe, () => store.isFocused(id));
|
|
73
87
|
return { id, hasFocus };
|
|
74
88
|
}
|
|
75
89
|
|
|
@@ -106,7 +120,7 @@ function InputRouter({ children }) {
|
|
|
106
120
|
}
|
|
107
121
|
|
|
108
122
|
// src/core/focus/useFocusScope.ts
|
|
109
|
-
import { useCallback, useContext as useContext3, useEffect as useEffect4, useId as useId3, useMemo as useMemo2, useSyncExternalStore as
|
|
123
|
+
import { useCallback, useContext as useContext3, useEffect as useEffect4, useId as useId3, useMemo as useMemo2, useSyncExternalStore as useSyncExternalStore3 } from "react";
|
|
110
124
|
function useFocusScope(options) {
|
|
111
125
|
var _a;
|
|
112
126
|
const id = useId3();
|
|
@@ -121,8 +135,8 @@ function useFocusScope(options) {
|
|
|
121
135
|
store.unregisterNode(id);
|
|
122
136
|
};
|
|
123
137
|
}, [id, parentId, store]);
|
|
124
|
-
const hasFocus =
|
|
125
|
-
const isPassive =
|
|
138
|
+
const hasFocus = useSyncExternalStore3(subscribe, () => store.isFocused(id));
|
|
139
|
+
const isPassive = useSyncExternalStore3(subscribe, () => store.isPassive(id));
|
|
126
140
|
const next = useCallback(() => store.navigateSibling("next", true, id), [store, id]);
|
|
127
141
|
const prev = useCallback(() => store.navigateSibling("prev", true, id), [store, id]);
|
|
128
142
|
const nextShallow = useCallback(() => store.navigateSibling("next", true, id, true), [store, id]);
|
|
@@ -136,12 +150,25 @@ function useFocusScope(options) {
|
|
|
136
150
|
store.unregisterKeybindings(id, keybindingRegistrationId);
|
|
137
151
|
};
|
|
138
152
|
}, [id, keybindingRegistrationId, store]);
|
|
139
|
-
|
|
153
|
+
useEffect4(() => {
|
|
154
|
+
if (!store.hasFocusScopeComponent(id)) {
|
|
155
|
+
throw new GigglesError(
|
|
156
|
+
"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."
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
}, [id, store]);
|
|
160
|
+
return { id, hasFocus, isPassive, next, prev, nextShallow, prevShallow, escape, drillIn };
|
|
140
161
|
}
|
|
141
162
|
|
|
142
163
|
// src/core/focus/FocusScope.tsx
|
|
164
|
+
import { useEffect as useEffect5 } from "react";
|
|
143
165
|
import { jsx as jsx3 } from "react/jsx-runtime";
|
|
144
166
|
function FocusScope({ handle, children }) {
|
|
167
|
+
const store = useStore();
|
|
168
|
+
useEffect5(() => {
|
|
169
|
+
store.registerFocusScopeComponent(handle.id);
|
|
170
|
+
return () => store.unregisterFocusScopeComponent(handle.id);
|
|
171
|
+
}, [handle.id, store]);
|
|
145
172
|
return /* @__PURE__ */ jsx3(ScopeIdContext.Provider, { value: handle.id, children });
|
|
146
173
|
}
|
|
147
174
|
|
|
@@ -177,7 +204,9 @@ var FocusStore = class {
|
|
|
177
204
|
passiveSet = /* @__PURE__ */ new Set();
|
|
178
205
|
pendingFocusFirstChild = /* @__PURE__ */ new Set();
|
|
179
206
|
trapNodeId = null;
|
|
207
|
+
renderedScopes = /* @__PURE__ */ new Set();
|
|
180
208
|
listeners = /* @__PURE__ */ new Set();
|
|
209
|
+
version = 0;
|
|
181
210
|
// nodeId → registrationId → BindingRegistration
|
|
182
211
|
// Keybindings register synchronously during render; nodes register in useEffect.
|
|
183
212
|
// A keybinding may exist for a node that has not yet appeared in the node tree —
|
|
@@ -191,10 +220,14 @@ var FocusStore = class {
|
|
|
191
220
|
return () => this.listeners.delete(listener);
|
|
192
221
|
}
|
|
193
222
|
notify() {
|
|
223
|
+
this.version++;
|
|
194
224
|
for (const listener of this.listeners) {
|
|
195
225
|
listener();
|
|
196
226
|
}
|
|
197
227
|
}
|
|
228
|
+
getVersion() {
|
|
229
|
+
return this.version;
|
|
230
|
+
}
|
|
198
231
|
// ---------------------------------------------------------------------------
|
|
199
232
|
// Registration
|
|
200
233
|
// ---------------------------------------------------------------------------
|
|
@@ -377,6 +410,15 @@ var FocusStore = class {
|
|
|
377
410
|
// ---------------------------------------------------------------------------
|
|
378
411
|
// Trap
|
|
379
412
|
// ---------------------------------------------------------------------------
|
|
413
|
+
registerFocusScopeComponent(id) {
|
|
414
|
+
this.renderedScopes.add(id);
|
|
415
|
+
}
|
|
416
|
+
unregisterFocusScopeComponent(id) {
|
|
417
|
+
this.renderedScopes.delete(id);
|
|
418
|
+
}
|
|
419
|
+
hasFocusScopeComponent(id) {
|
|
420
|
+
return this.renderedScopes.has(id);
|
|
421
|
+
}
|
|
380
422
|
setTrap(nodeId) {
|
|
381
423
|
this.trapNodeId = nodeId;
|
|
382
424
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -41,8 +41,6 @@ type FocusScopeHandle = {
|
|
|
41
41
|
id: string;
|
|
42
42
|
hasFocus: boolean;
|
|
43
43
|
isPassive: boolean;
|
|
44
|
-
};
|
|
45
|
-
type FocusScopeHelpers = {
|
|
46
44
|
next: () => void;
|
|
47
45
|
prev: () => void;
|
|
48
46
|
nextShallow: () => void;
|
|
@@ -50,6 +48,7 @@ type FocusScopeHelpers = {
|
|
|
50
48
|
escape: () => void;
|
|
51
49
|
drillIn: () => void;
|
|
52
50
|
};
|
|
51
|
+
type FocusScopeHelpers = Pick<FocusScopeHandle, 'next' | 'prev' | 'nextShallow' | 'prevShallow' | 'escape' | 'drillIn'>;
|
|
53
52
|
type FocusScopeOptions = {
|
|
54
53
|
parent?: FocusScopeHandle;
|
|
55
54
|
keybindings?: Keybindings | ((helpers: FocusScopeHelpers) => Keybindings);
|
package/dist/index.js
CHANGED
package/dist/terminal/index.d.ts
CHANGED
|
@@ -4,6 +4,11 @@ type TerminalSize = {
|
|
|
4
4
|
rows: number;
|
|
5
5
|
columns: number;
|
|
6
6
|
};
|
|
7
|
+
type ShellOutHandle = {
|
|
8
|
+
run: (command: string) => Promise<{
|
|
9
|
+
exitCode: number;
|
|
10
|
+
}>;
|
|
11
|
+
};
|
|
7
12
|
type SpawnOptions = SpawnOptionsWithoutStdio & {
|
|
8
13
|
/**
|
|
9
14
|
* Inject FORCE_COLOR=1 and TERM=xterm-256color into the child process
|
|
@@ -28,12 +33,8 @@ declare function useTerminalSize(): TerminalSize;
|
|
|
28
33
|
|
|
29
34
|
declare function useTerminalFocus(callback: (focused: boolean) => void): void;
|
|
30
35
|
|
|
31
|
-
declare function useShellOut():
|
|
32
|
-
run: (command: string) => Promise<{
|
|
33
|
-
exitCode: number;
|
|
34
|
-
}>;
|
|
35
|
-
};
|
|
36
|
+
declare function useShellOut(): ShellOutHandle;
|
|
36
37
|
|
|
37
38
|
declare function useSpawn(): SpawnHandle;
|
|
38
39
|
|
|
39
|
-
export { type SpawnHandle, type SpawnOptions, type SpawnOutputLine, type TerminalSize, useShellOut, useSpawn, useTerminalFocus, useTerminalSize };
|
|
40
|
+
export { type ShellOutHandle, type SpawnHandle, type SpawnOptions, type SpawnOutputLine, type TerminalSize, useShellOut, useSpawn, useTerminalFocus, useTerminalSize };
|
package/dist/ui/index.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "giggles",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.2",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -40,7 +40,8 @@
|
|
|
40
40
|
"scripts": {
|
|
41
41
|
"build": "tsup",
|
|
42
42
|
"build:watch": "nodemon --watch src --ext ts,tsx --exec tsup",
|
|
43
|
-
"play": "tsx --watch",
|
|
43
|
+
"play": "f() { tsx --watch playground/examples/$1.tsx; }; f",
|
|
44
|
+
"record": "f() { vhs playground/tapes/$1.tape; }; f",
|
|
44
45
|
"dev:docs": "pnpm build && concurrently --kill-others \"pnpm build:watch\" \"pnpm --filter documentation dev\"",
|
|
45
46
|
"lint": "prettier --write . && eslint . --fix"
|
|
46
47
|
},
|