@hrnec06/react_utils 1.2.4 → 1.2.7

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,6 +1,6 @@
1
1
  {
2
2
  "name": "@hrnec06/react_utils",
3
- "version": "1.2.4",
3
+ "version": "1.2.7",
4
4
  "description": "A debugger component for react.",
5
5
  "exports": {
6
6
  ".": "./src/index.tsx"
@@ -26,7 +26,7 @@
26
26
  "typescript": "^5.8.3"
27
27
  },
28
28
  "dependencies": {
29
- "@hrnec06/util": "^1.4.14",
29
+ "@hrnec06/util": "^1.4.21",
30
30
  "clsx": "^2.1.1",
31
31
  "lucide-react": "^0.525.0",
32
32
  "react": "^19.1.0",
@@ -31,7 +31,7 @@ export default function ParseValueSimple({
31
31
  if ((decimals = parts[1].substring(0, MAX_LENGTH - short.length - 1)) && decimals.length > 0)
32
32
  short += '.' + decimals;
33
33
 
34
- return <ValueKeyword value={short} />;
34
+ return <ValueNumber value={short} />;
35
35
  }
36
36
 
37
37
  return <ValueNumber value={value} />
@@ -17,7 +17,7 @@ export default function ValueFunction({
17
17
  <span className="text-[#f2824a]">
18
18
  {'ƒ '}
19
19
  </span>
20
- <span>
20
+ <span className="text-white">
21
21
  {`${value.name}(${args})`}
22
22
  </span>
23
23
  </>
@@ -1,5 +1,5 @@
1
1
  interface ValueNumberProps {
2
- value: number | bigint
2
+ value: number | bigint | string
3
3
  }
4
4
  export default function ValueNumber({
5
5
  value
@@ -11,7 +11,7 @@ export default function ValueSymbol({
11
11
  <>
12
12
  <ValueKeyword value="Symbol" />
13
13
  <Char_Bracket text="(" />
14
- <span>
14
+ <span className="text-white">
15
15
  {value.description}
16
16
  </span>
17
17
  <Char_Bracket text=")" />
package/src/index.tsx CHANGED
@@ -4,7 +4,9 @@ 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
+
7
8
  import Debugger from './debugger/Debugger';
9
+ import ResizeableBox from "./ui/ResizeableBox";
8
10
 
9
11
  export {
10
12
  useKeyListener,
@@ -13,5 +15,6 @@ export {
13
15
  useUpdateEffect,
14
16
  useWindowSize,
15
17
  useSignal,
16
- Debugger
18
+ Debugger,
19
+ ResizeableBox
17
20
  };
@@ -0,0 +1,351 @@
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
+ }