@hrnec06/react_utils 1.5.1 → 1.7.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.
Files changed (31) hide show
  1. package/package.json +1 -1
  2. package/src/components/ContextMenu/ContextMenu.tsx +235 -0
  3. package/src/components/ContextMenu/ContextMenu.types.ts +17 -0
  4. package/src/components/ContextMenu/ContextMenuCtx.tsx +33 -0
  5. package/src/components/ContextMenu/ContextMenuItem.tsx +147 -0
  6. package/src/components/ContextMenu/ContextMenuRoot.tsx +28 -0
  7. package/src/components/ContextMenu/ContextMenuSection.tsx +28 -0
  8. package/src/components/Debugger/Debugger.tsx +86 -84
  9. package/src/components/Debugger/DebuggerTerminal.tsx +18 -49
  10. package/src/components/Debugger/parser/DebugParser.tsx +121 -14
  11. package/src/components/Debugger/parser/DebugTerminal.tsx +24 -7
  12. package/src/components/Debugger/parser/ValueArray.tsx +22 -7
  13. package/src/components/Debugger/parser/ValueBoolean.tsx +15 -4
  14. package/src/components/Debugger/parser/ValueConstant.tsx +21 -0
  15. package/src/components/Debugger/parser/ValueFunction.tsx +17 -5
  16. package/src/components/Debugger/parser/ValueNumber.tsx +14 -4
  17. package/src/components/Debugger/parser/ValueObject.tsx +61 -17
  18. package/src/components/Debugger/parser/ValueString.tsx +13 -4
  19. package/src/components/ResizeableBox/ResizeableBox.tsx +1 -1
  20. package/src/hooks/useDebounce.ts +26 -0
  21. package/src/hooks/useDefaultValue.ts +8 -0
  22. package/src/hooks/useEfficientRef.ts +3 -2
  23. package/src/hooks/useEvent.ts +15 -0
  24. package/src/hooks/useLatestRef.ts +12 -0
  25. package/src/hooks/useLocalStorage.ts +134 -0
  26. package/src/hooks/useSyncRef.ts +17 -0
  27. package/src/hooks/useTransition.ts +2 -1
  28. package/src/index.ts +16 -5
  29. package/src/lib/errors/ContextError.ts +11 -0
  30. package/src/hooks/useEfficientState.ts +0 -9
  31. package/src/hooks/useUpdatedRef.ts +0 -12
@@ -1,10 +1,18 @@
1
+ import { forwardRef, useImperativeHandle } from "react";
2
+ import { ParseValueRef } from "./DebugParser";
3
+
1
4
  interface ValueFunctionProps {
2
5
  // eslint-disable-next-line @typescript-eslint/no-unsafe-function-type
3
6
  value: Function
4
7
  }
5
- export default function ValueFunction({
8
+ const ValueFunction = forwardRef<ParseValueRef, ValueFunctionProps>(({
6
9
  value
7
- }: ValueFunctionProps) {
10
+ }, ref) => {
11
+ useImperativeHandle(ref, () => ({
12
+ name: "Function",
13
+ copyValue: value.toString()
14
+ }), [value]);
15
+
8
16
  const argsMatch = value.toString().match(/^[^(]*\(\s*([^)]*)\)/);
9
17
  let args: string = '';
10
18
 
@@ -12,14 +20,18 @@ export default function ValueFunction({
12
20
  args = argsMatch[1]!;
13
21
  }
14
22
 
23
+ const displayValue = `${value.name}(${args})`;
24
+
15
25
  return (
16
26
  <>
17
27
  <span className="text-[#f2824a]">
18
28
  {'ƒ '}
19
29
  </span>
20
30
  <span className="text-white">
21
- {`${value.name}(${args})`}
31
+ {displayValue}
22
32
  </span>
23
33
  </>
24
- )
25
- }
34
+ );
35
+ });
36
+
37
+ export default ValueFunction;
@@ -1,12 +1,22 @@
1
+ import { forwardRef, useImperativeHandle } from "react"
2
+ import { ParseValueRef } from "./DebugParser";
3
+
1
4
  interface ValueNumberProps {
2
5
  value: number | bigint | string
3
6
  }
