react-state-custom 1.0.22 → 1.0.24

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 (79) hide show
  1. package/.github/copilot-instructions.md +17 -2
  2. package/.github/workflows/deploy.yml +56 -0
  3. package/API_DOCUMENTATION.md +132 -3
  4. package/README.md +69 -1
  5. package/dist/dev-tool/DataViewComponent.d.ts +6 -0
  6. package/dist/dev-tool/DevTool.d.ts +5 -0
  7. package/dist/dev-tool/DevToolState.d.ts +9 -0
  8. package/dist/dev-tool/StateLabelRender.d.ts +2 -0
  9. package/dist/dev-tool/useHighlight.d.ts +11 -0
  10. package/dist/dev.d.ts +0 -1
  11. package/dist/examples/Playground.d.ts +1 -0
  12. package/dist/examples/cart/app.d.ts +1 -0
  13. package/dist/examples/cart/index.d.ts +3 -0
  14. package/dist/examples/cart/state.d.ts +23 -0
  15. package/dist/examples/cart/view.d.ts +4 -0
  16. package/dist/examples/counter/app.d.ts +1 -0
  17. package/dist/examples/counter/index.d.ts +2 -0
  18. package/dist/examples/counter/state.d.ts +6 -0
  19. package/dist/examples/counter/view.d.ts +2 -0
  20. package/dist/examples/form/app.d.ts +1 -0
  21. package/dist/examples/form/index.d.ts +3 -0
  22. package/dist/examples/form/state.d.ts +16 -0
  23. package/dist/examples/form/view.d.ts +4 -0
  24. package/dist/examples/timer/app.d.ts +1 -0
  25. package/dist/examples/timer/index.d.ts +2 -0
  26. package/dist/examples/timer/state.d.ts +11 -0
  27. package/dist/examples/timer/view.d.ts +4 -0
  28. package/dist/examples/todo/app.d.ts +1 -0
  29. package/dist/examples/todo/index.d.ts +3 -0
  30. package/dist/examples/todo/state.d.ts +17 -0
  31. package/dist/examples/todo/view.d.ts +4 -0
  32. package/dist/index.d.ts +2 -1
  33. package/dist/index.es.js +297 -397
  34. package/dist/index.es.js.map +1 -1
  35. package/dist/index.umd.js +1 -1
  36. package/dist/index.umd.js.map +1 -1
  37. package/dist/react-state-custom.css +1 -1
  38. package/dist/state-utils/ctx.d.ts +3 -2
  39. package/package.json +7 -2
  40. package/src/dev-tool/DataViewComponent.tsx +17 -0
  41. package/src/dev-tool/DevTool.css +134 -0
  42. package/src/{DevTool.tsx → dev-tool/DevTool.tsx} +3 -2
  43. package/src/dev-tool/DevToolState.tsx +78 -0
  44. package/src/dev-tool/StateLabelRender.tsx +38 -0
  45. package/src/dev-tool/useHighlight.tsx +56 -0
  46. package/src/dev.tsx +4 -11
  47. package/src/examples/Playground.tsx +180 -0
  48. package/src/examples/cart/app.tsx +16 -0
  49. package/src/examples/cart/index.ts +3 -0
  50. package/src/examples/cart/state.ts +67 -0
  51. package/src/examples/cart/view.tsx +62 -0
  52. package/src/examples/counter/app.tsx +14 -0
  53. package/src/examples/counter/index.ts +2 -0
  54. package/src/examples/counter/state.ts +22 -0
  55. package/src/examples/counter/state.tsx?raw +0 -0
  56. package/src/examples/counter/view.tsx +20 -0
  57. package/src/examples/form/app.tsx +16 -0
  58. package/src/examples/form/index.ts +3 -0
  59. package/src/examples/form/state.ts +58 -0
  60. package/src/examples/form/view.tsx +53 -0
  61. package/src/examples/timer/app.tsx +16 -0
  62. package/src/examples/timer/index.ts +2 -0
  63. package/src/examples/timer/state.ts +43 -0
  64. package/src/examples/timer/view.tsx +26 -0
  65. package/src/examples/todo/app.tsx +16 -0
  66. package/src/examples/todo/index.ts +3 -0
  67. package/src/examples/todo/state.ts +54 -0
  68. package/src/examples/todo/view.tsx +47 -0
  69. package/src/index.ts +2 -1
  70. package/src/state-utils/ctx.ts +36 -13
  71. package/src/vite-env.d.ts +6 -0
  72. package/tsconfig.json +12 -3
  73. package/vite.config.dev.ts +6 -1
  74. package/dist/DevTool.d.ts +0 -4
  75. package/dist/DevToolState.d.ts +0 -15
  76. package/dist/Test.d.ts +0 -1
  77. package/src/DevTool.css +0 -192
  78. package/src/DevToolState.tsx +0 -319
  79. package/src/Test.tsx +0 -97
