dev-react-microstore 4.0.1 → 5.0.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.
@@ -0,0 +1,112 @@
1
+ import { useStoreSelector } from '../../../src/index'
2
+ import store from '../store'
3
+ import { useRef } from 'react'
4
+
5
+ export default function Counter() {
6
+ const { count } = useStoreSelector(store, ['count'])
7
+
8
+ const timeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null)
9
+ const intervalRef = useRef<ReturnType<typeof setInterval> | null>(null)
10
+ const isLongPressRef = useRef(false)
11
+
12
+ const START_DELAY = 250
13
+ const REPEAT_DELAY = 50
14
+
15
+ const handleCountIncrement = () => {
16
+ const {count} = store.select(['count'])
17
+ store.set({ count: count + 1 })
18
+ }
19
+
20
+ const handleCountDecrement = () => {
21
+ const {count} = store.select(['count'])
22
+ store.set({ count: count - 1 })
23
+ }
24
+
25
+ const handleCountReset = () => {
26
+ store.set({ count: 0 })
27
+ }
28
+
29
+ const startIncrement = () => {
30
+ isLongPressRef.current = false
31
+ timeoutRef.current = setTimeout(() => {
32
+ isLongPressRef.current = true
33
+ handleCountIncrement()
34
+ intervalRef.current = setInterval(() => {
35
+ handleCountIncrement()
36
+ }, REPEAT_DELAY)
37
+ }, START_DELAY)
38
+ }
39
+
40
+ const startDecrement = () => {
41
+ isLongPressRef.current = false
42
+ timeoutRef.current = setTimeout(() => {
43
+ isLongPressRef.current = true
44
+ handleCountDecrement()
45
+ intervalRef.current = setInterval(() => {
46
+ handleCountDecrement()
47
+ }, REPEAT_DELAY)
48
+ }, START_DELAY)
49
+ }
50
+
51
+ const stopPress = () => {
52
+ if (timeoutRef.current) {
53
+ clearTimeout(timeoutRef.current)
54
+ timeoutRef.current = null
55
+ }
56
+ if (intervalRef.current) {
57
+ clearInterval(intervalRef.current)
58
+ intervalRef.current = null
59
+ }
60
+ }
61
+
62
+ const handleIncrementClick = () => {
63
+ if (!isLongPressRef.current) {
64
+ handleCountIncrement()
65
+ }
66
+ isLongPressRef.current = false
67
+ }
68
+
69
+ const handleDecrementClick = () => {
70
+ if (!isLongPressRef.current) {
71
+ handleCountDecrement()
72
+ }
73
+ isLongPressRef.current = false
74
+ }
75
+
76
+ return (
77
+ <section className="section">
78
+ <h3>🔢 Counter (with validation middleware)</h3>
79
+ <div className="counter">
80
+ <span className="count-display">Count: {count}</span>
81
+ <div className="button-group">
82
+ <button
83
+ onMouseDown={startDecrement}
84
+ onMouseUp={stopPress}
85
+ onMouseLeave={stopPress}
86
+ onTouchStart={startDecrement}
87
+ onTouchEnd={stopPress}
88
+ onClick={handleDecrementClick}
89
+ >
90
+ -1
91
+ </button>
92
+ <button
93
+ onMouseDown={startIncrement}
94
+ onMouseUp={stopPress}
95
+ onMouseLeave={stopPress}
96
+ onTouchStart={startIncrement}
97
+ onTouchEnd={stopPress}
98
+ onClick={handleIncrementClick}
99
+ >
100
+ +1
101
+ </button>
102
+ <button onClick={handleCountReset}>Reset</button>
103
+ </div>
104
+ <p className="help-text">
105
+ Negative values are validated and blocked by middleware
106
+ <br />
107
+ <strong>Hold buttons to rapidly increment/decrement!</strong>
108
+ </p>
109
+ </div>
110
+ </section>
111
+ )
112
+ }
@@ -0,0 +1,466 @@
1
+ import { useEffect } from 'react'
2
+ import { useStoreSelector } from '../../../src/index'
3
+ import store from '../store'
4
+
5
+ // Child component that only animates when player health changes
6
+ function PlayerHealthBar() {
7
+ const { gameState } = useStoreSelector(store, [
8
+ {
9
+ gameState: (prev, next) => {
10
+ // Only re-render if player health changes
11
+ return prev.player.health === next.player.health
12
+ }
13
+ }
14
+ ])
15
+
16
+ return (
17
+ <div className="health-bar" key={gameState.player.health}>
18
+ <div className="bar-label">Player Health</div>
19
+ <div className="bar-container">
20
+ <div
21
+ className="bar-fill health-fill animate-change"
22
+ style={{ width: `${gameState.player.health}%` }}
23
+ >
24
+ {gameState.player.health}/100
25
+ </div>
26
+ </div>
27
+ </div>
28
+ )
29
+ }
30
+
31
+ // Child component that only animates when player mana changes
32
+ function PlayerManaBar() {
33
+ const { gameState } = useStoreSelector(store, [
34
+ {
35
+ gameState: (prev, next) => {
36
+ // Only re-render if player mana changes
37
+ return prev.player.mana === next.player.mana
38
+ }
39
+ }
40
+ ])
41
+
42
+ return (
43
+ <div className="mana-bar" key={gameState.player.mana}>
44
+ <div className="bar-label">Player Mana</div>
45
+ <div className="bar-container">
46
+ <div
47
+ className="bar-fill mana-fill animate-change"
48
+ style={{ width: `${gameState.player.mana * 2}%` }}
49
+ >
50
+ {gameState.player.mana}/50
51
+ </div>
52
+ </div>
53
+ </div>
54
+ )
55
+ }
56
+
57
+ // Child component that only animates when enemy health changes
58
+ function EnemyHealthBar() {
59
+ const { gameState } = useStoreSelector(store, [
60
+ {
61
+ gameState: (prev, next) => {
62
+ // Only re-render if enemy health, name, or max health changes
63
+ return prev.enemy.health === next.enemy.health &&
64
+ prev.enemy.name === next.enemy.name
65
+ }
66
+ }
67
+ ])
68
+
69
+ // Get max health based on enemy type
70
+ const getMaxHealth = (enemyName: string) => {
71
+ const enemies: Record<string, number> = {
72
+ 'Goblin': 30,
73
+ 'Orc': 50,
74
+ 'Troll': 75,
75
+ 'Dragon': 120,
76
+ 'Wizard': 80,
77
+ 'Demon Lord': 150
78
+ }
79
+ return enemies[enemyName] || 30
80
+ }
81
+
82
+ const maxHealth = getMaxHealth(gameState.enemy.name)
83
+ const healthPercent = (gameState.enemy.health / maxHealth) * 100
84
+
85
+ return (
86
+ <div className="enemy-health" key={`${gameState.enemy.name}-${gameState.enemy.health}`}>
87
+ <div className="bar-label">Enemy: {gameState.enemy.name}</div>
88
+ <div className="bar-container">
89
+ <div
90
+ className="bar-fill enemy-fill animate-change"
91
+ style={{ width: `${healthPercent}%` }}
92
+ >
93
+ {gameState.enemy.health}/{maxHealth}
94
+ </div>
95
+ </div>
96
+ </div>
97
+ )
98
+ }
99
+
100
+ // Child component that only animates when UI notifications change
101
+ function NotificationPanel() {
102
+ const { gameState } = useStoreSelector(store, [
103
+ {
104
+ gameState: (prev, next) => {
105
+ // Only re-render if notifications change
106
+ return JSON.stringify(prev.ui.notifications) === JSON.stringify(next.ui.notifications)
107
+ }
108
+ }
109
+ ])
110
+
111
+ return (
112
+ <div className="notifications" key={gameState.ui.notifications.length}>
113
+ <div className="notification-header">Combat Log</div>
114
+ <div className="notification-list animate-change">
115
+ {gameState.ui.notifications.length === 0 ? (
116
+ <div className="no-notifications">Ready for battle!</div>
117
+ ) : (
118
+ gameState.ui.notifications.slice(-5).map((notification, index) => (
119
+ <div key={index} className="notification-item">
120
+ {notification}
121
+ </div>
122
+ ))
123
+ )}
124
+ </div>
125
+ </div>
126
+ )
127
+ }
128
+
129
+ export default function CustomCompare() {
130
+ const { gameState } = useStoreSelector(store, ['gameState'])
131
+
132
+ // Handle enemy AI turn automatically
133
+ useEffect(() => {
134
+ if (gameState.turn === 'enemy' && !gameState.isProcessingTurn && gameState.player.health > 0) {
135
+ // Set processing flag to prevent multiple AI actions
136
+ store.set({
137
+ gameState: {
138
+ ...gameState,
139
+ isProcessingTurn: true
140
+ }
141
+ })
142
+
143
+ // AI action after 1 second delay
144
+ setTimeout(() => {
145
+ const currentState = store.get().gameState
146
+
147
+ if (currentState.player.health <= 0) return // Player already dead
148
+
149
+ // AI chooses action based on enemy type
150
+ const enemyActions = {
151
+ 'Goblin': ['scratch', 'bite'],
152
+ 'Orc': ['club', 'roar', 'charge'],
153
+ 'Troll': ['smash', 'regenerate', 'throw_rock'],
154
+ 'Dragon': ['fire_breath', 'claw', 'wing_attack'],
155
+ 'Wizard': ['magic_missile', 'lightning', 'heal'],
156
+ 'Demon Lord': ['dark_magic', 'soul_drain', 'summon']
157
+ }
158
+
159
+ const availableActions = enemyActions[currentState.enemy.name as keyof typeof enemyActions] || ['attack']
160
+ const action = availableActions[Math.floor(Math.random() * availableActions.length)]
161
+
162
+ let damage = 0
163
+ let actionText = ''
164
+ let newEnemyHealth = currentState.enemy.health
165
+
166
+ switch (action) {
167
+ case 'scratch':
168
+ case 'bite':
169
+ case 'attack':
170
+ damage = 8
171
+ actionText = `${currentState.enemy.name} attacks for ${damage} damage!`
172
+ break
173
+ case 'club':
174
+ case 'smash':
175
+ case 'claw':
176
+ damage = 12
177
+ actionText = `${currentState.enemy.name} uses ${action} for ${damage} damage!`
178
+ break
179
+ case 'fire_breath':
180
+ case 'dark_magic':
181
+ damage = 18
182
+ actionText = `${currentState.enemy.name} casts ${action.replace('_', ' ')} for ${damage} damage!`
183
+ break
184
+ case 'regenerate':
185
+ case 'heal': {
186
+ const maxHealth = getMaxHealth(currentState.enemy.name)
187
+ const healAmount = 15
188
+ newEnemyHealth = Math.min(maxHealth, currentState.enemy.health + healAmount)
189
+ actionText = `${currentState.enemy.name} heals for ${newEnemyHealth - currentState.enemy.health} HP!`
190
+ break
191
+ }
192
+ default:
193
+ damage = 10
194
+ actionText = `${currentState.enemy.name} uses ${action.replace('_', ' ')} for ${damage} damage!`
195
+ }
196
+
197
+ const newPlayerHealth = Math.max(0, currentState.player.health - damage)
198
+ const newNotifications = [...currentState.ui.notifications, actionText]
199
+
200
+ store.set({
201
+ gameState: {
202
+ ...currentState,
203
+ player: { ...currentState.player, health: newPlayerHealth },
204
+ enemy: { ...currentState.enemy, health: newEnemyHealth },
205
+ ui: { ...currentState.ui, notifications: newNotifications },
206
+ turn: 'player',
207
+ isProcessingTurn: false
208
+ }
209
+ })
210
+ }, 1000)
211
+ }
212
+ }, [gameState.turn, gameState.isProcessingTurn, gameState])
213
+
214
+ const getMaxHealth = (enemyName: string) => {
215
+ const enemies: Record<string, number> = {
216
+ 'Goblin': 30,
217
+ 'Orc': 50,
218
+ 'Troll': 75,
219
+ 'Dragon': 120,
220
+ 'Wizard': 80,
221
+ 'Demon Lord': 150
222
+ }
223
+ return enemies[enemyName] || 30
224
+ }
225
+
226
+ const playerAction = (actionType: 'attack' | 'heal' | 'magic' | 'restore' | 'rest') => {
227
+ if (gameState.turn !== 'player' || gameState.isProcessingTurn) return
228
+
229
+ let newPlayerHealth = gameState.player.health
230
+ let newPlayerMana = gameState.player.mana
231
+ let newEnemyHealth = gameState.enemy.health
232
+ let actionText = ''
233
+ let enemyDefeated = false
234
+
235
+ switch (actionType) {
236
+ case 'attack': {
237
+ const damage = 15
238
+ newEnemyHealth = Math.max(0, gameState.enemy.health - damage)
239
+ actionText = `You attack ${gameState.enemy.name} for ${damage} damage!`
240
+ enemyDefeated = newEnemyHealth === 0
241
+ break
242
+ }
243
+ case 'heal': {
244
+ if (gameState.player.mana < 10) {
245
+ actionText = 'Not enough mana for healing!'
246
+ break
247
+ }
248
+ const healAmount = 25
249
+ newPlayerHealth = Math.min(100, gameState.player.health + healAmount)
250
+ newPlayerMana = gameState.player.mana - 10
251
+ actionText = `You heal for ${newPlayerHealth - gameState.player.health} HP! (-10 mana)`
252
+ break
253
+ }
254
+ case 'magic': {
255
+ if (gameState.player.mana < 10) {
256
+ actionText = 'Not enough mana!'
257
+ break
258
+ }
259
+ const magicDamage = 25
260
+ newEnemyHealth = Math.max(0, gameState.enemy.health - magicDamage)
261
+ newPlayerMana = gameState.player.mana - 10
262
+ actionText = `You cast fireball for ${magicDamage} damage!`
263
+ enemyDefeated = newEnemyHealth === 0
264
+ break
265
+ }
266
+ case 'restore': {
267
+ const restoreAmount = 20
268
+ newPlayerMana = Math.min(50, gameState.player.mana + restoreAmount)
269
+ actionText = `You meditate and restore ${newPlayerMana - gameState.player.mana} mana!`
270
+ break
271
+ }
272
+ case 'rest': {
273
+ const restoreAmount = 15
274
+ newPlayerHealth = Math.min(100, gameState.player.health + restoreAmount)
275
+ actionText = `You rest and restore ${newPlayerHealth - gameState.player.health} HP!`
276
+ break
277
+ }
278
+ }
279
+
280
+ let finalNotifications = [...gameState.ui.notifications, actionText]
281
+ let nextTurn: 'player' | 'enemy' = 'enemy'
282
+ let newEnemy = gameState.enemy
283
+
284
+ if (enemyDefeated && gameState.enemy.isAlive) {
285
+ // Enemy defeated - check if it's the final boss
286
+ if (gameState.enemy.name === 'Demon Lord') {
287
+ // Player wins the game!
288
+ finalNotifications = ['Demon Lord defeated! You are victorious!']
289
+ newEnemy = { ...gameState.enemy, health: 0, isAlive: false }
290
+ nextTurn = 'player'
291
+ } else {
292
+ // Spawn next enemy
293
+ const enemies = [
294
+ { name: 'Goblin', health: 30, damage: 5 },
295
+ { name: 'Orc', health: 50, damage: 8 },
296
+ { name: 'Troll', health: 75, damage: 12 },
297
+ { name: 'Dragon', health: 120, damage: 20 },
298
+ { name: 'Wizard', health: 80, damage: 25 },
299
+ { name: 'Demon Lord', health: 150, damage: 30 }
300
+ ]
301
+
302
+ const currentEnemyIndex = enemies.findIndex(e => e.name === gameState.enemy.name)
303
+ const nextEnemyIndex = Math.min(currentEnemyIndex + 1, enemies.length - 1)
304
+ const nextEnemy = enemies[nextEnemyIndex]
305
+
306
+ // Clear all logs and only keep the defeat message
307
+ finalNotifications = [`${gameState.enemy.name} defeated! ${nextEnemy.name} appears!`]
308
+ newEnemy = { ...nextEnemy, isAlive: true }
309
+ nextTurn = 'player' // Stay on player turn for new enemy
310
+ }
311
+ } else {
312
+ // Just update enemy health if not defeated
313
+ newEnemy = { ...gameState.enemy, health: newEnemyHealth }
314
+ }
315
+
316
+ store.set({
317
+ gameState: {
318
+ ...gameState,
319
+ player: { ...gameState.player, health: newPlayerHealth, mana: newPlayerMana },
320
+ enemy: newEnemy,
321
+ ui: { ...gameState.ui, notifications: finalNotifications },
322
+ turn: nextTurn,
323
+ isProcessingTurn: false
324
+ }
325
+ })
326
+ }
327
+
328
+ const resetGame = () => {
329
+ store.set({
330
+ gameState: {
331
+ player: {
332
+ name: 'Player 1',
333
+ health: 100,
334
+ mana: 50,
335
+ level: 1,
336
+ experience: 0
337
+ },
338
+ enemy: {
339
+ name: 'Goblin',
340
+ health: 30,
341
+ damage: 5,
342
+ isAlive: true
343
+ },
344
+ ui: {
345
+ showInventory: false,
346
+ showMap: false,
347
+ notifications: []
348
+ },
349
+ turn: 'player',
350
+ isProcessingTurn: false
351
+ }
352
+ })
353
+ }
354
+
355
+ const isPlayerDefeated = gameState.player.health <= 0
356
+ const isPlayerTurn = gameState.turn === 'player'
357
+ const canAct = isPlayerTurn && !gameState.isProcessingTurn && !isPlayerDefeated
358
+ const isGameWon = gameState.enemy.name === 'Demon Lord' && !gameState.enemy.isAlive
359
+
360
+ // Add defeat log when player is defeated
361
+ useEffect(() => {
362
+ if (isPlayerDefeated && gameState.ui.notifications.length > 0 &&
363
+ !gameState.ui.notifications.some(n => n.includes('You have been defeated'))) {
364
+ store.set({
365
+ gameState: {
366
+ ...gameState,
367
+ ui: {
368
+ ...gameState.ui,
369
+ notifications: [...gameState.ui.notifications, 'You have been defeated!']
370
+ }
371
+ }
372
+ })
373
+ }
374
+ }, [isPlayerDefeated, gameState])
375
+
376
+ return (
377
+ <section className="section">
378
+ <h3>⚙️ Custom Compare Functions Demo</h3>
379
+ <p className="demo-description">
380
+ Each UI component below uses a custom compare function to only re-render when its specific data changes.
381
+ Watch the flash animations - they only trigger when that component's subscribed data updates!
382
+ </p>
383
+
384
+ {isPlayerDefeated ? (
385
+ <div className="game-over">
386
+ <div className="defeated-message">DEFEATED</div>
387
+ <button onClick={resetGame}>Reset Game</button>
388
+ </div>
389
+ ) : isGameWon ? (
390
+ <div className="game-over">
391
+ <div className="defeated-message" style={{color: '#4CAF50'}}>VICTORY!</div>
392
+ <button onClick={resetGame}>Play Again</button>
393
+ </div>
394
+ ) : (
395
+ <>
396
+ <div className="turn-indicator">
397
+ {gameState.isProcessingTurn ? (
398
+ <div className="processing-turn">Processing...</div>
399
+ ) : (
400
+ <div className={`turn-display ${gameState.turn}`}>
401
+ {gameState.turn === 'player' ? '🗡️ Your Turn' : '👹 Enemy Turn'}
402
+ </div>
403
+ )}
404
+ </div>
405
+
406
+ <div className="game-controls">
407
+ <button
408
+ onClick={() => playerAction('attack')}
409
+ disabled={!canAct}
410
+ >
411
+ ⚔️ Attack (15 dmg)
412
+ </button>
413
+ <button
414
+ onClick={() => playerAction('heal')}
415
+ disabled={!canAct}
416
+ >
417
+ ❤️ Heal (+25 HP, -10 mana)
418
+ </button>
419
+ <button
420
+ onClick={() => playerAction('magic')}
421
+ disabled={!canAct || gameState.player.mana < 10}
422
+ >
423
+ 🔥 Fireball (25 dmg, -10 mana)
424
+ </button>
425
+ <button
426
+ onClick={() => playerAction('restore')}
427
+ disabled={!canAct}
428
+ >
429
+ 🧘 Restore Mana (+20)
430
+ </button>
431
+ <button
432
+ onClick={() => playerAction('rest')}
433
+ disabled={!canAct}
434
+ >
435
+ 😴 Rest (+15 HP)
436
+ </button>
437
+ <button onClick={resetGame} className="small">Reset Game</button>
438
+ </div>
439
+ </>
440
+ )}
441
+
442
+ <div className="game-ui">
443
+ <div className="player-stats">
444
+ <PlayerHealthBar />
445
+ <PlayerManaBar />
446
+ </div>
447
+
448
+ <div className="enemy-stats">
449
+ <EnemyHealthBar />
450
+ </div>
451
+
452
+ <div className="ui-panel">
453
+ <NotificationPanel />
454
+ </div>
455
+ </div>
456
+
457
+ <div className="help-text">
458
+ <strong>Fine-grained subscriptions:</strong> Each health/mana bar only re-renders when its specific data changes.
459
+ <br />
460
+ <strong>Performance optimization:</strong> PlayerHealthBar won't re-render when mana changes, EnemyHealthBar won't re-render when player stats change, etc.
461
+ <br />
462
+ The flash animations demonstrate exactly when each component updates - notice how they're independent!
463
+ </div>
464
+ </section>
465
+ )
466
+ }
@@ -0,0 +1,28 @@
1
+ import { useStoreSelector } from '../../../src/index'
2
+ import store from '../store'
3
+
4
+ export default function Logs() {
5
+ const { logs } = useStoreSelector(store, ['logs'])
6
+
7
+ const handleClearLogs = () => {
8
+ store.set({ logs: [] })
9
+ }
10
+
11
+ return (
12
+ <section className="section">
13
+ <h3>📜 Middleware Logs</h3>
14
+ <div className="logs-header">
15
+ <span>{logs.length} logs</span>
16
+ <button onClick={handleClearLogs} className="small">Clear</button>
17
+ </div>
18
+ <div className="logs">
19
+ {logs.map(log => (
20
+ <div key={log.id} className={`log-entry ${log.type}`}>
21
+ <span className="timestamp">[{log.timestamp}]</span>
22
+ <pre className="message">{log.message}</pre>
23
+ </div>
24
+ ))}
25
+ </div>
26
+ </section>
27
+ )
28
+ }
@@ -0,0 +1,38 @@
1
+ import { useStoreSelector } from '../../../src/index'
2
+ import store, { updateSearchQuery } from '../store'
3
+
4
+ export default function Search() {
5
+ const { searchResults, isSearching } = useStoreSelector(store, ['searchResults', 'isSearching'])
6
+
7
+ return (
8
+ <section className="section">
9
+ <h3>🔍 Debounced Search</h3>
10
+ <div className="search-form">
11
+ <input
12
+ type="text"
13
+ placeholder="Search technologies..."
14
+ onChange={(e) => updateSearchQuery(e.target.value)}
15
+ />
16
+ {isSearching && <span className="searching">Searching...</span>}
17
+ </div>
18
+ <div className="search-results">
19
+ {searchResults.length > 0 ? (
20
+ <div>
21
+ <p>{searchResults.length} results:</p>
22
+ <ul>
23
+ {searchResults.map((result: string) => (
24
+ <li key={result}>{result}</li>
25
+ ))}
26
+ </ul>
27
+ </div>
28
+
29
+ ) : (
30
+ <p>Type to search technologies...</p>
31
+ )}
32
+ </div>
33
+ <p className="help-text">
34
+ Search is debounced by 300ms - watch the logs!
35
+ </p>
36
+ </section>
37
+ )
38
+ }
@@ -0,0 +1,25 @@
1
+ import { useStoreSelector } from '../../../src/index'
2
+ import store from '../store'
3
+
4
+ export default function ThemeToggle() {
5
+ const { theme } = useStoreSelector(store, ['theme'])
6
+
7
+ const handleThemeToggle = () => {
8
+ store.set({ theme: theme === 'light' ? 'dark' : 'light' })
9
+ }
10
+
11
+ return (
12
+ <section className="section">
13
+ <h3>🎨 Theme (persisted)</h3>
14
+ <div className="theme-controls">
15
+ <span>Current theme: <strong>{theme}</strong></span>
16
+ <button onClick={handleThemeToggle}>
17
+ Switch to {theme === 'light' ? 'dark' : 'light'}
18
+ </button>
19
+ <p className="help-text">
20
+ Theme persists across page reloads!
21
+ </p>
22
+ </div>
23
+ </section>
24
+ )
25
+ }