@hrnec06/react_utils 1.1.0 → 1.2.1

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,16 +1,9 @@
1
1
  {
2
2
  "name": "@hrnec06/react_utils",
3
- "version": "1.1.0",
3
+ "version": "1.2.1",
4
4
  "description": "A debugger component for react.",
5
5
  "exports": {
6
- ".": {
7
- "types": {
8
- "require": "./dist/index.d.ts",
9
- "import": "./dist/index.d.mts"
10
- },
11
- "require": "./dist/index.js",
12
- "import": "./dist/index.mjs"
13
- }
6
+ ".": "./src/index.tsx"
14
7
  },
15
8
  "keywords": [],
16
9
  "author": {
@@ -20,7 +13,7 @@
20
13
  },
21
14
  "license": "ISC",
22
15
  "files": [
23
- "dist"
16
+ "src"
24
17
  ],
25
18
  "publishConfig": {
26
19
  "access": "public",
@@ -38,7 +31,6 @@
38
31
  "react": "^19.1.0"
39
32
  },
40
33
  "scripts": {
41
- "build": "tsup src/index.tsx --format cjs,esm --dts",
42
34
  "dev": "tsup src/index.tsx --format cjs,esm --dts --watch"
43
35
  }
44
36
  }
@@ -0,0 +1,297 @@
1
+ import { minmax, Nullable, Vector2 } from "@hrnec06/util";
2
+ import { useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
3
+ import {DebuggerContext} from "./DebuggerContext";
4
+ import clsx from "clsx";
5
+ import ResizeBorder from "./DebuggerWindowResize";
6
+ import ScrollBar from "./DebuggerScrollBar";
7
+ import useKeyListener from "../hooks/useKeyListener";
8
+ import useUpdateEffect from "../hooks/useUpdateEffect";
9
+ import useUpdatedRef from "../hooks/useUpdatedRef";
10
+ import ParseValue from "./parser/DebugParser";
11
+
12
+ interface DebugProps {
13
+ value: unknown,
14
+ openPaths?: string[],
15
+ excludePaths?: string[],
16
+ size?: 'normal' | 'big' | 'tiny',
17
+ compactArrays?: number,
18
+ autoHeight?: boolean,
19
+ openRoot?: boolean
20
+ }
21
+ export default function Debug({
22
+ value,
23
+ openPaths,
24
+ excludePaths,
25
+ size = 'normal',
26
+ compactArrays = 1,
27
+ autoHeight = false,
28
+ openRoot = true
29
+ }: DebugProps) {
30
+ // Config
31
+ const LS_POS_KEY = 'debugger_position';
32
+ const LS_SIZE_KEY = 'debugger_size';
33
+ const LS_EXPAND_KEY = 'debugger_expanded';
34
+
35
+ // Keybinds
36
+ const f9_pressed = useKeyListener('F9');
37
+
38
+ // Position
39
+ const [position, setPosition] = useState<Vector2>([0, 0]);
40
+ const [grab, setGrab] = useState<Nullable<{
41
+ windowOrigin: Vector2,
42
+ positionOrigin: Vector2
43
+ }>>(null);
44
+ const [grabOffset, setGrabOffset] = useState<Nullable<Vector2>>(null);
45
+
46
+ // Resize
47
+ const [windowSize, setWindowSize] = useState<Vector2>([500, 500]);
48
+
49
+ // Scroll
50
+ const [scrollHeight, setScrollHeight] = useState(0);
51
+ const [containerHeight, setContainerHeight] = useState(0);
52
+ const [scrollProgress, setScrollProgress] = useState(0);
53
+
54
+ const font = useMemo(() => {
55
+ switch (size) {
56
+ case 'tiny': return { fontSize: 12, lineHeight: 16 };
57
+ case 'normal': return { fontSize: 14, lineHeight: 20 };
58
+ case 'big': return { fontSize: 16, lineHeight: 24 };
59
+ }
60
+ }, [size]);
61
+
62
+ // HTML Refs
63
+ const containerRef = useRef<HTMLDivElement>(null);
64
+
65
+ // Drag handler
66
+ useEffect(() => {
67
+ const mouseUp = (e: MouseEvent) => {
68
+ if (!grab) return;
69
+
70
+ setPosition(finalPositionRef.current);
71
+ setGrab(null);
72
+ setGrabOffset(null);
73
+ }
74
+
75
+ const mouseMove = (e: MouseEvent) => {
76
+ if (!grab) return;
77
+
78
+ setGrabOffset([
79
+ e.clientX - grab.windowOrigin[0],
80
+ e.clientY - grab.windowOrigin[1]
81
+ ]);
82
+ }
83
+
84
+ document.addEventListener('mouseup', mouseUp);
85
+ document.addEventListener('mousemove', mouseMove);
86
+
87
+ return () => {
88
+ document.removeEventListener('mouseup', mouseUp);
89
+ document.removeEventListener('mousemove', mouseMove);
90
+ }
91
+ }, [grab]);
92
+
93
+ // Scrolling
94
+ useLayoutEffect(() => {
95
+ updateScrollHeight();
96
+ }, [windowSize, value, size, containerRef]);
97
+
98
+ const isScrollable = useMemo(() => scrollHeight > 0, [scrollHeight]);
99
+
100
+ // Load saved position
101
+ useEffect(() => {
102
+ try {
103
+ const saved_position = localStorage.getItem(LS_POS_KEY);
104
+ if (saved_position) {
105
+ const [v1, v2] = saved_position.split(',');
106
+
107
+ if (v1 === undefined || v2 === undefined)
108
+ throw new Error('Invalid vector: ' + saved_position);
109
+
110
+ const [v1_p, v2_p] = [parseInt(v1), parseInt(v2)];
111
+
112
+ if (isNaN(v1_p) || isNaN(v2_p))
113
+ throw new Error('Invalid vector values: ' + saved_position);
114
+
115
+ setPosition([v1_p, v2_p]);
116
+ }
117
+ } catch (error) {
118
+ console.error(`Error loading saved position: `, error);
119
+
120
+ localStorage.removeItem(LS_POS_KEY);
121
+ }
122
+ }, []);
123
+
124
+ // Load saved size
125
+ useEffect(() => {
126
+ try {
127
+ const saved_size = localStorage.getItem(LS_SIZE_KEY);
128
+ if (saved_size) {
129
+ const [v1, v2] = saved_size.split(',');
130
+
131
+ if (v1 === undefined || v2 === undefined)
132
+ throw new Error('Invalid vector: ' + saved_size);
133
+
134
+ const [v1_p, v2_p] = [parseInt(v1), parseInt(v2)];
135
+
136
+ if (isNaN(v1_p) || isNaN(v2_p))
137
+ throw new Error('Invalid vector values: ' + saved_size);
138
+
139
+ setWindowSize([v1_p, v2_p]);
140
+ }
141
+ } catch (error) {
142
+ console.error(`Error loading saved size: `, error);
143
+
144
+ localStorage.removeItem(LS_POS_KEY);
145
+ }
146
+ }, []);
147
+
148
+ // Save saved position
149
+ useUpdateEffect(() => {
150
+ localStorage.setItem(LS_POS_KEY, `${position[0]},${position[1]}`);
151
+ }, [position]);
152
+
153
+ // Save saved size
154
+ useUpdateEffect(() => {
155
+ localStorage.setItem(LS_SIZE_KEY, `${windowSize[0]},${windowSize[1]}`);
156
+ }, [windowSize]);
157
+
158
+ // Reset position
159
+ useEffect(() => {
160
+ if (!f9_pressed)
161
+ return;
162
+
163
+ setPosition([0, 0]);
164
+ }, [f9_pressed]);
165
+
166
+ // Compute window position
167
+ const finalPosition: Vector2 = useMemo(() => {
168
+ return [
169
+ position[0] + (grabOffset?.[0] ?? 0),
170
+ position[1] + (grabOffset?.[1] ?? 0),
171
+ ]
172
+ }, [position, grabOffset]);
173
+ const finalPositionRef = useUpdatedRef(finalPosition);
174
+
175
+ // Logic
176
+
177
+ const calculateContainerHeight = () => {
178
+ if (!containerRef.current) return 0;
179
+
180
+ return containerRef.current.clientHeight;
181
+ }
182
+
183
+ const calculateScrollHeight = () => {
184
+ if (!containerRef.current) return 0;
185
+
186
+ return containerRef.current.scrollHeight - calculateContainerHeight();
187
+ }
188
+
189
+ const updateScrollHeight = () => {
190
+ if (!containerRef.current) return;
191
+
192
+ setScrollHeight(calculateScrollHeight());
193
+ setContainerHeight(calculateContainerHeight());
194
+ }
195
+
196
+ // Handlers
197
+ const handleMouseDown = (e: React.MouseEvent<HTMLDivElement>) => {
198
+ e.preventDefault();
199
+
200
+ setGrab({
201
+ positionOrigin: position,
202
+ windowOrigin: [e.clientX, e.clientY]
203
+ });
204
+ }
205
+
206
+ const handleExpandChange = (path: string[], state: boolean) => {
207
+ if (!containerRef.current) return;
208
+
209
+ setScrollProgress(minmax((scrollProgress * scrollHeight) / calculateScrollHeight(), 0, 1));
210
+
211
+ updateScrollHeight();
212
+ }
213
+
214
+ const handleWheel = (e: React.WheelEvent<HTMLDivElement>) => {
215
+ const moveByPX = font.lineHeight * 3;
216
+
217
+ let addProgress = moveByPX / scrollHeight;
218
+
219
+ if (e.deltaY < 0)
220
+ addProgress *= -1;
221
+
222
+ setScrollProgress(p => minmax(p + addProgress, 0, 1));
223
+ }
224
+
225
+ return (
226
+ <DebuggerContext.Provider
227
+ value={{
228
+ paths: {
229
+ exclude: excludePaths ?? [],
230
+ open: openPaths ?? []
231
+ },
232
+ options: {
233
+ compactArrays: compactArrays,
234
+ autoHeight: autoHeight,
235
+ openRoot: openRoot
236
+ },
237
+ window: {
238
+ resize: setWindowSize,
239
+ size: windowSize
240
+ },
241
+ placement: {
242
+ reposition: setPosition,
243
+ position: position
244
+ },
245
+ event: {
246
+ expand: handleExpandChange
247
+ },
248
+ scroll: {
249
+ setProgress: setScrollProgress,
250
+ progress: scrollProgress,
251
+ isScrollable: isScrollable
252
+ }
253
+ }}
254
+ >
255
+ <div className="fixed pointer-events-none w-full h-full left-0 top-0 overflow-hidden">
256
+ <div
257
+ className={clsx(
258
+ "absolute font-jetbrains pointer-events-auto",
259
+ size === 'tiny' && 'text-xs',
260
+ size === 'normal' && 'text-sm',
261
+ size === 'big' && 'text-base'
262
+ )}
263
+ style={{
264
+ left: finalPosition[0],
265
+ top: finalPosition[1],
266
+ width: windowSize[0],
267
+ height: windowSize[1],
268
+ }}
269
+ >
270
+ <ResizeBorder />
271
+
272
+ <div
273
+ onWheel={handleWheel}
274
+ className="bg-[#1f1f1f] shadow-lg rounded-md border border-[#4b4b4b] w-full h-full overflow-hidden flex"
275
+ >
276
+ <div
277
+ ref={containerRef}
278
+ onMouseDown={handleMouseDown}
279
+ className=" cursor-grab w-full"
280
+ style={{
281
+ transform: `translateY(${-(scrollProgress * scrollHeight)}px)`
282
+ }}
283
+ >
284
+ <div className="p-3 pl-5">
285
+ <ParseValue value={value} />
286
+ </div>
287
+ </div>
288
+ <ScrollBar
289
+ containerHeight={containerHeight}
290
+ scrollHeight={scrollHeight}
291
+ />
292
+ </div>
293
+ </div>
294
+ </div>
295
+ </DebuggerContext.Provider>
296
+ );
297
+ }
@@ -0,0 +1,41 @@
1
+ import { Optional, ReactUtils, Vector2 } from "@hrnec06/util";
2
+ import { createContext, useContext } from "react";
3
+
4
+ interface DebuggerContext {
5
+ paths: {
6
+ open: string[],
7
+ exclude: string[]
8
+ },
9
+ options: {
10
+ compactArrays: number,
11
+ autoHeight: boolean,
12
+ openRoot: boolean
13
+ },
14
+ window: {
15
+ resize: ReactUtils.SetState<Vector2>,
16
+ size: Vector2
17
+ },
18
+ placement: {
19
+ reposition: ReactUtils.SetState<Vector2>,
20
+ position: Vector2
21
+ },
22
+ event: {
23
+ expand: (path: string[], state: boolean) => void
24
+ },
25
+ scroll: {
26
+ setProgress: ReactUtils.SetState<number>,
27
+ progress: number,
28
+ isScrollable: boolean
29
+ }
30
+ }
31
+
32
+ export const DebuggerContext = createContext<Optional<DebuggerContext>>(undefined);
33
+
34
+ export default function useDebugger() {
35
+ const ctx = useContext(DebuggerContext);
36
+
37
+ if (!ctx)
38
+ throw new Error(`useDebugger may be used only within DebuggerContext.Provider!`);
39
+
40
+ return ctx;
41
+ }
@@ -0,0 +1,30 @@
1
+ export function matchPath(_path: string[], openPaths: string[]) {
2
+ for (const openPath of openPaths) {
3
+ const matchers = openPath.split('.');
4
+ const path = [..._path];
5
+
6
+ let passed = true;
7
+
8
+ while (path.length && passed) {
9
+ const pathItem = path.shift();
10
+ const matcher = matchers.shift();
11
+
12
+ if (matcher === '**')
13
+ return true;
14
+
15
+ if (matcher === '*') {
16
+ if (path.length > 1)
17
+ passed = false;
18
+ continue;
19
+ }
20
+
21
+ if (pathItem !== matcher)
22
+ passed = false;
23
+ }
24
+
25
+ if (path.length === 0 && passed)
26
+ return true;
27
+ }
28
+
29
+ return false;
30
+ }
@@ -0,0 +1,108 @@
1
+ import { minmax, Nullable, Vector2 } from "@hrnec06/util";
2
+ import { useEffect, useMemo, useRef, useState } from "react";
3
+ import useDebugger from "./DebuggerContext";
4
+ import useListener from "../hooks/useListener";
5
+
6
+ interface ScrollBarProps {
7
+ containerHeight: number,
8
+ scrollHeight: number
9
+ }
10
+ export default function ScrollBar({
11
+ containerHeight,
12
+ scrollHeight
13
+ }: ScrollBarProps) {
14
+ const debug = useDebugger();
15
+
16
+ const [wrapperHeight, setWrapperHeight] = useState(0);
17
+
18
+ const barHeight = (containerHeight / (scrollHeight + containerHeight)) * wrapperHeight;
19
+ const barTop = ((wrapperHeight - barHeight) * debug.scroll.progress);
20
+
21
+ const [grab, setGrab] = useState<Nullable<{
22
+ windowOrigin: number,
23
+ positionOrigin: number
24
+ }>>(null);
25
+ const [grabOffset, setGrabOffset] = useState<Nullable<number>>(null);
26
+
27
+ const wrapperRef = useRef<HTMLDivElement>(null);
28
+
29
+ useEffect(() => {
30
+ if (!wrapperRef.current) return;
31
+
32
+ const observer = new ResizeObserver(() => updateWrapperHeight());
33
+
34
+ observer.observe(wrapperRef.current);
35
+
36
+ updateWrapperHeight();
37
+
38
+ return () => {
39
+ observer.disconnect();
40
+ }
41
+ }, [wrapperRef]);
42
+
43
+ useListener(document, 'mouseup', (event) => {
44
+ if (!grab) return;
45
+
46
+ setGrab(null);
47
+ setGrabOffset(null);
48
+ }, [grab]);
49
+
50
+ useListener(document, 'mousemove', (event) => {
51
+ if (!grab) return;
52
+
53
+ setGrabOffset(event.clientY - (grab.windowOrigin - grab.positionOrigin * (wrapperHeight - barHeight)));
54
+ }, [grab]);
55
+
56
+ useEffect(() => {
57
+ if (grabOffset === null) return;
58
+
59
+ const fixedOffset = minmax(grabOffset, 0, wrapperHeight - barHeight) / (wrapperHeight - barHeight);
60
+
61
+ debug.scroll.setProgress(fixedOffset);
62
+ }, [grabOffset]);
63
+
64
+ const updateWrapperHeight = () => {
65
+ if (!wrapperRef.current) return;
66
+
67
+ setWrapperHeight(wrapperRef.current.clientHeight);
68
+ }
69
+
70
+ const handleWraperMouseDown = (e: React.MouseEvent<HTMLDivElement>) => {
71
+ e.preventDefault();
72
+
73
+ if (grab) return;
74
+ }
75
+
76
+ const handleMouseDown = (e: React.MouseEvent<HTMLDivElement>) => {
77
+ e.preventDefault();
78
+
79
+ if (grab) return;
80
+
81
+ setGrab({
82
+ positionOrigin: debug.scroll.progress,
83
+ windowOrigin: e.clientY
84
+ });
85
+ }
86
+
87
+ return (
88
+ <div className="p-1 border-l border-l-[#2b2b2b]">
89
+ <div
90
+ onMouseDown={handleWraperMouseDown}
91
+ ref={wrapperRef}
92
+ className="h-full shrink-0"
93
+ >
94
+ {debug.scroll.isScrollable && wrapperHeight > 0 && (
95
+ <div
96
+ onMouseDown={handleMouseDown}
97
+
98
+ className="bg-[#3b3b3b] hover:bg-[#4b4b4b] w-2 rounded-lg"
99
+ style={{
100
+ transform: `translateY(${barTop}px)`,
101
+ height: `${barHeight}px`
102
+ }}
103
+ />
104
+ )}
105
+ </div>
106
+ </div>
107
+ )
108
+ }
@@ -0,0 +1,63 @@
1
+ import clsx from "clsx";
2
+ import { ChevronDown } from "lucide-react";
3
+
4
+ export function Char_Colon() {
5
+ return (
6
+ <span className="text-[#ccccab]">
7
+ {': '}
8
+ </span>
9
+ );
10
+ }
11
+
12
+ export function Char_Comma() {
13
+ return (
14
+ <span className="text-[#ccccab]">
15
+ {', '}
16
+ </span>
17
+ );
18
+ }
19
+
20
+ interface Char_Bracket_Props {
21
+ text: string
22
+ }
23
+ export function Char_Bracket({
24
+ text
25
+ }: Char_Bracket_Props) {
26
+ return (
27
+ <span className="text-[#da70d6]">
28
+ {text}
29
+ </span>
30
+ );
31
+ }
32
+
33
+ interface Chevron_Toggle_Props {
34
+ expanded: boolean,
35
+ onToggle?: (expanded: boolean) => void,
36
+ }
37
+ export function Chevron_Toggle({
38
+ expanded,
39
+ onToggle
40
+ }: Chevron_Toggle_Props) {
41
+ const handleClick = () => {
42
+ onToggle?.(!expanded);
43
+ }
44
+
45
+ return (
46
+ <button
47
+ onClick={handleClick}
48
+ className={clsx(
49
+ "absolute",
50
+ 'left-1 translate-y-0.5'
51
+ )}
52
+ >
53
+ {
54
+ <ChevronDown
55
+ className={clsx(
56
+ "size-4 text-[#4b4b4b] hover:text-zinc-500",
57
+ !expanded && '-rotate-90'
58
+ )}
59
+ />
60
+ }
61
+ </button>
62
+ );
63
+ }
@@ -0,0 +1,136 @@
1
+ import { cloneVector2, Nullable, removeVector2, Vector2 } from "@hrnec06/util";
2
+ import { useEffect, useState } from "react";
3
+ import useDebugger from "./DebuggerContext";
4
+ import clsx from "clsx";
5
+
6
+ export default function ResizeBorder() {
7
+ return (
8
+ <>
9
+ <ResizeBar
10
+ className="-top-1.5 h-3 w-full cursor-ns-resize"
11
+ applyReposition
12
+ onMove={([, y]) => [0, y]}
13
+ />
14
+ <ResizeBar
15
+ className="-right-1.5 h-full w-3 cursor-ew-resize"
16
+ onMove={([x]) => [-x, 0]}
17
+ />
18
+ <ResizeBar
19
+ className="-bottom-1.5 h-3 w-full cursor-ns-resize"
20
+ onMove={([, y]) => [0, -y]}
21
+ />
22
+ <ResizeBar
23
+ className="-left-1.5 h-full w-3 cursor-ew-resize"
24
+ applyReposition
25
+ onMove={([x,]) => [x, 0]}
26
+ />
27
+
28
+ <ResizeBar
29
+ className="-top-1.5 -left-1.5 size-3 cursor-nwse-resize"
30
+ applyReposition
31
+ onMove={([x, y]) => [x, y]}
32
+ />
33
+ <ResizeBar
34
+ className="-top-1.5 -right-1.5 size-3 cursor-nesw-resize"
35
+ applyReposition={'y'}
36
+ onMove={([x, y]) => [-x, y]}
37
+ />
38
+ <ResizeBar
39
+ className="-bottom-1.5 -right-1.5 size-3 cursor-nwse-resize"
40
+ onMove={([x, y]) => [-x, -y]}
41
+ />
42
+ <ResizeBar
43
+ className="-bottom-1.5 -left-1.5 size-3 cursor-nesw-resize"
44
+ applyReposition={'x'}
45
+ onMove={([x, y]) => [x, -y]}
46
+ />
47
+ </>
48
+ )
49
+ }
50
+
51
+ interface ResizeBarProps {
52
+ className: string,
53
+ applyReposition?: boolean | 'x' | 'y',
54
+
55
+ onMove: (offset: Vector2, originalSize: Vector2, originalPosition: Vector2) => Vector2
56
+ }
57
+ function ResizeBar({
58
+ className,
59
+ applyReposition = false,
60
+
61
+ onMove
62
+ }: ResizeBarProps) {
63
+ const debug = useDebugger();
64
+
65
+ const [drag, setDrag] = useState<Nullable<{
66
+ dragOrigin: Vector2,
67
+ originalSize: Vector2,
68
+ originalPosition: Vector2
69
+ }>>(null);
70
+
71
+ useEffect(() => {
72
+ const mouseUp = () => {
73
+ if (!drag) return;
74
+
75
+ setDrag(null);
76
+ }
77
+
78
+ const mouseMove = (e: MouseEvent) => {
79
+ if (!drag) return;
80
+
81
+ const offset: Vector2 = [
82
+ e.clientX - drag.dragOrigin[0],
83
+ e.clientY - drag.dragOrigin[1],
84
+ ];
85
+
86
+ const newSize = onMove(
87
+ offset,
88
+ drag.originalSize,
89
+ drag.originalPosition
90
+ );
91
+
92
+ newSize[0] = drag.originalSize[0] - newSize[0];
93
+ newSize[1] = drag.originalSize[1] - newSize[1];
94
+
95
+ if (applyReposition !== false) {
96
+ const repositionOffset = cloneVector2(newSize);
97
+ removeVector2(repositionOffset, drag.originalSize);
98
+
99
+ debug.placement.reposition([
100
+ drag.originalPosition[0] - ((applyReposition === true || applyReposition === 'x') ? repositionOffset[0] : 0),
101
+ drag.originalPosition[1] - ((applyReposition === true || applyReposition === 'y') ? repositionOffset[1] : 0)
102
+ ]);
103
+ }
104
+
105
+ debug.window.resize(newSize);
106
+ }
107
+
108
+ document.addEventListener('mouseup', mouseUp);
109
+ document.addEventListener('mousemove', mouseMove);
110
+
111
+ return () => {
112
+ document.removeEventListener('mouseup', mouseUp);
113
+ document.removeEventListener('mousemove', mouseMove);
114
+ }
115
+ }, [drag]);
116
+
117
+ const handleMouseDown = (e: React.MouseEvent<HTMLDivElement>) => {
118
+ e.preventDefault();
119
+
120
+ setDrag({
121
+ dragOrigin: [e.clientX, e.clientY],
122
+ originalSize: debug.window.size,
123
+ originalPosition: debug.placement.position
124
+ });
125
+ }
126
+
127
+ return (
128
+ <div
129
+ onMouseDown={handleMouseDown}
130
+ className={clsx(
131
+ 'absolute',
132
+ className,
133
+ )}
134
+ />
135
+ );
136
+ }