@@ -0,0 +1,43 @@
1
+ import { createRootCtx, createAutoCtx } from '../../index'
2
+ import { useCallback, useEffect, useState } from 'react'
3
+
4
+ export const { useCtxState: useTimerCtx } = createAutoCtx(
5
+ createRootCtx(
6
+ "timer",
7
+ ({ timerId }: { timerId: string }) => {
8
+ const [milliseconds, setMilliseconds] = useState(0)
9
+ const [isRunning, setIsRunning] = useState(false)
10
+
11
+ useEffect(() => {
12
+ if (!isRunning) return
13
+ const interval = setInterval(() => {
14
+ setMilliseconds(ms => ms + 10)
15
+ }, 10)
16
+ return () => clearInterval(interval)
17
+ }, [isRunning])
18
+
19
+ const totalSeconds = Math.floor(milliseconds / 1000)
20
+ const minutes = Math.floor(totalSeconds / 60)
21
+ const seconds = totalSeconds % 60
22
+ const ms = Math.floor((milliseconds % 1000) / 10)
23
+ const formattedTime = `${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}.${ms.toString().padStart(2, '0')}`
24
+
25
+ const start = useCallback(() => setIsRunning(true), [])
26
+ const pause = useCallback(() => setIsRunning(false), [])
27
+ const reset = useCallback(() => {
28
+ setIsRunning(false)
29
+ setMilliseconds(0)
30
+ }, [])
31
+
32
+ return {
33
+ timerId,
34
+ milliseconds,
35
+ isRunning,
36
+ formattedTime,
37
+ start,
38
+ pause,
39
+ reset,
40
+ }
41
+ }
42
+ )
43
+ )
@@ -0,0 +1,26 @@
1
+ import { useQuickSubscribe } from '../../index'
2
+ import { useTimerCtx } from './state'
3
+
4
+ export const TimerExample = ({ timerId = "main-timer" }: { timerId?: string }) => {
5
+ const { formattedTime, isRunning, start, pause, reset } =
6
+ useQuickSubscribe(useTimerCtx({ timerId }))
7
+
8
+ return (
9
+ <div style={{ padding: '1rem', border: '1px solid #ccc', marginBottom: '1rem' }}>
10
+ <h3>Timer ({timerId})</h3>
11
+ <div style={{ fontSize: '2rem', fontFamily: 'monospace', marginBottom: '1rem' }}>
12
+ {formattedTime}
13
+ </div>
14
+ <div style={{ display: 'flex', gap: '0.5rem' }}>
15
+ {!isRunning ? (
16
+ <button onClick={start}>Start</button>
17
+ ) : (
18
+ <button onClick={pause}>Pause</button>
19
+ )}
20
+ <button onClick={reset}>Reset</button>
21
+ </div>
22
+ </div>
23
+ )
24
+ }
25
+
26
+ export default TimerExample
@@ -0,0 +1,16 @@
1
+ import { TodoExample } from './view'
2
+
3
+ export default function App() {
4
+ return (
5
+ <>
6
+ {/* <AutoRootCtx/> */}
7
+ <TodoExample listId="main" />
8
+ <TodoExample listId="work" />
9
+ <p style={{ color: '#666', fontSize: '0.875rem' }}>
10
+ Multiple independent todo lists showing how contexts can be scoped by parameters (listId).
11
+ Each list maintains its own state.
12
+ </p>
13
+ {/* <DevToolContainer Component={DataView} /> */}
14
+ </>
15
+ )
16
+ }
@@ -0,0 +1,3 @@
1
+ export { TodoExample, TodoExample as default } from './view'
2
+ export { useTodoCtx } from './state'
3
+ export type { Todo } from './state'
@@ -0,0 +1,54 @@
1
+ import { createRootCtx, createAutoCtx } from '../../index'
2
+ import { useCallback, useState } from 'react'
3
+
4
+ export interface Todo {
5
+ id: string
6
+ text: string
7
+ completed: boolean
8
+ }
9
+
10
+ export const { useCtxState: useTodoCtx } = createAutoCtx(
11
+ createRootCtx(
12
+ "todos",
13
+ ({ listId }: { listId: string }) => {
14
+ const [todos, setTodos] = useState<Todo[]>([])
15
+ const [input, setInput] = useState('')
16
+
17
+ const addTodo = useCallback(() => {
18
+ if (input.trim()) {
19
+ setTodos(prev => [...prev, {
20
+ id: Date.now().toString(),
21
+ text: input.trim(),
22
+ completed: false
23
+ }])
24
+ setInput('')
25
+ }
26
+ }, [input])
27
+
28
+ const toggleTodo = useCallback((id: string) => {
29
+ setTodos(prev => prev.map(t =>
30
+ t.id === id ? { ...t, completed: !t.completed } : t
31
+ ))
32
+ }, [])
33
+
34
+ const removeTodo = useCallback((id: string) => {
35
+ setTodos(prev => prev.filter(t => t.id !== id))
36
+ }, [])
37
+
38
+ const clearCompleted = useCallback(() => {
39
+ setTodos(prev => prev.filter(t => !t.completed))
40
+ }, [])
41
+
42
+ return {
43
+ listId,
44
+ todos,
45
+ input,
46
+ setInput,
47
+ addTodo,
48
+ toggleTodo,
49
+ removeTodo,
50
+ clearCompleted,
51
+ }
52
+ }
53
+ )
54
+ )
@@ -0,0 +1,47 @@
1
+ import { useQuickSubscribe } from '../../index'
2
+ import { useTodoCtx } from './state'
3
+
4
+ export const TodoExample = ({ listId = "main" }: { listId?: string }) => {
5
+ const { todos, input, setInput, addTodo, toggleTodo, removeTodo, clearCompleted } =
6
+ useQuickSubscribe(useTodoCtx({ listId }))
7
+
8
+ return (
9
+ <div style={{ padding: '1rem', border: '1px solid #ccc', marginBottom: '1rem' }}>
10
+ <h3>Todo List ({listId})</h3>
11
+ <div style={{ display: 'flex', gap: '0.5rem', marginBottom: '1rem' }}>
12
+ <input
13
+ value={input ?? ''}
14
+ onChange={(e) => setInput?.(e.target.value)}
15
+ onKeyDown={(e) => e.key === 'Enter' && addTodo?.()}
16
+ placeholder="Add todo..."
17
+ style={{ flex: 1, padding: '0.25rem' }}
18
+ />
19
+ <button onClick={addTodo}>Add</button>
20
+ </div>
21
+ <ul style={{ listStyle: 'none', padding: 0 }}>
22
+ {todos?.map(todo => (
23
+ <li key={todo.id} style={{ display: 'flex', gap: '0.5rem', marginBottom: '0.5rem' }}>
24
+ <input
25
+ type="checkbox"
26
+ checked={todo.completed}
27
+ onChange={() => toggleTodo?.(todo.id)}
28
+ />
29
+ <span style={{
30
+ flex: 1,
31
+ textDecoration: todo.completed ? 'line-through' : 'none',
32
+ opacity: todo.completed ? 0.6 : 1
33
+ }}>
34
+ {todo.text}
35
+ </span>
36
+ <button onClick={() => removeTodo?.(todo.id)}>×</button>
37
+ </li>
38
+ ))}
39
+ </ul>
40
+ {todos?.some(t => t.completed) && (
41
+ <button onClick={clearCompleted}>Clear Completed</button>
42
+ )}
43
+ </div>
44
+ )
45
+ }
46
+
47
+ export default TodoExample
package/src/index.ts CHANGED
@@ -18,4 +18,5 @@ export { useArrayHash } from "./state-utils/useArrayHash"
18
18
 
