create-fluxstack 1.14.0 → 1.16.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 (76) hide show
  1. package/LLMD/INDEX.md +4 -3
  2. package/LLMD/resources/live-binary-delta.md +507 -0
  3. package/LLMD/resources/live-components.md +208 -12
  4. package/LLMD/resources/live-rooms.md +731 -333
  5. package/app/client/.live-stubs/LiveAdminPanel.js +5 -0
  6. package/app/client/.live-stubs/LiveCounter.js +9 -0
  7. package/app/client/.live-stubs/LiveForm.js +11 -0
  8. package/app/client/.live-stubs/LiveLocalCounter.js +8 -0
  9. package/app/client/.live-stubs/LivePingPong.js +10 -0
  10. package/app/client/.live-stubs/LiveRoomChat.js +11 -0
  11. package/app/client/.live-stubs/LiveSharedCounter.js +10 -0
  12. package/app/client/.live-stubs/LiveUpload.js +15 -0
  13. package/app/client/src/App.tsx +19 -7
  14. package/app/client/src/components/AppLayout.tsx +18 -10
  15. package/app/client/src/live/PingPongDemo.tsx +199 -0
  16. package/app/client/src/live/RoomChatDemo.tsx +187 -22
  17. package/app/client/src/live/SharedCounterDemo.tsx +142 -0
  18. package/app/server/auth/DevAuthProvider.ts +2 -2
  19. package/app/server/auth/JWTAuthProvider.example.ts +2 -2
  20. package/app/server/index.ts +2 -2
  21. package/app/server/live/LiveAdminPanel.ts +1 -1
  22. package/app/server/live/LivePingPong.ts +61 -0
  23. package/app/server/live/LiveProtectedChat.ts +1 -1
  24. package/app/server/live/LiveRoomChat.ts +106 -38
  25. package/app/server/live/LiveSharedCounter.ts +73 -0
  26. package/app/server/live/rooms/ChatRoom.ts +68 -0
  27. package/app/server/live/rooms/CounterRoom.ts +51 -0
  28. package/app/server/live/rooms/DirectoryRoom.ts +42 -0
  29. package/app/server/live/rooms/PingRoom.ts +40 -0
  30. package/app/server/routes/room.routes.ts +1 -2
  31. package/core/build/live-components-generator.ts +11 -2
  32. package/core/build/vite-plugins.ts +28 -0
  33. package/core/client/hooks/useLiveUpload.ts +3 -4
  34. package/core/client/index.ts +25 -35
  35. package/core/framework/server.ts +1 -1
  36. package/core/server/index.ts +1 -2
  37. package/core/server/live/auto-generated-components.ts +5 -8
  38. package/core/server/live/index.ts +90 -21
  39. package/core/server/live/websocket-plugin.ts +54 -1079
  40. package/core/types/types.ts +76 -1025
  41. package/core/utils/version.ts +1 -1
  42. package/create-fluxstack.ts +1 -1
  43. package/package.json +100 -95
  44. package/plugins/crypto-auth/index.ts +1 -1
  45. package/plugins/crypto-auth/server/CryptoAuthLiveProvider.ts +2 -2
  46. package/tsconfig.json +4 -1
  47. package/vite.config.ts +40 -12
  48. package/app/client/src/live/ChatDemo.tsx +0 -107
  49. package/app/client/src/live/LiveDebuggerPanel.tsx +0 -779
  50. package/app/server/live/LiveChat.ts +0 -78
  51. package/core/client/LiveComponentsProvider.tsx +0 -531
  52. package/core/client/components/Live.tsx +0 -111
  53. package/core/client/components/LiveDebugger.tsx +0 -1324
  54. package/core/client/hooks/AdaptiveChunkSizer.ts +0 -215
  55. package/core/client/hooks/state-validator.ts +0 -130
  56. package/core/client/hooks/useChunkedUpload.ts +0 -359
  57. package/core/client/hooks/useLiveChunkedUpload.ts +0 -87
  58. package/core/client/hooks/useLiveComponent.ts +0 -853
  59. package/core/client/hooks/useLiveDebugger.ts +0 -392
  60. package/core/client/hooks/useRoom.ts +0 -409
  61. package/core/client/hooks/useRoomProxy.ts +0 -382
  62. package/core/server/live/ComponentRegistry.ts +0 -1128
  63. package/core/server/live/FileUploadManager.ts +0 -446
  64. package/core/server/live/LiveComponentPerformanceMonitor.ts +0 -931
  65. package/core/server/live/LiveDebugger.ts +0 -462
  66. package/core/server/live/LiveLogger.ts +0 -144
  67. package/core/server/live/LiveRoomManager.ts +0 -278
  68. package/core/server/live/RoomEventBus.ts +0 -234
  69. package/core/server/live/RoomStateManager.ts +0 -172
  70. package/core/server/live/SingleConnectionManager.ts +0 -0
  71. package/core/server/live/StateSignature.ts +0 -705
  72. package/core/server/live/WebSocketConnectionManager.ts +0 -710
  73. package/core/server/live/auth/LiveAuthContext.ts +0 -71
  74. package/core/server/live/auth/LiveAuthManager.ts +0 -304
  75. package/core/server/live/auth/index.ts +0 -19
  76. package/core/server/live/auth/types.ts +0 -179
