@hrnec06/react_utils 1.2.7 → 1.3.3

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 +4 -3
  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 → index.ts} +13 -4
  15. package/src/debugger/DebuggerWindowResize.tsx +0 -136
  16. package/src/ui/ResizeableBox.tsx +0 -351
  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,9 +1,9 @@
1
1
  {
2
2
  "name": "@hrnec06/react_utils",
3
- "version": "1.2.7",
3
+ "version": "1.3.3",
4
4
  "description": "A debugger component for react.",
5
5
  "exports": {
6
- ".": "./src/index.tsx"
6
+ ".": "./src/index.ts"
7
7
  },
8
8
  "keywords": [],
9
9
  "author": {
@@ -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;
@@ -3,10 +3,13 @@ import useListener from "./hooks/useListener";
3
3
  import useUpdatedRef from "./hooks/useUpdatedRef";
4
4
  import useUpdateEffect from "./hooks/useUpdateEffect";
5
5
  import useWindowSize from "./hooks/useWindowSize";
6
- import useSignal from "./hooks/useSignal";
7
6
 
8
- import Debugger from './debugger/Debugger';
9
- import ResizeableBox from "./ui/ResizeableBox";
7
+ import useSignal, { Signal } from "./hooks/useSignal";
8
+
9
+ import useLazySignal, { LazySignal } from "./hooks/useLazySignal";
10
+
11
+ import Debugger from './components/Debugger/Debugger';
12
+ import ResizeableBox from "./components/ResizeableBox/ResizeableBox";
10
13
 
11
14
  export {
12
15
  useKeyListener,
@@ -14,7 +17,13 @@ export {
14
17
  useUpdatedRef,
15
18
  useUpdateEffect,
16
19
  useWindowSize,
20
+
17
21
  useSignal,
22
+ Signal,
23
+
24
+ useLazySignal,
25
+ LazySignal,
26
+
18
27
  Debugger,
19
- ResizeableBox
28
+ ResizeableBox,
20
29
  };
@@ -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,351 +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
- e.preventDefault();
101
- e.stopPropagation();
102
-
103
- drag.value = null;
104
- }, [drag.value === null]);
105
-
106
- useListener(document, 'mousemove', (e) => {
107
- if (drag.value === null || !onResizeRef.current)
108
- return;
109
-
110
- e.preventDefault();
111
- e.stopPropagation();
112
-
113
- const offset: Vector2 = [
114
- e.clientX - drag.value.dragOrigin[0],
115
- e.clientY - drag.value.dragOrigin[1],
116
- ];
117
-
118
- const sizeOffset = drag.value.mouseMove(offset);
119
- const positionOffset: Vector2 = [0, 0];
120
-
121
- if (drag.value.applyReposition !== false)
122
- {
123
- const offset: Vector2 = cloneVector2(sizeOffset);
124
-
125
- positionOffset[0] = (drag.value.applyReposition === true || drag.value.applyReposition === 'x') ? -offset[0] : 0;
126
- positionOffset[1] = (drag.value.applyReposition === true || drag.value.applyReposition === 'y') ? -offset[1] : 0;
127
- }
128
-
129
- if (drag.value.originalSize)
130
- addVector2(sizeOffset, drag.value.originalSize);
131
-
132
- if (drag.value.originalOffset)
133
- addVector2(positionOffset, drag.value.originalOffset);
134
-
135
- onResizeRef.current(sizeOffset, positionOffset);
136
- }, [drag.value === null]);
137
-
138
- return (
139
- <ResizeableBoxContext.Provider
140
- value={{
141
- size: $size,
142
- offset: $offset,
143
-
144
- options: {
145
- dragAreaSize: $dragAreaSize,
146
- allowedSides: $allowedSides,
147
- allowCorners: $allowCorners,
148
- insetAreas: $insetAreas,
149
- },
150
-
151
- drag: drag
152
- }}
153
- >
154
- <div
155
- {...props}
156
-
157
- className={clsx(
158
- props.className,
159
- "relative w-full h-full"
160
- )}
161
- >
162
- <DragAreas />
163
-
164
- {props.children}
165
- </div>
166
- </ResizeableBoxContext.Provider>
167
- )
168
- }
169
-
170
- interface DragAreasProps {
171
-
172
- }
173
- function DragAreas({
174
-
175
- }: DragAreasProps)
176
- {
177
- return (
178
- <>
179
- <DragArea
180
- spanHorizontal
181
- top
182
-
183
- applyReposition={'y'}
184
- onMove={([, y]) => [0, -y]}
185
- />
186
-
187
- <DragArea
188
- spanVertical
189
- right
190
-
191
- onMove={([x]) => [x, 0]}
192
- />
193
-
194
- <DragArea
195
- spanHorizontal
196
- bottom
197
-
198
- onMove={([, y]) => [0, y]}
199
- />
200
-
201
- <DragArea
202
- spanVertical
203
- left
204
-
205
- applyReposition={'x'}
206
- onMove={([x]) => [-x, 0]}
207
- />
208
-
209
- {/* Corners */}
210
-
211
- <DragArea
212
- isCorner
213
- left
214
- top
215
-
216
- applyReposition={true}
217
- onMove={([x, y]) => [-x, -y]}
218
- />
219
-
220
- <DragArea
221
- isCorner
222
- right
223
- top
224
-
225
- applyReposition={'y'}
226
- onMove={([x, y]) => [x, -y]}
227
- />
228
-
229
- <DragArea
230
- isCorner
231
- left
232
- bottom
233
-
234
- applyReposition={'x'}
235
- onMove={([x, y]) => [-x, y]}
236
- />
237
-
238
- <DragArea
239
- isCorner
240
- right
241
- bottom
242
-
243
- onMove={([x, y]) => [x, y]}
244
- />
245
- </>
246
- );
247
- }
248
-
249
- interface DragAreaProps {
250
- isCorner?: boolean,
251
-
252
- spanVertical?: boolean,
253
- spanHorizontal?: boolean,
254
-
255
- onMove: (offset: Vector2) => Vector2,
256
- applyReposition?: RepositionMode,
257
-
258
- top?: boolean,
259
- right?: boolean,
260
- bottom?: boolean,
261
- left?: boolean
262
- }
263
- function DragArea({
264
- isCorner,
265
-
266
- spanVertical,
267
- spanHorizontal,
268
-
269
- onMove,
270
- applyReposition = false,
271
-
272
- ...enabledSides
273
- }: DragAreaProps)
274
- {
275
- const box = useResizeableBox();
276
-
277
- const isAllowed = (side: DragSide) => box.options.allowedSides.includes(side);
278
-
279
- if (isCorner && !box.options.allowCorners)
280
- return undefined;
281
-
282
- if (
283
- (enabledSides.top && !isAllowed('top')) ||
284
- (enabledSides.right && !isAllowed('right')) ||
285
- (enabledSides.bottom && !isAllowed('bottom')) ||
286
- (enabledSides.left && !isAllowed('left'))
287
- )
288
- return undefined;
289
-
290
- if (isCorner && Array.isArray(box.options.allowCorners))
291
- {
292
- if (!box.options.allowCorners.some((combo) => enabledSides[combo[0]] && enabledSides[combo[1]]))
293
- return undefined;
294
- }
295
-
296
- const handleMouseDown = (e: React.MouseEvent<HTMLDivElement>) => {
297
- if (box.drag.value !== null || !matchMouseEvent(e.nativeEvent, MouseButton.Left))
298
- return;
299
-
300
- e.preventDefault();
301
- e.stopPropagation();
302
-
303
- box.drag.value = {
304
- originalSize: box.size,
305
- originalOffset: box.offset,
306
- dragOrigin: [e.clientX, e.clientY],
307
- mouseMove: onMove,
308
- applyReposition: applyReposition,
309
- };
310
- }
311
-
312
- return (
313
- <div
314
- onMouseDown={handleMouseDown}
315
-
316
- className={clsx(
317
- "absolute",
318
- "select-none",
319
-
320
- spanVertical ? 'h-full' : clsx(
321
- box.options.dragAreaSize === 'small' && 'h-2',
322
- box.options.dragAreaSize === 'medium' && 'h-4',
323
- box.options.dragAreaSize === 'big' && 'h-6'
324
- ),
325
-
326
- spanHorizontal ? 'w-full' : clsx(
327
- box.options.dragAreaSize === 'small' && 'w-2',
328
- box.options.dragAreaSize === 'medium' && 'w-4',
329
- box.options.dragAreaSize === 'big' && 'w-6'
330
- ),
331
-
332
- enabledSides.top && clsx('bottom-full', box.options.insetAreas && 'translate-y-1/2'),
333
- enabledSides.right && clsx('left-full', box.options.insetAreas && '-translate-x-1/2'),
334
- enabledSides.bottom && clsx('top-full', box.options.insetAreas && '-translate-y-1/2'),
335
- enabledSides.left && clsx('right-full', box.options.insetAreas && 'translate-x-1/2'),
336
-
337
- matchBool({
338
- "cursor-nw-resize": enabledSides.top && enabledSides.left,
339
- "cursor-ne-resize": enabledSides.top && enabledSides.right,
340
- "cursor-se-resize": enabledSides.bottom && enabledSides.right,
341
- "cursor-sw-resize": enabledSides.bottom && enabledSides.left,
342
-
343
- "cursor-n-resize": enabledSides.top,
344
- "cursor-e-resize": enabledSides.right,
345
- "cursor-s-resize": enabledSides.bottom,
346
- "cursor-w-resize": enabledSides.left
347
- }),
348
- )}
349
- />
350
- );
351
- }