@hrnec06/react_utils 1.4.1 → 1.5.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/package.json +4 -3
- package/src/components/Debugger/Debugger.tsx +5 -2
- package/src/components/Debugger/DebuggerSymbols.tsx +6 -1
- package/src/components/Debugger/DebuggerTerminal.tsx +336 -0
- package/src/components/Debugger/parser/DebugParser.tsx +5 -0
- package/src/components/Debugger/parser/DebugParserSimple.tsx +12 -0
- package/src/components/Debugger/parser/DebugTerminal.tsx +341 -0
- package/src/components/Dialog/Dialog.tsx +50 -0
- package/src/components/Dialog/DialogBackdrop.tsx +45 -0
- package/src/components/Dialog/DialogCloseButton.tsx +25 -0
- package/src/components/Dialog/DialogContext.ts +21 -0
- package/src/components/Dialog/DialogPanel.tsx +35 -0
- package/src/components/ResizeableBox/DragAreas.tsx +26 -23
- package/src/components/ResizeableBox/ResizeableBox.tsx +6 -2
- package/src/hooks/useDisposables.ts +13 -0
- package/src/hooks/useFlags.ts +32 -0
- package/src/hooks/useNamespacedId.ts +19 -0
- package/src/hooks/useTransition.ts +261 -0
- package/src/index.ts +11 -4
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hrnec06/react_utils",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.5.0",
|
|
4
4
|
"description": "A debugger component for react.",
|
|
5
5
|
"exports": {
|
|
6
6
|
".": "./src/index.ts"
|
|
@@ -26,12 +26,13 @@
|
|
|
26
26
|
"typescript": "^5.8.3"
|
|
27
27
|
},
|
|
28
28
|
"dependencies": {
|
|
29
|
-
"@hrnec06/util": "^1.
|
|
29
|
+
"@hrnec06/util": "^1.6.1",
|
|
30
30
|
"clsx": "^2.1.1",
|
|
31
31
|
"lucide-react": "^0.525.0",
|
|
32
32
|
"react": "^19.1.0",
|
|
33
33
|
"react-dom": "^19.2.3",
|
|
34
|
-
"uuid": "^13.0.0"
|
|
34
|
+
"uuid": "^13.0.0",
|
|
35
|
+
"zod": "^4.2.1"
|
|
35
36
|
},
|
|
36
37
|
"scripts": {
|
|
37
38
|
"dev": "tsup src/index.tsx --format cjs,esm --dts --watch"
|
|
@@ -19,7 +19,7 @@ interface DebugProps {
|
|
|
19
19
|
autoHeight?: boolean,
|
|
20
20
|
openRoot?: boolean
|
|
21
21
|
}
|
|
22
|
-
|
|
22
|
+
function Debug({
|
|
23
23
|
value,
|
|
24
24
|
openPaths,
|
|
25
25
|
excludePaths,
|
|
@@ -312,4 +312,7 @@ export default function Debug({
|
|
|
312
312
|
),
|
|
313
313
|
document.body
|
|
314
314
|
);
|
|
315
|
-
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
export * as terminal from "./DebuggerTerminal";
|
|
318
|
+
export { Debug };
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import clsx from "clsx";
|
|
2
|
-
import { ChevronDown } from "lucide-react";
|
|
2
|
+
import { ChevronDown, Terminal } from "lucide-react";
|
|
3
3
|
|
|
4
4
|
export function Char_Colon() {
|
|
5
5
|
return (
|
|
@@ -60,4 +60,9 @@ export function Chevron_Toggle({
|
|
|
60
60
|
}
|
|
61
61
|
</button>
|
|
62
62
|
);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function Terminal_Icon()
|
|
66
|
+
{
|
|
67
|
+
return ( <Terminal /> );
|
|
63
68
|
}
|
|
@@ -0,0 +1,336 @@
|
|
|
1
|
+
import React, { createContext, useContext, useEffect, useMemo, useState } from "react";
|
|
2
|
+
import { assert, Optional, ReactUtils } from "@hrnec06/util";
|
|
3
|
+
import useUpdateEffect from "../../hooks/useUpdateEffect";
|
|
4
|
+
import useNamespacedId from "../../hooks/useNamespacedId";
|
|
5
|
+
import z from "zod";
|
|
6
|
+
|
|
7
|
+
const TERMINAL_NAMESPACE = "terminal";
|
|
8
|
+
|
|
9
|
+
enum LogRole {
|
|
10
|
+
Input,
|
|
11
|
+
History
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
enum LogType {
|
|
15
|
+
Info,
|
|
16
|
+
Warning,
|
|
17
|
+
Error,
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
interface LogItem {
|
|
21
|
+
value: unknown,
|
|
22
|
+
role: LogRole,
|
|
23
|
+
type: LogType
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
interface TerminalContext {
|
|
27
|
+
terminal: Terminal,
|
|
28
|
+
parent?: TerminalContext
|
|
29
|
+
}
|
|
30
|
+
const TerminalContext = createContext<Optional<TerminalContext>>(undefined);
|
|
31
|
+
|
|
32
|
+
type OnInputCallback = (command: string, args: string[], terminal: Terminal) => boolean;
|
|
33
|
+
|
|
34
|
+
class Terminal
|
|
35
|
+
{
|
|
36
|
+
public readonly id: string;
|
|
37
|
+
public readonly name: string;
|
|
38
|
+
private readonly onInput?: OnInputCallback;
|
|
39
|
+
|
|
40
|
+
private history: ReactUtils.State<LogItem[]>;
|
|
41
|
+
private inputHistory: ReactUtils.State<string[]>;
|
|
42
|
+
|
|
43
|
+
public parentTerminal?: Terminal;
|
|
44
|
+
|
|
45
|
+
constructor(
|
|
46
|
+
id: string,
|
|
47
|
+
name: string,
|
|
48
|
+
history: ReactUtils.State<LogItem[]>,
|
|
49
|
+
inputHistory: ReactUtils.State<string[]>,
|
|
50
|
+
onInput?: OnInputCallback,
|
|
51
|
+
)
|
|
52
|
+
{
|
|
53
|
+
this.id = id;
|
|
54
|
+
this.name = name;
|
|
55
|
+
this.history = history;
|
|
56
|
+
this.inputHistory = inputHistory;
|
|
57
|
+
this.onInput = onInput;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
protected addLog(value: unknown, type: LogType, role: LogRole)
|
|
61
|
+
{
|
|
62
|
+
const newEntry = {
|
|
63
|
+
value: value,
|
|
64
|
+
role: role,
|
|
65
|
+
type: type
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
this.history[1](prev => [...prev, newEntry]);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
public _mount(history: ReactUtils.State<LogItem[]>)
|
|
72
|
+
{
|
|
73
|
+
this.history = history;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
public getLsKey(): string
|
|
77
|
+
{
|
|
78
|
+
return "terminal." + this.id;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
public clear(): Terminal
|
|
82
|
+
{
|
|
83
|
+
this.history[1]([]);
|
|
84
|
+
|
|
85
|
+
return this;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
public log(value: unknown, type: LogType = LogType.Info): Terminal
|
|
89
|
+
{
|
|
90
|
+
this.addLog(value, type, LogRole.History);
|
|
91
|
+
|
|
92
|
+
return this;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
public getLog(): LogItem[]
|
|
96
|
+
{
|
|
97
|
+
return this.history[0];
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
public getInputHistory(): string[]
|
|
101
|
+
{
|
|
102
|
+
return this.inputHistory[0];
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
public hasInput(): boolean
|
|
106
|
+
{
|
|
107
|
+
return this.onInput !== undefined;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
public input(value: string): boolean
|
|
111
|
+
{
|
|
112
|
+
if (!this.onInput)
|
|
113
|
+
return false;
|
|
114
|
+
|
|
115
|
+
this.inputHistory[1](prev => [
|
|
116
|
+
...prev.filter((item) => item !== value),
|
|
117
|
+
value
|
|
118
|
+
]);
|
|
119
|
+
|
|
120
|
+
this.addLog(value, LogType.Info, LogRole.Input);
|
|
121
|
+
|
|
122
|
+
try
|
|
123
|
+
{
|
|
124
|
+
const args = value.split(" ");
|
|
125
|
+
const command = args.shift();
|
|
126
|
+
assert(typeof command === "string");
|
|
127
|
+
|
|
128
|
+
this.handleCommand(command, args);
|
|
129
|
+
|
|
130
|
+
return true;
|
|
131
|
+
}
|
|
132
|
+
catch (error)
|
|
133
|
+
{
|
|
134
|
+
if (typeof error === "string")
|
|
135
|
+
{
|
|
136
|
+
this.log(error, LogType.Error);
|
|
137
|
+
}
|
|
138
|
+
else if (error instanceof Error)
|
|
139
|
+
{
|
|
140
|
+
this.log(error.message, LogType.Error);
|
|
141
|
+
}
|
|
142
|
+
else
|
|
143
|
+
{
|
|
144
|
+
this.log("Unknown error.", LogType.Error);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return false;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
private handleCommand(command: string, args: string[])
|
|
152
|
+
{
|
|
153
|
+
switch (command)
|
|
154
|
+
{
|
|
155
|
+
case 'clear':
|
|
156
|
+
{
|
|
157
|
+
this.clear();
|
|
158
|
+
this.log("Console was cleared!");
|
|
159
|
+
|
|
160
|
+
return true;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
case 'json':
|
|
164
|
+
{
|
|
165
|
+
const rest = args.join('');
|
|
166
|
+
|
|
167
|
+
try
|
|
168
|
+
{
|
|
169
|
+
const parsed = JSON.parse(rest);
|
|
170
|
+
|
|
171
|
+
this.log(parsed);
|
|
172
|
+
}
|
|
173
|
+
catch (error)
|
|
174
|
+
{
|
|
175
|
+
assert(error instanceof SyntaxError);
|
|
176
|
+
|
|
177
|
+
throw `Invalid JSON: ${error.message}`;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
return true;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
default:
|
|
184
|
+
{
|
|
185
|
+
if (this.onInput && this.onInput(command, args, this))
|
|
186
|
+
{
|
|
187
|
+
return true;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
throw `Invalid command: ${command}`;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
interface TerminalContextProviderProps extends React.PropsWithChildren {
|
|
197
|
+
terminal: Terminal
|
|
198
|
+
}
|
|
199
|
+
function TerminalContextProvider({
|
|
200
|
+
terminal,
|
|
201
|
+
children
|
|
202
|
+
}: TerminalContextProviderProps)
|
|
203
|
+
{
|
|
204
|
+
const parent = findTerminalContext();
|
|
205
|
+
|
|
206
|
+
return (
|
|
207
|
+
<TerminalContext.Provider
|
|
208
|
+
value={{
|
|
209
|
+
terminal: terminal,
|
|
210
|
+
parent: parent
|
|
211
|
+
}}
|
|
212
|
+
>
|
|
213
|
+
{children}
|
|
214
|
+
</TerminalContext.Provider>
|
|
215
|
+
)
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Returns the default history for a terminal
|
|
220
|
+
*/
|
|
221
|
+
function getDefaultHistory(terminal: Terminal)
|
|
222
|
+
{
|
|
223
|
+
try
|
|
224
|
+
{
|
|
225
|
+
const savedValue = localStorage.getItem(terminal.getLsKey());
|
|
226
|
+
|
|
227
|
+
const savedValueJSON = JSON.parse(savedValue ?? "[]");
|
|
228
|
+
|
|
229
|
+
const schema = z.array(z.object({
|
|
230
|
+
value: z.any(),
|
|
231
|
+
role: z.enum(LogRole),
|
|
232
|
+
type: z.enum(LogType)
|
|
233
|
+
}));
|
|
234
|
+
|
|
235
|
+
const parsed = schema.parse(savedValueJSON);
|
|
236
|
+
|
|
237
|
+
return parsed;
|
|
238
|
+
}
|
|
239
|
+
catch (error)
|
|
240
|
+
{
|
|
241
|
+
localStorage.removeItem(terminal.getLsKey());
|
|
242
|
+
console.error("Terminal data parse failed: ", error);
|
|
243
|
+
|
|
244
|
+
return [];
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* Recursively browses the contexts to find a matching terminal
|
|
250
|
+
*/
|
|
251
|
+
function matchTerminal(name: string, ctx: TerminalContext): Optional<TerminalContext>
|
|
252
|
+
{
|
|
253
|
+
if (name === ctx.terminal.name)
|
|
254
|
+
return ctx;
|
|
255
|
+
|
|
256
|
+
if (ctx.parent)
|
|
257
|
+
return matchTerminal(name, ctx.parent);
|
|
258
|
+
|
|
259
|
+
return undefined;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Attempts to find a TerminalContext matching the query
|
|
264
|
+
*/
|
|
265
|
+
function findTerminalContext(name?: string): Optional<TerminalContext>
|
|
266
|
+
{
|
|
267
|
+
const ctx = useContext(TerminalContext);
|
|
268
|
+
|
|
269
|
+
if (ctx && name)
|
|
270
|
+
{
|
|
271
|
+
return matchTerminal(name, ctx);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
return ctx;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* Get terminal context
|
|
279
|
+
*/
|
|
280
|
+
function useTerminalContext(name: string, shouldThrow: true): Terminal;
|
|
281
|
+
function useTerminalContext(name?: string, shouldThrow?: false): Optional<Terminal>
|
|
282
|
+
function useTerminalContext(name?: string, shouldThrow: boolean = false): Optional<Terminal>
|
|
283
|
+
{
|
|
284
|
+
const ctx = findTerminalContext(name);
|
|
285
|
+
|
|
286
|
+
if (shouldThrow === true && ctx === undefined)
|
|
287
|
+
{
|
|
288
|
+
if (name)
|
|
289
|
+
throw new Error(`Terminal with the name "${name}" was not found.`);
|
|
290
|
+
else
|
|
291
|
+
throw new Error(`useTerminalContext may be used only within TerminalContext.Provider!`);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
return ctx?.terminal;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* Initializes a terminal
|
|
299
|
+
*/
|
|
300
|
+
function useTerminal(
|
|
301
|
+
name: string,
|
|
302
|
+
onInput?: OnInputCallback,
|
|
303
|
+
): Terminal
|
|
304
|
+
{
|
|
305
|
+
const id = useNamespacedId(TERMINAL_NAMESPACE);
|
|
306
|
+
|
|
307
|
+
const history = useState<LogItem[]>([]);
|
|
308
|
+
const inputHistory = useState<string[]>([]);
|
|
309
|
+
|
|
310
|
+
const terminal = useMemo(() => {
|
|
311
|
+
return new Terminal(id, name, history, inputHistory, onInput);
|
|
312
|
+
}, [name, history, onInput]);
|
|
313
|
+
|
|
314
|
+
useEffect(() => {
|
|
315
|
+
const def = getDefaultHistory(terminal);
|
|
316
|
+
|
|
317
|
+
history[1](def);
|
|
318
|
+
}, []);
|
|
319
|
+
|
|
320
|
+
useUpdateEffect(() => {
|
|
321
|
+
localStorage.setItem(terminal.getLsKey(), JSON.stringify(history[0]));
|
|
322
|
+
}, [history]);
|
|
323
|
+
|
|
324
|
+
return terminal;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
export {
|
|
328
|
+
LogRole,
|
|
329
|
+
LogType,
|
|
330
|
+
Terminal,
|
|
331
|
+
TerminalContextProvider,
|
|
332
|
+
useTerminal,
|
|
333
|
+
useTerminalContext,
|
|
334
|
+
|
|
335
|
+
type LogItem,
|
|
336
|
+
};
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import useDebugger from "../DebuggerContext";
|
|
2
2
|
import { matchPath } from "../DebuggerLogic";
|
|
3
|
+
import { Terminal } from "../DebuggerTerminal";
|
|
4
|
+
import DebugTerminal from "./DebugTerminal";
|
|
3
5
|
import ValueArray from "./ValueArray";
|
|
4
6
|
import ValueBoolean from "./ValueBoolean";
|
|
5
7
|
import ValueFunction from "./ValueFunction";
|
|
@@ -35,6 +37,9 @@ export default function ParseValue({
|
|
|
35
37
|
|
|
36
38
|
const shouldBeExpanded = (isRootObject && debug.options.openRoot) || (matchPath(path, debug.paths.open) && !matchPath(path, debug.paths.exclude));
|
|
37
39
|
|
|
40
|
+
if (value instanceof Terminal)
|
|
41
|
+
return <DebugTerminal terminal={value} path={path} defaultExpanded={shouldBeExpanded} />
|
|
42
|
+
|
|
38
43
|
if (Array.isArray(value)) {
|
|
39
44
|
return <ValueArray value={value} path={path} defaultExpanded={shouldBeExpanded} />
|
|
40
45
|
}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { TerminalIcon } from "lucide-react";
|
|
2
|
+
import { Terminal } from "../DebuggerTerminal";
|
|
1
3
|
import ValueBoolean from "./ValueBoolean";
|
|
2
4
|
import ValueFunction from "./ValueFunction";
|
|
3
5
|
import ValueKeyword from "./ValueKeyword";
|
|
@@ -42,6 +44,16 @@ export default function ParseValueSimple({
|
|
|
42
44
|
if (value === null)
|
|
43
45
|
return <ValueKeyword value="null" />
|
|
44
46
|
|
|
47
|
+
if (value instanceof Terminal)
|
|
48
|
+
return (
|
|
49
|
+
<div className="inline-flex gap-1 align-bottom text-white">
|
|
50
|
+
<div className="size-5">
|
|
51
|
+
<TerminalIcon />
|
|
52
|
+
</div>
|
|
53
|
+
<span>{value.name}</span>
|
|
54
|
+
</div>)
|
|
55
|
+
;
|
|
56
|
+
|
|
45
57
|
const className = Object.getPrototypeOf(value) !== Object.prototype ? value.constructor.name : null;
|
|
46
58
|
|
|
47
59
|
if (className === null || className.length > MAX_LENGTH)
|