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.
- package/LLMD/resources/live-components.md +207 -12
- package/app/client/.live-stubs/LiveAdminPanel.js +5 -0
- package/app/client/.live-stubs/LiveChat.js +7 -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/LiveRoomChat.js +10 -0
- package/app/client/.live-stubs/LiveTodoList.js +9 -0
- package/app/client/.live-stubs/LiveUpload.js +15 -0
- package/app/client/src/App.tsx +11 -0
- package/app/client/src/components/AppLayout.tsx +16 -8
- package/app/client/src/live/LiveDebuggerPanel.tsx +1 -1
- package/app/client/src/live/TodoListDemo.tsx +158 -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/LiveProtectedChat.ts +1 -1
- package/app/server/live/LiveTodoList.ts +110 -0
- package/app/server/routes/room.routes.ts +1 -2
- package/core/build/live-components-generator.ts +1 -1
- package/core/build/vite-plugins.ts +28 -0
- package/core/client/components/LiveDebugger.tsx +1 -1
- package/core/client/hooks/useLiveUpload.ts +3 -4
- package/core/client/index.ts +37 -31
- package/core/framework/server.ts +1 -1
- package/core/server/index.ts +1 -2
- package/core/server/live/auto-generated-components.ts +6 -3
- package/core/server/live/index.ts +95 -21
- package/core/server/live/websocket-plugin.ts +27 -1087
- package/core/types/types.ts +76 -1025
- package/core/utils/version.ts +1 -1
- package/create-fluxstack.ts +1 -1
- package/package.json +5 -1
- package/plugins/crypto-auth/index.ts +1 -1
- package/plugins/crypto-auth/server/CryptoAuthLiveProvider.ts +2 -2
- package/vite.config.ts +40 -12
- package/core/client/LiveComponentsProvider.tsx +0 -531
- package/core/client/components/Live.tsx +0 -111
- 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,12 +1,16 @@
|
|
|
1
1
|
# Live Components
|
|
2
2
|
|
|
3
|
-
**Version:** 1.
|
|
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
|
|
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
|
-
|
|
292
|
+
visitors: 0,
|
|
293
|
+
alerts: [] as string[],
|
|
294
|
+
lastRefresh: ''
|
|
121
295
|
}
|
|
122
296
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
297
|
+
protected async onMount() {
|
|
298
|
+
this.state.visitors++
|
|
299
|
+
this.state.lastRefresh = new Date().toISOString()
|
|
300
|
+
}
|
|
127
301
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
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
|
-
-
|
|
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,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,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
|
+
}
|
package/app/client/src/App.tsx
CHANGED
|
@@ -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
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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 '
|
|
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 '@
|
|
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
|
|
|
@@ -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
|