@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hrnec06/react_utils",
3
- "version": "1.4.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.4.21",
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
- export default function Debug({
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)