19
19
  export { useQuickSubscribe } from "./state-utils/useQuickSubscribe"
20
20
 
21
- export { DevToolContainer } from "./DevTool"
21
+ export { DevToolContainer } from "./dev-tool/DevTool"
22
+ export type { DataViewComponent } from "./dev-tool/DataViewComponent"
@@ -4,12 +4,22 @@ import { useArrayHash } from "./useArrayHash"
4
4
 
5
5
 
6
6
 
7
- class DataEvent extends Event {
7
+ const CHANGE_EVENT = "@--change-event"
8
+
9
+ class DataEvent<D> extends Event {
8
10
  constructor(
9
- public event: string,
10
- public value: any
11
+ public event: keyof D,
12
+ public value: D[typeof event] | undefined
11
13
  ) {
12
- super(event);
14
+ super(String(event));
15
+ }
16
+ }
17
+
18
+ class ChangeEvent<D> extends Event {
19
+ constructor(
20
+ public value: DataEvent<D>
21
+ ) {
22
+ super(CHANGE_EVENT, value);
13
23
  }
14
24
  }
15
25
 
@@ -28,8 +38,6 @@ export class Context<D> extends EventTarget {
28
38
  super();
29
39
  }
30
40
 
31
- // private event = new EventEmitter()
32
-
33
41
  /**
34
42
  * The current data held by the context.
35
43
  */
@@ -48,9 +56,9 @@ export class Context<D> extends EventTarget {
48
56
 
49
57
  if (value != this.data[key]) {
50
58
  this.data[key] = value
51
- // console.count("[COUNT] " + String(key))
52
- // this.event.emit(String(key), { value })
53
- this.dispatchEvent(new DataEvent(String(key), value))
59
+ let event = new DataEvent(key, value);
60
+ this.dispatchEvent(event);
61
+ this.dispatchEvent(new ChangeEvent(event))
54
62
  }
55
63
  }
56
64
 
@@ -71,7 +79,22 @@ export class Context<D> extends EventTarget {
71
79
 
72
80
  if (key in this.data) _listener(this.data[key])
73
81
 
74
- return () => (this.removeEventListener(String(key), listener), undefined)
82
+ return () => this.removeEventListener(String(key), listener)
83
+ }
84
+
85
+ public subscribeAll(_listener: (changeKey: keyof D, newData: Partial<D>) => void) {
86
+
87
+ const listener = (event: any) => {
88
+ if (event instanceof ChangeEvent) {
89
+ const { value: data } = event
90
+ _listener(data.event as any as keyof D, this.data)
91
+ }
92
+ }
93
+
94
+ this.addEventListener(String(CHANGE_EVENT), listener)
95
+
96
+ return () => this.removeEventListener(String(CHANGE_EVENT), listener)
97
+
75
98
  }
76
99
 
77
100
  }
