react-state-custom 1.0.19 → 1.0.21
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/.github/copilot-instructions.md +42 -0
- package/dist/DevTool.d.ts +4 -0
- package/dist/DevToolState.d.ts +11 -0
- package/dist/Test.d.ts +1 -0
- package/dist/dev.d.ts +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.es.js +440 -202
- package/dist/index.es.js.map +1 -1
- package/dist/index.umd.js +1 -1
- package/dist/index.umd.js.map +1 -1
- package/dist/react-state-custom.css +1 -0
- package/dist/state-utils/ctx.d.ts +3 -1
- package/dist/state-utils/utils.d.ts +3 -1
- package/index.html +14 -0
- package/package.json +4 -2
- package/src/DevTool.css +192 -0
- package/src/DevTool.tsx +19 -0
- package/src/DevToolState.tsx +316 -0
- package/src/Test.tsx +90 -0
- package/src/dev.tsx +14 -0
- package/src/index.ts +1 -1
- package/src/state-utils/utils.ts +9 -4
- package/vite.config.dev.ts +11 -0
- package/.yarnrc.yml +0 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "react-state-custom",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.21",
|
|
4
4
|
"description": "The `react-state-custom` library provides a powerful set of tools for managing shared state in React applications",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.umd.js",
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
},
|
|
16
16
|
"scripts": {
|
|
17
17
|
"build": "vite build",
|
|
18
|
-
"dev": "vite",
|
|
18
|
+
"dev": "vite --config vite.config.dev.ts",
|
|
19
19
|
"test": "echo \"No tests specified\" && exit 0"
|
|
20
20
|
},
|
|
21
21
|
"keywords": [
|
|
@@ -36,6 +36,8 @@
|
|
|
36
36
|
"@types/react": "19",
|
|
37
37
|
"@types/react-dom": "19",
|
|
38
38
|
"@vitejs/plugin-react": "^5.0.0",
|
|
39
|
+
"react": "^19.2.0",
|
|
40
|
+
"react-dom": "^19.2.0",
|
|
39
41
|
"typescript": "^5.8.3",
|
|
40
42
|
"vite": "7",
|
|
41
43
|
"vite-bundle-analyzer": "^1.1.0",
|
package/src/DevTool.css
ADDED
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
:root {
|
|
2
|
+
color-scheme: light dark;
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
.react-state-dev-btn {
|
|
6
|
+
position: fixed;
|
|
7
|
+
bottom: 30px;
|
|
8
|
+
right: 30px;
|
|
9
|
+
transition: opacity 0.3s;
|
|
10
|
+
|
|
11
|
+
&[data-active="true"] {
|
|
12
|
+
opacity: 0;
|
|
13
|
+
pointer-events: none;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
.react-state-dev-container {
|
|
19
|
+
--color: light-dark(#333b3c, #efefec);
|
|
20
|
+
--bg-color: light-dark(#f9f9f9, #212121);
|
|
21
|
+
align-items: stretch;
|
|
22
|
+
font-size: 14px;
|
|
23
|
+
color: var(--color);
|
|
24
|
+
background-color: var(--bg-color);
|
|
25
|
+
position: fixed;
|
|
26
|
+
bottom: 0px;
|
|
27
|
+
right: 0px;
|
|
28
|
+
left: 0px;
|
|
29
|
+
opacity: 0;
|
|
30
|
+
pointer-events: none;
|
|
31
|
+
transition: opacity 0.3s, transform 0.3s;
|
|
32
|
+
transform: translateY(100%);
|
|
33
|
+
padding: 0.5em;
|
|
34
|
+
|
|
35
|
+
&[data-active="true"] {
|
|
36
|
+
opacity: 1;
|
|
37
|
+
pointer-events: all;
|
|
38
|
+
transform: translateY(0%);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
.close-btn {
|
|
42
|
+
position: absolute;
|
|
43
|
+
top: -20px;
|
|
44
|
+
right: 0px;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
.main-panel {
|
|
48
|
+
display: flex;
|
|
49
|
+
flex-direction: row;
|
|
50
|
+
gap: 1em;
|
|
51
|
+
padding: 1em;
|
|
52
|
+
resize: vertical;
|
|
53
|
+
min-height: 200px;
|
|
54
|
+
max-height: 60vh;
|
|
55
|
+
background-color: #8882;
|
|
56
|
+
overflow: hidden;
|
|
57
|
+
justify-items: stretch;
|
|
58
|
+
|
|
59
|
+
.state-list {
|
|
60
|
+
max-width: 50%;
|
|
61
|
+
min-width: 100px;
|
|
62
|
+
overflow: auto;
|
|
63
|
+
align-self: stretch;
|
|
64
|
+
resize: horizontal;
|
|
65
|
+
|
|
66
|
+
.state-key {
|
|
67
|
+
cursor: pointer;
|
|
68
|
+
padding: 0.2em;
|
|
69
|
+
border-bottom: solid 1px #8884;
|
|
70
|
+
font-family: monospace;
|
|
71
|
+
overflow: hidden;
|
|
72
|
+
text-overflow: ellipsis;
|
|
73
|
+
white-space: nowrap;
|
|
74
|
+
|
|
75
|
+
&[data-active="true"] {
|
|
76
|
+
background-color: light-dark(#0003, #fff3);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
.state-view {
|
|
83
|
+
flex: 1;
|
|
84
|
+
overflow: auto;
|
|
85
|
+
border-inline-start: solid 1px #8888;;
|
|
86
|
+
padding-inline-start: 1em;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
.jv-root {
|
|
92
|
+
font-family: monospace;
|
|
93
|
+
user-select: none;
|
|
94
|
+
|
|
95
|
+
.jv-name {
|
|
96
|
+
opacity: 0.8;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
.jv-type {
|
|
100
|
+
opacity: 0.5;
|
|
101
|
+
font-size: smaller;
|
|
102
|
+
padding-inline: 0.4em;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
.jv-cursor {
|
|
106
|
+
cursor: pointer;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
.jv-field {
|
|
110
|
+
margin-block: 1px;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
.jv-field .jv-field {
|
|
114
|
+
transition: border-color 1s, background-color 1s;
|
|
115
|
+
border-color: #0000;
|
|
116
|
+
background-color: #0000;
|
|
117
|
+
border-width: 1px;
|
|
118
|
+
border-style: solid;
|
|
119
|
+
|
|
120
|
+
&.jv-updated {
|
|
121
|
+
transition: border-color 0.00s, background-color 0.00s;
|
|
122
|
+
border-color: #f00;
|
|
123
|
+
background-color: #f001;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
.jv-field-obj>.jv-value {
|
|
128
|
+
padding-inline-start: 1em;
|
|
129
|
+
margin-inline-start: 0.6em;
|
|
130
|
+
border-inline-start: solid 1px #8888;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
.jv-field-obj>:first-child>:first-child>.jv-name {
|
|
134
|
+
cursor: pointer;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
.jv-field-obj {
|
|
139
|
+
--lv: 0;
|
|
140
|
+
|
|
141
|
+
.jv-field-obj {
|
|
142
|
+
--lv: 1;
|
|
143
|
+
|
|
144
|
+
.jv-field-obj {
|
|
145
|
+
--lv: 2;
|
|
146
|
+
|
|
147
|
+
.jv-field-obj {
|
|
148
|
+
--lv: 3;
|
|
149
|
+
|
|
150
|
+
.jv-field-obj {
|
|
151
|
+
--lv: 4;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
.jv-field-obj>:first-child {
|
|
159
|
+
container-type: scroll-state;
|
|
160
|
+
position: sticky;
|
|
161
|
+
top: calc(var(--lv, 0) * 1.25em);
|
|
162
|
+
z-index: calc(10 - var(--lv, 0));
|
|
163
|
+
|
|
164
|
+
@container scroll-state(stuck: top) {
|
|
165
|
+
>div {
|
|
166
|
+
background-color: var(--bg-color);
|
|
167
|
+
border-bottom: solid 1px #8884;
|
|
168
|
+
font-weight: bold;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
.jv-field-string> :is(.jv-type, .jv-value) {
|
|
178
|
+
color: orange;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
.jv-field-number> :is(.jv-type, .jv-value) {
|
|
182
|
+
color: red;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
.jv-field-boolean> :is(.jv-type, .jv-value) {
|
|
186
|
+
color: #08f;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
.jv-field-function> :is(.jv-type, .jv-value) {
|
|
190
|
+
color: #08f;
|
|
191
|
+
}
|
|
192
|
+
}
|
package/src/DevTool.tsx
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import "./devTool.css"
|
|
2
|
+
import { useState } from "react"
|
|
3
|
+
import { DevToolState } from "./DevToolState";
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
export const DevToolContainer = ({ toggleButton = "[x]", ...props }) => {
|
|
7
|
+
const [active, setActive] = useState(false);
|
|
8
|
+
return <>
|
|
9
|
+
<button className="react-state-dev-btn" data-active={active} onClick={() => setActive(true)} {...props}>
|
|
10
|
+
{props?.children ?? "Toggle Dev Tool"}
|
|
11
|
+
</button>
|
|
12
|
+
<div className="react-state-dev-container" data-active={active}>
|
|
13
|
+
<button className="close-btn" onClick={() => setActive(false)}>
|
|
14
|
+
[x]
|
|
15
|
+
</button>
|
|
16
|
+
<DevToolState />
|
|
17
|
+
</div>
|
|
18
|
+
</>
|
|
19
|
+
}
|
|
@@ -0,0 +1,316 @@
|
|
|
1
|
+
import { getContext } from "./state-utils/ctx"
|
|
2
|
+
import React, { Dispatch, SetStateAction, useCallback, useEffect, useMemo, useRef, useState } from "react"
|
|
3
|
+
import "./devTool.css"
|
|
4
|
+
|
|
5
|
+
const cache = getContext.cache
|
|
6
|
+
|
|
7
|
+
export const DevToolState = ({ }) => {
|
|
8
|
+
const [selectedKey, setKey] = useState("")
|
|
9
|
+
return <div className="main-panel">
|
|
10
|
+
<div className="state-list">
|
|
11
|
+
{[...cache.keys()]
|
|
12
|
+
.map(e => JSON.parse(e)?.[0])
|
|
13
|
+
.filter(e => e != "auto-ctx")
|
|
14
|
+
.map(e => <div
|
|
15
|
+
className="state-key"
|
|
16
|
+
data-active={e == selectedKey}
|
|
17
|
+
onClick={() => setKey(e)}>{e}
|
|
18
|
+
</div>)}
|
|
19
|
+
</div>
|
|
20
|
+
<div className="state-view" >
|
|
21
|
+
<StateView dataKey={selectedKey} key={selectedKey} />
|
|
22
|
+
</div>
|
|
23
|
+
</div>
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export const StateView: React.FC<{ dataKey: string }> = ({ dataKey }) => {
|
|
27
|
+
const ctx = getContext(dataKey)
|
|
28
|
+
const [currentData, setCurrentData] = useState({ ...ctx?.data })
|
|
29
|
+
|
|
30
|
+
useEffect(() => {
|
|
31
|
+
let checkState = { ...currentData }
|
|
32
|
+
let interval = setInterval(() => {
|
|
33
|
+
|
|
34
|
+
let isDiff = false
|
|
35
|
+
|
|
36
|
+
for (let i in ctx?.data) {
|
|
37
|
+
if (ctx?.data?.[i] != checkState[i]) {
|
|
38
|
+
checkState[i] = ctx?.data?.[i];
|
|
39
|
+
isDiff = true;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
if (isDiff) setCurrentData({ ...checkState })
|
|
43
|
+
}, 200)
|
|
44
|
+
|
|
45
|
+
return () => clearInterval(interval)
|
|
46
|
+
}, [ctx])
|
|
47
|
+
|
|
48
|
+
return <JSONView
|
|
49
|
+
value={currentData}
|
|
50
|
+
name="ROOT"
|
|
51
|
+
expandLevel={1}
|
|
52
|
+
style={{ }}
|
|
53
|
+
/>
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
type JSONViewProps = {
|
|
57
|
+
value: any,
|
|
58
|
+
path?: string[],
|
|
59
|
+
name?: string,
|
|
60
|
+
expandRoot: Record<string, boolean>,
|
|
61
|
+
setExpandRoot: Dispatch<SetStateAction<Record<string, boolean>>>,
|
|
62
|
+
expandLevel: number | boolean,
|
|
63
|
+
currentField?: any
|
|
64
|
+
currentType?: any,
|
|
65
|
+
isGrouped?: boolean,
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
const splitArray = <T,>(array: T[], max = 10) => {
|
|
70
|
+
return Object.fromEntries(
|
|
71
|
+
new Array(Math.ceil((array.length + 1) / max))
|
|
72
|
+
.fill(0)
|
|
73
|
+
.map((_, i, a) => new Array(i == a.length - 1 ? array.length % max : max)
|
|
74
|
+
.fill(0)
|
|
75
|
+
.map((_, j) => i * max + j)
|
|
76
|
+
)
|
|
77
|
+
.filter(e => e.length)
|
|
78
|
+
.map(keys => [`${keys.at(0)}..${keys.at(-1)}`, Object.fromEntries(
|
|
79
|
+
keys.map(k => [k, array[k]])
|
|
80
|
+
)])
|
|
81
|
+
)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
const splitObject = (object: any, max = 10) => {
|
|
86
|
+
const keys = Object.keys(object);
|
|
87
|
+
return Object.fromEntries(
|
|
88
|
+
Array(Math.ceil((keys.length + 1) / max))
|
|
89
|
+
.fill(0)
|
|
90
|
+
.map((_, i, a) => new Array(i == a.length - 1 ? keys.length % max : max)
|
|
91
|
+
.fill(0)
|
|
92
|
+
.map((_, j) => i * max + j)
|
|
93
|
+
)
|
|
94
|
+
.filter(e => e.length)
|
|
95
|
+
.map((e) => e.map(i => keys.at(i)))
|
|
96
|
+
.map(sortedKeys => [
|
|
97
|
+
`${sortedKeys.at(0)?.slice(0, 15)}...${sortedKeys.at(-1)?.slice(0, 15)}`,
|
|
98
|
+
Object.fromEntries(sortedKeys.map(key => [key, object[key as any]]))]
|
|
99
|
+
)
|
|
100
|
+
)
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
const useExpandState = ({ path, expandLevel, expandRoot, setExpandRoot }: JSONViewProps) => {
|
|
105
|
+
const expandKeys = path?.join("%") ?? "";
|
|
106
|
+
|
|
107
|
+
const defaultExpand = typeof expandLevel == "boolean"
|
|
108
|
+
? expandLevel
|
|
109
|
+
: (typeof expandLevel == 'number' && expandLevel > 0)
|
|
110
|
+
|
|
111
|
+
const isExpand = useMemo(
|
|
112
|
+
() => expandRoot?.[expandKeys] ?? defaultExpand,
|
|
113
|
+
[expandRoot?.[expandKeys], expandKeys]
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
const setExpand = useCallback(
|
|
117
|
+
(value: boolean) => setExpandRoot((r: object) => ({ ...r, [expandKeys]: value })),
|
|
118
|
+
[expandRoot, expandKeys]
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
return { isExpand, setExpand }
|
|
122
|
+
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const ChangeFlashWrappper: React.FC<React.ComponentProps<'div'> & { value: any, deepCompare?: boolean }> = ({ value, deepCompare = false, ...rest }) => {
|
|
126
|
+
|
|
127
|
+
const ref = useRef<HTMLElement>(undefined)
|
|
128
|
+
const refValue = useRef(value);
|
|
129
|
+
|
|
130
|
+
useEffect(() => {
|
|
131
|
+
if (ref.current) {
|
|
132
|
+
let isDiff = deepCompare && value && refValue.current
|
|
133
|
+
? (
|
|
134
|
+
Object.keys(value).length != Object.keys(refValue.current).length
|
|
135
|
+
|| Object.keys(value).some(key => value[key] != refValue.current[key])
|
|
136
|
+
) : value != refValue.current
|
|
137
|
+
if (isDiff) {
|
|
138
|
+
refValue.current = value;
|
|
139
|
+
ref.current.classList.add('jv-updated');
|
|
140
|
+
let t = requestAnimationFrame(() => ref.current?.classList.remove('jv-updated'));
|
|
141
|
+
return () => cancelAnimationFrame(t)
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
}, [value, deepCompare, ref])
|
|
146
|
+
|
|
147
|
+
return <div {...rest} ref={ref as any} />
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const JSONViewObj: React.FC<JSONViewProps> = (props) => {
|
|
151
|
+
|
|
152
|
+
const {
|
|
153
|
+
currentField,
|
|
154
|
+
value, path = [], name, expandRoot, setExpandRoot,
|
|
155
|
+
expandLevel,
|
|
156
|
+
isGrouped,
|
|
157
|
+
} = props
|
|
158
|
+
|
|
159
|
+
const isArray = value instanceof Array
|
|
160
|
+
|
|
161
|
+
const { isExpand, setExpand } = useExpandState(props)
|
|
162
|
+
|
|
163
|
+
const childExpandLevel = typeof expandLevel == "number" ? expandLevel - 1 : expandLevel
|
|
164
|
+
|
|
165
|
+
const shouldGroup = Object.entries(value).length > 10
|
|
166
|
+
|
|
167
|
+
const groupedChilds = useMemo(
|
|
168
|
+
() => shouldGroup
|
|
169
|
+
? (value instanceof Array) ? splitArray(value, 10) : splitObject(value, 10)
|
|
170
|
+
: value,
|
|
171
|
+
[value, shouldGroup, splitArray]
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
return (isExpand) ? <ChangeFlashWrappper className="jv-field jv-field-obj" value={value} deepCompare={isGrouped}>
|
|
175
|
+
{currentField && <div>
|
|
176
|
+
<div onClick={() => setExpand(false)}>
|
|
177
|
+
<span className="jv-name">{currentField}</span>
|
|
178
|
+
<span>:</span>
|
|
179
|
+
<span>[-]</span>
|
|
180
|
+
<span className="jv-type">{Object.keys(value).length} items </span>
|
|
181
|
+
<span> {isArray ? "[" : "{"} </span>
|
|
182
|
+
</div>
|
|
183
|
+
</div>}
|
|
184
|
+
<div className="jv-value">
|
|
185
|
+
{Object
|
|
186
|
+
.entries(groupedChilds)
|
|
187
|
+
.map(([k, v], index) => <JSONViewCurr
|
|
188
|
+
{...{
|
|
189
|
+
name, expandRoot, setExpandRoot,
|
|
190
|
+
expandLevel: childExpandLevel,
|
|
191
|
+
value: v,
|
|
192
|
+
isGrouped: shouldGroup,
|
|
193
|
+
}}
|
|
194
|
+
key={[...path, shouldGroup ? index : k].join("%")}
|
|
195
|
+
path={[...path, k]}
|
|
196
|
+
/>)}
|
|
197
|
+
</div>
|
|
198
|
+
{currentField && <div>
|
|
199
|
+
<span> {isArray ? "]" : "}"} </span>
|
|
200
|
+
</div>}
|
|
201
|
+
</ChangeFlashWrappper> : <ChangeFlashWrappper className="jv-field jv-field-obj" value={value} deepCompare={isGrouped}>
|
|
202
|
+
<div>
|
|
203
|
+
<div onClick={() => setExpand(true)}>
|
|
204
|
+
<span className="jv-name">{currentField}</span>
|
|
205
|
+
{currentField && <span>:</span>}
|
|
206
|
+
{currentField && <span>[+]</span>}
|
|
207
|
+
<span className="jv-type">{Object.keys(value).length} items </span>
|
|
208
|
+
<span> {isArray ? "[" : "{"} </span>
|
|
209
|
+
<span> ... </span>
|
|
210
|
+
<span> {isArray ? "]" : "}"} </span>
|
|
211
|
+
</div>
|
|
212
|
+
</div>
|
|
213
|
+
</ChangeFlashWrappper>
|
|
214
|
+
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
const StringViewObj: React.FC<JSONViewProps> = (props) => {
|
|
218
|
+
|
|
219
|
+
const { currentType, currentField, value, } = props
|
|
220
|
+
|
|
221
|
+
const { isExpand, setExpand } = useExpandState(props)
|
|
222
|
+
|
|
223
|
+
const useExpand = String(value).length > 50
|
|
224
|
+
|
|
225
|
+
const renderString = useExpand && !isExpand
|
|
226
|
+
? `${String(value).slice(0, 15)}...${String(value).slice(-15, -1)}`
|
|
227
|
+
: String(value)
|
|
228
|
+
|
|
229
|
+
return <ChangeFlashWrappper
|
|
230
|
+
value={props.value}
|
|
231
|
+
className={`jv-field jv-field-${currentType} ${useExpand ? 'jv-cursor' : ''}`}
|
|
232
|
+
onClick={() => setExpand(!isExpand)}>
|
|
233
|
+
<span className="jv-name">{currentField}</span>
|
|
234
|
+
<span>:</span>
|
|
235
|
+
<span className="jv-type">{currentType}, lng={value?.length}</span>
|
|
236
|
+
<span className="jv-value">"{renderString}"</span>
|
|
237
|
+
<span>,</span>
|
|
238
|
+
</ChangeFlashWrappper>
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
const FunctionViewObj: React.FC<JSONViewProps> = (props) => {
|
|
243
|
+
|
|
244
|
+
const { currentType, currentField, value, } = props
|
|
245
|
+
|
|
246
|
+
const { isExpand, setExpand } = useExpandState(props)
|
|
247
|
+
|
|
248
|
+
const useExpand = String(value).length > 50
|
|
249
|
+
|
|
250
|
+
const renderString = useExpand && !isExpand
|
|
251
|
+
? `${String(value).slice(0, 15)}...${String(value).slice(-15, -1)}`
|
|
252
|
+
: String(value)
|
|
253
|
+
|
|
254
|
+
return <ChangeFlashWrappper
|
|
255
|
+
value={props.value}
|
|
256
|
+
className={`jv-field jv-field-${currentType} ${useExpand ? 'jv-cursor' : ''}`}
|
|
257
|
+
onClick={() => setExpand(!isExpand)}>
|
|
258
|
+
<span className="jv-name">{currentField}</span>
|
|
259
|
+
<span>:</span>
|
|
260
|
+
<span className="jv-type">{currentType}</span>
|
|
261
|
+
<span className="jv-value">"{renderString}"</span>
|
|
262
|
+
<span>,</span>
|
|
263
|
+
</ChangeFlashWrappper>
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
const DefaultValueView: React.FC<JSONViewProps> = (props) => {
|
|
267
|
+
|
|
268
|
+
const { currentType, currentField, value, } = props
|
|
269
|
+
|
|
270
|
+
return <ChangeFlashWrappper
|
|
271
|
+
value={props.value}
|
|
272
|
+
className={`jv-field jv-field-${currentType}`}>
|
|
273
|
+
<span className="jv-name">{currentField}</span>
|
|
274
|
+
<span>:</span>
|
|
275
|
+
<span className="jv-type">{currentType}</span>
|
|
276
|
+
<span className="jv-value">{String(value)}</span>
|
|
277
|
+
<span>,</span>
|
|
278
|
+
</ChangeFlashWrappper>
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
const JSONViewCurr: React.FC<Omit<JSONViewProps, 'currentField'>> = (props) => {
|
|
282
|
+
|
|
283
|
+
const { value, path = [], name } = props
|
|
284
|
+
|
|
285
|
+
const currentField = path.at(-1) ?? name ?? undefined;
|
|
286
|
+
|
|
287
|
+
const currentType = typeof value
|
|
288
|
+
|
|
289
|
+
switch (currentType) {
|
|
290
|
+
case "object":
|
|
291
|
+
return <JSONViewObj {...props} {...{ currentField, currentType }} />
|
|
292
|
+
case "string":
|
|
293
|
+
return <StringViewObj {...props} {...{ currentField, currentType }} />
|
|
294
|
+
case "function":
|
|
295
|
+
return <FunctionViewObj {...props} {...{ currentField, currentType }} />
|
|
296
|
+
case "number":
|
|
297
|
+
case "boolean":
|
|
298
|
+
case "bigint":
|
|
299
|
+
case "symbol":
|
|
300
|
+
case "undefined":
|
|
301
|
+
default:
|
|
302
|
+
return <DefaultValueView {...props} {...{ currentField, currentType }} />
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
export const JSONView: React.FC<{ value: any, name?: string, style?: any, expandLevel?: number | boolean }> = ({ value, name, style, expandLevel = false }) => {
|
|
307
|
+
|
|
308
|
+
const [expandRoot, setExpandRoot] = useState<Record<string, boolean>>({})
|
|
309
|
+
|
|
310
|
+
return <div className="jv-root" style={style}>
|
|
311
|
+
<JSONViewCurr
|
|
312
|
+
path={[]}
|
|
313
|
+
{...{ name, value, expandRoot, setExpandRoot, expandLevel }}
|
|
314
|
+
/>
|
|
315
|
+
</div>
|
|
316
|
+
}
|
package/src/Test.tsx
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
|
|
2
|
+
import { createAutoCtx } from './state-utils/createAutoCtx'
|
|
3
|
+
import { createRootCtx } from './state-utils/createRootCtx'
|
|
4
|
+
import { useCallback, useState } from 'react'
|
|
5
|
+
import { useQuickSubscribe } from './state-utils/useQuickSubscribe'
|
|
6
|
+
|
|
7
|
+
const { useCtxState: useDevCtx } = createAutoCtx(
|
|
8
|
+
createRootCtx(
|
|
9
|
+
"devState",
|
|
10
|
+
({ }) => {
|
|
11
|
+
const [state, setState] = useState(0)
|
|
12
|
+
return {
|
|
13
|
+
state,
|
|
14
|
+
increase: useCallback(() => setState(f => f + 1), [setState]),
|
|
15
|
+
decrease: useCallback(() => setState(f => f - 1), [setState]),
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
)
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
const { useCtxState: useDevAdvanceCtx } = createAutoCtx(
|
|
24
|
+
createRootCtx(
|
|
25
|
+
"devADVState",
|
|
26
|
+
({ id }: { id: string }) => {
|
|
27
|
+
const [counter, setCounter] = useState(0)
|
|
28
|
+
const [state, setState] = useState(0)
|
|
29
|
+
const [history, setHistory] = useState([state])
|
|
30
|
+
const [historyMap, setHistoryMap] = useState({})
|
|
31
|
+
|
|
32
|
+
return {
|
|
33
|
+
id,
|
|
34
|
+
state,
|
|
35
|
+
bifIntState: BigInt(state),
|
|
36
|
+
computed: id + state,
|
|
37
|
+
history,
|
|
38
|
+
counter,
|
|
39
|
+
historyMap,
|
|
40
|
+
testSrt: "aiwioj ".repeat(counter * 3),
|
|
41
|
+
increase: useCallback(() => setState(f => {
|
|
42
|
+
setCounter(c => {
|
|
43
|
+
setHistory(h => [...h, f]);
|
|
44
|
+
setHistoryMap(m => ({ ...m, ['state--' + c]: { f, d: Date.now() } }));
|
|
45
|
+
return c + 1
|
|
46
|
+
})
|
|
47
|
+
return f + 1
|
|
48
|
+
}), [setState]),
|
|
49
|
+
decrease: useCallback(() => setState(f => {
|
|
50
|
+
setCounter(c => {
|
|
51
|
+
setHistory(h => [...h, f]);
|
|
52
|
+
setHistoryMap(m => ({ ...m, ['state--' + c]: { f, d: Date.now() } }));
|
|
53
|
+
return c + 1
|
|
54
|
+
})
|
|
55
|
+
return f - 1
|
|
56
|
+
}), [setState])
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
)
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
export const Test = ({ }) => {
|
|
64
|
+
const { state, decrease, increase } = useQuickSubscribe(useDevCtx({}))
|
|
65
|
+
const { state: advState, computed: advComputed, increase: advIncreaser, decrease: advDecrise } = useQuickSubscribe(useDevAdvanceCtx({ id: "name" }))
|
|
66
|
+
|
|
67
|
+
useDevAdvanceCtx({ id: "2123132" })
|
|
68
|
+
useDevAdvanceCtx({ id: "dfgfd" })
|
|
69
|
+
useDevAdvanceCtx({ id: "443" })
|
|
70
|
+
useDevAdvanceCtx({ id: "w3sef" })
|
|
71
|
+
useDevAdvanceCtx({ id: "erere" })
|
|
72
|
+
useDevAdvanceCtx({ id: "sdfdsf" })
|
|
73
|
+
useDevAdvanceCtx({ id: "asdasd" })
|
|
74
|
+
useDevAdvanceCtx({ id: "66666" })
|
|
75
|
+
useDevAdvanceCtx({ id: "dddd" })
|
|
76
|
+
useDevAdvanceCtx({ id: "eeeee" })
|
|
77
|
+
useDevAdvanceCtx({ id: "44444" })
|
|
78
|
+
useDevAdvanceCtx({ id: ";;;;" })
|
|
79
|
+
|
|
80
|
+
return <div>
|
|
81
|
+
<hr />
|
|
82
|
+
<button onClick={increase}>[+]</button>
|
|
83
|
+
<span>{state}</span>
|
|
84
|
+
<button onClick={decrease}>[-]</button>
|
|
85
|
+
<hr />
|
|
86
|
+
<button onClick={advIncreaser}>[+]</button>
|
|
87
|
+
<span>{advState}:{advComputed}:</span>
|
|
88
|
+
<button onClick={advDecrise}>[-]</button>
|
|
89
|
+
</div>
|
|
90
|
+
}
|
package/src/dev.tsx
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { createRoot } from 'react-dom/client'
|
|
2
|
+
import { DevToolContainer } from './DevTool'
|
|
3
|
+
import { AutoRootCtx, createAutoCtx } from './state-utils/createAutoCtx'
|
|
4
|
+
import { Test } from './Test'
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
createRoot(document.getElementById('root')!).render(
|
|
9
|
+
<>
|
|
10
|
+
<DevToolContainer />
|
|
11
|
+
<Test/>
|
|
12
|
+
<AutoRootCtx/>
|
|
13
|
+
</>
|
|
14
|
+
)
|
package/src/index.ts
CHANGED
package/src/state-utils/utils.ts
CHANGED
|
@@ -12,7 +12,7 @@ export function debounce<T extends (...args: any[]) => any>(
|
|
|
12
12
|
timeout = setTimeout(() => {
|
|
13
13
|
func(...args);
|
|
14
14
|
}, wait);
|
|
15
|
-
} as any;
|
|
15
|
+
} as any;
|
|
16
16
|
|
|
17
17
|
fn.cancel = () => clearTimeout(timeout!);
|
|
18
18
|
|
|
@@ -22,10 +22,11 @@ export function debounce<T extends (...args: any[]) => any>(
|
|
|
22
22
|
// Memoize function
|
|
23
23
|
export function memoize<T extends (...args: any[]) => any>(
|
|
24
24
|
func: T
|
|
25
|
-
): (...args: Parameters<T>) => ReturnType<T> {
|
|
25
|
+
): ((...args: Parameters<T>) => ReturnType<T>) & { cache: Map<string, ReturnType<T>> } {
|
|
26
|
+
|
|
26
27
|
const cache = new Map<string, ReturnType<T>>();
|
|
27
28
|
|
|
28
|
-
|
|
29
|
+
const cachedFunc: any = function (...args: Parameters<T>): ReturnType<T> {
|
|
29
30
|
const key = JSON.stringify(args);
|
|
30
31
|
if (cache.has(key)) {
|
|
31
32
|
return cache.get(key) as ReturnType<T>;
|
|
@@ -33,6 +34,10 @@ export function memoize<T extends (...args: any[]) => any>(
|
|
|
33
34
|
const result = func(...args);
|
|
34
35
|
cache.set(key, result);
|
|
35
36
|
return result;
|
|
36
|
-
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
cachedFunc.cache = cache;
|
|
40
|
+
|
|
41
|
+
return cachedFunc
|
|
37
42
|
}
|
|
38
43
|
|