@hrnec06/react_utils 1.5.0 → 1.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hrnec06/react_utils",
3
- "version": "1.5.0",
3
+ "version": "1.6.0",
4
4
  "description": "A debugger component for react.",
5
5
  "exports": {
6
6
  ".": "./src/index.ts"
@@ -6,9 +6,13 @@ import clsx from "clsx";
6
6
  import ScrollBar from "./DebuggerScrollBar";
7
7
  import ParseValue from "./parser/DebugParser";
8
8
  import useKeyListener from "../../hooks/useKeyListener";
9
- import useUpdateEffect from "../../hooks/useUpdateEffect";
10
9
  import useUpdatedRef from "../../hooks/useUpdatedRef";
11
10
  import ResizeableBox from "../ResizeableBox/ResizeableBox";
11
+ import useNamespacedId from "../../hooks/useNamespacedId";
12
+ import useLocalStorage from "../../hooks/useLocalStorage";
13
+ import z from "zod";
14
+
15
+ const DEBUG_NAMESPACE = "debugger";
12
16
 
13
17
  interface DebugProps {
14
18
  value: unknown,
@@ -29,24 +33,28 @@ function Debug({
29
33
  openRoot = true
30
34
  }: DebugProps) {
31
35
  // Config
32
- const LS_POS_KEY = 'debugger_position';
33
- const LS_SIZE_KEY = 'debugger_size';
34
- const LS_EXPAND_KEY = 'debugger_expanded';
36
+
37
+ const id = useNamespacedId(DEBUG_NAMESPACE);
38
+
39
+ const { LS_POS_KEY, LS_SIZE_KEY, LS_EXPAND_KEY } = useMemo(() => ({
40
+ LS_POS_KEY: id + ".position",
41
+ LS_SIZE_KEY: id + ".size",
42
+ LS_EXPAND_KEY: id + ".expanded"
43
+ }), [id]);
35
44
 
36
45
  // Keybinds
37
46
  const f9_pressed = useKeyListener('F9');
38
47
 
39
- // Position
40
- const [position, setPosition] = useState<Vector2>([0, 0]);
48
+ // Position and resize
49
+ const [position, setPosition] = useLocalStorage<Vector2>(LS_POS_KEY, [0, 0], z.tuple([z.number(), z.number()]));
50
+ const [windowSize, setWindowSize] = useLocalStorage<Vector2>(LS_SIZE_KEY, [500, 500], z.tuple([z.number(), z.number()]));
51
+
41
52
  const [grab, setGrab] = useState<Nullable<{
42
53
  windowOrigin: Vector2,
43
54
  positionOrigin: Vector2
44
55
  }>>(null);
45
56
  const [grabOffset, setGrabOffset] = useState<Nullable<Vector2>>(null);
46
57
 
47
- // Resize
48
- const [windowSize, setWindowSize] = useState<Vector2>([500, 500]);
49
-
50
58
  // Scroll
51
59
  const [scrollHeight, setScrollHeight] = useState(0);
52
60
  const [containerHeight, setContainerHeight] = useState(0);
@@ -98,64 +106,6 @@ function Debug({
98
106
 
99
107
  const isScrollable = useMemo(() => scrollHeight > 0, [scrollHeight]);
100
108
 
101
- // Load saved position
102
- useEffect(() => {
103
- try {
104
- const saved_position = localStorage.getItem(LS_POS_KEY);
105
- if (saved_position) {
106
- const [v1, v2] = saved_position.split(',');
107
-
108
- if (v1 === undefined || v2 === undefined)
109
- throw new Error('Invalid vector: ' + saved_position);
110
-
111
- const [v1_p, v2_p] = [parseInt(v1), parseInt(v2)];
112
-
113
- if (isNaN(v1_p) || isNaN(v2_p))
114
- throw new Error('Invalid vector values: ' + saved_position);
115
-
116
- setPosition([v1_p, v2_p]);
117
- }
118
- } catch (error) {
119
- console.error(`Error loading saved position: `, error);
120
-
121
- localStorage.removeItem(LS_POS_KEY);
122
- }
123
- }, []);
124
-
125
- // Load saved size
126
- useEffect(() => {
127
- try {
128
- const saved_size = localStorage.getItem(LS_SIZE_KEY);
129
- if (saved_size) {
130
- const [v1, v2] = saved_size.split(',');
131
-
132
- if (v1 === undefined || v2 === undefined)
133
- throw new Error('Invalid vector: ' + saved_size);
134
-
135
- const [v1_p, v2_p] = [parseInt(v1), parseInt(v2)];
136
-
137
- if (isNaN(v1_p) || isNaN(v2_p))
138
- throw new Error('Invalid vector values: ' + saved_size);
139
-
140
- setWindowSize([v1_p, v2_p]);
141
- }
142
- } catch (error) {
143
- console.error(`Error loading saved size: `, error);
144
-
145
- localStorage.removeItem(LS_POS_KEY);
146
- }
147
- }, []);
148
-
149
- // Save saved position
150
- useUpdateEffect(() => {
151
- localStorage.setItem(LS_POS_KEY, `${position[0]},${position[1]}`);
152
- }, [position]);
153
-
154
- // Save saved size
155
- useUpdateEffect(() => {
156
- localStorage.setItem(LS_SIZE_KEY, `${windowSize[0]},${windowSize[1]}`);
157
- }, [windowSize]);
158
-
159
109
  // Reset position
160
110
  useEffect(() => {
161
111
  if (!f9_pressed)
@@ -291,7 +241,7 @@ function Debug({
291
241
  <div
292
242
  ref={containerRef}
293
243
  onMouseDown={handleMouseDown}
294
- className=" cursor-grab w-full"
244
+ className="cursor-grab w-full"
295
245
  style={{
296
246
  transform: `translateY(${-(scrollProgress * scrollHeight)}px)`
297
247
  }}
@@ -3,6 +3,7 @@ import { assert, Optional, ReactUtils } from "@hrnec06/util";
3
3
  import useUpdateEffect from "../../hooks/useUpdateEffect";
4
4
  import useNamespacedId from "../../hooks/useNamespacedId";
5
5
  import z from "zod";
6
+ import useLocalStorage from "../../hooks/useLocalStorage";
6
7
 
7
8
  const TERMINAL_NAMESPACE = "terminal";
8
9
 
@@ -73,11 +74,6 @@ class Terminal
73
74
  this.history = history;
74
75
  }
75
76
 
76
- public getLsKey(): string
77
- {
78
- return "terminal." + this.id;
79
- }
80
-
81
77
  public clear(): Terminal
82
78
  {
83
79
  this.history[1]([]);
@@ -214,37 +210,6 @@ function TerminalContextProvider({
214
210
  </TerminalContext.Provider>
215
211
  )
216
212
  }
217
-
218
- /**
219
- * Returns the default history for a terminal
220
- */
221
- function getDefaultHistory(terminal: Terminal)
222
- {
223
- try
224
- {
225
- const savedValue = localStorage.getItem(terminal.getLsKey());
226
-
227
- const savedValueJSON = JSON.parse(savedValue ?? "[]");
228
-
229
- const schema = z.array(z.object({
230
- value: z.any(),
231
- role: z.enum(LogRole),
232
- type: z.enum(LogType)
233
- }));
234
-
235
- const parsed = schema.parse(savedValueJSON);
236
-
237
- return parsed;
238
- }
239
- catch (error)
240
- {
241
- localStorage.removeItem(terminal.getLsKey());
242
- console.error("Terminal data parse failed: ", error);
243
-
244
- return [];
245
- }
246
- }
247
-
248
213
  /**
249
214
  * Recursively browses the contexts to find a matching terminal
250
215
  */
@@ -304,22 +269,26 @@ function useTerminal(
304
269
  {
305
270
  const id = useNamespacedId(TERMINAL_NAMESPACE);
306
271
 
307
- const history = useState<LogItem[]>([]);
272
+ const history = useLocalStorage<LogItem[]>(
273
+ "terminal." + id,
274
+ [],
275
+ z.array(z.object({
276
+ value: z.any(),
277
+ role: z.enum(LogRole),
278
+ type: z.enum(LogType)
279
+ }))
280
+ );
281
+
282
+ // const history = useState<LogItem[]>(storage);
308
283
  const inputHistory = useState<string[]>([]);
309
284
 
310
285
  const terminal = useMemo(() => {
311
286
  return new Terminal(id, name, history, inputHistory, onInput);
312
287
  }, [name, history, onInput]);
313
288
 
314
- useEffect(() => {
315
- const def = getDefaultHistory(terminal);
316
-
317
- history[1](def);
318
- }, []);
319
-
320
- useUpdateEffect(() => {
321
- localStorage.setItem(terminal.getLsKey(), JSON.stringify(history[0]));
322
- }, [history]);
289
+ // useUpdateEffect(() => {
290
+ // setStorage(history[0]);
291
+ // }, [history]);
323
292
 
324
293
  return terminal;
325
294
  }
@@ -0,0 +1,8 @@
1
+ import { useState } from "react";
2
+
3
+ export default function useDefaultValue<T>(value: T | (() => T)): T
4
+ {
5
+ const [_] = useState(value);
6
+
7
+ return _;
8
+ }
@@ -1,8 +1,9 @@
1
1
  import { useMemo, useRef } from "react";
2
+ import useDefaultValue from "./useDefaultValue";
2
3
 
3
4
  export default function useEfficientRef<T>(callback: () => T): React.RefObject<T>
4
5
  {
5
- const defaultValue = useMemo(() => callback(), []);
6
+ const value = useDefaultValue(callback);
6
7
 
7
- return useRef<T>(defaultValue);
8
+ return useRef<T>(value);
8
9
  }
@@ -0,0 +1,134 @@
1
+ import { disposables, Listenable, Nullable, Optional, ReactUtils } from "@hrnec06/util";
2
+ import useSignal, { Signal } from "./useSignal";
3
+ import useUpdateEffect from "./useUpdateEffect";
4
+ import { useCallback, useEffect, useMemo, useRef, useState, useSyncExternalStore } from "react";
5
+ import useDisposables from "./useDisposables";
6
+ import useDefaultValue from "./useDefaultValue";
7
+ import z from "zod";
8
+
9
+ type StateValue<T> = T | StateCallback<T>;
10
+ type StateCallback<T> = (value: T) => T;
11
+
12
+ function isStateCallback<T>(v: StateValue<T>): v is StateCallback<T> {
13
+ return typeof v === "function";
14
+ }
15
+
16
+ function encodeValue(value: unknown): Optional<string>
17
+ {
18
+ if (value === undefined)
19
+ return undefined;
20
+
21
+ try
22
+ {
23
+ const encoded = JSON.stringify(value);
24
+
25
+ return encoded;
26
+ }
27
+ catch (error)
28
+ {
29
+ console.error(error);
30
+
31
+ return undefined;
32
+ }
33
+ }
34
+
35
+ function decodeValue(value: Nullable<string>): unknown
36
+ {
37
+ if (value === null)
38
+ return undefined;
39
+
40
+ try
41
+ {
42
+ const decoded = JSON.parse(value);
43
+
44
+ return decoded;
45
+ }
46
+ catch (error)
47
+ {
48
+ console.error(error);
49
+
50
+ return undefined;
51
+ }
52
+ }
53
+
54
+ class StorageEventEmitter extends Listenable<{
55
+ storage: void
56
+ }> {
57
+ constructor
58
+ (
59
+ private readonly key: string
60
+ )
61
+ {
62
+ super();
63
+ }
64
+
65
+ public setLocalStorage = (value: string) => {
66
+ localStorage.setItem(this.key, value);
67
+
68
+ this.triggerListener("storage");
69
+ }
70
+
71
+ public removeLocalStorage = () => {
72
+ localStorage.removeItem(this.key);
73
+
74
+ this.triggerListener("storage");
75
+ }
76
+
77
+ public getLocalStorage = () => {
78
+ return localStorage.getItem(this.key);
79
+ }
80
+
81
+ public subscribe = (onStoreChange: () => void) => {
82
+ const id = this.on("storage", onStoreChange);
83
+
84
+ return () => {
85
+ this.off("storage", id);
86
+ }
87
+ }
88
+ }
89
+
90
+ export default function useLocalStorage<T>(key: string, defaultValue: T, schema?: z.ZodType): ReactUtils.State<T>
91
+ {
92
+ const storageManager = useMemo(() => new StorageEventEmitter(key), [key]);
93
+ const [currentValue, setCurrentValue] = useState(defaultValue);
94
+
95
+ const store = useSyncExternalStore(
96
+ storageManager.subscribe,
97
+ storageManager.getLocalStorage,
98
+ );
99
+
100
+ useEffect(() => {
101
+ const decoded = decodeValue(store);
102
+
103
+ if (decoded === undefined || ((schema && !schema.safeParse(decoded).success) || (typeof decoded !== typeof defaultValue)))
104
+ {
105
+ storageManager.removeLocalStorage();
106
+
107
+ setCurrentValue(defaultValue);
108
+ return;
109
+ }
110
+
111
+ setCurrentValue(decoded as T);
112
+ }, [store]);
113
+
114
+ const setState = useCallback((value: StateValue<T>) => {
115
+ setCurrentValue(prevCurrentValue => {
116
+ const newValue = isStateCallback(value) ? value(prevCurrentValue) : value;
117
+
118
+ const encoded = encodeValue(newValue);
119
+
120
+ if (encoded === undefined)
121
+ {
122
+ storageManager.removeLocalStorage();
123
+ }
124
+ else
125
+ {
126
+ storageManager.setLocalStorage(encoded);
127
+ }
128
+
129
+ return newValue;
130
+ });
131
+ }, [key, currentValue]);
132
+
133
+ return [currentValue, setState];
134
+ }
package/src/index.ts CHANGED
@@ -1,12 +1,16 @@
1
+ import useDefaultValue from "./hooks/useDefaultValue";
2
+ import useDisposables from "./hooks/useDisposables";
1
3
  import useKeyListener from "./hooks/useKeyListener";
2
4
  import useListener from "./hooks/useListener";
3
5
  import useUpdatedRef from "./hooks/useUpdatedRef";
4
6
  import useUpdateEffect from "./hooks/useUpdateEffect";
5
7
  import useWindowSize from "./hooks/useWindowSize";
6
8
  import useEfficientRef from "./hooks/useEfficientRef";
7
- import useEfficientState from "./hooks/useEfficientState";
8
9
  import useUUID from "./hooks/useUUID";
9
10
  import useFlags from "./hooks/useFlags";
11
+ import useNamespacedId from "./hooks/useNamespacedId";
12
+ import useTransition from "./hooks/useTransition";
13
+ import useLocalStorage from "./hooks/useLocalStorage";
10
14
 
11
15
  import useSignal, { Signal } from "./hooks/useSignal";
12
16
  import useLazySignal, { LazySignal } from "./hooks/useLazySignal";
@@ -18,17 +22,22 @@ import Dialog from "./components/Dialog/Dialog";
18
22
 
19
23
  import * as util from './lib/utils';
20
24
 
25
+
21
26
  export {
22
27
  // Hooks
28
+ useDefaultValue,
29
+ useDisposables,
30
+ useEfficientRef,
31
+ useFlags,
23
32
  useKeyListener,
24
33
  useListener,
34
+ useNamespacedId,
35
+ useTransition,
25
36
  useUpdatedRef,
26
37
  useUpdateEffect,
27
- useWindowSize,
28
- useEfficientRef,
29
- useEfficientState,
30
38
  useUUID,
31
- useFlags,
39
+ useWindowSize,
40
+ useLocalStorage,
32
41
 
33
42
  // Signals
34
43
  useSignal,
@@ -1,9 +0,0 @@
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
- }