@hrnec06/react_utils 1.2.6 → 1.3.2

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.
Files changed (29) hide show
  1. package/package.json +3 -2
  2. package/src/{debugger → components/Debugger}/Debugger.tsx +31 -21
  3. package/src/{debugger → components/Debugger}/DebuggerScrollBar.tsx +3 -3
  4. package/src/components/ResizeableBox/DragAreas.tsx +182 -0
  5. package/src/components/ResizeableBox/ResizeableBox.tsx +156 -0
  6. package/src/components/ResizeableBox/ResizeableBox.types.ts +23 -0
  7. package/src/components/ResizeableBox/ResizeableBoxContext.ts +30 -0
  8. package/src/hooks/useEfficientState.ts +9 -0
  9. package/src/hooks/useLazySignal.ts +64 -0
  10. package/src/hooks/useListener.ts +1 -1
  11. package/src/hooks/useSignal.ts +2 -2
  12. package/src/hooks/useUUID.ts +6 -0
  13. package/src/hooks/useWindowSize.ts +9 -12
  14. package/src/index.tsx +6 -3
  15. package/src/debugger/DebuggerWindowResize.tsx +0 -136
  16. package/src/ui/ResizeableBox.tsx +0 -342
  17. /package/src/{debugger → components/Debugger}/DebuggerContext.ts +0 -0
  18. /package/src/{debugger → components/Debugger}/DebuggerLogic.ts +0 -0
  19. /package/src/{debugger → components/Debugger}/DebuggerSymbols.tsx +0 -0
  20. /package/src/{debugger → components/Debugger}/parser/DebugParser.tsx +0 -0
  21. /package/src/{debugger → components/Debugger}/parser/DebugParserSimple.tsx +0 -0
  22. /package/src/{debugger → components/Debugger}/parser/ValueArray.tsx +0 -0
  23. /package/src/{debugger → components/Debugger}/parser/ValueBoolean.tsx +0 -0
  24. /package/src/{debugger → components/Debugger}/parser/ValueFunction.tsx +0 -0
  25. /package/src/{debugger → components/Debugger}/parser/ValueKeyword.tsx +0 -0
  26. /package/src/{debugger → components/Debugger}/parser/ValueNumber.tsx +0 -0
  27. /package/src/{debugger → components/Debugger}/parser/ValueObject.tsx +0 -0
  28. /package/src/{debugger → components/Debugger}/parser/ValueString.tsx +0 -0
  29. /package/src/{debugger → components/Debugger}/parser/ValueSymbol.tsx +0 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hrnec06/react_utils",
3
- "version": "1.2.6",
3
+ "version": "1.3.2",
4
4
  "description": "A debugger component for react.",
5
5
  "exports": {
6
6
  ".": "./src/index.tsx"
@@ -30,7 +30,8 @@
30
30
  "clsx": "^2.1.1",
31
31
  "lucide-react": "^0.525.0",
32
32
  "react": "^19.1.0",
33
- "react-dom": "^19.2.3"
33
+ "react-dom": "^19.2.3",
34
+ "uuid": "^13.0.0"
34
35
  },