4
- export default function ValueNumber({
7
+ const ValueNumber = forwardRef<ParseValueRef, ValueNumberProps>(({
5
8
  value
6
- }: ValueNumberProps) {
9
+ }, ref) => {
10
+ useImperativeHandle(ref, () => ({
11
+ name: "Number",
12
+ copyValue: value.toString()
13
+ }), [value]);
14
+
7
15
  return (
8
16
  <span className="text-[#a7ce9b]">
9
17
  {value}
10
18
  </span>
11
- )
12
- }
19
+ );
20
+ });
21
+
22
+ export default ValueNumber;
@@ -1,25 +1,41 @@
1
- import React, { useLayoutEffect, useMemo, useState } from "react";
1
+ import React, { forwardRef, useImperativeHandle, useLayoutEffect, useMemo, useRef, useState } from "react";
2
2
  import useDebugger from "../DebuggerContext";
3
- import { entries, keys } from "@hrnec06/util";
3
+ import { entries, keys, Nullable } from "@hrnec06/util";
4
4
  import { Char_Bracket, Char_Colon, Char_Comma, Chevron_Toggle } from "../DebuggerSymbols";
5
- import ParseValue from "./DebugParser";
5
+ import ParseValue, { DebugCTXMProvider, ParseValueRef } from "./DebugParser";
6
+ import useSyncRef from "../../../hooks/useSyncRef";
7
+ import { useCTXMListener } from "../../ContextMenu/ContextMenuCtx";
6
8
 
7
9
  interface ValueObjectProps {
8
10
  value: object,
9
11
  path: string[],
10
12
  defaultExpanded?: boolean
11
13
  }
