@hrnec06/react_utils 1.4.2 → 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/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)
|
|
@@ -0,0 +1,341 @@
|
|
|
1
|
+
import { createContext, useContext, useLayoutEffect, useRef, useState } from "react";
|
|
2
|
+
import { LogItem, LogRole, LogType, Terminal } from "../DebuggerTerminal"
|
|
3
|
+
import clsx from "clsx";
|
|
4
|
+
import { KeyboardButton, matchBool, matchKeyEvent, matchMouseEvent, MouseButton, Optional } from "@hrnec06/util";
|
|
5
|
+
import ParseValue from "./DebugParser";
|
|
6
|
+
import { ChevronRight, CircleAlert, SendIcon, TerminalIcon, Trash, TriangleAlert } from "lucide-react";
|
|
7
|
+
import ResizeableBox from "../../ResizeableBox/ResizeableBox";
|
|
8
|
+
import { Chevron_Toggle } from "../DebuggerSymbols";
|
|
9
|
+
|
|
10
|
+
interface LocalTerminalContext {
|
|
11
|
+
terminal: Terminal,
|
|
12
|
+
path: string[]
|
|
13
|
+
}
|
|
14
|
+
const LocalTerminalContext = createContext<Optional<LocalTerminalContext>>(undefined);
|
|
15
|
+
|
|
16
|
+
function useLocalTerminalContext()
|
|
17
|
+
{
|
|
18
|
+
const ctx = useContext(LocalTerminalContext);
|
|
19
|
+
|
|
20
|
+
if (!ctx)
|
|
21
|
+
throw new Error("useLocalTerminalContext may be used only within LocalTerminalContext.Provider!");
|
|
22
|
+
|
|
23
|
+
return ctx;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
interface DebugTerminalProps {
|
|
27
|
+
terminal: Terminal,
|
|
28
|
+
path: string[],
|
|
29
|
+
defaultExpanded?: boolean
|
|
30
|
+
}
|
|
31
|
+
export default function DebugTerminal({ terminal, path, defaultExpanded }: DebugTerminalProps)
|
|
32
|
+
{
|
|
33
|
+
const [showing, setShowing] = useState(!!defaultExpanded);
|
|
34
|
+
|
|
35
|
+
const handleToggle = (value: boolean) => {
|
|
36
|
+
setShowing(value);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return (
|
|
40
|
+
<LocalTerminalContext.Provider
|
|
41
|
+
value={{
|
|
42
|
+
terminal: terminal,
|
|
43
|
+
path: path
|
|
44
|
+
}}
|
|
45
|
+
>
|
|
46
|
+
<span className="text-white inline-flex items-center align-bottom">
|
|
47
|
+
<div className="mr-1">
|
|
48
|
+
<TerminalIcon className="size-5" />
|
|
49
|
+
</div>
|
|
50
|
+
|
|
51
|
+
<span>
|
|
52
|
+
{terminal.name}
|
|
53
|
+
</span>
|
|
54
|
+
|
|
55
|
+
<Chevron_Toggle
|
|
56
|
+
expanded={showing}
|
|
57
|
+
onToggle={handleToggle}
|
|
58
|
+
/>
|
|
59
|
+
</span>
|
|
60
|
+
|
|
61
|
+
<TerminalMain show={showing} />
|
|
62
|
+
</LocalTerminalContext.Provider>
|
|
63
|
+
)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
interface TerminalMainProps {
|
|
67
|
+
show: boolean
|
|
68
|
+
}
|
|
69
|
+
function TerminalMain({ show }: TerminalMainProps)
|
|
70
|
+
{
|
|
71
|
+
const [terminalHeight, setTerminalHeight] = useState(300);
|
|
72
|
+
|
|
73
|
+
const handleMouseDown = (e: React.MouseEvent<HTMLDivElement>) => {
|
|
74
|
+
e.stopPropagation();
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (!show)
|
|
78
|
+
return undefined;
|
|
79
|
+
|
|
80
|
+
return (
|
|
81
|
+
<div
|
|
82
|
+
className="block"
|
|
83
|
+
style={{
|
|
84
|
+
height: terminalHeight
|
|
85
|
+
}}
|
|
86
|
+
>
|
|
87
|
+
<ResizeableBox
|
|
88
|
+
$allowedSides={["bottom"]}
|
|
89
|
+
$onResize={(size) => {
|
|
90
|
+
setTerminalHeight(size[1]);
|
|
91
|
+
}}
|
|
92
|
+
$size={[0, terminalHeight]}
|
|
93
|
+
>
|
|
94
|
+
<div
|
|
95
|
+
className={clsx(
|
|
96
|
+
"flex flex-col h-full",
|
|
97
|
+
"border-b border-dashed border-b-zinc-700 pb-3"
|
|
98
|
+
)}
|
|
99
|
+
|
|
100
|
+
onMouseDown={handleMouseDown}
|
|
101
|
+
>
|
|
102
|
+
<TerminalLog />
|
|
103
|
+
|
|
104
|
+
<TerminalInput />
|
|
105
|
+
</div>
|
|
106
|
+
</ResizeableBox>
|
|
107
|
+
</div>
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function TerminalLog()
|
|
112
|
+
{
|
|
113
|
+
const { terminal } = useLocalTerminalContext();
|
|
114
|
+
|
|
115
|
+
const divRef = useRef<HTMLDivElement>(null);
|
|
116
|
+
|
|
117
|
+
useLayoutEffect(() => {
|
|
118
|
+
if (!divRef.current) return;
|
|
119
|
+
|
|
120
|
+
divRef.current.scrollTop = divRef.current.scrollHeight;
|
|
121
|
+
}, [divRef, terminal.getLog()]);
|
|
122
|
+
|
|
123
|
+
const handleWheel = (e: React.WheelEvent<HTMLDivElement>) => {
|
|
124
|
+
e.stopPropagation();
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return (
|
|
128
|
+
<div
|
|
129
|
+
className={clsx(
|
|
130
|
+
"grid flex-1 overflow-auto relative",
|
|
131
|
+
"bg-zinc-900 mt-2 p-2 rounded-md"
|
|
132
|
+
)}
|
|
133
|
+
|
|
134
|
+
onWheel={handleWheel}
|
|
135
|
+
>
|
|
136
|
+
{!!terminal.getLog().length && (<TerminalClearButton />)}
|
|
137
|
+
|
|
138
|
+
<div
|
|
139
|
+
ref={divRef}
|
|
140
|
+
|
|
141
|
+
className="flex flex-col max-h-full overflow-y-auto mt-auto relative"
|
|
142
|
+
>
|
|
143
|
+
{terminal.getLog().map((item, ix) => (
|
|
144
|
+
<TerminalItem
|
|
145
|
+
key={ix}
|
|
146
|
+
item={item}
|
|
147
|
+
index={ix}
|
|
148
|
+
/>
|
|
149
|
+
))}
|
|
150
|
+
</div>
|
|
151
|
+
</div>
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
interface TerminalItemProps {
|
|
156
|
+
item: LogItem,
|
|
157
|
+
index: number
|
|
158
|
+
}
|
|
159
|
+
function TerminalItem({ item, index }: TerminalItemProps)
|
|
160
|
+
{
|
|
161
|
+
const { path } = useLocalTerminalContext();
|
|
162
|
+
|
|
163
|
+
return (
|
|
164
|
+
<div
|
|
165
|
+
className={clsx(
|
|
166
|
+
"grid grid-cols-[18px_24px_1fr]",
|
|
167
|
+
matchBool({
|
|
168
|
+
"text-red-400": item.type === LogType.Error,
|
|
169
|
+
"text-yellow-400": item.type === LogType.Warning,
|
|
170
|
+
}) ?? "text-white"
|
|
171
|
+
)}
|
|
172
|
+
>
|
|
173
|
+
<div />
|
|
174
|
+
|
|
175
|
+
<div className="flex pt-0.5">
|
|
176
|
+
{(() => {
|
|
177
|
+
switch (true)
|
|
178
|
+
{
|
|
179
|
+
case item.role === LogRole.Input: return (<ChevronRight className="size-4" />);
|
|
180
|
+
case item.type === LogType.Error: return (<TriangleAlert className="size-4" />);
|
|
181
|
+
case item.type === LogType.Warning: return (<CircleAlert className="size-4" />)
|
|
182
|
+
}
|
|
183
|
+
return null;
|
|
184
|
+
})()}
|
|
185
|
+
</div>
|
|
186
|
+
<div>
|
|
187
|
+
{
|
|
188
|
+
typeof item.value === "string"
|
|
189
|
+
? (
|
|
190
|
+
<span
|
|
191
|
+
className={clsx(
|
|
192
|
+
|
|
193
|
+
)}
|
|
194
|
+
>
|
|
195
|
+
{item.value}
|
|
196
|
+
</span>
|
|
197
|
+
)
|
|
198
|
+
: (
|
|
199
|
+
<ParseValue value={item.value} path={[...path, `${index}`]} />
|
|
200
|
+
)
|
|
201
|
+
}
|
|
202
|
+
</div>
|
|
203
|
+
</div>
|
|
204
|
+
);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function TerminalClearButton()
|
|
208
|
+
{
|
|
209
|
+
const { terminal } = useLocalTerminalContext();
|
|
210
|
+
|
|
211
|
+
const handleClick = () => {
|
|
212
|
+
terminal.clear();
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
return (
|
|
216
|
+
<button
|
|
217
|
+
onClick={handleClick}
|
|
218
|
+
className={clsx(
|
|
219
|
+
"absolute right-8 top-2",
|
|
220
|
+
"text-zinc-500 hover:text-zinc-200 cursor-pointer"
|
|
221
|
+
)}
|
|
222
|
+
>
|
|
223
|
+
<Trash className="size-5" />
|
|
224
|
+
</button>
|
|
225
|
+
)
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function TerminalInput()
|
|
229
|
+
{
|
|
230
|
+
const { terminal } = useLocalTerminalContext();
|
|
231
|
+
|
|
232
|
+
const [input, setInput] = useState("");
|
|
233
|
+
const [historyOffset, setHistoryOffset] = useState(0);
|
|
234
|
+
|
|
235
|
+
const inputRef = useRef<HTMLInputElement>(null);
|
|
236
|
+
|
|
237
|
+
if (!terminal.hasInput())
|
|
238
|
+
return undefined;
|
|
239
|
+
|
|
240
|
+
const send = () => {
|
|
241
|
+
const value = input.trim();
|
|
242
|
+
|
|
243
|
+
if (!value)
|
|
244
|
+
return;
|
|
245
|
+
|
|
246
|
+
setInput("");
|
|
247
|
+
setHistoryOffset(0);
|
|
248
|
+
|
|
249
|
+
const result = terminal.input(value);
|
|
250
|
+
|
|
251
|
+
console.log(result);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
255
|
+
setInput(e.target.value);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
|
|
259
|
+
e.stopPropagation();
|
|
260
|
+
|
|
261
|
+
if (matchKeyEvent(e.nativeEvent, KeyboardButton.Enter))
|
|
262
|
+
{
|
|
263
|
+
send();
|
|
264
|
+
}
|
|
265
|
+
else if (matchKeyEvent(e.nativeEvent, KeyboardButton.ArrowUp))
|
|
266
|
+
{
|
|
267
|
+
e.preventDefault();
|
|
268
|
+
|
|
269
|
+
const history = terminal.getInputHistory();
|
|
270
|
+
const item = history[history.length - 1 - historyOffset];
|
|
271
|
+
|
|
272
|
+
if (item !== undefined)
|
|
273
|
+
{
|
|
274
|
+
setHistoryOffset(prev => prev + 1);
|
|
275
|
+
setInput(item);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
else if (matchKeyEvent(e.nativeEvent, KeyboardButton.ArrowDown))
|
|
279
|
+
{
|
|
280
|
+
e.preventDefault();
|
|
281
|
+
|
|
282
|
+
const history = terminal.getInputHistory();
|
|
283
|
+
const item = history[history.length + 1 - historyOffset];
|
|
284
|
+
|
|
285
|
+
if (item !== undefined)
|
|
286
|
+
{
|
|
287
|
+
setHistoryOffset(prev => prev - 1);
|
|
288
|
+
setInput(item);
|
|
289
|
+
}
|
|
290
|
+
else
|
|
291
|
+
{
|
|
292
|
+
setInput("");
|
|
293
|
+
setHistoryOffset(0);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
const handleClick = (e: React.MouseEvent<HTMLButtonElement>) => {
|
|
299
|
+
if (!matchMouseEvent(e.nativeEvent, MouseButton.Left)) return;
|
|
300
|
+
|
|
301
|
+
e.stopPropagation();
|
|
302
|
+
|
|
303
|
+
send();
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
return (
|
|
307
|
+
<div
|
|
308
|
+
className={clsx(
|
|
309
|
+
"flex shrink-0 overflow-hidden",
|
|
310
|
+
"bg-zinc-700 rounded-md text-white mt-2",
|
|
311
|
+
"focus-within:ring-2 ring-sky-500 transition-all"
|
|
312
|
+
)}
|
|
313
|
+
>
|
|
314
|
+
<div className="flex items-center justify-center px-2">
|
|
315
|
+
<TerminalIcon className="size-5 text-zinc-500" />
|
|
316
|
+
</div>
|
|
317
|
+
|
|
318
|
+
<input
|
|
319
|
+
ref={inputRef}
|
|
320
|
+
|
|
321
|
+
type="text"
|
|
322
|
+
className={clsx(
|
|
323
|
+
"flex-1",
|
|
324
|
+
"outline-none"
|
|
325
|
+
)}
|
|
326
|
+
|
|
327
|
+
onChange={handleChange}
|
|
328
|
+
onKeyDown={handleKeyDown}
|
|
329
|
+
|
|
330
|
+
value={input}
|
|
331
|
+
/>
|
|
332
|
+
|
|
333
|
+
<button
|
|
334
|
+
className="p-2 cursor-pointer hover:bg-zinc-600 transition-colors rounded-md"
|
|
335
|
+
onClick={handleClick}
|
|
336
|
+
>
|
|
337
|
+
<SendIcon className="size-5" />
|
|
338
|
+
</button>
|
|
339
|
+
</div>
|
|
340
|
+
)
|
|
341
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { KeyboardButton } from "@hrnec06/util";
|
|
2
|
+
import useKeyListener from "../../hooks/useKeyListener";
|
|
3
|
+
import useTransition from "../../hooks/useTransition";
|
|
4
|
+
import DialogBackdrop from "./DialogBackdrop";
|
|
5
|
+
import DialogCloseButton from "./DialogCloseButton";
|
|
6
|
+
import { DialogContext } from "./DialogContext";
|
|
7
|
+
import DialogPanel from "./DialogPanel";
|
|
8
|
+
import { useEffect } from "react";
|
|
9
|
+
|
|
10
|
+
interface DialogProps extends React.PropsWithChildren {
|
|
11
|
+
open?: boolean,
|
|
12
|
+
duration?: number,
|
|
13
|
+
onClose?: () => void
|
|
14
|
+
}
|
|
15
|
+
function Dialog({
|
|
16
|
+
open = false,
|
|
17
|
+
duration = 300,
|
|
18
|
+
onClose,
|
|
19
|
+
children,
|
|
20
|
+
}: DialogProps)
|
|
21
|
+
{
|
|
22
|
+
const pressingEsc = useKeyListener(KeyboardButton.Escape);
|
|
23
|
+
|
|
24
|
+
useEffect(() => {
|
|
25
|
+
if (!open || !pressingEsc) return;
|
|
26
|
+
|
|
27
|
+
onClose?.();
|
|
28
|
+
}, [pressingEsc]);
|
|
29
|
+
|
|
30
|
+
return (
|
|
31
|
+
<DialogContext.Provider
|
|
32
|
+
value={{
|
|
33
|
+
open: open,
|
|
34
|
+
onClose: onClose,
|
|
35
|
+
|
|
36
|
+
options: {
|
|
37
|
+
animationDuration: duration
|
|
38
|
+
},
|
|
39
|
+
}}
|
|
40
|
+
>
|
|
41
|
+
{children}
|
|
42
|
+
</DialogContext.Provider>
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
Dialog.Backdrop = DialogBackdrop;
|
|
47
|
+
Dialog.CloseButton = DialogCloseButton;
|
|
48
|
+
Dialog.Panel = DialogPanel;
|
|
49
|
+
|
|
50
|
+
export default Dialog;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import clsx from "clsx";
|
|
2
|
+
import useDialog from "./DialogContext";
|
|
3
|
+
import useTransition, { transitionDataAttributes } from "../../hooks/useTransition";
|
|
4
|
+
import { useRef } from "react";
|
|
5
|
+
|
|
6
|
+
interface DialogBackdropProps extends Omit<React.HTMLProps<HTMLDivElement>, 'children'>
|
|
7
|
+
{
|
|
8
|
+
$transition?: boolean
|
|
9
|
+
}
|
|
10
|
+
export default function DialogBackdrop({
|
|
11
|
+
$transition = false,
|
|
12
|
+
|
|
13
|
+
...props
|
|
14
|
+
}: DialogBackdropProps)
|
|
15
|
+
{
|
|
16
|
+
const dialog = useDialog();
|
|
17
|
+
|
|
18
|
+
const divRef = useRef<HTMLDivElement>(null);
|
|
19
|
+
|
|
20
|
+
const [visible, transitionData] = useTransition($transition, divRef.current, dialog.open);
|
|
21
|
+
|
|
22
|
+
if (!visible)
|
|
23
|
+
return undefined;
|
|
24
|
+
|
|
25
|
+
const handleClick = () => {
|
|
26
|
+
console.log("close");
|
|
27
|
+
|
|
28
|
+
dialog.onClose?.();
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return (
|
|
32
|
+
<div
|
|
33
|
+
{...props}
|
|
34
|
+
{...transitionDataAttributes(transitionData)}
|
|
35
|
+
|
|
36
|
+
ref={divRef}
|
|
37
|
+
|
|
38
|
+
onClick={handleClick}
|
|
39
|
+
|
|
40
|
+
className={clsx(
|
|
41
|
+
props.className,
|
|
42
|
+
)}
|
|
43
|
+
/>
|
|
44
|
+
)
|
|
45
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import useDialog from "./DialogContext";
|
|
2
|
+
|
|
3
|
+
interface DialogCloseButtonProps extends React.HTMLAttributes<HTMLButtonElement> {
|
|
4
|
+
|
|
5
|
+
}
|
|
6
|
+
export default function DialogCloseButton({
|
|
7
|
+
...props
|
|
8
|
+
}: DialogCloseButtonProps)
|
|
9
|
+
{
|
|
10
|
+
const dialog = useDialog();
|
|
11
|
+
|
|
12
|
+
const handleClick = () => {
|
|
13
|
+
dialog.onClose?.();
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
return (
|
|
17
|
+
<button
|
|
18
|
+
{...props}
|
|
19
|
+
|
|
20
|
+
onClick={handleClick}
|
|
21
|
+
>
|
|
22
|
+
{props.children}
|
|
23
|
+
</button>
|
|
24
|
+
)
|
|
25
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { Optional } from "@hrnec06/util";
|
|
2
|
+
import { createContext, useContext } from "react";
|
|
3
|
+
|
|
4
|
+
interface DialogContext {
|
|
5
|
+
open: boolean,
|
|
6
|
+
options: {
|
|
7
|
+
animationDuration: number,
|
|
8
|
+
},
|
|
9
|
+
onClose?: () => void
|
|
10
|
+
}
|
|
11
|
+
export const DialogContext = createContext<Optional<DialogContext>>(undefined);
|
|
12
|
+
|
|
13
|
+
export default function useDialog()
|
|
14
|
+
{
|
|
15
|
+
const ctx = useContext(DialogContext);
|
|
16
|
+
|
|
17
|
+
if (!ctx)
|
|
18
|
+
throw new Error("useDialog may be used only within DialogContext.Provider!");
|
|
19
|
+
|
|
20
|
+
return ctx;
|
|
21
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import clsx from "clsx";
|
|
2
|
+
import useDialog from "./DialogContext";
|
|
3
|
+
import useTransition, { transitionDataAttributes } from "../../hooks/useTransition";
|
|
4
|
+
import { useRef } from "react";
|
|
5
|
+
|
|
6
|
+
interface DialogPanelProps extends React.HTMLProps<HTMLDivElement> {
|
|
7
|
+
$transition?: boolean
|
|
8
|
+
}
|
|
9
|
+
export default function DialogPanel({
|
|
10
|
+
$transition = false,
|
|
11
|
+
...props
|
|
12
|
+
}: DialogPanelProps)
|
|
13
|
+
{
|
|
14
|
+
const dialog = useDialog();
|
|
15
|
+
|
|
16
|
+
const divRef = useRef<HTMLDivElement>(null);
|
|
17
|
+
|
|
18
|
+
const [visible, transitionData] = useTransition($transition, divRef.current, dialog.open);
|
|
19
|
+
|
|
20
|
+
if (!visible)
|
|
21
|
+
return undefined;
|
|
22
|
+
|
|
23
|
+
return (
|
|
24
|
+
<div
|
|
25
|
+
{...props}
|
|
26
|
+
{...transitionDataAttributes(transitionData)}
|
|
27
|
+
|
|
28
|
+
ref={divRef}
|
|
29
|
+
|
|
30
|
+
className={props.className}
|
|
31
|
+
>
|
|
32
|
+
{props.children}
|
|
33
|
+
</div>
|
|
34
|
+
)
|
|
35
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { useEffect, useState } from 'react'
|
|
2
|
+
import { disposables } from '@hrnec06/util';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* @credit https://github.com/tailwindlabs/headlessui/blob/main/packages/%40headlessui-react/src/hooks/use-disposables.ts
|
|
6
|
+
*/
|
|
7
|
+
export default function useDisposables() {
|
|
8
|
+
const [_disposables] = useState(disposables);
|
|
9
|
+
|
|
10
|
+
useEffect(() => () => _disposables.dispose(), [_disposables]);
|
|
11
|
+
|
|
12
|
+
return _disposables;
|
|
13
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { Flags, ReactUtils } from "@hrnec06/util";
|
|
2
|
+
import { useCallback, useMemo, useState } from "react";
|
|
3
|
+
|
|
4
|
+
export class FlagsManager extends Flags
|
|
5
|
+
{
|
|
6
|
+
constructor(
|
|
7
|
+
private setState: ReactUtils.SetState<number>,
|
|
8
|
+
initialFlags?: number,
|
|
9
|
+
)
|
|
10
|
+
{
|
|
11
|
+
super(initialFlags);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
public setFlags(flags: number): Flags {
|
|
15
|
+
this.flags = flags;
|
|
16
|
+
|
|
17
|
+
this.setState(flags);
|
|
18
|
+
|
|
19
|
+
return this;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function useFlags(initialFlags: number = 0): FlagsManager
|
|
24
|
+
{
|
|
25
|
+
const [_, setFlags] = useState(initialFlags);
|
|
26
|
+
|
|
27
|
+
const manager = useMemo(() => new FlagsManager(setFlags), []);
|
|
28
|
+
|
|
29
|
+
return manager;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export default useFlags;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { useState } from "react";
|
|
2
|
+
|
|
3
|
+
const namespaces: Record<string, number> = {};
|
|
4
|
+
|
|
5
|
+
function getId(namespace: string): string
|
|
6
|
+
{
|
|
7
|
+
const nextID = (namespaces[namespace] ?? 0) + 1;
|
|
8
|
+
|
|
9
|
+
namespaces[namespace] = nextID;
|
|
10
|
+
|
|
11
|
+
return `nid_${namespace}_${nextID}`;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export default function useNamespacedId(namespace: string): string
|
|
15
|
+
{
|
|
16
|
+
const [id] = useState(() => getId(namespace));
|
|
17
|
+
|
|
18
|
+
return id;
|
|
19
|
+
}
|
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
import { disposables, mapRecord, Nullable } from "@hrnec06/util";
|
|
2
|
+
import { useLayoutEffect, useRef, useState } from "react";
|
|
3
|
+
import useFlags from "./useFlags";
|
|
4
|
+
import useDisposables from "./useDisposables";
|
|
5
|
+
|
|
6
|
+
enum TransitionState {
|
|
7
|
+
None = 0,
|
|
8
|
+
|
|
9
|
+
Closed = 1 << 0,
|
|
10
|
+
|
|
11
|
+
Enter = 1 << 1,
|
|
12
|
+
Leave = 1 << 2,
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface TransitionStates {
|
|
16
|
+
enter?: boolean,
|
|
17
|
+
leave?: boolean,
|
|
18
|
+
closed?: boolean,
|
|
19
|
+
transition?: boolean
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function transitionDataAttributes(data: TransitionStates)
|
|
23
|
+
{
|
|
24
|
+
return mapRecord(data, (key, value) => [
|
|
25
|
+
`data-${key}`,
|
|
26
|
+
value === true ? "true" : undefined
|
|
27
|
+
]);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export default function useTransition(
|
|
31
|
+
enabled: boolean,
|
|
32
|
+
element: Nullable<HTMLElement>,
|
|
33
|
+
show: boolean
|
|
34
|
+
): [visible: boolean, data: TransitionStates]
|
|
35
|
+
{
|
|
36
|
+
const [visible, setVisible] = useState(show);
|
|
37
|
+
|
|
38
|
+
const flags = useFlags(
|
|
39
|
+
(enabled && visible) ? TransitionState.Enter | TransitionState.Closed : TransitionState.None
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
const inFlight = useRef(false);
|
|
43
|
+
const cancelledRef = useRef(false);
|
|
44
|
+
|
|
45
|
+
const d = useDisposables();
|
|
46
|
+
|
|
47
|
+
useLayoutEffect(() => {
|
|
48
|
+
if (!enabled) return;
|
|
49
|
+
|
|
50
|
+
if (show)
|
|
51
|
+
{
|
|
52
|
+
setVisible(true);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (!element)
|
|
56
|
+
{
|
|
57
|
+
if (show)
|
|
58
|
+
{
|
|
59
|
+
flags.addFlag(TransitionState.Enter | TransitionState.Closed);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return transition(
|
|
66
|
+
element,
|
|
67
|
+
{
|
|
68
|
+
inFlight: inFlight,
|
|
69
|
+
prepare: () => {
|
|
70
|
+
if (cancelledRef.current)
|
|
71
|
+
{
|
|
72
|
+
cancelledRef.current = false;
|
|
73
|
+
}
|
|
74
|
+
else
|
|
75
|
+
{
|
|
76
|
+
cancelledRef.current = inFlight.current;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
inFlight.current = true;
|
|
80
|
+
|
|
81
|
+
if (cancelledRef.current) return;
|
|
82
|
+
|
|
83
|
+
if (show)
|
|
84
|
+
{
|
|
85
|
+
flags.addFlag(TransitionState.Enter | TransitionState.Closed);
|
|
86
|
+
flags.removeFlag(TransitionState.Leave);
|
|
87
|
+
}
|
|
88
|
+
else
|
|
89
|
+
{
|
|
90
|
+
flags.addFlag(TransitionState.Leave);
|
|
91
|
+
flags.removeFlag(TransitionState.Enter);
|
|
92
|
+
}
|
|
93
|
+
},
|
|
94
|
+
run: () => {
|
|
95
|
+
if (cancelledRef.current)
|
|
96
|
+
{
|
|
97
|
+
if (show)
|
|
98
|
+
{
|
|
99
|
+
flags.addFlag(TransitionState.Leave);
|
|
100
|
+
flags.removeFlag(TransitionState.Enter | TransitionState.Closed);
|
|
101
|
+
}
|
|
102
|
+
else
|
|
103
|
+
{
|
|
104
|
+
flags.addFlag(TransitionState.Enter | TransitionState.Closed);
|
|
105
|
+
flags.removeFlag(TransitionState.Leave);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
else
|
|
109
|
+
{
|
|
110
|
+
if (show)
|
|
111
|
+
{
|
|
112
|
+
flags.removeFlag(TransitionState.Closed);
|
|
113
|
+
}
|
|
114
|
+
else
|
|
115
|
+
{
|
|
116
|
+
flags.addFlag(TransitionState.Closed);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
},
|
|
120
|
+
done: () => {
|
|
121
|
+
if (cancelledRef.current)
|
|
122
|
+
{
|
|
123
|
+
if (hasPendingTransitions(element))
|
|
124
|
+
{
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
inFlight.current = false;
|
|
130
|
+
|
|
131
|
+
flags.removeFlag(TransitionState.Enter | TransitionState.Leave | TransitionState.Closed);
|
|
132
|
+
|
|
133
|
+
if (!show)
|
|
134
|
+
{
|
|
135
|
+
setVisible(false);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
);
|
|
140
|
+
}, [enabled, show, element, d]);
|
|
141
|
+
|
|
142
|
+
if (!enabled)
|
|
143
|
+
{
|
|
144
|
+
return [
|
|
145
|
+
show,
|
|
146
|
+
{
|
|
147
|
+
closed: undefined,
|
|
148
|
+
enter: undefined,
|
|
149
|
+
leave: undefined,
|
|
150
|
+
transition: undefined
|
|
151
|
+
}
|
|
152
|
+
] as const;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
return [
|
|
156
|
+
visible,
|
|
157
|
+
{
|
|
158
|
+
closed: flags.hasFlag(TransitionState.Closed),
|
|
159
|
+
enter: flags.hasFlag(TransitionState.Enter),
|
|
160
|
+
leave: flags.hasFlag(TransitionState.Leave),
|
|
161
|
+
transition: flags.hasFlag(TransitionState.Enter) || flags.hasFlag(TransitionState.Leave)
|
|
162
|
+
}
|
|
163
|
+
] as const;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function transition(
|
|
167
|
+
node: HTMLElement,
|
|
168
|
+
{
|
|
169
|
+
prepare,
|
|
170
|
+
run,
|
|
171
|
+
done,
|
|
172
|
+
inFlight
|
|
173
|
+
}: {
|
|
174
|
+
prepare: () => void,
|
|
175
|
+
run: () => void,
|
|
176
|
+
done: () => void,
|
|
177
|
+
inFlight: React.RefObject<boolean>
|
|
178
|
+
}
|
|
179
|
+
)
|
|
180
|
+
{
|
|
181
|
+
const d = disposables();
|
|
182
|
+
|
|
183
|
+
prepareTransition(node, {
|
|
184
|
+
prepare,
|
|
185
|
+
inFlight
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
d.nextFrame(() => {
|
|
189
|
+
run();
|
|
190
|
+
|
|
191
|
+
d.requestAnimationFrame(() => {
|
|
192
|
+
d.add(waitForTransition(node, done));
|
|
193
|
+
});
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
return d.dispose;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function waitForTransition(node: Nullable<HTMLElement>, done: () => void)
|
|
200
|
+
{
|
|
201
|
+
const d = disposables();
|
|
202
|
+
|
|
203
|
+
if (!node) return d.dispose;
|
|
204
|
+
|
|
205
|
+
let cancelled = false;
|
|
206
|
+
d.add(() => {
|
|
207
|
+
cancelled = true;
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
const transitions = node.getAnimations?.().filter((animation) => animation instanceof CSSTransition) ?? [];
|
|
211
|
+
|
|
212
|
+
if (transitions.length === 0)
|
|
213
|
+
{
|
|
214
|
+
done();
|
|
215
|
+
|
|
216
|
+
return d.dispose;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
Promise.allSettled(transitions.map((transition) => transition.finished))
|
|
220
|
+
.then(() => {
|
|
221
|
+
if (!cancelled)
|
|
222
|
+
{
|
|
223
|
+
done();
|
|
224
|
+
}
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
return d.dispose;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function prepareTransition(
|
|
231
|
+
node: HTMLElement,
|
|
232
|
+
{ inFlight, prepare }: { inFlight?: React.RefObject<boolean>, prepare: () => void }
|
|
233
|
+
)
|
|
234
|
+
{
|
|
235
|
+
if (inFlight?.current)
|
|
236
|
+
{
|
|
237
|
+
prepare();
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
const previous = node.style.transition;
|
|
242
|
+
|
|
243
|
+
node.style.transition = "none";
|
|
244
|
+
|
|
245
|
+
prepare();
|
|
246
|
+
|
|
247
|
+
// Trigger a reflow, flushing the CSS changes
|
|
248
|
+
node.offsetHeight;
|
|
249
|
+
|
|
250
|
+
// Reset the transition to what it was before
|
|
251
|
+
node.style.transition = previous;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function hasPendingTransitions(node: HTMLElement): boolean
|
|
255
|
+
{
|
|
256
|
+
const animations = node.getAnimations?.() ?? []
|
|
257
|
+
|
|
258
|
+
return animations.some((animation) => {
|
|
259
|
+
return animation instanceof CSSTransition && animation.playState !== 'finished'
|
|
260
|
+
})
|
|
261
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -6,17 +6,20 @@ import useWindowSize from "./hooks/useWindowSize";
|
|
|
6
6
|
import useEfficientRef from "./hooks/useEfficientRef";
|
|
7
7
|
import useEfficientState from "./hooks/useEfficientState";
|
|
8
8
|
import useUUID from "./hooks/useUUID";
|
|
9
|
+
import useFlags from "./hooks/useFlags";
|
|
9
10
|
|
|
10
11
|
import useSignal, { Signal } from "./hooks/useSignal";
|
|
11
|
-
|
|
12
12
|
import useLazySignal, { LazySignal } from "./hooks/useLazySignal";
|
|
13
13
|
|
|
14
|
-
import Debugger from './components/Debugger/Debugger';
|
|
14
|
+
import * as Debugger from './components/Debugger/Debugger';
|
|
15
|
+
|
|
15
16
|
import ResizeableBox from "./components/ResizeableBox/ResizeableBox";
|
|
17
|
+
import Dialog from "./components/Dialog/Dialog";
|
|
16
18
|
|
|
17
19
|
import * as util from './lib/utils';
|
|
18
20
|
|
|
19
21
|
export {
|
|
22
|
+
// Hooks
|
|
20
23
|
useKeyListener,
|
|
21
24
|
useListener,
|
|
22
25
|
useUpdatedRef,
|
|
@@ -25,15 +28,19 @@ export {
|
|
|
25
28
|
useEfficientRef,
|
|
26
29
|
useEfficientState,
|
|
27
30
|
useUUID,
|
|
31
|
+
useFlags,
|
|
28
32
|
|
|
33
|
+
// Signals
|
|
29
34
|
useSignal,
|
|
30
35
|
Signal,
|
|
31
|
-
|
|
32
36
|
useLazySignal,
|
|
33
37
|
LazySignal,
|
|
34
38
|
|
|
39
|
+
// Components
|
|
35
40
|
Debugger,
|
|
36
41
|
ResizeableBox,
|
|
42
|
+
Dialog,
|
|
37
43
|
|
|
38
|
-
|
|
44
|
+
// Utilities
|
|
45
|
+
util,
|
|
39
46
|
};
|