create-fluxstack 1.15.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 +1 -0
- package/LLMD/resources/live-rooms.md +731 -333
- package/app/client/.live-stubs/LivePingPong.js +10 -0
- package/app/client/.live-stubs/LiveRoomChat.js +3 -2
- package/app/client/.live-stubs/LiveSharedCounter.js +10 -0
- package/app/client/src/App.tsx +15 -14
- package/app/client/src/components/AppLayout.tsx +4 -4
- 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/live/LivePingPong.ts +61 -0
- 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/core/build/live-components-generator.ts +10 -1
- package/core/client/index.ts +0 -16
- package/core/server/live/auto-generated-components.ts +3 -9
- package/core/server/live/index.ts +0 -5
- package/core/server/live/websocket-plugin.ts +37 -2
- package/core/utils/version.ts +1 -1
- package/package.json +100 -99
- package/tsconfig.json +4 -1
- package/app/client/.live-stubs/LiveChat.js +0 -7
- package/app/client/.live-stubs/LiveTodoList.js +0 -9
- package/app/client/src/live/ChatDemo.tsx +0 -107
- package/app/client/src/live/LiveDebuggerPanel.tsx +0 -779
- package/app/client/src/live/TodoListDemo.tsx +0 -158
- package/app/server/live/LiveChat.ts +0 -78
- package/app/server/live/LiveTodoList.ts +0 -110
- package/core/client/components/LiveDebugger.tsx +0 -1324
|
@@ -1,158 +0,0 @@
|
|
|
1
|
-
// TodoListDemo - Lista de tarefas colaborativa em tempo real
|
|
2
|
-
|
|
3
|
-
import { useState } from 'react'
|
|
4
|
-
import { Live } from '@/core/client'
|
|
5
|
-
import { LiveTodoList } from '@server/live/LiveTodoList'
|
|
6
|
-
|
|
7
|
-
export function TodoListDemo() {
|
|
8
|
-
const [text, setText] = useState('')
|
|
9
|
-
|
|
10
|
-
const todoList = Live.use(LiveTodoList, {
|
|
11
|
-
room: 'global-todos'
|
|
12
|
-
})
|
|
13
|
-
|
|
14
|
-
const handleAdd = async () => {
|
|
15
|
-
if (!text.trim()) return
|
|
16
|
-
await todoList.addTodo({ text })
|
|
17
|
-
setText('')
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
const todos = todoList.$state.todos ?? []
|
|
21
|
-
const doneCount = todos.filter((t: any) => t.done).length
|
|
22
|
-
const pendingCount = todos.length - doneCount
|
|
23
|
-
|
|
24
|
-
return (
|
|
25
|
-
<div className="bg-white/5 backdrop-blur-sm border border-white/10 rounded-2xl p-5 sm:p-8 max-w-lg w-full mx-auto">
|
|
26
|
-
<h2 className="text-xl sm:text-2xl font-bold text-white mb-2 text-center">
|
|
27
|
-
Todo List Colaborativo
|
|
28
|
-
</h2>
|
|
29
|
-
|
|
30
|
-
<p className="text-gray-400 text-xs sm:text-sm text-center mb-4">
|
|
31
|
-
Abra em várias abas - todos compartilham a mesma lista!
|
|
32
|
-
</p>
|
|
33
|
-
|
|
34
|
-
{/* Status bar */}
|
|
35
|
-
<div className="flex flex-wrap justify-center gap-2 mb-6">
|
|
36
|
-
<div className={`flex items-center gap-2 px-3 py-1 rounded-full text-xs ${
|
|
37
|
-
todoList.$connected
|
|
38
|
-
? 'bg-emerald-500/20 text-emerald-300'
|
|
39
|
-
: 'bg-red-500/20 text-red-300'
|
|
40
|
-
}`}>
|
|
41
|
-
<div className={`w-2 h-2 rounded-full ${
|
|
42
|
-
todoList.$connected ? 'bg-emerald-400' : 'bg-red-400'
|
|
43
|
-
}`} />
|
|
44
|
-
{todoList.$connected ? 'Conectado' : 'Desconectado'}
|
|
45
|
-
</div>
|
|
46
|
-
|
|
47
|
-
<div className="flex items-center gap-2 px-3 py-1 rounded-full text-xs bg-blue-500/20 text-blue-300">
|
|
48
|
-
{todoList.$state.connectedUsers} online
|
|
49
|
-
</div>
|
|
50
|
-
|
|
51
|
-
<div className="flex items-center gap-2 px-3 py-1 rounded-full text-xs bg-purple-500/20 text-purple-300">
|
|
52
|
-
{todoList.$state.totalCreated} criadas
|
|
53
|
-
</div>
|
|
54
|
-
</div>
|
|
55
|
-
|
|
56
|
-
{/* Input */}
|
|
57
|
-
<div className="flex gap-2 mb-6">
|
|
58
|
-
<input
|
|
59
|
-
type="text"
|
|
60
|
-
value={text}
|
|
61
|
-
onChange={(e) => setText(e.target.value)}
|
|
62
|
-
onKeyDown={(e) => e.key === 'Enter' && handleAdd()}
|
|
63
|
-
placeholder="Nova tarefa..."
|
|
64
|
-
className="flex-1 bg-white/5 border border-white/10 rounded-xl px-4 py-3 text-white placeholder-gray-500 focus:outline-none focus:border-purple-500/50 transition-colors"
|
|
65
|
-
/>
|
|
66
|
-
<button
|
|
67
|
-
onClick={handleAdd}
|
|
68
|
-
disabled={!todoList.$connected || !text.trim()}
|
|
69
|
-
className="px-5 py-3 bg-purple-500/20 hover:bg-purple-500/30 border border-purple-500/30 text-purple-300 rounded-xl transition-all disabled:opacity-50 font-medium"
|
|
70
|
-
>
|
|
71
|
-
+
|
|
72
|
-
</button>
|
|
73
|
-
</div>
|
|
74
|
-
|
|
75
|
-
{/* Stats */}
|
|
76
|
-
{todos.length > 0 && (
|
|
77
|
-
<div className="flex justify-between items-center mb-4 text-xs text-gray-500">
|
|
78
|
-
<span>{pendingCount} pendente(s) / {doneCount} feita(s)</span>
|
|
79
|
-
{doneCount > 0 && (
|
|
80
|
-
<button
|
|
81
|
-
onClick={() => todoList.clearCompleted()}
|
|
82
|
-
className="text-red-400 hover:text-red-300 transition-colors"
|
|
83
|
-
>
|
|
84
|
-
Limpar feitas
|
|
85
|
-
</button>
|
|
86
|
-
)}
|
|
87
|
-
</div>
|
|
88
|
-
)}
|
|
89
|
-
|
|
90
|
-
{/* Todo list */}
|
|
91
|
-
<div className="space-y-2 max-h-80 overflow-y-auto">
|
|
92
|
-
{todos.length === 0 ? (
|
|
93
|
-
<p className="text-gray-500 text-sm text-center py-8">
|
|
94
|
-
Nenhuma tarefa ainda. Adicione uma acima!
|
|
95
|
-
</p>
|
|
96
|
-
) : (
|
|
97
|
-
todos.map((todo: any) => (
|
|
98
|
-
<div
|
|
99
|
-
key={todo.id}
|
|
100
|
-
className={`flex items-center gap-3 p-3 rounded-xl border transition-all ${
|
|
101
|
-
todo.done
|
|
102
|
-
? 'bg-emerald-500/5 border-emerald-500/20'
|
|
103
|
-
: 'bg-white/5 border-white/10'
|
|
104
|
-
}`}
|
|
105
|
-
>
|
|
106
|
-
<button
|
|
107
|
-
onClick={() => todoList.toggleTodo({ id: todo.id })}
|
|
108
|
-
className={`w-5 h-5 rounded-md border-2 flex items-center justify-center transition-all flex-shrink-0 ${
|
|
109
|
-
todo.done
|
|
110
|
-
? 'bg-emerald-500 border-emerald-500 text-white'
|
|
111
|
-
: 'border-gray-500 hover:border-purple-400'
|
|
112
|
-
}`}
|
|
113
|
-
>
|
|
114
|
-
{todo.done && (
|
|
115
|
-
<svg className="w-3 h-3" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={3}>
|
|
116
|
-
<path strokeLinecap="round" strokeLinejoin="round" d="M5 13l4 4L19 7" />
|
|
117
|
-
</svg>
|
|
118
|
-
)}
|
|
119
|
-
</button>
|
|
120
|
-
|
|
121
|
-
<span className={`flex-1 text-sm ${
|
|
122
|
-
todo.done ? 'text-gray-500 line-through' : 'text-white'
|
|
123
|
-
}`}>
|
|
124
|
-
{todo.text}
|
|
125
|
-
</span>
|
|
126
|
-
|
|
127
|
-
<span className="text-[10px] text-gray-600 flex-shrink-0">
|
|
128
|
-
{todo.createdBy}
|
|
129
|
-
</span>
|
|
130
|
-
|
|
131
|
-
<button
|
|
132
|
-
onClick={() => todoList.removeTodo({ id: todo.id })}
|
|
133
|
-
className="text-gray-600 hover:text-red-400 transition-colors flex-shrink-0"
|
|
134
|
-
>
|
|
135
|
-
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
|
136
|
-
<path strokeLinecap="round" strokeLinejoin="round" d="M6 18L18 6M6 6l12 12" />
|
|
137
|
-
</svg>
|
|
138
|
-
</button>
|
|
139
|
-
</div>
|
|
140
|
-
))
|
|
141
|
-
)}
|
|
142
|
-
</div>
|
|
143
|
-
|
|
144
|
-
{/* Loading indicator */}
|
|
145
|
-
{todoList.$loading && (
|
|
146
|
-
<div className="flex justify-center mt-4">
|
|
147
|
-
<div className="w-5 h-5 border-2 border-purple-500 border-t-transparent rounded-full animate-spin" />
|
|
148
|
-
</div>
|
|
149
|
-
)}
|
|
150
|
-
|
|
151
|
-
<div className="mt-6 pt-4 border-t border-white/10">
|
|
152
|
-
<p className="text-gray-500 text-xs text-center">
|
|
153
|
-
Usando <code className="text-purple-400">Live.use()</code> + <code className="text-purple-400">Room Events</code>
|
|
154
|
-
</p>
|
|
155
|
-
</div>
|
|
156
|
-
</div>
|
|
157
|
-
)
|
|
158
|
-
}
|
|
@@ -1,78 +0,0 @@
|
|
|
1
|
-
// LiveChat - Chat compartilhado por sala
|
|
2
|
-
|
|
3
|
-
import { LiveComponent, type FluxStackWebSocket } from '@core/types/types'
|
|
4
|
-
|
|
5
|
-
// Componente Cliente (Ctrl+Click para navegar)
|
|
6
|
-
import type { ChatDemo as _Client } from '@client/src/live/ChatDemo'
|
|
7
|
-
|
|
8
|
-
export type ChatMessage = {
|
|
9
|
-
id: string
|
|
10
|
-
user: string
|
|
11
|
-
text: string
|
|
12
|
-
timestamp: number
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
export class LiveChat extends LiveComponent<typeof LiveChat.defaultState> {
|
|
16
|
-
static componentName = 'LiveChat'
|
|
17
|
-
static publicActions = ['sendMessage'] as const
|
|
18
|
-
static defaultState = {
|
|
19
|
-
messages: [] as ChatMessage[]
|
|
20
|
-
}
|
|
21
|
-
protected roomType = 'chat'
|
|
22
|
-
private maxMessages = 50
|
|
23
|
-
private static roomHistory = new Map<string, ChatMessage[]>()
|
|
24
|
-
|
|
25
|
-
constructor(initialState: Partial<typeof LiveChat.defaultState> = {}, ws: FluxStackWebSocket, options?: { room?: string; userId?: string }) {
|
|
26
|
-
super(initialState, ws, options)
|
|
27
|
-
|
|
28
|
-
this.onRoomEvent<ChatMessage>('NEW_MESSAGE', (message) => {
|
|
29
|
-
this.addMessage(message)
|
|
30
|
-
})
|
|
31
|
-
|
|
32
|
-
if (this.room) {
|
|
33
|
-
const history = LiveChat.roomHistory.get(this.room) || []
|
|
34
|
-
if (history.length > 0) {
|
|
35
|
-
this.setState({ messages: history })
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
private addMessage(message: ChatMessage) {
|
|
41
|
-
const next = [...this.state.messages, message].slice(-this.maxMessages)
|
|
42
|
-
if (this.room) {
|
|
43
|
-
LiveChat.roomHistory.set(this.room, next)
|
|
44
|
-
}
|
|
45
|
-
this.setState({ messages: next })
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
async sendMessage(payload: { user: string; text: string }) {
|
|
49
|
-
const text = payload.text?.trim()
|
|
50
|
-
const user = payload.user?.trim() || 'anonymous'
|
|
51
|
-
|
|
52
|
-
if (!text) {
|
|
53
|
-
throw new Error('Message cannot be empty')
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
if (text.length > 500) {
|
|
57
|
-
throw new Error('Message too long')
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
const message: ChatMessage = {
|
|
61
|
-
id: `msg-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
|
|
62
|
-
user,
|
|
63
|
-
text,
|
|
64
|
-
timestamp: Date.now()
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
const next = [...this.state.messages, message].slice(-this.maxMessages)
|
|
68
|
-
if (this.room) {
|
|
69
|
-
LiveChat.roomHistory.set(this.room, next)
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
this.emitRoomEventWithState('NEW_MESSAGE', message, {
|
|
73
|
-
messages: next
|
|
74
|
-
})
|
|
75
|
-
|
|
76
|
-
return { success: true }
|
|
77
|
-
}
|
|
78
|
-
}
|
|
@@ -1,110 +0,0 @@
|
|
|
1
|
-
// LiveTodoList - Lista de tarefas colaborativa em tempo real
|
|
2
|
-
// Testa: state mutations, room events, multiple actions, arrays no state
|
|
3
|
-
|
|
4
|
-
import { LiveComponent, type FluxStackWebSocket } from '@core/types/types'
|
|
5
|
-
|
|
6
|
-
// Componente Cliente (Ctrl+Click para navegar)
|
|
7
|
-
import type { TodoListDemo as _Client } from '@client/src/live/TodoListDemo'
|
|
8
|
-
|
|
9
|
-
interface TodoItem {
|
|
10
|
-
id: string
|
|
11
|
-
text: string
|
|
12
|
-
done: boolean
|
|
13
|
-
createdBy: string
|
|
14
|
-
createdAt: number
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
export class LiveTodoList extends LiveComponent<typeof LiveTodoList.defaultState> {
|
|
18
|
-
static componentName = 'LiveTodoList'
|
|
19
|
-
static publicActions = ['addTodo', 'toggleTodo', 'removeTodo', 'clearCompleted'] as const
|
|
20
|
-
static defaultState = {
|
|
21
|
-
todos: [] as TodoItem[],
|
|
22
|
-
totalCreated: 0,
|
|
23
|
-
connectedUsers: 0
|
|
24
|
-
}
|
|
25
|
-
protected roomType = 'todo'
|
|
26
|
-
|
|
27
|
-
constructor(initialState: Partial<typeof LiveTodoList.defaultState> = {}, ws: FluxStackWebSocket, options?: { room?: string; userId?: string }) {
|
|
28
|
-
super(initialState, ws, options)
|
|
29
|
-
|
|
30
|
-
this.onRoomEvent<{ todos: TodoItem[]; totalCreated: number }>('TODOS_CHANGED', (data) => {
|
|
31
|
-
this.setState({ todos: data.todos, totalCreated: data.totalCreated })
|
|
32
|
-
})
|
|
33
|
-
|
|
34
|
-
this.onRoomEvent<{ connectedUsers: number }>('USER_COUNT_CHANGED', (data) => {
|
|
35
|
-
this.setState({ connectedUsers: data.connectedUsers })
|
|
36
|
-
})
|
|
37
|
-
|
|
38
|
-
const newCount = this.state.connectedUsers + 1
|
|
39
|
-
this.emitRoomEventWithState('USER_COUNT_CHANGED', { connectedUsers: newCount }, { connectedUsers: newCount })
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
async addTodo(payload: { text: string }) {
|
|
43
|
-
if (!payload.text?.trim()) {
|
|
44
|
-
return { success: false, error: 'Text is required' }
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
const todo: TodoItem = {
|
|
48
|
-
id: `todo-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
|
|
49
|
-
text: payload.text.trim(),
|
|
50
|
-
done: false,
|
|
51
|
-
createdBy: this.userId || 'anonymous',
|
|
52
|
-
createdAt: Date.now()
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
const nextTodos = [...this.state.todos, todo]
|
|
56
|
-
const nextTotal = this.state.totalCreated + 1
|
|
57
|
-
|
|
58
|
-
this.emitRoomEventWithState(
|
|
59
|
-
'TODOS_CHANGED',
|
|
60
|
-
{ todos: nextTodos, totalCreated: nextTotal },
|
|
61
|
-
{ todos: nextTodos, totalCreated: nextTotal }
|
|
62
|
-
)
|
|
63
|
-
|
|
64
|
-
return { success: true, todo }
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
async toggleTodo(payload: { id: string }) {
|
|
68
|
-
const nextTodos = this.state.todos.map(t =>
|
|
69
|
-
t.id === payload.id ? { ...t, done: !t.done } : t
|
|
70
|
-
)
|
|
71
|
-
|
|
72
|
-
this.emitRoomEventWithState(
|
|
73
|
-
'TODOS_CHANGED',
|
|
74
|
-
{ todos: nextTodos, totalCreated: this.state.totalCreated },
|
|
75
|
-
{ todos: nextTodos }
|
|
76
|
-
)
|
|
77
|
-
|
|
78
|
-
return { success: true }
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
async removeTodo(payload: { id: string }) {
|
|
82
|
-
const nextTodos = this.state.todos.filter(t => t.id !== payload.id)
|
|
83
|
-
|
|
84
|
-
this.emitRoomEventWithState(
|
|
85
|
-
'TODOS_CHANGED',
|
|
86
|
-
{ todos: nextTodos, totalCreated: this.state.totalCreated },
|
|
87
|
-
{ todos: nextTodos }
|
|
88
|
-
)
|
|
89
|
-
|
|
90
|
-
return { success: true }
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
async clearCompleted() {
|
|
94
|
-
const nextTodos = this.state.todos.filter(t => !t.done)
|
|
95
|
-
|
|
96
|
-
this.emitRoomEventWithState(
|
|
97
|
-
'TODOS_CHANGED',
|
|
98
|
-
{ todos: nextTodos, totalCreated: this.state.totalCreated },
|
|
99
|
-
{ todos: nextTodos }
|
|
100
|
-
)
|
|
101
|
-
|
|
102
|
-
return { success: true, removed: this.state.todos.length - nextTodos.length }
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
destroy() {
|
|
106
|
-
const newCount = Math.max(0, this.state.connectedUsers - 1)
|
|
107
|
-
this.emitRoomEvent('USER_COUNT_CHANGED', { connectedUsers: newCount })
|
|
108
|
-
super.destroy()
|
|
109
|
-
}
|
|
110
|
-
}
|