@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.
@@ -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
+ }
@@ -100,29 +100,32 @@ function DragArea({
100
100
  onMove,
101
101
  applyReposition = false,
102
102
 
103
- ...enabledSides
103
+ ...assignedSides
104
104
  }: DragAreaProps)
105
105
  {
106
106
  const box = useResizeableBox();
107
107
 
108
108
  const isAllowed = (side: RB.DragSide) => box.options.allowedSides.includes(side);
109
109
 
110
+ // Is a corner and allowCorners are either empty or false
110
111
  if (isCorner && !box.options.allowCorners)
111
112
  return undefined;
112
113
 
113
- if (
114
- (enabledSides.top && !isAllowed('top')) ||
115
- (enabledSides.right && !isAllowed('right')) ||
116
- (enabledSides.bottom && !isAllowed('bottom')) ||
117
- (enabledSides.left && !isAllowed('left'))
118
- )
119
- return undefined;
120
-
114
+ // Is corner and allowedCorners are array
121
115
  if (isCorner && Array.isArray(box.options.allowCorners))
122
116
  {
123
- if (!box.options.allowCorners.some((combo) => enabledSides[combo[0]] && enabledSides[combo[1]]))
117
+ if (!box.options.allowCorners.some((combo) => assignedSides[combo[0]] && assignedSides[combo[1]]))
124
118
  return undefined;
125
119
  }
120
+ else if (
121
+ (assignedSides.top && !isAllowed('top')) ||
122
+ (assignedSides.right && !isAllowed('right')) ||
123
+ (assignedSides.bottom && !isAllowed('bottom')) ||
124
+ (assignedSides.left && !isAllowed('left'))
125
+ )
126
+ {
127
+ return undefined;
128
+ }
126
129
 
127
130
  const handleMouseDown = (e: React.MouseEvent<HTMLDivElement>) => {
128
131
  if (box.drag.value !== null || !matchMouseEvent(e.nativeEvent, MouseButton.Left))
@@ -160,21 +163,21 @@ function DragArea({
160
163
  box.options.dragAreaSize === 'big' && 'w-6'
161
164
  ),
162
165
 
163
- enabledSides.top && clsx('bottom-full', box.options.insetAreas && 'translate-y-1/2'),
164
- enabledSides.right && clsx('left-full', box.options.insetAreas && '-translate-x-1/2'),
165
- enabledSides.bottom && clsx('top-full', box.options.insetAreas && '-translate-y-1/2'),
166
- enabledSides.left && clsx('right-full', box.options.insetAreas && 'translate-x-1/2'),
166
+ assignedSides.top && clsx('bottom-full', box.options.insetAreas && 'translate-y-1/2'),
167
+ assignedSides.right && clsx('left-full', box.options.insetAreas && '-translate-x-1/2'),
168
+ assignedSides.bottom && clsx('top-full', box.options.insetAreas && '-translate-y-1/2'),
169
+ assignedSides.left && clsx('right-full', box.options.insetAreas && 'translate-x-1/2'),
167
170
 
168
171
  matchBool({
169
- "cursor-nw-resize": enabledSides.top && enabledSides.left,
170
- "cursor-ne-resize": enabledSides.top && enabledSides.right,
171
- "cursor-se-resize": enabledSides.bottom && enabledSides.right,
172
- "cursor-sw-resize": enabledSides.bottom && enabledSides.left,
173
-
174
- "cursor-n-resize": enabledSides.top,
175
- "cursor-e-resize": enabledSides.right,
176
- "cursor-s-resize": enabledSides.bottom,
177
- "cursor-w-resize": enabledSides.left
172
+ "cursor-nw-resize": assignedSides.top && assignedSides.left,
173
+ "cursor-ne-resize": assignedSides.top && assignedSides.right,
174
+ "cursor-se-resize": assignedSides.bottom && assignedSides.right,
175
+ "cursor-sw-resize": assignedSides.bottom && assignedSides.left,
176
+
177
+ "cursor-n-resize": assignedSides.top,
178
+ "cursor-e-resize": assignedSides.right,
179
+ "cursor-s-resize": assignedSides.bottom,
180
+ "cursor-w-resize": assignedSides.left
178
181
  }),
179
182
  )}
180
183
  />
@@ -32,8 +32,12 @@ interface ResizeableBoxProps extends React.HTMLProps<HTMLDivElement>
32
32
  */
33
33
  $allowedSides?: RB.DragSide[],
34
34
  /**
35
- * Allow corner resizers?
36
- * A corner will be visible only if the 2 neighbouring sides are visible
35
+ * Allow corner resizers
36
+ *
37
+ * Possible values:
38
+ * - `true`: Corners will be applied automatically based on allowed sides
39
+ * - `false`: No corners
40
+ * - `Vector2<RB.DragSide>[]`: Explicitely define allowed corners
37
41
  */
38
42
  $allowCorners?: boolean | Vector2<RB.DragSide>[],
39
43
  /**
@@ -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
+ }