35
36
  "scripts": {
36
37
  "dev": "tsup src/index.tsx --format cjs,esm --dts --watch"
@@ -3,12 +3,12 @@ import { useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
3
3
  import { createPortal } from 'react-dom';
4
4
  import {DebuggerContext} from "./DebuggerContext";
5
5
  import clsx from "clsx";
6
- import ResizeBorder from "./DebuggerWindowResize";
7
6
  import ScrollBar from "./DebuggerScrollBar";
8
- import useKeyListener from "../hooks/useKeyListener";
9
- import useUpdateEffect from "../hooks/useUpdateEffect";
10
- import useUpdatedRef from "../hooks/useUpdatedRef";
11
7
  import ParseValue from "./parser/DebugParser";
8
+ import useKeyListener from "../../hooks/useKeyListener";
9
+ import useUpdateEffect from "../../hooks/useUpdateEffect";
10
+ import useUpdatedRef from "../../hooks/useUpdatedRef";
11
+ import ResizeableBox from "../ResizeableBox/ResizeableBox";
12
12
 
13
13
  interface DebugProps {
14
14
  value: unknown,
@@ -227,6 +227,11 @@ export default function Debug({
227
227
  setScrollProgress(p => minmax(p + addProgress, 0, 1));
228
228
  }
229
229
 
230
+ const handleResize = (size: Vector2, offset: Vector2) => {
231
+ setWindowSize(size);
232
+ setPosition(offset);
233
+ }
234
+
230
235
  return createPortal(
231
236
  (
232
237
  <DebuggerContext.Provider
@@ -273,29 +278,34 @@ export default function Debug({
273
278
  height: windowSize[1],
274
279
  }}
275
280
  >
276
- <ResizeBorder />
281
+ <ResizeableBox
282
+ $size={windowSize}
283
+ $offset={finalPosition}
277
284
 
278
- <div
279
- onWheel={handleWheel}
280
- className="bg-[#1f1f1f] shadow-lg rounded-md border border-[#4b4b4b] w-full h-full overflow-hidden flex"
285
+ $onResize={handleResize}
281
286
  >
282
287
  <div
283
- ref={containerRef}
284
- onMouseDown={handleMouseDown}
285
- className=" cursor-grab w-full"
286
- style={{
287
- transform: `translateY(${-(scrollProgress * scrollHeight)}px)`
288
- }}
288
+ onWheel={handleWheel}
289
+ className="bg-[#1f1f1f] shadow-lg rounded-md border border-[#4b4b4b] w-full h-full overflow-hidden flex"
289
290
  >
290
- <div className="p-3 pl-5">
291
- <ParseValue value={value} />
291
+ <div
292
+ ref={containerRef}
293
+ onMouseDown={handleMouseDown}
294
+ className=" cursor-grab w-full"
295
+ style={{
296
+ transform: `translateY(${-(scrollProgress * scrollHeight)}px)`
297
+ }}
298
+ >
299
+ <div className="p-3 pl-5">
300
+ <ParseValue value={value} />
301
+ </div>
292
302
  </div>
303
+ <ScrollBar
304
+ containerHeight={containerHeight}
305
+ scrollHeight={scrollHeight}
306
+ />
293
307
  </div>
294
- <ScrollBar
295
- containerHeight={containerHeight}
296
- scrollHeight={scrollHeight}
297
- />
298
- </div>
308
+ </ResizeableBox>
299
309
  </div>
300
310
  </div>
301
311
  </DebuggerContext.Provider>
@@ -1,7 +1,7 @@
1
- import { minmax, Nullable, Vector2 } from "@hrnec06/util";
2
- import { useEffect, useMemo, useRef, useState } from "react";
1
+ import { minmax, Nullable } from "@hrnec06/util";
2
+ import { useEffect, useRef, useState } from "react";
3
3
  import useDebugger from "./DebuggerContext";
4
- import useListener from "../hooks/useListener";
4
+ import useListener from "../../hooks/useListener";
5
5
 
6
6
  interface ScrollBarProps {
7
7
  containerHeight: number,
@@ -0,0 +1,182 @@
1
+ import { matchBool, matchMouseEvent, MouseButton, Vector2 } from "@hrnec06/util";
2
+ import clsx from "clsx";
3
+ import useResizeableBox from "./ResizeableBoxContext";
4
+ import RB from "./ResizeableBox.types";
5
+
6
+ export default function DragAreas()
7
+ {
8
+ return (
9
+ <>
10
+ <DragArea
11
+ spanHorizontal
12
+ top
13
+
14
+ applyReposition={'y'}
15
+ onMove={([, y]) => [0, -y]}
16
+ />
17
+
18
+ <DragArea
19
+ spanVertical
20
+ right
21
+
22
+ onMove={([x]) => [x, 0]}
23
+ />
24
+
25
+ <DragArea
26
+ spanHorizontal
27
+ bottom
28
+
29
+ onMove={([, y]) => [0, y]}
30
+ />
31
+
32
+ <DragArea
33
+ spanVertical
34
+ left
35
+
36
+ applyReposition={'x'}
37
+ onMove={([x]) => [-x, 0]}
38
+ />
39
+
40
+ {/* Corners */}
41
+
42
+ <DragArea
43
+ isCorner
44
+ left
45
+ top
46
+
47
+ applyReposition={true}
48
+ onMove={([x, y]) => [-x, -y]}
49
+ />
50
+
51
+ <DragArea
52
+ isCorner
53
+ right
54
+ top
55
+
56
+ applyReposition={'y'}
57
+ onMove={([x, y]) => [x, -y]}
58
+ />
59
+
60
+ <DragArea
61
+ isCorner
62
+ left
63
+ bottom
64
+
65
+ applyReposition={'x'}
66
+ onMove={([x, y]) => [-x, y]}
67
+ />
68
+
69
+ <DragArea
70
+ isCorner
71
+ right
72
+ bottom
73
+
74
+ onMove={([x, y]) => [x, y]}
75
+ />
76
+ </>
77
+ );
78
+ }
79
+
80
+ interface DragAreaProps {
81
+ isCorner?: boolean,
82
+
83
+ spanVertical?: boolean,
84
+ spanHorizontal?: boolean,
85
+
86
+ onMove: (offset: Vector2) => Vector2,
87
+ applyReposition?: RB.RepositionMode,
88
+
89
+ top?: boolean,
90
+ right?: boolean,
91
+ bottom?: boolean,
92
+ left?: boolean
93
+ }
94
+ function DragArea({
95
+ isCorner,
96
+
97
+ spanVertical,
98
+ spanHorizontal,
99
+
100
+ onMove,
101
+ applyReposition = false,
102
+
103
+ ...enabledSides
104
+ }: DragAreaProps)
105
+ {
106
+ const box = useResizeableBox();
107
+
108
+ const isAllowed = (side: RB.DragSide) => box.options.allowedSides.includes(side);
109
+
110
+ if (isCorner && !box.options.allowCorners)
111
+ return undefined;
112
+
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
+
121
+ if (isCorner && Array.isArray(box.options.allowCorners))
122
+ {
123
+ if (!box.options.allowCorners.some((combo) => enabledSides[combo[0]] && enabledSides[combo[1]]))
124
+ return undefined;
125
+ }
126
+
127
+ const handleMouseDown = (e: React.MouseEvent<HTMLDivElement>) => {
128
+ if (box.drag.value !== null || !matchMouseEvent(e.nativeEvent, MouseButton.Left))
129
+ return;
130
+
131
+ e.preventDefault();
132
+ e.stopPropagation();
133
+
134
+ box.drag.value = {
135
+ originalSize: box.size,
136
+ originalOffset: box.offset,
137
+ dragOrigin: [e.clientX, e.clientY],
138
+ mouseMove: onMove,
139
+ applyReposition: applyReposition,
140
+ };
141
+ }
142
+
143
+ return (
144
+ <div
145
+ onMouseDown={handleMouseDown}
146
+
147
+ className={clsx(
148
+ "absolute z-20",
149
+ "select-none",
150
+
151
+ spanVertical ? 'h-full' : clsx(
152
+ box.options.dragAreaSize === 'small' && 'h-2',
153
+ box.options.dragAreaSize === 'medium' && 'h-4',
154
+ box.options.dragAreaSize === 'big' && 'h-6'
155
+ ),
156
+
157
+ spanHorizontal ? 'w-full' : clsx(
158
+ box.options.dragAreaSize === 'small' && 'w-2',
159
+ box.options.dragAreaSize === 'medium' && 'w-4',
160
+ box.options.dragAreaSize === 'big' && 'w-6'
161
+ ),
162
+
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'),
167
+
168
+ 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
178
+ }),
179
+ )}
180
+ />
181
+ );
182
+ }
@@ -0,0 +1,156 @@
1
+ import { addVector2, cloneVector2, matchMouseEvent, MouseButton, Nullable, Optional, Vector2 } from "@hrnec06/util";
2
+ import clsx from "clsx";
3
+ import { useEffect } from "react";
4
+ import DragAreas from "./DragAreas";
5
+ import useSignal from "../../hooks/useSignal";
6
+ import useUpdatedRef from "../../hooks/useUpdatedRef";
7
+ import useListener from "../../hooks/useListener";
8
+ import { ResizeableBoxContext } from "./ResizeableBoxContext";
9
+ import RB from "./ResizeableBox.types";
10
+
11
+ // Type definitions
12
+
13
+ // Component
14
+
15
+ interface ResizeableBoxProps extends React.HTMLProps<HTMLDivElement>
16
+ {
17
+ /**
18
+ * Current size of the box. Affects onResize callback.
19
+ */
20
+ $size?: Vector2,
21
+ /**
22
+ * Current offset of the box. Affects onResize callback.
23
+ */
24
+ $offset?: Vector2,
25
+
26
+ /**
27
+ * Size of the areas
28
+ */
29
+ $dragAreaSize?: RB.DragAreaSize,
30
+ /**
31
+ * Allowed sides
32
+ */
33
+ $allowedSides?: RB.DragSide[],
34
+ /**
35
+ * Allow corner resizers?
36
+ * A corner will be visible only if the 2 neighbouring sides are visible
37
+ */
38
+ $allowCorners?: boolean | Vector2<RB.DragSide>[],
39
+ /**
40
+ * The drag areas will be inset inside the box by 50%
41
+ */
42
+ $insetAreas?: boolean,
43
+
44
+ /**
45
+ * If $size or $offset is specified, onResize provides the new size or offset with changes already applied.
46
+ */
47
+ $onResize?: RB.ResizeEvent,
48
+ /**
49
+ * Emits when resizing starts
50
+ */
51
+ $onStart?: RB.StartEvent,
52
+ /**
53
+ * Emits when resizing ends
54
+ */
55
+ $onEnd?: RB.EndEvent,
56
+ }
57
+ export default function ResizeableBox({
58
+ $size,
59
+ $offset,
60
+
61
+ $dragAreaSize = "medium",
62
+ $allowedSides = ['top', 'right', 'bottom', 'left'],
63
+ $allowCorners = true,
64
+ $insetAreas = true,
65
+
66
+ $onResize,
67
+ $onStart,
68
+ $onEnd,
69
+
70
+ ...props
71
+ }: ResizeableBoxProps)
72
+ {
73
+ const drag = useSignal<Nullable<RB.DragState>>(null);
74
+
75
+ const onResizeRef = useUpdatedRef($onResize);
76
+
77
+ useEffect(() => {
78
+ if (drag.value === null)
79
+ $onEnd && $onEnd();
80
+ else
81
+ $onStart && $onStart();
82
+ }, [drag.value]);
83
+
84
+ useListener(document, 'mouseup', (e) => {
85
+ if (drag.value === null || !matchMouseEvent(e, MouseButton.Left))
86
+ return;
87
+
88
+ e.preventDefault();
89
+ e.stopPropagation();
90
+
91
+ drag.value = null;
92
+ }, [drag.value === null]);
93
+
94
+ useListener(document, 'mousemove', (e) => {
95
+ if (drag.value === null || !onResizeRef.current)
96
+ return;
97
+
98
+ e.preventDefault();
99
+ e.stopPropagation();
100
+
101
+ const offset: Vector2 = [
102
+ e.clientX - drag.value.dragOrigin[0],
103
+ e.clientY - drag.value.dragOrigin[1],
104
+ ];
105
+
106
+ const sizeOffset = drag.value.mouseMove(offset);
107
+ const positionOffset: Vector2 = [0, 0];
108
+
109
+ if (drag.value.applyReposition !== false)
110
+ {
111
+ const offset: Vector2 = cloneVector2(sizeOffset);
112
+
113
+ positionOffset[0] = (drag.value.applyReposition === true || drag.value.applyReposition === 'x') ? -offset[0] : 0;
114
+ positionOffset[1] = (drag.value.applyReposition === true || drag.value.applyReposition === 'y') ? -offset[1] : 0;
115
+ }
116
+
117
+ if (drag.value.originalSize)
118
+ addVector2(sizeOffset, drag.value.originalSize);
119
+
120
+ if (drag.value.originalOffset)
121
+ addVector2(positionOffset, drag.value.originalOffset);
122
+
123
+ onResizeRef.current(sizeOffset, positionOffset);
124
+ }, [drag.value === null]);
125
+
126
+ return (
127
+ <ResizeableBoxContext.Provider
128
+ value={{
129
+ size: $size,
130
+ offset: $offset,
131
+
132
+ options: {
133
+ dragAreaSize: $dragAreaSize,
134
+ allowedSides: $allowedSides,
135
+ allowCorners: $allowCorners,
136
+ insetAreas: $insetAreas,
137
+ },
138
+
139
+ drag: drag
140
+ }}
141
+ >
142
+ <div
143
+ {...props}
144
+
145
+ className={clsx(
146
+ props.className,
147
+ "relative w-full h-full"
148
+ )}
149
+ >
150
+ <DragAreas />
151
+
152
+ {props.children}
153
+ </div>
154
+ </ResizeableBoxContext.Provider>
155
+ )
156
+ }
@@ -0,0 +1,23 @@
1
+ import { Vector2 } from "@hrnec06/util";
2
+
3
+ namespace RB {
4
+ export type DragAreaSize = 'small' | 'medium' | 'big';
5
+ export type DragSide = 'top' | 'right' | 'bottom' | 'left';
6
+ export type ResizeEvent = (size: Vector2, offset: Vector2) => void;
7
+ export type StartEvent = () => void;
8
+ export type EndEvent = () => void;
9
+
10
+ export type RepositionMode = boolean | 'x' | 'y';
11
+ export type MouseMoveHandler = (offset: Vector2) => Vector2;
12
+
13
+ export interface DragState {
14
+ originalSize?: Vector2,
15
+ originalOffset?: Vector2,
16
+
17
+ dragOrigin: Vector2,
18
+ applyReposition: RB.RepositionMode,
19
+ mouseMove: MouseMoveHandler
20
+ }
21
+ }
22
+
23
+ export default RB;
@@ -0,0 +1,30 @@
1
+ import { Nullable, Optional, Vector2 } from "@hrnec06/util";
2
+ import { createContext, useContext } from "react";
3
+ import { Signal } from "../../hooks/useSignal";
4
+ import RB from "./ResizeableBox.types";
5
+
6
+ // Context
7
+ interface ResizeableBoxContext {
8
+ size?: Vector2,
9
+ offset?: Vector2,
10
+
11
+ options: {
12
+ insetAreas: boolean,
13
+ dragAreaSize: RB.DragAreaSize,
14
+ allowedSides: RB.DragSide[],
15
+ allowCorners: boolean | Vector2<RB.DragSide>[],
16
+ },
17
+
18
+ drag: Signal<Nullable<RB.DragState>>,
19
+ }
20
+ export const ResizeableBoxContext = createContext<Optional<ResizeableBoxContext>>(undefined);
21
+
22
+ export default function useResizeableBox()
23
+ {
24
+ const ctx = useContext(ResizeableBoxContext);
25
+
26
+ if (!ctx)
27
+ throw new Error("useResizeableBox may be used only within ResizeableBoxContext.Provider!");
28
+
29
+ return ctx;
30
+ }
@@ -0,0 +1,9 @@
1
+ import { ReactUtils } from "@hrnec06/util";
2
+ import { useMemo, useState } from "react";
3
+
4
+ export default function useEfficientState<T>(callback: () => T): [T, ReactUtils.SetState<T>]
5
+ {
6
+ const defaultValue = useMemo(() => callback(), []);
7
+
8
+ return useState<T>(defaultValue);
9
+ }
@@ -0,0 +1,64 @@
1
+ import { ReactUtils } from "@hrnec06/util";
2
+ import { Signal } from "./useSignal";
3
+ import { useState } from "react";
4
+
5
+ const LazyNoValue = Symbol("LazyNoValue");
6
+
7
+ type LazyValue<T> = T | typeof LazyNoValue;
8
+
9
+ export default function useLazySignal<T>(callback: () => T): LazySignal<T>
10
+ {
11
+ const [_value, _setValue] = useState<LazyValue<T>>(LazyNoValue);
12
+
13
+ const signal = new LazySignal(_value, _setValue, callback);
14
+
15
+ return signal;
16
+ }
17
+
18
+ export class LazySignal<T> extends Signal<LazyValue<T>>
19
+ {
20
+ public static LazyNoValue: typeof LazyNoValue = LazyNoValue;
21
+
22
+ constructor(
23
+ _value: LazyValue<T>,
24
+ setState: ReactUtils.SetState<T | typeof LazyNoValue>,
25
+ private lazyCallback: () => T
26
+ )
27
+ {
28
+ super(_value, setState);
29
+ }
30
+
31
+ public set value(value: T | ((prev: LazyValue<T>) => T))
32
+ {
33
+ this.setState(value);
34
+ }
35
+
36
+ public get value(): T
37
+ {
38
+ if (this._value === LazyNoValue)
39
+ {
40
+ const result = this.loadValue();
41
+
42
+ return result;
43
+ }
44
+
45
+ return this._value;
46
+ }
47
+
48
+ public loadValue(): T
49
+ {
50
+ if (this.hasValue())
51
+ return this._value as T;
52
+
53
+ const result = this.lazyCallback();
54
+
55
+ this.setState(result);
56
+
57
+ return result;
58
+ }
59
+
60
+ public hasValue(): boolean
61
+ {
62
+ return this._value !== LazyNoValue;
63
+ }
64
+ }
@@ -1,6 +1,6 @@
1
1
  import { DependencyList, useEffect } from "react"
2
2
 
3
- export default function useListener<E extends keyof GlobalEventHandlersEventMap>(element: Node, event: E, listener: (event: GlobalEventHandlersEventMap[E]) => void, deps?: DependencyList) {
3
+ export default function useListener<E extends keyof GlobalEventHandlersEventMap>(element: EventTarget, event: E, listener: (event: GlobalEventHandlersEventMap[E]) => void, deps?: DependencyList) {
4
4
  useEffect(() => {
5
5
  element.addEventListener(event, listener as EventListenerOrEventListenerObject);
6
6
 
@@ -13,8 +13,8 @@ export default function useSignal<T>(value: T): Signal<T>
13
13
  export class Signal<T>
14
14
  {
15
15
  constructor(
16
- private _value: T,
17
- private setState: ReactUtils.SetState<T>
16
+ protected _value: T,
17
+ protected setState: ReactUtils.SetState<T>
18
18
  )
19
19
  {
20
20
  }
@@ -0,0 +1,6 @@
1
+ import { useMemo } from "react";
2
+ import { v4 as uuidv4} from 'uuid';
3
+
4
+ export default function useUUID() {
5
+ return useMemo(() => uuidv4(), []);
6
+ }
@@ -1,21 +1,18 @@
1
1
  import { Vector2 } from "@hrnec06/util";
2
2
  import { useEffect, useState } from "react";
3
+ import useListener from "./useListener";
4
+ import useLazySignal, { LazySignal } from "./useLazySignal";
3
5
 
4
- export default function useWindowSize(): Vector2 {
5
- const [dimensions, setDimensions] = useState<Vector2>([1920, 1080]);
6
+ export default function useWindowSize(): LazySignal<Vector2>
7
+ {
8
+ const dimensions = useLazySignal<Vector2>(() => [ window.innerWidth, window.innerHeight ]);
6
9
 
7
10
  useEffect(() => {
8
- setDimensions([window.innerWidth, window.innerHeight]);
9
-
10
- const listener = () => {
11
- setDimensions([window.innerWidth, window.innerHeight]);
12
- }
13
-
14
- window.addEventListener('resize', listener);
11
+ dimensions.value = [window.innerWidth, window.innerHeight];
12
+ }, []);
15
13
 
16
- return () => {
17
- window.removeEventListener('resize', listener);
18
- }
14
+ useListener(window, 'resize', () => {
15
+ dimensions.value = [window.innerWidth, window.innerHeight];
19
16
  }, []);
20
17
 
21
18
  return dimensions;
package/src/index.tsx CHANGED
@@ -4,9 +4,10 @@ import useUpdatedRef from "./hooks/useUpdatedRef";
4
4
  import useUpdateEffect from "./hooks/useUpdateEffect";
5
5
  import useWindowSize from "./hooks/useWindowSize";
6
6
  import useSignal from "./hooks/useSignal";
7
+ import useLazySignal from "./hooks/useLazySignal";
7
8
 
8
- import Debugger from './debugger/Debugger';
9
- import ResizeableBox from "./ui/ResizeableBox";
9
+ import Debugger from './components/Debugger/Debugger';
10
+ import ResizeableBox from "./components/ResizeableBox/ResizeableBox";
10
11
 
11
12
  export {
12
13
  useKeyListener,
@@ -15,6 +16,8 @@ export {
15
16
  useUpdateEffect,
16
17
  useWindowSize,
17
18
  useSignal,
19
+ useLazySignal,
20
+
18
21
  Debugger,
19
- ResizeableBox
22
+ ResizeableBox,
20
23
  };
@@ -1,136 +0,0 @@
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
- }
@@ -1,342 +0,0 @@
1
- import { addVector2, cloneVector2, match, matchBool, matchMouseEvent, MouseButton, Nullable, Optional, Vector2 } from "@hrnec06/util";
2
- import clsx from "clsx";
3
- import { createContext, useContext, useEffect, useState } from "react";
4
- import useSignal, { Signal } from "../hooks/useSignal";
5
- import useListener from "../hooks/useListener";
6
- import { useUpdatedRef } from "..";
7
-
8
- // Type definitions
9
- type DragAreaSize = 'small' | 'medium' | 'big';
10
- type DragSide = 'top' | 'right' | 'bottom' | 'left';
11
- type ResizeEvent = (size: Vector2, offset: Vector2) => void;
12
- type StartEvent = () => void;
13
- type EndEvent = () => void;
14
-
15
- type RepositionMode = boolean | 'x' | 'y';
16
- type MouseMoveHandler = (offset: Vector2) => Vector2
17
-
18
- interface DragState {
19
- originalSize?: Vector2,
20
- originalOffset?: Vector2,
21
-
22
- dragOrigin: Vector2,
23
- applyReposition: RepositionMode,
24
- mouseMove: MouseMoveHandler
25
- }
26
-
27
- // Context
28
- interface ResizeableBoxContext {
29
- size?: Vector2,
30
- offset?: Vector2,
31
-
32
- options: {
33
- insetAreas: boolean,
34
- dragAreaSize: DragAreaSize,
35
- allowedSides: DragSide[],
36
- allowCorners: boolean | Vector2<DragSide>[],
37
- },
38
-
39
- drag: Signal<Nullable<DragState>>,
40
- }
41
- const ResizeableBoxContext = createContext<Optional<ResizeableBoxContext>>(undefined);
42
-
43
- function useResizeableBox()
44
- {
45
- const ctx = useContext(ResizeableBoxContext);
46
-
47
- if (!ctx)
48
- throw new Error("useResizeableBox may be used only within ResizeableBoxContext.Provider!");
49
-
50
- return ctx;
51
- }
52
-
53
- // Component
54
-
55
- interface ResizeableBoxProps extends React.HTMLProps<HTMLDivElement>
56
- {
57
- $size?: Vector2,
58
- $offset?: Vector2,
59
-
60
- $dragAreaSize?: DragAreaSize,
61
- $allowedSides?: DragSide[],
62
- $allowCorners?: boolean | Vector2<DragSide>[],
63
- $insetAreas?: boolean,
64
-
65
- $onResize?: ResizeEvent,
66
- $onStart?: StartEvent,
67
- $onEnd?: EndEvent,
68
- }
69
- export default function ResizeableBox({
70
- $size,
71
- $offset,
72
-
73
- $dragAreaSize = "medium",
74
- $allowedSides = ['top', 'right', 'bottom', 'left'],
75
- $allowCorners = true,
76
- $insetAreas = true,
77
-
78
- $onResize,
79
- $onStart,
80
- $onEnd,
81
-
82
- ...props
83
- }: ResizeableBoxProps)
84
- {
85
- const drag = useSignal<Nullable<DragState>>(null);
86
-
87
- const onResizeRef = useUpdatedRef($onResize);
88
-
89
- useEffect(() => {
90
- if (drag.value === null)
91
- $onEnd && $onEnd();
92
- else
93
- $onStart && $onStart();
94
- }, [drag.value]);
95
-
96
- useListener(document, 'mouseup', (e) => {
97
- if (drag.value === null || !matchMouseEvent(e, MouseButton.Left))
98
- return;
99
-
100
- drag.value = null;
101
- }, [drag.value === null]);
102
-
103
- useListener(document, 'mousemove', (e) => {
104
- if (drag.value === null || !onResizeRef.current)
105
- return;
106
-
107
- const offset: Vector2 = [
108
- e.clientX - drag.value.dragOrigin[0],
109
- e.clientY - drag.value.dragOrigin[1],
110
- ];
111
-
112
- const sizeOffset = drag.value.mouseMove(offset);
113
- const positionOffset: Vector2 = [0, 0];
114
-
115
- if (drag.value.applyReposition !== false)
116
- {
117
- const offset: Vector2 = cloneVector2(sizeOffset);
118
-
119
- positionOffset[0] = (drag.value.applyReposition === true || drag.value.applyReposition === 'x') ? -offset[0] : 0;
120
- positionOffset[1] = (drag.value.applyReposition === true || drag.value.applyReposition === 'y') ? -offset[1] : 0;
121
- }
122
-
123
- if (drag.value.originalSize)
124
- addVector2(sizeOffset, drag.value.originalSize);
125
-
126
- if (drag.value.originalOffset)
127
- addVector2(positionOffset, drag.value.originalOffset);
128
-
129
- onResizeRef.current(sizeOffset, positionOffset);
130
- }, [drag.value === null]);
131
-
132
- return (
133
- <ResizeableBoxContext.Provider
134
- value={{
135
- size: $size,
136
- offset: $offset,
137
-
138
- options: {
139
- dragAreaSize: $dragAreaSize,
140
- allowedSides: $allowedSides,
141
- allowCorners: $allowCorners,
142
- insetAreas: $insetAreas,
143
- },
144
-
145
- drag: drag
146
- }}
147
- >
148
- <div
149
- {...props}
150
-
151
- className={clsx(
152
- props.className,
153
- "relative w-full h-full"
154
- )}
155
- >
156
- <DragAreas />
157
-
158
- {props.children}
159
- </div>
160
- </ResizeableBoxContext.Provider>
161
- )
162
- }
163
-
164
- interface DragAreasProps {
165
-
166
- }
167
- function DragAreas({
168
-
169
- }: DragAreasProps)
170
- {
171
- return (
172
- <>
173
- <DragArea
174
- spanHorizontal
175
- top
176
-
177
- applyReposition={'y'}
178
- onMove={([, y]) => [0, -y]}
179
- />
180
-
181
- <DragArea
182
- spanVertical
183
- right
184
-
185
- onMove={([x]) => [x, 0]}
186
- />
187
-
188
- <DragArea
189
- spanHorizontal
190
- bottom
191
-
192
- onMove={([, y]) => [0, y]}
193
- />
194
-
195
- <DragArea
196
- spanVertical
197
- left
198
-
199
- applyReposition={'x'}
200
- onMove={([x]) => [-x, 0]}
201
- />
202
-
203
- {/* Corners */}
204
-
205
- <DragArea
206
- isCorner
207
- left
208
- top
209
-
210
- applyReposition={true}
211
- onMove={([x, y]) => [-x, -y]}
212
- />
213
-
214
- <DragArea
215
- isCorner
216
- right
217
- top
218
-
219
- applyReposition={'y'}
220
- onMove={([x, y]) => [x, -y]}
221
- />
222
-
223
- <DragArea
224
- isCorner
225
- left
226
- bottom
227
-
228
- applyReposition={'x'}
229
- onMove={([x, y]) => [-x, y]}
230
- />
231
-
232
- <DragArea
233
- isCorner
234
- right
235
- bottom
236
-
237
- onMove={([x, y]) => [x, y]}
238
- />
239
- </>
240
- );
241
- }
242
-
243
- interface DragAreaProps {
244
- isCorner?: boolean,
245
-
246
- spanVertical?: boolean,
247
- spanHorizontal?: boolean,
248
-
249
- onMove: (offset: Vector2) => Vector2,
250
- applyReposition?: RepositionMode,
251
-
252
- top?: boolean,
253
- right?: boolean,
254
- bottom?: boolean,
255
- left?: boolean
256
- }
257
- function DragArea({
258
- isCorner,
259
-
260
- spanVertical,
261
- spanHorizontal,
262
-
263
- onMove,
264
- applyReposition = false,
265
-
266
- ...enabledSides
267
- }: DragAreaProps)
268
- {
269
- const box = useResizeableBox();
270
-
271
- const isAllowed = (side: DragSide) => box.options.allowedSides.includes(side);
272
-
273
- if (isCorner && !box.options.allowCorners)
274
- return undefined;
275
-
276
- if (
277
- (enabledSides.top && !isAllowed('top')) ||
278
- (enabledSides.right && !isAllowed('right')) ||
279
- (enabledSides.bottom && !isAllowed('bottom')) ||
280
- (enabledSides.left && !isAllowed('left'))
281
- )
282
- return undefined;
283
-
284
- if (isCorner && Array.isArray(box.options.allowCorners))
285
- {
286
- if (!box.options.allowCorners.some((combo) => enabledSides[combo[0]] && enabledSides[combo[1]]))
287
- return undefined;
288
- }
289
-
290
- const handleMouseDown = (e: React.MouseEvent<HTMLDivElement>) => {
291
- if (box.drag.value !== null || !matchMouseEvent(e.nativeEvent, MouseButton.Left))
292
- return;
293
-
294
- box.drag.value = {
295
- originalSize: box.size,
296
- originalOffset: box.offset,
297
- dragOrigin: [e.clientX, e.clientY],
298
- mouseMove: onMove,
299
- applyReposition: applyReposition,
300
- };
301
- }
302
-
303
- return (
304
- <div
305
- onMouseDown={handleMouseDown}
306
-
307
- className={clsx(
308
- "absolute",
309
- "select-none",
310
-
311
- spanVertical ? 'h-full' : clsx(
312
- box.options.dragAreaSize === 'small' && 'h-2',
313
- box.options.dragAreaSize === 'medium' && 'h-4',
314
- box.options.dragAreaSize === 'big' && 'h-6'
315
- ),
316
-
317
- spanHorizontal ? 'w-full' : clsx(
318
- box.options.dragAreaSize === 'small' && 'w-2',
319
- box.options.dragAreaSize === 'medium' && 'w-4',
320
- box.options.dragAreaSize === 'big' && 'w-6'
321
- ),
322
-
323
- enabledSides.top && clsx('bottom-full', box.options.insetAreas && 'translate-y-1/2'),
324
- enabledSides.right && clsx('left-full', box.options.insetAreas && '-translate-x-1/2'),
325
- enabledSides.bottom && clsx('top-full', box.options.insetAreas && '-translate-y-1/2'),
326
- enabledSides.left && clsx('right-full', box.options.insetAreas && 'translate-x-1/2'),
327
-
328
- matchBool({
329
- "cursor-nw-resize": enabledSides.top && enabledSides.left,
330
- "cursor-ne-resize": enabledSides.top && enabledSides.right,
331
- "cursor-se-resize": enabledSides.bottom && enabledSides.right,
332
- "cursor-sw-resize": enabledSides.bottom && enabledSides.left,
333
-
334
- "cursor-n-resize": enabledSides.top,
335
- "cursor-e-resize": enabledSides.right,
336
- "cursor-s-resize": enabledSides.bottom,
337
- "cursor-w-resize": enabledSides.left
338
- }),
339
- )}
340
- />
341
- );
342
- }