create-fluxstack 1.14.0 → 1.15.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 (62) hide show
  1. package/LLMD/resources/live-components.md +207 -12
  2. package/app/client/.live-stubs/LiveAdminPanel.js +5 -0
  3. package/app/client/.live-stubs/LiveChat.js +7 -0
  4. package/app/client/.live-stubs/LiveCounter.js +9 -0
  5. package/app/client/.live-stubs/LiveForm.js +11 -0
  6. package/app/client/.live-stubs/LiveLocalCounter.js +8 -0
  7. package/app/client/.live-stubs/LiveRoomChat.js +10 -0
  8. package/app/client/.live-stubs/LiveTodoList.js +9 -0
  9. package/app/client/.live-stubs/LiveUpload.js +15 -0
  10. package/app/client/src/App.tsx +11 -0
  11. package/app/client/src/components/AppLayout.tsx +16 -8
  12. package/app/client/src/live/LiveDebuggerPanel.tsx +1 -1
  13. package/app/client/src/live/TodoListDemo.tsx +158 -0
  14. package/app/server/auth/DevAuthProvider.ts +2 -2
  15. package/app/server/auth/JWTAuthProvider.example.ts +2 -2
  16. package/app/server/index.ts +2 -2
  17. package/app/server/live/LiveAdminPanel.ts +1 -1
  18. package/app/server/live/LiveProtectedChat.ts +1 -1
  19. package/app/server/live/LiveTodoList.ts +110 -0
  20. package/app/server/routes/room.routes.ts +1 -2
  21. package/core/build/live-components-generator.ts +1 -1
  22. package/core/build/vite-plugins.ts +28 -0
  23. package/core/client/components/LiveDebugger.tsx +1 -1
  24. package/core/client/hooks/useLiveUpload.ts +3 -4
  25. package/core/client/index.ts +37 -31
  26. package/core/framework/server.ts +1 -1
  27. package/core/server/index.ts +1 -2
  28. package/core/server/live/auto-generated-components.ts +6 -3
  29. package/core/server/live/index.ts +95 -21
  30. package/core/server/live/websocket-plugin.ts +27 -1087
  31. package/core/types/types.ts +76 -1025
  32. package/core/utils/version.ts +1 -1
  33. package/create-fluxstack.ts +1 -1
  34. package/package.json +5 -1
  35. package/plugins/crypto-auth/index.ts +1 -1
  36. package/plugins/crypto-auth/server/CryptoAuthLiveProvider.ts +2 -2
  37. package/vite.config.ts +40 -12
  38. package/core/client/LiveComponentsProvider.tsx +0 -531
  39. package/core/client/components/Live.tsx +0 -111
  40. package/core/client/hooks/AdaptiveChunkSizer.ts +0 -215
  41. package/core/client/hooks/state-validator.ts +0 -130
  42. package/core/client/hooks/useChunkedUpload.ts +0 -359
  43. package/core/client/hooks/useLiveChunkedUpload.ts +0 -87
  44. package/core/client/hooks/useLiveComponent.ts +0 -853
  45. package/core/client/hooks/useLiveDebugger.ts +0 -392
  46. package/core/client/hooks/useRoom.ts +0 -409
  47. package/core/client/hooks/useRoomProxy.ts +0 -382
  48. package/core/server/live/ComponentRegistry.ts +0 -1128
  49. package/core/server/live/FileUploadManager.ts +0 -446
  50. package/core/server/live/LiveComponentPerformanceMonitor.ts +0 -931
  51. package/core/server/live/LiveDebugger.ts +0 -462
  52. package/core/server/live/LiveLogger.ts +0 -144
  53. package/core/server/live/LiveRoomManager.ts +0 -278
  54. package/core/server/live/RoomEventBus.ts +0 -234
  55. package/core/server/live/RoomStateManager.ts +0 -172
  56. package/core/server/live/SingleConnectionManager.ts +0 -0
  57. package/core/server/live/StateSignature.ts +0 -705
  58. package/core/server/live/WebSocketConnectionManager.ts +0 -710
  59. package/core/server/live/auth/LiveAuthContext.ts +0 -71
  60. package/core/server/live/auth/LiveAuthManager.ts +0 -304
  61. package/core/server/live/auth/index.ts +0 -19
  62. package/core/server/live/auth/types.ts +0 -179
