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.
- package/LLMD/INDEX.md +4 -3
- package/LLMD/resources/live-binary-delta.md +507 -0
- package/LLMD/resources/live-components.md +208 -12
- package/LLMD/resources/live-rooms.md +731 -333
- package/app/client/.live-stubs/LiveAdminPanel.js +5 -0
- package/app/client/.live-stubs/LiveCounter.js +9 -0
- package/app/client/.live-stubs/LiveForm.js +11 -0
- package/app/client/.live-stubs/LiveLocalCounter.js +8 -0
- package/app/client/.live-stubs/LivePingPong.js +10 -0
- package/app/client/.live-stubs/LiveRoomChat.js +11 -0
- package/app/client/.live-stubs/LiveSharedCounter.js +10 -0
- package/app/client/.live-stubs/LiveUpload.js +15 -0
- package/app/client/src/App.tsx +19 -7
- package/app/client/src/components/AppLayout.tsx +18 -10
- package/app/client/src/live/PingPongDemo.tsx +199 -0
- package/app/client/src/live/RoomChatDemo.tsx +187 -22
- package/app/client/src/live/SharedCounterDemo.tsx +142 -0
- package/app/server/auth/DevAuthProvider.ts +2 -2
- package/app/server/auth/JWTAuthProvider.example.ts +2 -2
- package/app/server/index.ts +2 -2
- package/app/server/live/LiveAdminPanel.ts +1 -1
- package/app/server/live/LivePingPong.ts +61 -0
- package/app/server/live/LiveProtectedChat.ts +1 -1
- package/app/server/live/LiveRoomChat.ts +106 -38
- package/app/server/live/LiveSharedCounter.ts +73 -0
- package/app/server/live/rooms/ChatRoom.ts +68 -0
- package/app/server/live/rooms/CounterRoom.ts +51 -0
- package/app/server/live/rooms/DirectoryRoom.ts +42 -0
- package/app/server/live/rooms/PingRoom.ts +40 -0
- package/app/server/routes/room.routes.ts +1 -2
- package/core/build/live-components-generator.ts +11 -2
- package/core/build/vite-plugins.ts +28 -0
- package/core/client/hooks/useLiveUpload.ts +3 -4
- package/core/client/index.ts +25 -35
- package/core/framework/server.ts +1 -1
- package/core/server/index.ts +1 -2
- package/core/server/live/auto-generated-components.ts +5 -8
- package/core/server/live/index.ts +90 -21
- package/core/server/live/websocket-plugin.ts +54 -1079
- package/core/types/types.ts +76 -1025
- package/core/utils/version.ts +1 -1
- package/create-fluxstack.ts +1 -1
- package/package.json +100 -95
- package/plugins/crypto-auth/index.ts +1 -1
- package/plugins/crypto-auth/server/CryptoAuthLiveProvider.ts +2 -2
- package/tsconfig.json +4 -1
- package/vite.config.ts +40 -12
- package/app/client/src/live/ChatDemo.tsx +0 -107
- package/app/client/src/live/LiveDebuggerPanel.tsx +0 -779
- package/app/server/live/LiveChat.ts +0 -78
- package/core/client/LiveComponentsProvider.tsx +0 -531
- package/core/client/components/Live.tsx +0 -111
- package/core/client/components/LiveDebugger.tsx +0 -1324
- package/core/client/hooks/AdaptiveChunkSizer.ts +0 -215
- package/core/client/hooks/state-validator.ts +0 -130
- package/core/client/hooks/useChunkedUpload.ts +0 -359
- package/core/client/hooks/useLiveChunkedUpload.ts +0 -87
- package/core/client/hooks/useLiveComponent.ts +0 -853
- package/core/client/hooks/useLiveDebugger.ts +0 -392
- package/core/client/hooks/useRoom.ts +0 -409
- package/core/client/hooks/useRoomProxy.ts +0 -382
- package/core/server/live/ComponentRegistry.ts +0 -1128
- package/core/server/live/FileUploadManager.ts +0 -446
- package/core/server/live/LiveComponentPerformanceMonitor.ts +0 -931
- package/core/server/live/LiveDebugger.ts +0 -462
- package/core/server/live/LiveLogger.ts +0 -144
- package/core/server/live/LiveRoomManager.ts +0 -278
- package/core/server/live/RoomEventBus.ts +0 -234
- package/core/server/live/RoomStateManager.ts +0 -172
- package/core/server/live/SingleConnectionManager.ts +0 -0
- package/core/server/live/StateSignature.ts +0 -705
- package/core/server/live/WebSocketConnectionManager.ts +0 -710
- package/core/server/live/auth/LiveAuthContext.ts +0 -71
- package/core/server/live/auth/LiveAuthManager.ts +0 -304
- package/core/server/live/auth/index.ts +0 -19
- package/core/server/live/auth/types.ts +0 -179
|
@@ -1,18 +1,22 @@
|
|
|
1
|
-
//
|
|
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
|
|
8
|
-
{ id: 'geral', name: '
|
|
9
|
-
{ id: 'tech', name: '
|
|
10
|
-
{ id: 'random', name: '
|
|
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
|
-
|
|
36
|
-
if (
|
|
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
|
-
|
|
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
|
-
<
|
|
63
|
-
|
|
64
|
-
|
|
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.
|
|
78
|
-
|
|
153
|
+
<span className="flex items-center gap-2 min-w-0">
|
|
154
|
+
{room.isPrivate && <span className="text-xs shrink-0">🔒</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
|
-
|
|
165
|
+
>✕</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 {
|
|
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
|
+
←
|
|
107
188
|
</button>
|
|
108
189
|
<div>
|
|
109
|
-
<h3 className="text-white font-semibold">
|
|
110
|
-
{
|
|
190
|
+
<h3 className="text-white font-semibold flex items-center gap-2">
|
|
191
|
+
{joinedRoomsMap.get(activeRoom)?.isPrivate && <span className="text-xs">🔒</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"
|
|
243
|
+
<p className="text-4xl mb-4">←</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 '@
|
|
16
|
-
import { AuthenticatedContext } from '@
|
|
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 '@
|
|
17
|
-
import { AuthenticatedContext } from '@
|
|
16
|
+
} from '@fluxstack/live'
|
|
17
|
+
import { AuthenticatedContext } from '@fluxstack/live'
|
|
18
18
|
|
|
19
19
|
/**
|
|
20
20
|
* Exemplo de provider JWT para Live Components.
|
package/app/server/index.ts
CHANGED
|
@@ -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
|
|
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
|
|
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/
|
|
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/
|
|
12
|
+
import type { LiveComponentAuth, LiveActionAuthMap } from '@core/types/types'
|
|
13
13
|
|
|
14
14
|
interface ChatMessage {
|
|
15
15
|
id: number
|