@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
|
@@ -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
|
-
...
|
|
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
|
-
|
|
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) =>
|
|
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
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
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":
|
|
170
|
-
"cursor-ne-resize":
|
|
171
|
-
"cursor-se-resize":
|
|
172
|
-
"cursor-sw-resize":
|
|
173
|
-
|
|
174
|
-
"cursor-n-resize":
|
|
175
|
-
"cursor-e-resize":
|
|
176
|
-
"cursor-s-resize":
|
|
177
|
-
"cursor-w-resize":
|
|
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
|
-
*
|
|
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
|
+
}
|