@@ -1,12 +1,16 @@
1
1
  # Live Components
2
2
 
3
- **Version:** 1.13.0 | **Updated:** 2025-02-09
3
+ **Version:** 1.14.0 | **Updated:** 2025-02-27
4
4
 
5
5
  ## Quick Facts
6
6
 
7
7
  - Server-side state management with WebSocket sync
8
8
  - **Direct state access** - `this.count++` auto-syncs (v1.13.0)
9
+ - **Lifecycle hooks** - `onMount()` / `onDestroy()` for proper initialization and cleanup (v1.14.0)
10
+ - **HMR persistence** - `static persistent` + `this.$persistent` survives hot reloads (v1.14.0)
11
+ - **Singleton components** - `static singleton = true` for shared server-side instances (v1.14.0)
9
12
  - **Mandatory `publicActions`** - Only whitelisted methods are callable from client (secure by default)
13
+ - **Helpful error messages** - Forgotten `publicActions` entries show exactly what to fix (v1.14.0)
10
14
  - Automatic state persistence and re-hydration (with anti-replay nonces)
11
15
  - Room-based event system for multi-user sync
12
16
  - Type-safe client-server communication (FluxStackWebSocket)
@@ -53,6 +57,13 @@ export class LiveCounter extends LiveComponent<typeof LiveCounter.defaultState>
53
57
  }
54
58
  ```
55
59
 
60
+ ### Key Changes in v1.14.0
61
+
62
+ 1. **Lifecycle hooks** - `onMount()` (async) and `onDestroy()` (sync) replace constructor/destroy workarounds
63
+ 2. **HMR persistence** - `static persistent` + `this.$persistent` for data that survives hot module reloads
64
+ 3. **Singleton components** - `static singleton = true` for shared state across all connected clients
65
+ 4. **Better publicActions errors** - Clear message when a method exists but is missing from `publicActions`
66
+
56
67
  ### Key Changes in v1.13.0
57
68
 
58
69
  1. **Direct state access** - `this.count++` instead of `this.state.count++`
@@ -111,27 +122,207 @@ export class LiveCounter extends LiveComponent<typeof LiveCounter.defaultState>
111
122
  }
112
123
  ```
113
124
 
114
- ## Lifecycle Methods
125
+ ## Lifecycle Hooks (v1.14.0)
126
+
127
+ Full lifecycle hook system — no more constructor workarounds:
115
128
 
