giggles 0.0.3 → 0.1.0

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 ADDED
@@ -0,0 +1 @@
1
+ wip -- see [giggles.zzzzion.com](https://giggles.zzzzion.com)!
package/dist/index.d.ts CHANGED
@@ -1,2 +1,204 @@
1
+ import * as react_jsx_runtime from 'react/jsx-runtime';
2
+ import * as React$1 from 'react';
3
+ import React__default from 'react';
4
+ import { Key } from 'ink';
5
+ export { Key } from 'ink';
1
6
 
2
- export { }
7
+ type GigglesProviderProps = {
8
+ children: React__default.ReactNode;
9
+ };
10
+ /**
11
+ * Root provider for Giggles applications. Combines focus and input systems.
12
+ *
13
+ * Wraps your app in the necessary context providers and sets up the input router.
14
+ * All Giggles apps must be wrapped in this provider.
15
+ *
16
+ * @example
17
+ * ```tsx
18
+ * import { render } from 'ink';
19
+ * import { GigglesProvider } from 'giggles';
20
+ * import { App } from './App';
21
+ *
22
+ * render(
23
+ * <GigglesProvider>
24
+ * <App />
25
+ * </GigglesProvider>
26
+ * );
27
+ * ```
28
+ */
29
+ declare function GigglesProvider({ children }: GigglesProviderProps): react_jsx_runtime.JSX.Element;
30
+
31
+ type FocusGroupProps = {
32
+ children: React__default.ReactNode;
33
+ direction?: 'vertical' | 'horizontal';
34
+ value?: string;
35
+ wrap?: boolean;
36
+ navigable?: boolean;
37
+ };
38
+ declare function FocusGroup({ children, direction, value, wrap, navigable }: FocusGroupProps): react_jsx_runtime.JSX.Element;
39
+
40
+ declare const useFocus: () => {
41
+ id: string;
42
+ focused: boolean;
43
+ focus: () => void;
44
+ };
45
+
46
+ declare function useFocusState<T extends string>(initial: T): readonly [T, React$1.Dispatch<React$1.SetStateAction<T>>];
47
+
48
+ /**
49
+ * Handler function for a key binding.
50
+ *
51
+ * If a binding exists for a key, the handler will be called and the input will stop bubbling.
52
+ * Unhandled keys (those without bindings) automatically bubble up to parent components.
53
+ *
54
+ * For conditional handling, either:
55
+ * - Register bindings conditionally based on component state, or
56
+ * - Use internal logic within the handler to decide whether to perform an action
57
+ *
58
+ * @param input - The character string for the key pressed (e.g., 'j', 'q')
59
+ * @param key - Ink's Key object with boolean properties (downArrow, return, escape, etc.)
60
+ *
61
+ * @example
62
+ * ```tsx
63
+ * useKeybindings(focus, {
64
+ * j: () => moveDown(),
65
+ * k: () => moveUp(),
66
+ * return: () => selectItem()
67
+ * });
68
+ * ```
69
+ */
70
+ type KeyHandler = (input: string, key: Key) => void;
71
+ type SpecialKey = 'up' | 'down' | 'left' | 'right' | 'enter' | 'escape' | 'tab' | 'backspace' | 'delete' | 'pageup' | 'pagedown' | 'home' | 'end';
72
+ type KeyName = SpecialKey | (string & {});
73
+ /**
74
+ * Map of key names to handler functions.
75
+ *
76
+ * Keys are strings that match either:
77
+ * - Special keys (autocompleted): 'enter', 'escape', 'up', 'down', etc.
78
+ * - Character keys: 'j', 'k', 'a', '?', etc.
79
+ * - Modified keys: 'ctrl+c', 'ctrl+q', etc.
80
+ *
81
+ * @example
82
+ * ```tsx
83
+ * const bindings: Keybindings = {
84
+ * j: () => moveDown(),
85
+ * k: () => moveUp(),
86
+ * enter: () => selectItem(),
87
+ * 'ctrl+q': () => quit()
88
+ * };
89
+ * ```
90
+ */
91
+ type Keybindings = Partial<Record<KeyName, KeyHandler>>;
92
+ /**
93
+ * Options for configuring keybinding behavior.
94
+ */
95
+ type KeybindingOptions = {
96
+ /**
97
+ * Enable capture mode. When true, explicit bindings are checked first,
98
+ * then all remaining keystrokes go to `onKeypress`. Nothing bubbles.
99
+ *
100
+ * Used for text inputs and components that need to receive all keystrokes.
101
+ */
102
+ capture?: boolean;
103
+ /**
104
+ * Handler for keystrokes in capture mode that don't match explicit bindings.
105
+ *
106
+ * Only called when `capture` is true.
107
+ */
108
+ onKeypress?: (input: string, key: Key) => void;
109
+ /**
110
+ * Optional layer identifier for organizing keybindings.
111
+ *
112
+ * Can be used for debugging or building tooling that inspects keybindings.
113
+ * Has no effect on input routing behavior.
114
+ */
115
+ layer?: string;
116
+ };
117
+
118
+ /**
119
+ * Registers keybindings that only fire when this component is focused.
120
+ *
121
+ * Solves Ink's global `useInput` problem by routing keypresses through the focus tree.
122
+ * Only the focused component receives input. Unhandled keys bubble up to parent components.
123
+ *
124
+ * @param focus - The focus handle from `useFocus()`. Ties bindings to this component's focus node.
125
+ * @param bindings - Map of key names to handler functions. Handlers only fire when focused.
126
+ * @param options - Optional configuration for capture mode, keypress handlers, and layers.
127
+ *
128
+ * @example
129
+ * ```tsx
130
+ * function FileList() {
131
+ * const focus = useFocus();
132
+ *
133
+ * useKeybindings(focus, {
134
+ * j: () => moveDown(),
135
+ * k: () => moveUp(),
136
+ * enter: () => openFile()
137
+ * });
138
+ *
139
+ * return <List focused={focus.focused} items={files} />;
140
+ * }
141
+ * ```
142
+ *
143
+ * @example Capture mode for text input
144
+ * ```tsx
145
+ * function TextInput() {
146
+ * const focus = useFocus();
147
+ * const [value, setValue] = useState('');
148
+ *
149
+ * useKeybindings(
150
+ * focus,
151
+ * {
152
+ * escape: () => blur(),
153
+ * enter: () => submit(value)
154
+ * },
155
+ * {
156
+ * capture: true,
157
+ * onKeypress: (key) => setValue(v => v + key)
158
+ * }
159
+ * );
160
+ *
161
+ * return <Text>{value}</Text>;
162
+ * }
163
+ * ```
164
+ */
165
+ declare function useKeybindings(focus: {
166
+ id: string;
167
+ }, bindings: Keybindings, options?: KeybindingOptions): void;
168
+
169
+ type FocusTrapProps = {
170
+ children: React.ReactNode;
171
+ };
172
+ /**
173
+ * Stops input bubbling at a boundary. Used for modals and dialogs.
174
+ *
175
+ * When a key is pressed, it bubbles up the focus tree until handled.
176
+ * FocusTrap creates a barrier - if nothing inside the trap handles the key,
177
+ * it stops at the trap boundary instead of bubbling to parent components.
178
+ *
179
+ * @example
180
+ * ```tsx
181
+ * function App() {
182
+ * const [showModal, setShowModal] = useState(false);
183
+ *
184
+ * useKeybindings(focus, {
185
+ * '?': () => setShowModal(true),
186
+ * q: () => exit()
187
+ * });
188
+ *
189
+ * return (
190
+ * <Screen>
191
+ * <MainContent />
192
+ * {showModal && (
193
+ * <FocusTrap>
194
+ * <HelpModal onClose={() => setShowModal(false)} />
195
+ * </FocusTrap>
196
+ * )}
197
+ * </Screen>
198
+ * );
199
+ * }
200
+ * ```
201
+ */
202
+ declare function FocusTrap({ children }: FocusTrapProps): react_jsx_runtime.JSX.Element;
203
+
204
+ export { FocusGroup, FocusTrap, GigglesProvider, type KeyHandler, type KeybindingOptions, type Keybindings, useFocus, useFocusState, useKeybindings };
package/dist/index.js CHANGED
@@ -0,0 +1,391 @@
1
+ // src/core/focus/FocusContext.tsx
2
+ import { createContext, useCallback, useContext, useRef, useState } from "react";
3
+ import { jsx } from "react/jsx-runtime";
4
+ var FocusContext = createContext(null);
5
+ var FocusProvider = ({ children }) => {
6
+ const nodesRef = useRef(/* @__PURE__ */ new Map());
7
+ const [focusedId, setFocusedId] = useState(null);
8
+ const [activeBranchNodes, setActiveBranchNodes] = useState(/* @__PURE__ */ new Set());
9
+ const [activeBranchPath, setActiveBranchPath] = useState([]);
10
+ const focusNode = useCallback((id) => {
11
+ const nodes = nodesRef.current;
12
+ if (!nodes.has(id)) return;
13
+ setFocusedId((current) => {
14
+ if (current === id) return current;
15
+ const pathSet = /* @__PURE__ */ new Set();
16
+ const pathArray = [];
17
+ let currentNode = id;
18
+ while (currentNode) {
19
+ pathSet.add(currentNode);
20
+ pathArray.push(currentNode);
21
+ const node = nodes.get(currentNode);
22
+ currentNode = (node == null ? void 0 : node.parentId) ?? null;
23
+ }
24
+ setActiveBranchNodes(pathSet);
25
+ setActiveBranchPath(pathArray);
26
+ return id;
27
+ });
28
+ }, []);
29
+ const registerNode = useCallback(
30
+ (id, parentId) => {
31
+ const nodes = nodesRef.current;
32
+ const node = {
33
+ id,
34
+ parentId,
35
+ childrenIds: []
36
+ };
37
+ nodes.set(id, node);
38
+ if (parentId) {
39
+ const parent = nodes.get(parentId);
40
+ if (parent && !parent.childrenIds.includes(id)) {
41
+ parent.childrenIds.push(id);
42
+ }
43
+ }
44
+ nodes.forEach((existingNode) => {
45
+ if (existingNode.parentId === id && !node.childrenIds.includes(existingNode.id)) {
46
+ node.childrenIds.push(existingNode.id);
47
+ }
48
+ });
49
+ if (nodes.size === 1) {
50
+ focusNode(id);
51
+ }
52
+ },
53
+ [focusNode]
54
+ );
55
+ const unregisterNode = useCallback((id) => {
56
+ const nodes = nodesRef.current;
57
+ const node = nodes.get(id);
58
+ if (!node) return;
59
+ if (node.parentId) {
60
+ const parent = nodes.get(node.parentId);
61
+ if (parent) {
62
+ parent.childrenIds = parent.childrenIds.filter((childId) => childId !== id);
63
+ }
64
+ }
65
+ nodes.delete(id);
66
+ setFocusedId((current) => {
67
+ if (current !== id) return current;
68
+ if (node.parentId) {
69
+ const pathSet = /* @__PURE__ */ new Set();
70
+ const pathArray = [];
71
+ let currentNode = node.parentId;
72
+ while (currentNode) {
73
+ pathSet.add(currentNode);
74
+ pathArray.push(currentNode);
75
+ const n = nodes.get(currentNode);
76
+ currentNode = (n == null ? void 0 : n.parentId) ?? null;
77
+ }
78
+ setActiveBranchNodes(pathSet);
79
+ setActiveBranchPath(pathArray);
80
+ return node.parentId;
81
+ }
82
+ setActiveBranchNodes(/* @__PURE__ */ new Set());
83
+ setActiveBranchPath([]);
84
+ return null;
85
+ });
86
+ }, []);
87
+ const isFocused = useCallback(
88
+ (id) => {
89
+ return id === focusedId;
90
+ },
91
+ [focusedId]
92
+ );
93
+ const getFocusedId = useCallback(() => {
94
+ return focusedId;
95
+ }, [focusedId]);
96
+ const isInActiveBranch = useCallback(
97
+ (id) => {
98
+ return activeBranchNodes.has(id);
99
+ },
100
+ [activeBranchNodes]
101
+ );
102
+ const getActiveBranchPath = useCallback(() => {
103
+ return activeBranchPath;
104
+ }, [activeBranchPath]);
105
+ const navigateSibling = useCallback(
106
+ (direction, wrap = true) => {
107
+ const currentId = focusedId;
108
+ if (!currentId) return;
109
+ const nodes = nodesRef.current;
110
+ const currentNode = nodes.get(currentId);
111
+ if (!(currentNode == null ? void 0 : currentNode.parentId)) return;
112
+ const parent = nodes.get(currentNode.parentId);
113
+ if (!parent || parent.childrenIds.length === 0) return;
114
+ const siblings = parent.childrenIds;
115
+ const currentIndex = siblings.indexOf(currentId);
116
+ if (currentIndex === -1) return;
117
+ let nextIndex;
118
+ if (wrap) {
119
+ nextIndex = direction === "next" ? (currentIndex + 1) % siblings.length : (currentIndex - 1 + siblings.length) % siblings.length;
120
+ } else {
121
+ nextIndex = direction === "next" ? Math.min(currentIndex + 1, siblings.length - 1) : Math.max(currentIndex - 1, 0);
122
+ }
123
+ focusNode(siblings[nextIndex]);
124
+ },
125
+ [focusedId, focusNode]
126
+ );
127
+ return /* @__PURE__ */ jsx(
128
+ FocusContext.Provider,
129
+ {
130
+ value: {
131
+ registerNode,
132
+ unregisterNode,
133
+ focusNode,
134
+ isFocused,
135
+ getFocusedId,
136
+ isInActiveBranch,
137
+ getActiveBranchPath,
138
+ navigateSibling
139
+ },
140
+ children
141
+ }
142
+ );
143
+ };
144
+ var FocusNodeContext = createContext(null);
145
+ var useFocusContext = () => {
146
+ const context = useContext(FocusContext);
147
+ if (!context) {
148
+ throw new Error("useFocusContext must be used within a FocusProvider");
149
+ }
150
+ return context;
151
+ };
152
+
153
+ // src/core/focus/FocusGroup.tsx
154
+ import { useCallback as useCallback3, useEffect as useEffect4, useMemo, useRef as useRef3 } from "react";
155
+
156
+ // src/core/input/InputContext.tsx
157
+ import { createContext as createContext2, useCallback as useCallback2, useContext as useContext2, useRef as useRef2 } from "react";
158
+ import { jsx as jsx2 } from "react/jsx-runtime";
159
+ var InputContext = createContext2(null);
160
+ var InputProvider = ({ children }) => {
161
+ const bindingsRef = useRef2(/* @__PURE__ */ new Map());
162
+ const trapNodeIdRef = useRef2(null);
163
+ const registerKeybindings = useCallback2((nodeId, bindings, options) => {
164
+ const registration = {
165
+ bindings: new Map(Object.entries(bindings).filter((entry) => entry[1] != null)),
166
+ capture: (options == null ? void 0 : options.capture) ?? false,
167
+ onKeypress: options == null ? void 0 : options.onKeypress,
168
+ layer: options == null ? void 0 : options.layer
169
+ };
170
+ bindingsRef.current.set(nodeId, registration);
171
+ }, []);
172
+ const unregisterKeybindings = useCallback2((nodeId) => {
173
+ bindingsRef.current.delete(nodeId);
174
+ }, []);
175
+ const getNodeBindings = useCallback2((nodeId) => {
176
+ return bindingsRef.current.get(nodeId);
177
+ }, []);
178
+ const setTrap = useCallback2((nodeId) => {
179
+ trapNodeIdRef.current = nodeId;
180
+ }, []);
181
+ const clearTrap = useCallback2((nodeId) => {
182
+ if (trapNodeIdRef.current === nodeId) {
183
+ trapNodeIdRef.current = null;
184
+ }
185
+ }, []);
186
+ const getTrapNodeId = useCallback2(() => {
187
+ return trapNodeIdRef.current;
188
+ }, []);
189
+ const getAllBindings = useCallback2(() => {
190
+ const allBindings = [];
191
+ bindingsRef.current.forEach((nodeBindings, nodeId) => {
192
+ nodeBindings.bindings.forEach((handler, key) => {
193
+ allBindings.push({
194
+ nodeId,
195
+ key,
196
+ handler,
197
+ layer: nodeBindings.layer
198
+ });
199
+ });
200
+ });
201
+ return allBindings;
202
+ }, []);
203
+ return /* @__PURE__ */ jsx2(
204
+ InputContext.Provider,
205
+ {
206
+ value: {
207
+ registerKeybindings,
208
+ unregisterKeybindings,
209
+ getNodeBindings,
210
+ setTrap,
211
+ clearTrap,
212
+ getTrapNodeId,
213
+ getAllBindings
214
+ },
215
+ children
216
+ }
217
+ );
218
+ };
219
+ function useInputContext() {
220
+ const context = useContext2(InputContext);
221
+ if (!context) {
222
+ throw new Error("useInputContext must be used within an InputProvider");
223
+ }
224
+ return context;
225
+ }
226
+
227
+ // src/core/input/InputRouter.tsx
228
+ import { useInput } from "ink";
229
+
230
+ // src/core/input/normalizeKey.ts
231
+ function normalizeKey(input, key) {
232
+ if (key.downArrow) return "down";
233
+ if (key.upArrow) return "up";
234
+ if (key.leftArrow) return "left";
235
+ if (key.rightArrow) return "right";
236
+ if (key.return) return "enter";
237
+ if (key.escape) return "escape";
238
+ if (key.tab) return "tab";
239
+ if (key.backspace) return "backspace";
240
+ if (key.delete) return "delete";
241
+ if (key.pageUp) return "pageup";
242
+ if (key.pageDown) return "pagedown";
243
+ if (key.home) return "home";
244
+ if (key.end) return "end";
245
+ return input;
246
+ }
247
+
248
+ // src/core/input/InputRouter.tsx
249
+ import { Fragment, jsx as jsx3 } from "react/jsx-runtime";
250
+ function InputRouter({ children }) {
251
+ const { getFocusedId, getActiveBranchPath } = useFocusContext();
252
+ const { getNodeBindings, getTrapNodeId } = useInputContext();
253
+ useInput((input, key) => {
254
+ const focusedId = getFocusedId();
255
+ if (!focusedId) return;
256
+ const path = getActiveBranchPath();
257
+ const trapNodeId = getTrapNodeId();
258
+ const keyName = normalizeKey(input, key);
259
+ for (const nodeId of path) {
260
+ const nodeBindings = getNodeBindings(nodeId);
261
+ if (!nodeBindings) continue;
262
+ const handler = nodeBindings.bindings.get(keyName);
263
+ if (handler) {
264
+ handler(input, key);
265
+ return;
266
+ }
267
+ if (nodeBindings.capture && nodeBindings.onKeypress) {
268
+ nodeBindings.onKeypress(input, key);
269
+ return;
270
+ }
271
+ if (nodeId === trapNodeId) {
272
+ return;
273
+ }
274
+ }
275
+ });
276
+ return /* @__PURE__ */ jsx3(Fragment, { children });
277
+ }
278
+
279
+ // src/core/input/useKeybindings.tsx
280
+ import { useEffect } from "react";
281
+ function useKeybindings(focus, bindings, options) {
282
+ const { registerKeybindings, unregisterKeybindings } = useInputContext();
283
+ registerKeybindings(focus.id, bindings, options);
284
+ useEffect(() => {
285
+ return () => unregisterKeybindings(focus.id);
286
+ }, [focus.id, unregisterKeybindings]);
287
+ }
288
+
289
+ // src/core/input/FocusTrap.tsx
290
+ import { useEffect as useEffect2 } from "react";
291
+ import { Fragment as Fragment2, jsx as jsx4 } from "react/jsx-runtime";
292
+ function FocusTrap({ children }) {
293
+ const { id } = useFocus();
294
+ const { setTrap, clearTrap } = useInputContext();
295
+ useEffect2(() => {
296
+ setTrap(id);
297
+ return () => clearTrap(id);
298
+ }, [id, setTrap, clearTrap]);
299
+ return /* @__PURE__ */ jsx4(Fragment2, { children });
300
+ }
301
+
302
+ // src/core/focus/FocusBindContext.tsx
303
+ import { createContext as createContext3, useContext as useContext3 } from "react";
304
+ var FocusBindContext = createContext3(null);
305
+
306
+ // src/core/focus/useFocus.ts
307
+ import { useContext as useContext4, useEffect as useEffect3, useId } from "react";
308
+ var useFocus = () => {
309
+ const nodeId = useId();
310
+ const parentId = useContext4(FocusNodeContext);
311
+ const { focusNode, registerNode, unregisterNode, isFocused } = useFocusContext();
312
+ useEffect3(() => {
313
+ registerNode(nodeId, parentId);
314
+ return () => {
315
+ unregisterNode(nodeId);
316
+ };
317
+ }, [nodeId, parentId, registerNode, unregisterNode]);
318
+ return {
319
+ id: nodeId,
320
+ focused: isFocused(nodeId),
321
+ focus: () => focusNode(nodeId)
322
+ };
323
+ };
324
+
325
+ // src/core/focus/FocusGroup.tsx
326
+ import { jsx as jsx5 } from "react/jsx-runtime";
327
+ function FocusGroup({
328
+ children,
329
+ direction = "vertical",
330
+ value,
331
+ wrap = true,
332
+ navigable = true
333
+ }) {
334
+ const { id } = useFocus();
335
+ const { focusNode, navigateSibling } = useFocusContext();
336
+ const bindMapRef = useRef3(/* @__PURE__ */ new Map());
337
+ const register = useCallback3((logicalId, nodeId) => {
338
+ bindMapRef.current.set(logicalId, nodeId);
339
+ }, []);
340
+ const unregister = useCallback3((logicalId) => {
341
+ bindMapRef.current.delete(logicalId);
342
+ }, []);
343
+ useEffect4(() => {
344
+ if (value) {
345
+ const nodeId = bindMapRef.current.get(value);
346
+ if (nodeId) {
347
+ focusNode(nodeId);
348
+ }
349
+ }
350
+ }, [value, focusNode]);
351
+ const bindContextValue = value ? { register, unregister } : null;
352
+ const navigationKeys = useMemo(() => {
353
+ if (!navigable) return {};
354
+ const next = () => navigateSibling("next", wrap);
355
+ const prev = () => navigateSibling("prev", wrap);
356
+ return direction === "vertical" ? {
357
+ j: next,
358
+ k: prev,
359
+ down: next,
360
+ up: prev
361
+ } : {
362
+ l: next,
363
+ h: prev,
364
+ right: next,
365
+ left: prev
366
+ };
367
+ }, [navigable, direction, wrap, navigateSibling]);
368
+ useKeybindings({ id }, navigationKeys);
369
+ return /* @__PURE__ */ jsx5(FocusNodeContext.Provider, { value: id, children: /* @__PURE__ */ jsx5(FocusBindContext.Provider, { value: bindContextValue, children }) });
370
+ }
371
+
372
+ // src/core/focus/useFocusState.ts
373
+ import { useState as useState2 } from "react";
374
+ function useFocusState(initial) {
375
+ const [focused, setFocused] = useState2(initial);
376
+ return [focused, setFocused];
377
+ }
378
+
379
+ // src/core/GigglesProvider.tsx
380
+ import { jsx as jsx6 } from "react/jsx-runtime";
381
+ function GigglesProvider({ children }) {
382
+ return /* @__PURE__ */ jsx6(FocusProvider, { children: /* @__PURE__ */ jsx6(InputProvider, { children: /* @__PURE__ */ jsx6(InputRouter, { children }) }) });
383
+ }
384
+ export {
385
+ FocusGroup,
386
+ FocusTrap,
387
+ GigglesProvider,
388
+ useFocus,
389
+ useFocusState,
390
+ useKeybindings
391
+ };
package/package.json CHANGED
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "name": "giggles",
3
- "version": "0.0.3",
3
+ "version": "0.1.0",
4
4
  "license": "MIT",
5
5
  "repository": {
6
6
  "type": "git",
7
- "url": "git+https://github.com/zion-off/bento.git"
7
+ "url": "git+https://github.com/zion-off/giggles.git"
8
8
  },
9
9
  "type": "module",
10
10
  "main": "dist/index.js",
@@ -31,13 +31,13 @@
31
31
  "ink": "^6.0.0",
32
32
  "react": "^18.0.0 || ^19.0.0"
33
33
  },
34
- "dependencies": {},
35
34
  "devDependencies": {
36
35
  "@trivago/prettier-plugin-sort-imports": "^4.3.0",
37
36
  "@types/node": "^22.0.0",
38
37
  "@types/react": "^19.2.13",
39
38
  "eslint": "^9.0.0",
40
39
  "eslint-plugin-react": "^7.32.2",
40
+ "eslint-plugin-react-hooks": "^7.0.1",
41
41
  "ink": "^6.6.0",
42
42
  "nodemon": "^3.1.11",
43
43
  "prettier": "^2.8.7",