@@ -1,18 +1,22 @@
1
- // 🔥 RoomChatDemo - Chat multi-salas simplificado
1
+ // RoomChatDemo - Chat multi-salas with password-protected rooms
2
2
 
3
3
  import { useState, useEffect, useRef, useMemo } from 'react'
4
4
  import { Live } from '@/core/client'
5
5
  import { LiveRoomChat } from '@server/live/LiveRoomChat'
6
6
 
7
- const AVAILABLE_ROOMS = [
8
- { id: 'geral', name: '💬 Geral' },
9
- { id: 'tech', name: '💻 Tecnologia' },
10
- { id: 'random', name: '🎲 Random' },
11
- { id: 'vip', name: '⭐ VIP' }
7
+ const DEFAULT_ROOMS = [
8
+ { id: 'geral', name: 'Geral' },
9
+ { id: 'tech', name: 'Tecnologia' },
10
+ { id: 'random', name: 'Random' },
12
11
  ]
13
12
 
14
13
  export function RoomChatDemo() {
15
14
  const [text, setText] = useState('')
15
+ const [showCreateModal, setShowCreateModal] = useState(false)
16
+ const [passwordPrompt, setPasswordPrompt] = useState<{ roomId: string; roomName: string } | null>(null)
17
+ const [passwordInput, setPasswordInput] = useState('')
18
+ const [createForm, setCreateForm] = useState({ name: '', password: '' })
19
+ const [error, setError] = useState('')
16
20
  const messagesEndRef = useRef<HTMLDivElement>(null)
17
21
 
18
22
  const defaultUsername = useMemo(() => {
@@ -32,11 +36,69 @@ export function RoomChatDemo() {
32
36
  messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' })
33
37
  }, [activeMessages.length])
34
38
 
35
- const handleJoinRoom = async (roomId: string, roomName: string) => {
36
- if (chat.$rooms.includes(roomId)) {
39
+ useEffect(() => {
40
+ if (error) {
41
+ const t = setTimeout(() => setError(''), 3000)
42
+ return () => clearTimeout(t)
43
+ }
44
+ }, [error])
45
+
46
+ const joinedRoomIds = chat.$state.rooms.map(r => r.id)
47
+ const joinedRoomsMap = new Map(chat.$state.rooms.map(r => [r.id, r]))
48
+
49
+ const handleJoinRoom = async (roomId: string, roomName: string, isPrivate?: boolean) => {
50
+ if (joinedRoomIds.includes(roomId)) {
37
51
  await chat.switchRoom({ roomId })
52
+ return
53
+ }
54
+
55
+ // If the room is known to be private, prompt for password
56
+ if (isPrivate) {
57
+ setPasswordPrompt({ roomId, roomName })
58
+ setPasswordInput('')
59
+ return
60
+ }
61
+
62
+ // Try joining without password
63
+ const result = await chat.joinRoom({ roomId, roomName })
64
+ if (result && !result.success) {
65
+ // If rejected, might be password-protected — prompt
66
+ setPasswordPrompt({ roomId, roomName })
67
+ setPasswordInput('')
68
+ }
69
+ }
70
+
71
+ const handlePasswordSubmit = async () => {
72
+ if (!passwordPrompt) return
73
+ const result = await chat.joinRoom({
74
+ roomId: passwordPrompt.roomId,
75
+ roomName: passwordPrompt.roomName,
76
+ password: passwordInput
77
+ })
78
+ if (result && !result.success) {
79
+ setError(result.error || 'Senha incorreta')
80
+ } else {
81
+ setPasswordPrompt(null)
82
+ setPasswordInput('')
83
+ }
84
+ }
85
+
86
+ const handleCreateRoom = async () => {
87
+ const name = createForm.name.trim()
88
+ if (!name) return
89
+ const roomId = name.toLowerCase().replace(/\s+/g, '-').replace(/[^a-z0-9-]/g, '')
90
+ if (!roomId) return
91
+
92
+ const result = await chat.createRoom({
93
+ roomId,
94
+ roomName: name,
95
+ password: createForm.password || undefined
96
+ })
97
+ if (result && !result.success) {
98
+ setError(result.error || 'Falha ao criar sala')
38
99
  } else {
39
- await chat.joinRoom({ roomId, roomName })
100
+ setShowCreateModal(false)
101
+ setCreateForm({ name: '', password: '' })
40
102
  }
41
103
  }
42
104
 
@@ -46,6 +108,15 @@ export function RoomChatDemo() {
46
108
  setText('')
47
109
  }
48
110
 
111
+ // Combine default rooms + custom rooms from shared directory (visible to all users)
112
+ const customRooms = chat.$state.customRooms || []
113
+ const allRooms = [
114
+ ...DEFAULT_ROOMS.map(r => ({ ...r, isPrivate: joinedRoomsMap.get(r.id)?.isPrivate ?? false, createdBy: '' })),
115
+ ...customRooms
116
+ .filter(r => !DEFAULT_ROOMS.some(d => d.id === r.id))
117
+ .map(r => ({ id: r.id, name: r.name, isPrivate: r.isPrivate, createdBy: r.createdBy }))
118
+ ]
119
+
49
120
  return (
50
121
  <div className="flex flex-col md:flex-row h-[calc(100vh-200px)] md:h-[600px] w-full max-w-4xl mx-auto bg-gray-900 rounded-2xl overflow-hidden border border-white/10">
51
122
  {/* Sidebar */}
@@ -59,29 +130,39 @@ export function RoomChatDemo() {
59
130
  </div>
60
131
 
61
132
  <div className="flex-1 overflow-auto p-2">
62
- <p className="text-xs text-gray-500 px-2 py-1">SALAS</p>
63
- {AVAILABLE_ROOMS.map(room => {
64
- const isJoined = chat.$rooms.includes(room.id)
133
+ <div className="flex items-center justify-between px-2 py-1">
134
+ <p className="text-xs text-gray-500">SALAS</p>
135
+ <button
136
+ onClick={() => { setShowCreateModal(true); setCreateForm({ name: '', password: '' }) }}
137
+ className="text-xs text-purple-400 hover:text-purple-300"
138
+ >+ Criar</button>
139
+ </div>
140
+ {allRooms.map(room => {
141
+ const isJoined = joinedRoomIds.includes(room.id)
65
142
  const isActive = activeRoom === room.id
66
143
 
67
144
  return (
68
145
  <div
69
146
  key={room.id}
70
- onClick={() => handleJoinRoom(room.id, room.name)}
147
+ onClick={() => handleJoinRoom(room.id, room.name, room.isPrivate && !isJoined ? true : undefined)}
71
148
  className={`
72
149
  flex items-center justify-between px-3 py-2 rounded-lg cursor-pointer mb-1 transition-all group
73
150
  ${isActive ? 'bg-purple-500/20 text-purple-300' : isJoined ? 'bg-white/5 text-gray-300 hover:bg-white/10' : 'text-gray-500 hover:bg-white/5'}
74
151
  `}
75
152
  >
76
- <span className="flex items-center gap-2">
77
- {room.name}
78
- {isJoined && <span className="w-1.5 h-1.5 rounded-full bg-emerald-400" />}
153
+ <span className="flex items-center gap-2 min-w-0">
154
+ {room.isPrivate && <span className="text-xs shrink-0">&#128274;</span>}
155
+ <span className="truncate">
156
+ {room.name}
157
+ {room.createdBy && <span className="text-xs text-gray-600 ml-1">by {room.createdBy}</span>}
158
+ </span>
159
+ {isJoined && <span className="w-1.5 h-1.5 rounded-full bg-emerald-400 shrink-0" />}
79
160
  </span>
80
161
  {isJoined && !isActive && (
81
162
  <button
82
163
  onClick={(e) => { e.stopPropagation(); chat.leaveRoom({ roomId: room.id }) }}
83
164
  className="opacity-0 group-hover:opacity-100 text-red-400 hover:text-red-300 text-xs"
84
- >✕</button>
165
+ >&#10005;</button>
85
166
  )}
86
167
  </div>
87
168
  )
@@ -89,7 +170,7 @@ export function RoomChatDemo() {
89
170
  </div>
90
171
 
91
172
  <div className="p-3 border-t border-white/10">
92
- <p className="text-xs text-gray-500">Em {chat.$rooms.length} sala(s)</p>
173
+ <p className="text-xs text-gray-500">Em {joinedRoomIds.length} sala(s)</p>
93
174
  </div>
94
175
  </div>
95
176
 
@@ -103,11 +184,12 @@ export function RoomChatDemo() {
103
184
  onClick={() => chat.switchRoom({ roomId: '' })}
104
185
  className="md:hidden px-2 py-1 text-sm text-gray-400 hover:text-white"
105
186
  >
106
-
187
+ &#8592;
107
188
  </button>
108
189
  <div>
109
- <h3 className="text-white font-semibold">
110
- {chat.$state.rooms.find(r => r.id === activeRoom)?.name || activeRoom}
190
+ <h3 className="text-white font-semibold flex items-center gap-2">
191
+ {joinedRoomsMap.get(activeRoom)?.isPrivate && <span className="text-xs">&#128274;</span>}
192
+ {joinedRoomsMap.get(activeRoom)?.name || activeRoom}
111
193
  </h3>
112
194
  <p className="text-xs text-gray-500">{activeMessages.length} mensagens</p>
113
195
  </div>
@@ -158,12 +240,95 @@ export function RoomChatDemo() {
158
240
  ) : (
159
241
  <div className="flex-1 flex items-center justify-center text-gray-500">
160
242
  <div className="text-center">
161
- <p className="text-4xl mb-4">←</p>
243
+ <p className="text-4xl mb-4">&#8592;</p>
162
244
  <p>Selecione uma sala para começar</p>
163
245
  </div>
164
246
  </div>
165
247
  )}
166
248
  </div>
249
+
250
+ {/* Error toast */}
251
+ {error && (
252
+ <div className="absolute top-4 left-1/2 -translate-x-1/2 px-4 py-2 bg-red-500/90 text-white text-sm rounded-lg shadow-lg z-50">
253
+ {error}
254
+ </div>
255
+ )}
256
+
257
+ {/* Create Room Modal */}
258
+ {showCreateModal && (
259
+ <div className="absolute inset-0 bg-black/60 flex items-center justify-center z-40" onClick={() => setShowCreateModal(false)}>
260
+ <div className="bg-gray-800 rounded-2xl p-6 w-80 border border-white/10" onClick={e => e.stopPropagation()}>
261
+ <h3 className="text-white font-bold text-lg mb-4">Criar Sala</h3>
262
+ <div className="space-y-3">
263
+ <div>
264
+ <label className="text-xs text-gray-400 block mb-1">Nome da sala</label>
265
+ <input
266
+ value={createForm.name}
267
+ onChange={e => setCreateForm(f => ({ ...f, name: e.target.value }))}
268
+ placeholder="Minha Sala"
269
+ className="w-full px-3 py-2 rounded-lg bg-white/10 border border-white/20 text-white placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-purple-500/50 text-sm"
270
+ autoFocus
271
+ onKeyDown={e => { if (e.key === 'Enter') handleCreateRoom() }}
272
+ />
273
+ </div>
274
+ <div>
275
+ <label className="text-xs text-gray-400 block mb-1">Senha (opcional)</label>
276
+ <input
277
+ type="password"
278
+ value={createForm.password}
279
+ onChange={e => setCreateForm(f => ({ ...f, password: e.target.value }))}
280
+ placeholder="Deixe vazio para sala publica"
281
+ className="w-full px-3 py-2 rounded-lg bg-white/10 border border-white/20 text-white placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-purple-500/50 text-sm"
282
+ onKeyDown={e => { if (e.key === 'Enter') handleCreateRoom() }}
283
+ />
284
+ </div>
285
+ <div className="flex gap-2 pt-2">
286
+ <button
287
+ onClick={() => setShowCreateModal(false)}
288
+ className="flex-1 px-4 py-2 rounded-lg bg-white/10 text-gray-300 hover:bg-white/20 text-sm"
289
+ >Cancelar</button>
290
+ <button
291
+ onClick={handleCreateRoom}
292
+ disabled={!createForm.name.trim()}
293
+ className="flex-1 px-4 py-2 rounded-lg bg-purple-500/30 text-purple-200 hover:bg-purple-500/40 disabled:opacity-50 text-sm"
294
+ >Criar</button>
295
+ </div>
296
+ </div>
297
+ </div>
298
+ </div>
299
+ )}
300
+
301
+ {/* Password Prompt Modal */}
302
+ {passwordPrompt && (
303
+ <div className="absolute inset-0 bg-black/60 flex items-center justify-center z-40" onClick={() => setPasswordPrompt(null)}>
304
+ <div className="bg-gray-800 rounded-2xl p-6 w-80 border border-white/10" onClick={e => e.stopPropagation()}>
305
+ <h3 className="text-white font-bold text-lg mb-1">Sala Protegida</h3>
306
+ <p className="text-sm text-gray-400 mb-4">
307
+ A sala "{passwordPrompt.roomName}" requer senha.
308
+ </p>
309
+ <input
310
+ type="password"
311
+ value={passwordInput}
312
+ onChange={e => setPasswordInput(e.target.value)}
313
+ placeholder="Digite a senha..."
314
+ className="w-full px-3 py-2 rounded-lg bg-white/10 border border-white/20 text-white placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-purple-500/50 text-sm mb-3"
315
+ autoFocus
316
+ onKeyDown={e => { if (e.key === 'Enter') handlePasswordSubmit() }}
317
+ />
318
+ <div className="flex gap-2">
319
+ <button
320
+ onClick={() => setPasswordPrompt(null)}
321
+ className="flex-1 px-4 py-2 rounded-lg bg-white/10 text-gray-300 hover:bg-white/20 text-sm"
322
+ >Cancelar</button>
323
+ <button
324
+ onClick={handlePasswordSubmit}
325
+ disabled={!passwordInput}
326
+ className="flex-1 px-4 py-2 rounded-lg bg-purple-500/30 text-purple-200 hover:bg-purple-500/40 disabled:opacity-50 text-sm"
327
+ >Entrar</button>
328
+ </div>
329
+ </div>
330
+ </div>
331
+ )}
167
332
  </div>
168
333
  )
169
334
  }
@@ -0,0 +1,142 @@
1
+ // SharedCounterDemo - Contador compartilhado entre todas as abas
2
+ //
3
+ // Abra em varias abas - todos veem o mesmo valor!
4
+ // Usa o sistema de LiveRoom tipado com CounterRoom.
5
+ // Demo de client-side room events via $room().on()
6
+
7
+ import { useMemo, useState, useEffect, useRef } from 'react'
8
+ import { Live } from '@/core/client'
9
+ import { LiveSharedCounter } from '@server/live/LiveSharedCounter'
10
+ import type { CounterRoom } from '@server/live/rooms/CounterRoom'
11
+
12
+ interface FloatingEvent {
13
+ id: number
14
+ text: string
15
+ color: string
16
+ }
17
+
18
+ export function SharedCounterDemo() {
19
+ const username = useMemo(() => {
20
+ const adj = ['Happy', 'Cool', 'Fast', 'Smart', 'Brave'][Math.floor(Math.random() * 5)]
21
+ const noun = ['Panda', 'Tiger', 'Eagle', 'Wolf', 'Bear'][Math.floor(Math.random() * 5)]
22
+ return `${adj}${noun}${Math.floor(Math.random() * 100)}`
23
+ }, [])
24
+
25
+ const counter = Live.use(LiveSharedCounter, {
26
+ initialState: { ...LiveSharedCounter.defaultState, username }
27
+ })
28
+
29
+ // Client-side room events — floating animation
30
+ const [floats, setFloats] = useState<FloatingEvent[]>([])
31
+ const floatIdRef = useRef(0)
32
+
33
+ useEffect(() => {
34
+ const unsub = counter.$room<CounterRoom>('counter:global').on('counter:updated', (data) => {
35
+ const id = ++floatIdRef.current
36
+ const isReset = data.count === 0
37
+ const text = isReset ? '0' : data.count > 0 ? `+${data.count}` : `${data.count}`
38
+ const color = isReset ? 'text-yellow-400' : data.count > 0 ? 'text-emerald-400' : 'text-red-400'
39
+
40
+ setFloats(prev => [...prev, { id, text: `${text} (${data.updatedBy})`, color }])
41
+ setTimeout(() => setFloats(prev => prev.filter(f => f.id !== id)), 2000)
42
+ })
43
+ return unsub
44
+ }, [])
45
+
46
+ const count = counter.$state.count
47
+ const onlineCount = counter.$state.onlineCount
48
+ const lastUpdatedBy = counter.$state.lastUpdatedBy
49
+
50
+ return (
51
+ <div className="flex flex-col items-center gap-8 w-full max-w-md mx-auto">
52
+ {/* Header */}
53
+ <div className="text-center">
54
+ <h2 className="text-2xl font-bold text-white mb-2">Contador Compartilhado</h2>
55
+ <p className="text-sm text-gray-400">Abra em varias abas - todos veem o mesmo valor!</p>
56
+ </div>
57
+
58
+ {/* Connection + Online */}
59
+ <div className="flex items-center gap-4">
60
+ <div className="flex items-center gap-2">
61
+ <div className={`w-2 h-2 rounded-full ${counter.$connected ? 'bg-emerald-400' : 'bg-red-400'}`} />
62
+ <span className="text-sm text-gray-400">{counter.$connected ? 'Conectado' : 'Desconectado'}</span>
63
+ </div>
64
+ <div className="flex items-center gap-2 px-3 py-1 rounded-full bg-white/5 border border-white/10">
65
+ <span className="text-sm text-gray-400">{onlineCount} online</span>
66
+ </div>
67
+ <div className="px-3 py-1 rounded-full bg-purple-500/10 border border-purple-500/20">
68
+ <span className="text-xs text-purple-300">{username}</span>
69
+ </div>
70
+ </div>
71
+
72
+ {/* Counter Display */}
73
+ <div className="relative">
74
+ <div className="absolute inset-0 bg-purple-500/20 rounded-full blur-3xl" />
75
+ <div className="relative bg-gray-800/50 border border-white/10 rounded-3xl px-16 py-10 flex flex-col items-center">
76
+ <span className={`text-7xl font-black tabular-nums transition-colors ${
77
+ count > 0 ? 'text-emerald-400' : count < 0 ? 'text-red-400' : 'text-white'
78
+ }`}>
79
+ {count}
80
+ </span>
81
+ {lastUpdatedBy && (
82
+ <span className="text-xs text-gray-500 mt-3">
83
+ Ultimo: {lastUpdatedBy}
84
+ </span>
85
+ )}
86
+ </div>
87
+
88
+ {/* Floating room events */}
89
+ {floats.map(f => (
90
+ <span
91
+ key={f.id}
92
+ className={`absolute left-1/2 -translate-x-1/2 top-0 ${f.color} text-sm font-bold pointer-events-none animate-float-up`}
93
+ >
94
+ {f.text}
95
+ </span>
96
+ ))}
97
+ </div>
98
+
99
+ {/* Buttons */}
100
+ <div className="flex items-center gap-3">
101
+ <button
102
+ onClick={() => counter.decrement()}
103
+ disabled={counter.$loading}
104
+ className="w-14 h-14 rounded-2xl bg-red-500/20 border border-red-500/30 text-red-300 text-2xl font-bold hover:bg-red-500/30 active:scale-95 disabled:opacity-50 transition-all"
105
+ >
106
+ -
107
+ </button>
108
+ <button
109
+ onClick={() => counter.reset()}
110
+ disabled={counter.$loading}
111
+ className="px-6 h-14 rounded-2xl bg-white/10 border border-white/20 text-gray-300 text-sm font-medium hover:bg-white/20 active:scale-95 disabled:opacity-50 transition-all"
112
+ >
113
+ Reset
114
+ </button>
115
+ <button
116
+ onClick={() => counter.increment()}
117
+ disabled={counter.$loading}
118
+ className="w-14 h-14 rounded-2xl bg-emerald-500/20 border border-emerald-500/30 text-emerald-300 text-2xl font-bold hover:bg-emerald-500/30 active:scale-95 disabled:opacity-50 transition-all"
119
+ >
120
+ +
121
+ </button>
122
+ </div>
123
+
124
+ {/* Info */}
125
+ <div className="text-center text-xs text-gray-600 space-y-1">
126
+ <p>Powered by <code className="text-purple-400">LiveRoom</code> + <code className="text-purple-400">CounterRoom</code></p>
127
+ <p>Estado via component state + eventos via <code className="text-cyan-400">$room().on()</code></p>
128
+ </div>
129
+
130
+ {/* CSS animation */}
131
+ <style>{`
132
+ @keyframes float-up {
133
+ 0% { opacity: 1; transform: translateX(-50%) translateY(0); }
134
+ 100% { opacity: 0; transform: translateX(-50%) translateY(-60px); }
135
+ }
136
+ .animate-float-up {
137
+ animation: float-up 2s ease-out forwards;
138
+ }
139
+ `}</style>
140
+ </div>
141
+ )
142
+ }
@@ -12,8 +12,8 @@ import type {
12
12
  LiveAuthProvider,
13
13
  LiveAuthCredentials,
14
14
  LiveAuthContext,
15
- } from '@core/server/live/auth/types'
16
- import { AuthenticatedContext } from '@core/server/live/auth/LiveAuthContext'
15
+ } from '@fluxstack/live'
16
+ import { AuthenticatedContext } from '@fluxstack/live'
17
17
 
18
18
  interface DevUser {
19
19
  id: string
@@ -13,8 +13,8 @@ import type {
13
13
  LiveAuthProvider,
14
14
  LiveAuthCredentials,
15
15
  LiveAuthContext,
16
- } from '@core/server/live/auth/types'
17
- import { AuthenticatedContext } from '@core/server/live/auth/LiveAuthContext'
16
+ } from '@fluxstack/live'
17
+ import { AuthenticatedContext } from '@fluxstack/live'
18
18
 
19
19
  /**
20
20
  * Exemplo de provider JWT para Live Components.
@@ -13,12 +13,12 @@
13
13
  import { FluxStackFramework } from "@core/server"
14
14
  import { vitePlugin } from "@core/plugins/built-in/vite"
15
15
  import { swaggerPlugin } from "@core/plugins/built-in/swagger"
16
- import { liveComponentsPlugin } from "@core/server/live/websocket-plugin"
16
+ import { liveComponentsPlugin } from "@core/server/live"
17
17
  import { appInstance } from "@server/app"
18
18
  import { appConfig } from "@config"
19
19
 
20
20
  // 🔒 Auth provider para Live Components
21
- import { liveAuthManager } from "@core/server/live/auth"
21
+ import { liveAuthManager } from "@core/server/live"
22
22
  import { DevAuthProvider } from "./auth/DevAuthProvider"
23
23
 
24
24
  // 🔐 Auth system (Guard + Provider, Laravel-inspired)
@@ -11,7 +11,7 @@
11
11
  // Client link: import type { AdminPanelDemo as _Client } from '@client/src/live/AdminPanelDemo'
12
12
 
13
13
  import { LiveComponent } from '@core/types/types'
14
- import type { LiveComponentAuth, LiveActionAuthMap } from '@core/server/live/auth/types'
14
+ import type { LiveComponentAuth, LiveActionAuthMap } from '@core/types/types'
15
15
 
16
16
  // ===== State =====
17
17
 
@@ -0,0 +1,61 @@
1
+ // LivePingPong - Demo de Binary Codec (msgpack)
2
+ //
3
+ // Demonstra o wire format binario do sistema de rooms.
4
+ // Client envia ping, server responde pong via room event (msgpack).
5
+ // Round-trip time calculado no client.
6
+
7
+ import { LiveComponent, type FluxStackWebSocket } from '@core/types/types'
8
+ import { PingRoom } from './rooms/PingRoom'
9
+
10
+ // Componente Cliente (Ctrl+Click para navegar)
11
+ import type { PingPongDemo as _Client } from '@client/src/live/PingPongDemo'
12
+
13
+ export class LivePingPong extends LiveComponent<typeof LivePingPong.defaultState> {
14
+ static componentName = 'LivePingPong'
15
+ static publicActions = ['ping'] as const
16
+ static defaultState = {
17
+ username: '',
18
+ onlineCount: 0,
19
+ totalPings: 0,
20
+ lastPingBy: null as string | null,
21
+ }
22
+
23
+ private pongUnsub: (() => void) | null = null
24
+
25
+ constructor(
26
+ initialState: Partial<typeof LivePingPong.defaultState> = {},
27
+ ws: FluxStackWebSocket,
28
+ options?: { room?: string; userId?: string }
29
+ ) {
30
+ super(initialState, ws, options)
31
+
32
+ const room = this.$room(PingRoom, 'global')
33
+ room.join()
34
+
35
+ // Sync room state on join
36
+ this.setState({
37
+ onlineCount: room.state.onlineCount,
38
+ totalPings: room.state.totalPings,
39
+ lastPingBy: room.state.lastPingBy,
40
+ })
41
+
42
+ // Listen for pong events (binary msgpack)
43
+ this.pongUnsub = room.on('pong', (data) => {
44
+ this.setState({
45
+ totalPings: this.state.totalPings + 1,
46
+ lastPingBy: data.from,
47
+ })
48
+ })
49
+ }
50
+
51
+ async ping(payload: { seq: number }) {
52
+ const room = this.$room(PingRoom, 'global')
53
+ const total = room.ping(this.state.username || 'Anonymous', payload.seq)
54
+ return { success: true, total }
55
+ }
56
+
57
+ destroy() {
58
+ this.pongUnsub?.()
59
+ super.destroy()
60
+ }
61
+ }
@@ -9,7 +9,7 @@
9
9
  // import type { LiveProtectedChat as _Client } from '@client/src/live/ProtectedChat'
10
10
 
11
11
  import { LiveComponent } from '@core/types/types'
12
- import type { LiveComponentAuth, LiveActionAuthMap } from '@core/server/live/auth/types'
12
+ import type { LiveComponentAuth, LiveActionAuthMap } from '@core/types/types'
13
13
 
14
14
  interface ChatMessage {
15
15
  id: number