116
129
  ```typescript
117
130
  export class MyComponent extends LiveComponent<typeof MyComponent.defaultState> {
118
131
  static componentName = 'MyComponent'
132
+ static publicActions = ['doWork'] as const
133
+ static defaultState = { users: [] as string[], ready: false, currentRoom: '' }
134
+
135
+ private _pollTimer?: NodeJS.Timeout
136
+
137
+ // 1️⃣ Called when WebSocket connection is established (before onMount)
138
+ protected onConnect() {
139
+ console.log('WebSocket connected for this component')
140
+ }
141
+
142
+ // 2️⃣ Called AFTER component is fully mounted (rooms, auth, injections ready)
143
+ // Can be async!
144
+ protected async onMount() {
145
+ this.$room.join()
146
+ this.$room.on('user:joined', (user) => {
147
+ this.state.users = [...this.state.users, user]
148
+ })
149
+ const data = await fetchInitialData(this.$auth.user?.id)
150
+ this.state.ready = true
151
+ this._pollTimer = setInterval(() => this.poll(), 5000)
152
+ }
153
+
154
+ // Called after state is restored from localStorage (rehydration)
155
+ protected onRehydrate(previousState: typeof MyComponent.defaultState) {
156
+ if (!previousState.ready) {
157
+ this.state.ready = false // Re-validate stale state
158
+ }
159
+ }
160
+
161
+ // Called after any state mutation (proxy or setState)
162
+ protected onStateChange(changes: Partial<typeof MyComponent.defaultState>) {
163
+ if ('users' in changes) {
164
+ console.log(`User count: ${this.state.users.length}`)
165
+ }
166
+ }
167
+
168
+ // Called when joining a room
169
+ protected onRoomJoin(roomId: string) {
170
+ this.state.currentRoom = roomId
171
+ }
172
+
173
+ // Called when leaving a room
174
+ protected onRoomLeave(roomId: string) {
175
+ if (this.state.currentRoom === roomId) this.state.currentRoom = ''
176
+ }
177
+
178
+ // Called before each action — return false to cancel
179
+ protected onAction(action: string, payload: any) {
180
+ console.log(`[${this.id}] ${action}`, payload)
181
+ // return false // ← would cancel the action
182
+ }
183
+
184
+ // Called when WebSocket drops (NOT on intentional unmount)
185
+ protected onDisconnect() {
186
+ console.log('Connection lost — saving recovery data')
187
+ }
188
+
189
+ // Called BEFORE internal cleanup (sync only)
190
+ protected onDestroy() {
191
+ clearInterval(this._pollTimer)
192
+ }
193
+
194
+ async doWork() { /* ... */ }
195
+ private poll() { /* ... */ }
196
+ }
197
+ ```
198
+
199
+ ### Lifecycle Order
200
+
201
+ ```
202
+ WebSocket connects
203
+ └→ onConnect()
204
+ └→ onMount() ← async, rooms/auth ready
205
+ └→ [component active]
206
+ ├→ onAction(action, payload) ← before each action (return false to cancel)
207
+ ├→ onStateChange(changes) ← after each state mutation
208
+ ├→ onRoomJoin(roomId) ← when joining a room
209
+ └→ onRoomLeave(roomId) ← when leaving a room
210
+
211
+ Connection drops:
212
+ └→ onDisconnect() ← only on unexpected disconnect
213
+ └→ onDestroy() ← sync, before internal cleanup
214
+
215
+ Rehydration (reconnect with saved state):
216
+ └→ onConnect()
217
+ └→ onRehydrate(previousState)
218
+ └→ onMount()
219
+ ```
220
+
221
+ ### Rules
222
+
223
+ | Hook | Async? | When |
224
+ |------|--------|------|
225
+ | `onConnect()` | No | WebSocket established, before mount |
226
+ | `onMount()` | **Yes** | After all setup (rooms, auth, DI) |
227
+ | `onRehydrate(prevState)` | No | After state restored from localStorage |
228
+ | `onStateChange(changes)` | No | After every state mutation |
229
+ | `onRoomJoin(roomId)` | No | After `$room.join()` |
230
+ | `onRoomLeave(roomId)` | No | After `$room.leave()` |
231
+ | `onAction(action, payload)` | **Yes** | Before action execution (return `false` to cancel) |
232
+ | `onDisconnect()` | No | Connection lost (NOT intentional unmount) |
233
+ | `onDestroy()` | No | Before internal cleanup |
234
+
235
+ - All hooks are optional — override only what you need
236
+ - All hook errors are caught and logged — they never break the system
237
+ - Constructor is still needed ONLY for `this.onRoomEvent()` subscriptions
238
+ - All hooks are in BLOCKED_ACTIONS — clients cannot call them remotely
239
+
240
+ ## HMR Persistence (v1.14.0)
241
+
242
+ Data in `static persistent` survives Hot Module Replacement reloads via `globalThis`:
243
+
244
+ ```typescript
245
+ export class LiveMigration extends LiveComponent<typeof LiveMigration.defaultState> {
246
+ static componentName = 'LiveMigration'
247
+ static publicActions = ['runMigration'] as const
248
+ static defaultState = { status: 'idle', lastResult: '' }
249
+
250
+ // Define shape and defaults for persistent data
251
+ static persistent = {
252
+ cache: {} as Record<string, any>,
253
+ runCount: 0
254
+ }
255
+
256
+ protected onMount() {
257
+ this.$persistent.runCount++
258
+ console.log(`Mount #${this.$persistent.runCount}`) // Survives HMR!
259
+ }
260
+
261
+ async runMigration(payload: { key: string }) {
262
+ // Check HMR-safe cache
263
+ if (this.$persistent.cache[payload.key]) {
264
+ return { cached: true, result: this.$persistent.cache[payload.key] }
265
+ }
266
+
267
+ const result = await expensiveComputation(payload.key)
268
+ this.$persistent.cache[payload.key] = result
269
+ this.state.lastResult = result
270
+ return { cached: false, result }
271
+ }
272
+ }
273
+ ```
274
+
275
+ **Key facts:**
276
+ - `this.$persistent` reads from `globalThis.__fluxstack_persistent_{ComponentName}`
277
+ - Each component class has its own namespace
278
+ - Defaults come from `static persistent` — initialized once, then persisted
279
+ - Not sent to client — server-only
280
+ - `$persistent` is in BLOCKED_ACTIONS (can't be called from client)
281
+
282
+ ## Singleton Components (v1.14.0)
283
+
284
+ When `static singleton = true`, only ONE server-side instance exists. All clients share the same state:
285
+
286
+ ```typescript
287
+ export class LiveDashboard extends LiveComponent<typeof LiveDashboard.defaultState> {
288
+ static componentName = 'LiveDashboard'
289
+ static singleton = true // All clients share this instance
290
+ static publicActions = ['refresh', 'addAlert'] as const
119
291
  static defaultState = {
120
- // Define state here
292
+ visitors: 0,
293
+ alerts: [] as string[],
294
+ lastRefresh: ''
121
295
  }
122
296
 
123
- // Constructor ONLY needed if:
124
- // - Subscribing to room events
125
- // - Custom initialization logic
126
- // Otherwise, omit it entirely!
297
+ protected async onMount() {
298
+ this.state.visitors++
299
+ this.state.lastRefresh = new Date().toISOString()
300
+ }
127
301
 
128
- destroy() {
129
- // Cleanup subscriptions, timers, etc.
130
- super.destroy()
302
+ async refresh() {
303
+ const data = await fetchDashboardData()
304
+ this.setState(data) // Broadcasts to ALL connected clients
305
+ return { success: true }
306
+ }
307
+
308
+ async addAlert(payload: { message: string }) {
309
+ this.state.alerts = [...this.state.alerts, payload.message]
310
+ // All clients see the new alert instantly
311
+ return { success: true }
131
312
  }
132
313
  }
133
314
  ```
134
315
 
316
+ **How it works:**
317
+ - First client to mount creates the singleton instance
318
+ - Subsequent clients join the existing instance and receive current state
319
+ - `emit` / `setState` / `this.state.x = y` broadcast to ALL connected WebSockets
320
+ - When a client disconnects, it's removed from the singleton's connections
321
+ - When the LAST client disconnects, the singleton is destroyed
322
+ - Stats visible at `/api/live/stats` (shows singleton connection counts)
323
+
324
+ **Use cases:** Shared dashboards, global migration state, admin panels, live counters
325
+
135
326
  ## State Management
136
327
 
137
328
  ### Reactive State Proxy (How It Works)
@@ -665,19 +856,23 @@ export class MyComponent extends LiveComponent<State> {
665
856
  - Define `static defaultState` inside the class
666
857
  - Use `typeof ClassName.defaultState` for type parameter
667
858
  - Use `declare` for each state property (TypeScript type hint)
668
- - Call `super.destroy()` in destroy method if overriding
859
+ - Use `onMount()` for async initialization (rooms, auth, data fetching)
860
+ - Use `onDestroy()` for cleanup (timers, connections) — sync only
669
861
  - Use `emitRoomEventWithState` for state changes in rooms
670
862
  - Handle errors in actions (throw Error)
671
863
  - Add client link: `import type { Demo as _Client } from '@client/...'`
864
+ - Use `$persistent` for data that should survive HMR reloads
865
+ - Use `static singleton = true` for shared cross-client state
672
866
 
673
867
  **NEVER:**
674
868
  - Omit `static publicActions` (component will deny ALL remote actions)
675
869
  - Export separate `defaultState` constant (use static)
676
870
  - Create constructor just to call super() (not needed)
677
871
  - Forget `static componentName` (breaks minification)
872
+ - Override `destroy()` directly — use `onDestroy()` instead (v1.14.0)
678
873
  - Emit room events without subscribing first
679
874
  - Store non-serializable data in state
680
- - Use reserved names for state properties (id, state, ws, room, userId, $room, $rooms, $private, broadcastToRoom, roomType)
875
+ - Use reserved names for state properties (id, state, ws, room, userId, $room, $rooms, $private, $persistent, broadcastToRoom, roomType)
681
876
  - Include `setValue` in `publicActions` unless you trust clients to modify any state key
682
877
  - Store sensitive data (tokens, API keys, secrets) in `state` — use `$private` instead
683
878
 
@@ -0,0 +1,5 @@
1
+ export class LiveAdminPanel {
2
+ static componentName = 'LiveAdminPanel'
3
+ static defaultState = {}
4
+ static publicActions = ['getAuthInfo', 'init', 'listUsers', 'addUser', 'deleteUser', 'clearAudit']
5
+ }
@@ -0,0 +1,7 @@
1
+ export class LiveChat {
2
+ static componentName = 'LiveChat'
3
+ static defaultState = {
4
+ messages: []
5
+ }
6
+ static publicActions = ['sendMessage']
7
+ }
@@ -0,0 +1,9 @@
1
+ export class LiveCounter {
2
+ static componentName = 'LiveCounter'
3
+ static defaultState = {
4
+ count: 0,
5
+ lastUpdatedBy: null,
6
+ connectedUsers: 0
7
+ }
8
+ static publicActions = ['increment', 'decrement', 'reset']
9
+ }
@@ -0,0 +1,11 @@
1
+ export class LiveForm {
2
+ static componentName = 'LiveForm'
3
+ static defaultState = {
4
+ name: '',
5
+ email: '',
6
+ message: '',
7
+ submitted: false,
8
+ submittedAt: null
9
+ }
10
+ static publicActions = ['submit', 'reset', 'validate', 'setValue']
11
+ }
@@ -0,0 +1,8 @@
1
+ export class LiveLocalCounter {
2
+ static componentName = 'LiveLocalCounter'
3
+ static defaultState = {
4
+ count: 0,
5
+ clicks: 0
6
+ }
7
+ static publicActions = ['increment', 'decrement', 'reset']
8
+ }
@@ -0,0 +1,10 @@
1
+ export class LiveRoomChat {
2
+ static componentName = 'LiveRoomChat'
3
+ static defaultState = {
4
+ username: '',
5
+ activeRoom: null,
6
+ rooms: [],
7
+ messages: {}
8
+ }
9
+ static publicActions = ['joinRoom', 'leaveRoom', 'switchRoom', 'sendMessage', 'setUsername']
10
+ }
@@ -0,0 +1,9 @@
1
+ export class LiveTodoList {
2
+ static componentName = 'LiveTodoList'
3
+ static defaultState = {
4
+ todos: [],
5
+ totalCreated: 0,
6
+ connectedUsers: 0
7
+ }
8
+ static publicActions = ['addTodo', 'toggleTodo', 'removeTodo', 'clearCompleted']
9
+ }
@@ -0,0 +1,15 @@
1
+ export class LiveUpload {
2
+ static componentName = 'LiveUpload'
3
+ static defaultState = {
4
+ status: 'idle',
5
+ progress: 0,
6
+ fileName: '',
7
+ fileSize: 0,
8
+ fileType: '',
9
+ fileUrl: '',
10
+ bytesUploaded: 0,
11
+ totalBytes: 0,
12
+ error: null
13
+ }
14
+ static publicActions = ['startUpload', 'updateProgress', 'completeUpload', 'failUpload', 'reset']
15
+ }
@@ -8,6 +8,7 @@ import { UploadDemo } from './live/UploadDemo'
8
8
  import { ChatDemo } from './live/ChatDemo'
9
9
  import { RoomChatDemo } from './live/RoomChatDemo'
10
10
  import { AuthDemo } from './live/AuthDemo'
11
+ import { TodoListDemo } from './live/TodoListDemo'
11
12
  import { AppLayout } from './components/AppLayout'
12
13
  import { DemoPage } from './components/DemoPage'
13
14
  import { HomePage } from './pages/HomePage'
@@ -127,6 +128,16 @@ function AppContent() {
127
128
  </DemoPage>
128
129
  }
129
130
  />
131
+ <Route
132
+ path="/todo"
133
+ element={
134
+ <DemoPage
135
+ note={<>Lista de tarefas colaborativa usando <code className="text-purple-400">Live.use()</code> + <code className="text-purple-400">Room Events</code>!</>}
136
+ >
137
+ <TodoListDemo />
138
+ </DemoPage>
139
+ }
140
+ />
130
141
  <Route
131
142
  path="/auth"
132
143
  element={
@@ -12,6 +12,7 @@ const navItems = [
12
12
  { to: '/chat', label: 'Chat' },
13
13
  { to: '/room-chat', label: 'Room Chat' },
14
14
  { to: '/auth', label: 'Auth' },
15
+ { to: '/todo', label: 'Todo List' },
15
16
  { to: '/api-test', label: 'API Test' }
16
17
  ]
17
18
 
@@ -23,9 +24,13 @@ const routeFlameHue: Record<string, string> = {
23
24
  '/chat': '120deg', // verde
24
25
  '/room-chat': '240deg', // azul
25
26
  '/auth': '330deg', // vermelho
27
+ '/todo': '45deg', // laranja
26
28
  '/api-test': '90deg', // lima
27
29
  }
28
30
 
31
+ // Cache favicon blob URLs by hue to avoid recreating blobs on every navigation
32
+ const faviconUrlCache = new Map<string, string>()
33
+
29
34
  export function AppLayout() {
30
35
  const location = useLocation()
31
36
  const [menuOpen, setMenuOpen] = useState(false)
@@ -34,14 +39,18 @@ export function AppLayout() {
34
39
  const current = navItems.find(item => item.to === location.pathname)
35
40
  document.title = current ? `${current.label} - FluxStack` : 'FluxStack'
36
41
 
37
- // Dynamic favicon with hue-rotate
42
+ // Dynamic favicon with hue-rotate (cached per hue value)
38
43
  const hue = routeFlameHue[location.pathname] || '0deg'
39
- const colored = faviconSvg.replace(
40
- '<svg ',
41
- `<svg style="filter: hue-rotate(${hue})" `
42
- )
43
- const blob = new Blob([colored], { type: 'image/svg+xml' })
44
- const url = URL.createObjectURL(blob)
44
+ let url = faviconUrlCache.get(hue)
45
+ if (!url) {
46
+ const colored = faviconSvg.replace(
47
+ '<svg ',
48
+ `<svg style="filter: hue-rotate(${hue})" `
49
+ )
50
+ const blob = new Blob([colored], { type: 'image/svg+xml' })
51
+ url = URL.createObjectURL(blob)
52
+ faviconUrlCache.set(hue, url)
53
+ }
45
54
  let link = document.querySelector<HTMLLinkElement>('link[rel="icon"]')
46
55
  if (!link) {
47
56
  link = document.createElement('link')
@@ -50,7 +59,6 @@ export function AppLayout() {
50
59
  }
51
60
  link.type = 'image/svg+xml'
52
61
  link.href = url
53
- return () => URL.revokeObjectURL(url)
54
62
  }, [location.pathname])
55
63
 
56
64
  return (
@@ -7,7 +7,7 @@
7
7
  // - Filtering by component, event type, and search
8
8
 
9
9
  import { useState, useRef, useEffect, useCallback } from 'react'
10
- import { useLiveDebugger, type DebugEvent, type DebugEventType, type ComponentSnapshot, type DebugFilter } from '@/core/client/hooks/useLiveDebugger'
10
+ import { useLiveDebugger, type DebugEvent, type DebugEventType, type ComponentSnapshot, type DebugFilter } from '@fluxstack/live-react'
11
11
 
12
12
  // ===== Debugger Settings (shared with floating widget) =====
13
13
 
@@ -0,0 +1,158 @@
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
+ }
@@ -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
 
@@ -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