@@ -237,10 +260,10 @@ export const useDataSourceMultiple = <D, T extends readonly (keyof D)[]>(
237
260
  * @param keys - Keys to subscribe to.
238
261
  * @returns An object with the current values for the keys.
239
262
  */
240
- export const useDataSubscribeMultiple = <D, K extends keyof D>(
263
+ export const useDataSubscribeMultiple = <D, K extends (keyof D)[]>(
241
264
  ctx: Context<D> | undefined,
242
- ...keys: K[]
243
- ): Pick<D, K> => {
265
+ ...keys: K
266
+ ): { [i in keyof K]: D[K[i]] | undefined } => {
244
267
  const [, setCounter] = useState(0)
245
268
 
246
269
  const returnValues = keys.map(key => ctx?.data?.[key])
@@ -0,0 +1,6 @@
1
+ /// <reference types="vite/client" />
2
+
3
+ declare module '*?raw' {
4
+ const content: string
5
+ export default content
6
+ }
package/tsconfig.json CHANGED
@@ -2,7 +2,10 @@
2
2
  "compilerOptions": {
3
3
  "target": "esnext",
4
4
  "module": "esnext",
5
- "lib": ["dom", "esnext"],
5
+ "lib": [
6
+ "dom",
7
+ "esnext"
8
+ ],
6
9
  "moduleResolution": "node",
7
10
  "jsx": "react-jsx",
8
11
  "strict": true,
@@ -13,6 +16,12 @@
13
16
  "declaration": true,
14
17
  "sourceMap": true
15
18
  },
16
- "include": ["src"],
17
- "exclude": ["node_modules", "dist"]
19
+ "include": [
20
+ "vite/client",
21
+ "src"
22
+ ],
23
+ "exclude": [
24
+ "node_modules",
25
+ "dist"
26
+ ]
18
27
  }
@@ -2,10 +2,15 @@ import { defineConfig } from 'vite';
2
2
  import react from '@vitejs/plugin-react';
3
3
 
4
4
  export default defineConfig({
5
+ base: '/react-state-custom/',
5
6
  plugins: [
6
7
  react(),
7
8
  ],
8
9
  build: {
9
10
  target: 'esnext',
10
- }
11
+ outDir: 'demo-dist',
12
+ },
13
+ server: {
14
+ port: 3000,
15
+ },
11
16
  });
package/dist/DevTool.d.ts DELETED
@@ -1,4 +0,0 @@
1
- export declare const DevToolContainer: ({ toggleButton, ...props }: {
2
- [x: string]: any;
3
- toggleButton?: string | undefined;
4
- }) => import("react/jsx-runtime").JSX.Element;
@@ -1,15 +0,0 @@
1
- import { default as React } from 'react';
2
- export declare const DevToolState: ({}: {}) => import("react/jsx-runtime").JSX.Element;
3
- export declare const StateView: React.FC<{
4
- dataKey: string;
5
- }>;
6
- export declare const ChangeFlashWrappper: React.FC<React.ComponentProps<'div'> & {
7
- value: any;
8
- deepCompare?: boolean;
9
- }>;
10
- export declare const JSONView: React.FC<{
11
- value: any;
12
- name?: string;
13
- style?: any;
14
- expandLevel?: number | boolean;
15
- }>;
package/dist/Test.d.ts DELETED
@@ -1 +0,0 @@
1
- export declare const Test: ({}: {}) => import("react/jsx-runtime").JSX.Element;
package/src/DevTool.css DELETED
@@ -1,192 +0,0 @@
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
- }