12
- export default function ValueObject({
14
+ const ValueObject = forwardRef<ParseValueRef, ValueObjectProps>(({
13
15
  value,
14
16
  path,
15
17
  defaultExpanded = false
16
- }: ValueObjectProps) {
18
+ }, ref) => {
17
19
  const debug = useDebugger();
18
20
 
19
21
  const [expanded, setExpanded] = useState(defaultExpanded);
20
22
 
21
23
  const objectEntries = entries(value);
22
24
 
25
+ useImperativeHandle(ref, () => ({
26
+ name: `Object {${objectEntries.length}}`,
27
+ copyValue: JSON.stringify(value),
28
+ menu: [
29
+ {
30
+ id: "open-toggle",
31
+ label: expanded ? "Collapse" : "Expand",
32
+ onClick: () => {
33
+ setExpanded(!expanded);
34
+ }
35
+ }
36
+ ]
37
+ }), [value, expanded]);
38
+
23
39
  const collapsedPreview = useMemo(() => {
24
40
  const children: React.ReactNode[] = [];
25
41
  const keyList = keys(value);
@@ -48,6 +64,8 @@ export default function ValueObject({
48
64
  setExpanded(state);
49
65
  }
50
66
 
67
+ const ctxmListener = useCTXMListener();
68
+
51
69
  return (
52
70
  <>
53
71
  <Chevron_Toggle expanded={expanded} onToggle={handleExpand} />
@@ -65,16 +83,13 @@ export default function ValueObject({
65
83
  {
66
84
  objectEntries.map(([key, value], ix) => {
67
85
  return (
68
- <li key={ix}>
69
- <ValueObjectKey text={key} />
70
- <Char_Colon />
71
-
72
- <ParseValue value={value} path={[...path, `${key}`]} />
73
-
74
- {ix < objectEntries.length - 1 && (
75
- <Char_Comma />
76
- )}
77
- </li>
86
+ <ValueObjectEntry
87
+ key={ix}
88
+ entryKey={key}
89
+ value={value}
90
+ path={path}
91
+ isLast={ix === objectEntries.length - 1}
92
+ />
78
93
  );
79
94
  })
80
95
  }
@@ -97,7 +112,7 @@ export default function ValueObject({
97
112
  }
98
113
  </>
99
114
  );
100
- }
115
+ });
101
116
 
102
117
  interface ValueObjectKeyProps {
103
118
  text: string,
@@ -110,4 +125,33 @@ function ValueObjectKey({
110
125
  {text}
111
126
  </span>
112
127
  )
113
- }
128
+ }
129
+
130
+ interface ValueObjectEntryProps {
131
+ entryKey: string,
132
+ value: unknown,
133
+ path: string[],
134
+ isLast: boolean
135
+ }
136
+ function ValueObjectEntry({
137
+ entryKey: key,
138
+ value,
139
+ path,
140
+ isLast
141
+ }: ValueObjectEntryProps)
142
+ {
143
+ return (
144
+ <li>
145
+ <ParseValue value={value} path={[...path, `${key}`]}>
146
+ <ValueObjectKey text={key} />
147
+ <Char_Colon />
148
+ </ParseValue>
149
+
150
+ {!isLast && (
151
+ <Char_Comma />
152
+ )}
153
+ </li>
154
+ )
155
+ }
156
+
157
+ export default ValueObject;
@@ -1,16 +1,25 @@
1
1
  import { padString } from "@hrnec06/util"
2
+ import { forwardRef, useImperativeHandle } from "react"
3
+ import { ParseValueRef } from "./DebugParser";
2
4
 
3
5
  interface ValueStringProps {
4
6
  value: string,
5
7
  noEncase?: boolean
6
8
  }
7
- export default function ValueString({
9
+ const ValueString = forwardRef<ParseValueRef, ValueStringProps>(({
8
10
  value,
9
11
  noEncase = false
10
- }: ValueStringProps) {
12
+ }, ref) => {
13
+ useImperativeHandle(ref, () => ({
14
+ name: "String",
15
+ copyValue: value
16
+ }), [value]);
17
+
11
18
  return (
12
19
  <span className="text-[#ce9178]">
13
20
  {padString(value, noEncase ? 0 : 1, '"')}
14
21
  </span>
15
- )
16
- }
22
+ );
23
+ });
24
+
25
+ export default ValueString;
@@ -3,7 +3,7 @@ import clsx from "clsx";
3
3
  import { useEffect } from "react";
4
4
  import DragAreas from "./DragAreas";
5
5
  import useSignal from "../../hooks/useSignal";
6
- import useUpdatedRef from "../../hooks/useUpdatedRef";
6
+ import useUpdatedRef from "../../hooks/useLatestRef";
7
7
  import useListener from "../../hooks/useListener";
8
8
  import { ResizeableBoxContext } from "./ResizeableBoxContext";
9
9
  import RB from "./ResizeableBox.types";
@@ -0,0 +1,26 @@
1
+ import { useCallback, useLayoutEffect, useState } from "react";
2
+
3
+ export default function useDebounce<T>(value: T, delay: number, updateOnUnmount: boolean = false): T
4
+ {
5
+ const [debounced, setValue] = useState(value);
6
+
7
+ const update = useCallback((value: T) => setValue(value), []);
8
+
9
+ useLayoutEffect(() => {
10
+ let updated = false;
11
+
12
+ const t = setTimeout(() => {
13
+ update(value);
14
+ updated = true;
15
+ }, delay);
16
+
17
+ return () => {
18
+ clearTimeout(t);
19
+
20
+ if (!updated && updateOnUnmount)
21
+ update(value);
22
+ }
23
+ }, [value]);
24
+
25
+ return debounced;
26
+ }
@@ -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,15 @@
1
+ import { useCallback } from "react";
2
+ import useLatestRef from "./useLatestRef";
3
+
4
+ export default function useEvent<
5
+ F extends (...args: any[]) => any,
6
+ P extends any[] = Parameters<F>,
7
+ R = ReturnType<F>,
8
+ > (
9
+ cb: (...args: P) => R
10
+ )
11
+ {
12
+ const cache = useLatestRef(cb);
13
+
14
+ return useCallback((...args: P) => cache.current(...args), [cache]);
15
+ }
@@ -0,0 +1,12 @@
1
+ import { useLayoutEffect, useRef } from "react";
2
+ import useUpdateEffect from "./useUpdateEffect";
3
+
4
+ export default function useLatestRef<V>(value: V): React.RefObject<V> {
5
+ const ref = useRef(value);
6
+
7
+ useLayoutEffect(() => {
8
+ ref.current = value;
9
+ });
10
+
11
+ return ref;
12
+ }
@@ -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
+ }
@@ -0,0 +1,17 @@
1
+ import { Nullable } from '@hrnec06/util'
2
+ import { useCallback, useEffect, useLayoutEffect, useRef, useState } from 'react'
3
+ import useEvent from './useEvent';
4
+
5
+ export default function useSyncRef<T>(
6
+ ref: React.RefObject<T>,
7
+ state: (value: T) => void
8
+ )
9
+ {
10
+ const syncRefs = useEvent((value: T) => {
11
+ ref.current = value;
12
+
13
+ state(value);
14
+ });
15
+
16
+ return syncRefs;
17
+ }
@@ -1,5 +1,5 @@
1
1
  import { disposables, mapRecord, Nullable } from "@hrnec06/util";
2
- import { useLayoutEffect, useRef, useState } from "react";
2
+ import { useEffect, useEffectEvent, useLayoutEffect, useRef, useState } from "react";
3
3
  import useFlags from "./useFlags";
4
4
  import useDisposables from "./useDisposables";
5
5
 
@@ -235,6 +235,7 @@ function prepareTransition(
235
235
  if (inFlight?.current)
236
236
  {
237
237
  prepare();
238
+
238
239
  return;
239
240
  }
240
241
 
package/src/index.ts CHANGED
@@ -1,15 +1,15 @@
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
- import useUpdatedRef from "./hooks/useUpdatedRef";
4
5
  import useUpdateEffect from "./hooks/useUpdateEffect";
5
6
  import useWindowSize from "./hooks/useWindowSize";
6
7
  import useEfficientRef from "./hooks/useEfficientRef";
7
- import useEfficientState from "./hooks/useEfficientState";
8
8
  import useUUID from "./hooks/useUUID";
9
9
  import useFlags from "./hooks/useFlags";
10
- import useDisposables from "./hooks/useDisposables";
11
10
  import useNamespacedId from "./hooks/useNamespacedId";
12
11
  import useTransition from "./hooks/useTransition";
12
+ import useLocalStorage from "./hooks/useLocalStorage";
13
13
 
14
14
  import useSignal, { Signal } from "./hooks/useSignal";
15
15
  import useLazySignal, { LazySignal } from "./hooks/useLazySignal";
@@ -19,20 +19,30 @@ import * as Debugger from './components/Debugger/Debugger';
19
19
  import ResizeableBox from "./components/ResizeableBox/ResizeableBox";
20
20
  import Dialog from "./components/Dialog/Dialog";
21
21
 
22
+ import ContextMenu from "./components/ContextMenu/ContextMenu";
23
+
22
24
  import * as util from './lib/utils';
25
+ import useDebounce from "./hooks/useDebounce";
26
+ import useEvent from "./hooks/useEvent";
27
+ import useLatestRef from "./hooks/useLatestRef";
28
+ import useSyncRef from "./hooks/useSyncRef";
23
29
 
24
30
 
25
31
  export {
26
32
  // Hooks
33
+ useDebounce,
34
+ useDefaultValue,
27
35
  useDisposables,
28
36
  useEfficientRef,
29
- useEfficientState,
37
+ useEvent,
30
38
  useFlags,
31
39
  useKeyListener,
40
+ useLatestRef,
32
41
  useListener,
42
+ useLocalStorage,
33
43
  useNamespacedId,
44
+ useSyncRef,
34
45
  useTransition,
35
- useUpdatedRef,
36
46
  useUpdateEffect,
37
47
  useUUID,
38
48
  useWindowSize,
@@ -47,6 +57,7 @@ export {
47
57
  Debugger,
48
58
  ResizeableBox,
49
59
  Dialog,
60
+ ContextMenu,
50
61
 
51
62
  // Utilities
52
63
  util,
@@ -0,0 +1,11 @@
1
+ import { capitalizeString } from '@hrnec06/util';
2
+
3
+ export default class ContextError extends Error
4
+ {
5
+ constructor(context: string)
6
+ {
7
+ const name = capitalizeString(context.toLowerCase());
8
+
9
+ super(`use${name}() may be used only within ${name}Context.Provider!`)
10
+ }
11
+ }
@@ -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
- }
@@ -1,12 +0,0 @@
1
- import { useRef } from "react";
2
- import useUpdateEffect from "./useUpdateEffect";
3
-
4
- export default function useUpdatedRef<V>(value: V): React.RefObject<V> {
5
- const ref = useRef(value);
6
-
7
- useUpdateEffect(() => {
8
- ref.current = value;
9
- }, [value]);
10
-
11
- return ref;
12
- }