create-fluxstack 1.13.0 → 1.14.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/patterns/anti-patterns.md +100 -0
- package/LLMD/reference/routing.md +39 -39
- package/LLMD/resources/live-auth.md +20 -2
- package/LLMD/resources/live-components.md +94 -10
- package/LLMD/resources/live-logging.md +95 -33
- package/LLMD/resources/live-upload.md +59 -8
- package/app/client/index.html +2 -2
- package/app/client/public/favicon.svg +46 -0
- package/app/client/src/App.tsx +2 -1
- package/app/client/src/assets/fluxstack-static.svg +46 -0
- package/app/client/src/assets/fluxstack.svg +183 -0
- package/app/client/src/components/AppLayout.tsx +138 -9
- package/app/client/src/components/BackButton.tsx +13 -13
- package/app/client/src/components/DemoPage.tsx +4 -4
- package/app/client/src/live/AuthDemo.tsx +23 -21
- package/app/client/src/live/ChatDemo.tsx +2 -2
- package/app/client/src/live/CounterDemo.tsx +12 -12
- package/app/client/src/live/FormDemo.tsx +2 -2
- package/app/client/src/live/LiveDebuggerPanel.tsx +779 -0
- package/app/client/src/live/RoomChatDemo.tsx +24 -16
- package/app/client/src/main.tsx +13 -13
- package/app/client/src/pages/ApiTestPage.tsx +6 -6
- package/app/client/src/pages/HomePage.tsx +80 -52
- package/app/server/live/LiveAdminPanel.ts +1 -0
- package/app/server/live/LiveChat.ts +78 -77
- package/app/server/live/LiveCounter.ts +1 -1
- package/app/server/live/LiveForm.ts +1 -0
- package/app/server/live/LiveLocalCounter.ts +38 -37
- package/app/server/live/LiveProtectedChat.ts +1 -0
- package/app/server/live/LiveRoomChat.ts +1 -0
- package/app/server/live/LiveUpload.ts +1 -0
- package/app/server/live/register-components.ts +19 -19
- package/config/system/runtime.config.ts +4 -0
- package/core/build/optimizer.ts +235 -235
- package/core/client/components/Live.tsx +17 -11
- package/core/client/components/LiveDebugger.tsx +1324 -0
- package/core/client/hooks/AdaptiveChunkSizer.ts +215 -215
- package/core/client/hooks/useLiveComponent.ts +11 -1
- package/core/client/hooks/useLiveDebugger.ts +392 -0
- package/core/client/index.ts +14 -0
- package/core/plugins/built-in/index.ts +134 -134
- package/core/plugins/built-in/live-components/commands/create-live-component.ts +4 -0
- package/core/plugins/built-in/vite/index.ts +75 -21
- package/core/server/index.ts +15 -15
- package/core/server/live/ComponentRegistry.ts +55 -26
- package/core/server/live/FileUploadManager.ts +188 -24
- package/core/server/live/LiveDebugger.ts +462 -0
- package/core/server/live/LiveLogger.ts +38 -5
- package/core/server/live/LiveRoomManager.ts +17 -1
- package/core/server/live/StateSignature.ts +87 -27
- package/core/server/live/WebSocketConnectionManager.ts +11 -10
- package/core/server/live/auto-generated-components.ts +1 -1
- package/core/server/live/websocket-plugin.ts +233 -8
- package/core/server/plugins/static-files-plugin.ts +179 -69
- package/core/types/build.ts +219 -219
- package/core/types/plugin.ts +107 -107
- package/core/types/types.ts +145 -9
- package/core/utils/logger/startup-banner.ts +82 -82
- package/core/utils/version.ts +6 -6
- package/package.json +1 -1
- package/app/client/src/assets/react.svg +0 -1
|
@@ -0,0 +1,779 @@
|
|
|
1
|
+
// 🔍 FluxStack Live Component Debugger Panel
|
|
2
|
+
//
|
|
3
|
+
// Visual debugger for Live Components. Shows:
|
|
4
|
+
// - Active components with current state
|
|
5
|
+
// - Real-time event timeline (state changes, actions, rooms, errors)
|
|
6
|
+
// - Component detail view with state inspector
|
|
7
|
+
// - Filtering by component, event type, and search
|
|
8
|
+
|
|
9
|
+
import { useState, useRef, useEffect, useCallback } from 'react'
|
|
10
|
+
import { useLiveDebugger, type DebugEvent, type DebugEventType, type ComponentSnapshot, type DebugFilter } from '@/core/client/hooks/useLiveDebugger'
|
|
11
|
+
|
|
12
|
+
// ===== Debugger Settings (shared with floating widget) =====
|
|
13
|
+
|
|
14
|
+
interface DebuggerSettings {
|
|
15
|
+
fontSize: 'xs' | 'sm' | 'md' | 'lg'
|
|
16
|
+
showTimestamps: boolean
|
|
17
|
+
compactMode: boolean
|
|
18
|
+
wordWrap: boolean
|
|
19
|
+
maxEvents: number
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const FONT_SIZES: Record<DebuggerSettings['fontSize'], number> = { xs: 9, sm: 10, md: 11, lg: 13 }
|
|
23
|
+
|
|
24
|
+
const DEFAULT_SETTINGS: DebuggerSettings = {
|
|
25
|
+
fontSize: 'sm', showTimestamps: true, compactMode: false,
|
|
26
|
+
wordWrap: false, maxEvents: 300,
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const SETTINGS_KEY = 'fluxstack-debugger-settings'
|
|
30
|
+
|
|
31
|
+
function loadSettings(): DebuggerSettings {
|
|
32
|
+
try {
|
|
33
|
+
const stored = localStorage.getItem(SETTINGS_KEY)
|
|
34
|
+
if (stored) return { ...DEFAULT_SETTINGS, ...JSON.parse(stored) }
|
|
35
|
+
} catch { /* ignore */ }
|
|
36
|
+
return { ...DEFAULT_SETTINGS }
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function saveSettings(s: DebuggerSettings) {
|
|
40
|
+
try { localStorage.setItem(SETTINGS_KEY, JSON.stringify(s)) } catch { /* ignore */ }
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// ===== Event Type Config =====
|
|
44
|
+
|
|
45
|
+
const EVENT_COLORS: Record<DebugEventType, string> = {
|
|
46
|
+
COMPONENT_MOUNT: '#22c55e',
|
|
47
|
+
COMPONENT_UNMOUNT: '#ef4444',
|
|
48
|
+
COMPONENT_REHYDRATE: '#f59e0b',
|
|
49
|
+
STATE_CHANGE: '#3b82f6',
|
|
50
|
+
ACTION_CALL: '#8b5cf6',
|
|
51
|
+
ACTION_RESULT: '#06b6d4',
|
|
52
|
+
ACTION_ERROR: '#ef4444',
|
|
53
|
+
ROOM_JOIN: '#10b981',
|
|
54
|
+
ROOM_LEAVE: '#f97316',
|
|
55
|
+
ROOM_EMIT: '#6366f1',
|
|
56
|
+
ROOM_EVENT_RECEIVED: '#a855f7',
|
|
57
|
+
WS_CONNECT: '#22c55e',
|
|
58
|
+
WS_DISCONNECT: '#ef4444',
|
|
59
|
+
ERROR: '#dc2626',
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const EVENT_LABELS: Record<DebugEventType, string> = {
|
|
63
|
+
COMPONENT_MOUNT: 'Mount',
|
|
64
|
+
COMPONENT_UNMOUNT: 'Unmount',
|
|
65
|
+
COMPONENT_REHYDRATE: 'Rehydrate',
|
|
66
|
+
STATE_CHANGE: 'State',
|
|
67
|
+
ACTION_CALL: 'Action',
|
|
68
|
+
ACTION_RESULT: 'Result',
|
|
69
|
+
ACTION_ERROR: 'Error',
|
|
70
|
+
ROOM_JOIN: 'Room Join',
|
|
71
|
+
ROOM_LEAVE: 'Room Leave',
|
|
72
|
+
ROOM_EMIT: 'Room Emit',
|
|
73
|
+
ROOM_EVENT_RECEIVED: 'Room Event',
|
|
74
|
+
WS_CONNECT: 'WS Connect',
|
|
75
|
+
WS_DISCONNECT: 'WS Disconnect',
|
|
76
|
+
ERROR: 'Error',
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// ===== Helper Components =====
|
|
80
|
+
|
|
81
|
+
function Badge({ label, color, small }: { label: string; color: string; small?: boolean }) {
|
|
82
|
+
return (
|
|
83
|
+
<span
|
|
84
|
+
style={{
|
|
85
|
+
display: 'inline-block',
|
|
86
|
+
padding: small ? '1px 6px' : '2px 8px',
|
|
87
|
+
borderRadius: '4px',
|
|
88
|
+
fontSize: small ? '10px' : '11px',
|
|
89
|
+
fontWeight: 600,
|
|
90
|
+
fontFamily: 'monospace',
|
|
91
|
+
color: '#fff',
|
|
92
|
+
backgroundColor: color,
|
|
93
|
+
lineHeight: '1.4',
|
|
94
|
+
}}
|
|
95
|
+
>
|
|
96
|
+
{label}
|
|
97
|
+
</span>
|
|
98
|
+
)
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function TimeStamp({ ts }: { ts: number }) {
|
|
102
|
+
const d = new Date(ts)
|
|
103
|
+
const time = d.toLocaleTimeString('en-US', { hour12: false })
|
|
104
|
+
const ms = String(d.getMilliseconds()).padStart(3, '0')
|
|
105
|
+
return (
|
|
106
|
+
<span style={{ color: '#6b7280', fontFamily: 'monospace', fontSize: '11px' }}>
|
|
107
|
+
{time}.{ms}
|
|
108
|
+
</span>
|
|
109
|
+
)
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function JsonTree({ data, depth = 0 }: { data: unknown; depth?: number }) {
|
|
113
|
+
if (data === null || data === undefined) {
|
|
114
|
+
return <span style={{ color: '#9ca3af' }}>{String(data)}</span>
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (typeof data === 'boolean') {
|
|
118
|
+
return <span style={{ color: '#f59e0b' }}>{String(data)}</span>
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (typeof data === 'number') {
|
|
122
|
+
return <span style={{ color: '#3b82f6' }}>{data}</span>
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (typeof data === 'string') {
|
|
126
|
+
if (data.length > 100) {
|
|
127
|
+
return <span style={{ color: '#22c55e' }}>"{data.slice(0, 100)}..."</span>
|
|
128
|
+
}
|
|
129
|
+
return <span style={{ color: '#22c55e' }}>"{data}"</span>
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (Array.isArray(data)) {
|
|
133
|
+
if (data.length === 0) return <span style={{ color: '#9ca3af' }}>[]</span>
|
|
134
|
+
if (depth > 3) return <span style={{ color: '#9ca3af' }}>[...{data.length}]</span>
|
|
135
|
+
|
|
136
|
+
return (
|
|
137
|
+
<div style={{ paddingLeft: depth > 0 ? '16px' : 0 }}>
|
|
138
|
+
<span style={{ color: '#9ca3af' }}>[</span>
|
|
139
|
+
{data.map((item, i) => (
|
|
140
|
+
<div key={i} style={{ paddingLeft: '16px' }}>
|
|
141
|
+
<JsonTree data={item} depth={depth + 1} />
|
|
142
|
+
{i < data.length - 1 && <span style={{ color: '#9ca3af' }}>,</span>}
|
|
143
|
+
</div>
|
|
144
|
+
))}
|
|
145
|
+
<span style={{ color: '#9ca3af' }}>]</span>
|
|
146
|
+
</div>
|
|
147
|
+
)
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if (typeof data === 'object') {
|
|
151
|
+
const entries = Object.entries(data as Record<string, unknown>)
|
|
152
|
+
if (entries.length === 0) return <span style={{ color: '#9ca3af' }}>{'{}'}</span>
|
|
153
|
+
if (depth > 3) return <span style={{ color: '#9ca3af' }}>{'{'} ...{entries.length} {'}'}</span>
|
|
154
|
+
|
|
155
|
+
return (
|
|
156
|
+
<div style={{ paddingLeft: depth > 0 ? '16px' : 0 }}>
|
|
157
|
+
{entries.map(([key, value], i) => (
|
|
158
|
+
<div key={key} style={{ paddingLeft: '4px' }}>
|
|
159
|
+
<span style={{ color: '#e879f9' }}>{key}</span>
|
|
160
|
+
<span style={{ color: '#9ca3af' }}>: </span>
|
|
161
|
+
<JsonTree data={value} depth={depth + 1} />
|
|
162
|
+
{i < entries.length - 1 && <span style={{ color: '#9ca3af' }}>,</span>}
|
|
163
|
+
</div>
|
|
164
|
+
))}
|
|
165
|
+
</div>
|
|
166
|
+
)
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
return <span>{String(data)}</span>
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// ===== Component List =====
|
|
173
|
+
|
|
174
|
+
function ComponentList({
|
|
175
|
+
components,
|
|
176
|
+
selectedId,
|
|
177
|
+
onSelect,
|
|
178
|
+
}: {
|
|
179
|
+
components: ComponentSnapshot[]
|
|
180
|
+
selectedId: string | null
|
|
181
|
+
onSelect: (id: string | null) => void
|
|
182
|
+
}) {
|
|
183
|
+
return (
|
|
184
|
+
<div style={{ display: 'flex', flexDirection: 'column', gap: '4px' }}>
|
|
185
|
+
{/* "All" option */}
|
|
186
|
+
<button
|
|
187
|
+
onClick={() => onSelect(null)}
|
|
188
|
+
style={{
|
|
189
|
+
display: 'flex', alignItems: 'center', gap: '8px',
|
|
190
|
+
padding: '8px 12px', borderRadius: '6px', border: 'none', cursor: 'pointer',
|
|
191
|
+
textAlign: 'left', width: '100%',
|
|
192
|
+
backgroundColor: selectedId === null ? '#1e293b' : 'transparent',
|
|
193
|
+
color: selectedId === null ? '#e2e8f0' : '#94a3b8',
|
|
194
|
+
fontFamily: 'monospace', fontSize: '12px',
|
|
195
|
+
}}
|
|
196
|
+
>
|
|
197
|
+
All Components ({components.length})
|
|
198
|
+
</button>
|
|
199
|
+
|
|
200
|
+
{components.map(comp => (
|
|
201
|
+
<button
|
|
202
|
+
key={comp.componentId}
|
|
203
|
+
onClick={() => onSelect(comp.componentId)}
|
|
204
|
+
style={{
|
|
205
|
+
display: 'flex', flexDirection: 'column', gap: '4px',
|
|
206
|
+
padding: '8px 12px', borderRadius: '6px', border: 'none', cursor: 'pointer',
|
|
207
|
+
textAlign: 'left', width: '100%',
|
|
208
|
+
backgroundColor: selectedId === comp.componentId ? '#1e293b' : 'transparent',
|
|
209
|
+
color: selectedId === comp.componentId ? '#e2e8f0' : '#cbd5e1',
|
|
210
|
+
fontFamily: 'monospace', fontSize: '12px',
|
|
211
|
+
}}
|
|
212
|
+
>
|
|
213
|
+
<div style={{ display: 'flex', alignItems: 'center', gap: '6px' }}>
|
|
214
|
+
<span style={{ color: '#22c55e', fontSize: '8px' }}>●</span>
|
|
215
|
+
<strong>{comp.componentName}</strong>
|
|
216
|
+
</div>
|
|
217
|
+
<div style={{ display: 'flex', gap: '8px', fontSize: '10px', color: '#64748b' }}>
|
|
218
|
+
<span>A:{comp.actionCount}</span>
|
|
219
|
+
<span>S:{comp.stateChangeCount}</span>
|
|
220
|
+
{comp.errorCount > 0 && <span style={{ color: '#ef4444' }}>E:{comp.errorCount}</span>}
|
|
221
|
+
{comp.rooms.length > 0 && <span>R:{comp.rooms.join(',')}</span>}
|
|
222
|
+
</div>
|
|
223
|
+
</button>
|
|
224
|
+
))}
|
|
225
|
+
|
|
226
|
+
{components.length === 0 && (
|
|
227
|
+
<div style={{ padding: '16px', textAlign: 'center', color: '#64748b', fontSize: '12px' }}>
|
|
228
|
+
No active components
|
|
229
|
+
</div>
|
|
230
|
+
)}
|
|
231
|
+
</div>
|
|
232
|
+
)
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// ===== Event Row =====
|
|
236
|
+
|
|
237
|
+
function EventRow({ event, isSelected, settings }: {
|
|
238
|
+
event: DebugEvent; isSelected: boolean; settings: DebuggerSettings
|
|
239
|
+
}) {
|
|
240
|
+
const [expanded, setExpanded] = useState(false)
|
|
241
|
+
const color = EVENT_COLORS[event.type] || '#6b7280'
|
|
242
|
+
const label = EVENT_LABELS[event.type] || event.type
|
|
243
|
+
const fs = FONT_SIZES[settings.fontSize]
|
|
244
|
+
|
|
245
|
+
// Build summary line
|
|
246
|
+
let summary = ''
|
|
247
|
+
switch (event.type) {
|
|
248
|
+
case 'STATE_CHANGE': {
|
|
249
|
+
const delta = event.data.delta as Record<string, unknown> | undefined
|
|
250
|
+
if (delta) {
|
|
251
|
+
const keys = Object.keys(delta)
|
|
252
|
+
summary = keys.length <= 3
|
|
253
|
+
? keys.map(k => `${k} = ${JSON.stringify(delta[k])}`).join(', ')
|
|
254
|
+
: `${keys.length} properties changed`
|
|
255
|
+
}
|
|
256
|
+
break
|
|
257
|
+
}
|
|
258
|
+
case 'ACTION_CALL':
|
|
259
|
+
summary = `${event.data.action as string}(${event.data.payload ? JSON.stringify(event.data.payload).slice(0, 60) : ''})`
|
|
260
|
+
break
|
|
261
|
+
case 'ACTION_RESULT':
|
|
262
|
+
summary = `${event.data.action as string} → ${(event.data.duration as number)}ms`
|
|
263
|
+
break
|
|
264
|
+
case 'ACTION_ERROR':
|
|
265
|
+
summary = `${event.data.action as string} failed: ${event.data.error as string}`
|
|
266
|
+
break
|
|
267
|
+
case 'ROOM_JOIN':
|
|
268
|
+
case 'ROOM_LEAVE':
|
|
269
|
+
summary = event.data.roomId as string
|
|
270
|
+
break
|
|
271
|
+
case 'ROOM_EMIT':
|
|
272
|
+
summary = `${event.data.event as string} → ${event.data.roomId as string}`
|
|
273
|
+
break
|
|
274
|
+
case 'COMPONENT_MOUNT':
|
|
275
|
+
summary = event.data.room ? `room: ${event.data.room as string}` : ''
|
|
276
|
+
break
|
|
277
|
+
case 'ERROR':
|
|
278
|
+
summary = event.data.error as string
|
|
279
|
+
break
|
|
280
|
+
case 'WS_CONNECT':
|
|
281
|
+
case 'WS_DISCONNECT':
|
|
282
|
+
summary = event.data.connectionId as string
|
|
283
|
+
break
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
const py = settings.compactMode ? '3px' : '6px'
|
|
287
|
+
|
|
288
|
+
return (
|
|
289
|
+
<div
|
|
290
|
+
style={{
|
|
291
|
+
borderBottom: '1px solid #1e293b',
|
|
292
|
+
cursor: 'pointer',
|
|
293
|
+
backgroundColor: expanded ? '#0f172a' : 'transparent',
|
|
294
|
+
}}
|
|
295
|
+
onClick={() => setExpanded(!expanded)}
|
|
296
|
+
>
|
|
297
|
+
<div
|
|
298
|
+
style={{
|
|
299
|
+
display: 'flex', alignItems: 'center', gap: '8px',
|
|
300
|
+
padding: `${py} 12px`, fontSize: `${fs}px`,
|
|
301
|
+
}}
|
|
302
|
+
>
|
|
303
|
+
{settings.showTimestamps && <TimeStamp ts={event.timestamp} />}
|
|
304
|
+
<Badge label={label} color={color} small />
|
|
305
|
+
{event.componentName && (
|
|
306
|
+
<span style={{
|
|
307
|
+
color: isSelected ? '#e2e8f0' : '#64748b',
|
|
308
|
+
fontFamily: 'monospace', fontSize: `${fs}px`
|
|
309
|
+
}}>
|
|
310
|
+
{event.componentName}
|
|
311
|
+
</span>
|
|
312
|
+
)}
|
|
313
|
+
<span style={{
|
|
314
|
+
flex: 1, color: '#94a3b8', fontFamily: 'monospace', fontSize: `${fs}px`,
|
|
315
|
+
overflow: 'hidden', textOverflow: 'ellipsis',
|
|
316
|
+
whiteSpace: settings.wordWrap ? 'normal' : 'nowrap',
|
|
317
|
+
}}>
|
|
318
|
+
{summary}
|
|
319
|
+
</span>
|
|
320
|
+
<span style={{ color: '#475569', fontSize: `${Math.max(8, fs - 2)}px` }}>{expanded ? '\u25BC' : '\u25B6'}</span>
|
|
321
|
+
</div>
|
|
322
|
+
|
|
323
|
+
{expanded && (
|
|
324
|
+
<div style={{
|
|
325
|
+
padding: '8px 12px 12px 50px',
|
|
326
|
+
fontSize: `${fs}px`, fontFamily: 'monospace', color: '#cbd5e1',
|
|
327
|
+
backgroundColor: '#0f172a',
|
|
328
|
+
wordBreak: settings.wordWrap ? 'break-all' : undefined,
|
|
329
|
+
}}>
|
|
330
|
+
<JsonTree data={event.data} />
|
|
331
|
+
<div style={{ marginTop: '4px', fontSize: `${Math.max(8, fs - 2)}px`, color: '#475569' }}>
|
|
332
|
+
ID: {event.id} | Component: {event.componentId || 'global'}
|
|
333
|
+
</div>
|
|
334
|
+
</div>
|
|
335
|
+
)}
|
|
336
|
+
</div>
|
|
337
|
+
)
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
// ===== State Inspector =====
|
|
341
|
+
|
|
342
|
+
function StateInspector({ component, settings }: { component: ComponentSnapshot; settings: DebuggerSettings }) {
|
|
343
|
+
const uptime = Date.now() - component.mountedAt
|
|
344
|
+
const fs = FONT_SIZES[settings.fontSize]
|
|
345
|
+
|
|
346
|
+
return (
|
|
347
|
+
<div style={{
|
|
348
|
+
padding: '16px',
|
|
349
|
+
fontFamily: 'monospace', fontSize: `${fs + 1}px`, color: '#e2e8f0',
|
|
350
|
+
}}>
|
|
351
|
+
<h3 style={{ margin: '0 0 12px', fontSize: '14px', color: '#f1f5f9' }}>
|
|
352
|
+
{component.componentName}
|
|
353
|
+
</h3>
|
|
354
|
+
|
|
355
|
+
{/* Stats */}
|
|
356
|
+
<div style={{
|
|
357
|
+
display: 'grid', gridTemplateColumns: '1fr 1fr 1fr',
|
|
358
|
+
gap: '8px', marginBottom: '16px',
|
|
359
|
+
}}>
|
|
360
|
+
<div style={{ padding: '8px', backgroundColor: '#1e293b', borderRadius: '6px', textAlign: 'center' }}>
|
|
361
|
+
<div style={{ fontSize: '18px', fontWeight: 700, color: '#3b82f6' }}>{component.stateChangeCount}</div>
|
|
362
|
+
<div style={{ fontSize: '10px', color: '#64748b' }}>State Changes</div>
|
|
363
|
+
</div>
|
|
364
|
+
<div style={{ padding: '8px', backgroundColor: '#1e293b', borderRadius: '6px', textAlign: 'center' }}>
|
|
365
|
+
<div style={{ fontSize: '18px', fontWeight: 700, color: '#8b5cf6' }}>{component.actionCount}</div>
|
|
366
|
+
<div style={{ fontSize: '10px', color: '#64748b' }}>Actions</div>
|
|
367
|
+
</div>
|
|
368
|
+
<div style={{ padding: '8px', backgroundColor: '#1e293b', borderRadius: '6px', textAlign: 'center' }}>
|
|
369
|
+
<div style={{ fontSize: '18px', fontWeight: 700, color: component.errorCount > 0 ? '#ef4444' : '#22c55e' }}>
|
|
370
|
+
{component.errorCount}
|
|
371
|
+
</div>
|
|
372
|
+
<div style={{ fontSize: '10px', color: '#64748b' }}>Errors</div>
|
|
373
|
+
</div>
|
|
374
|
+
</div>
|
|
375
|
+
|
|
376
|
+
{/* Info */}
|
|
377
|
+
<div style={{ marginBottom: '16px', fontSize: '11px', color: '#94a3b8' }}>
|
|
378
|
+
<div>ID: {component.componentId}</div>
|
|
379
|
+
<div>Uptime: {formatDuration(uptime)}</div>
|
|
380
|
+
{component.rooms.length > 0 && (
|
|
381
|
+
<div>Rooms: {component.rooms.join(', ')}</div>
|
|
382
|
+
)}
|
|
383
|
+
</div>
|
|
384
|
+
|
|
385
|
+
{/* Current State */}
|
|
386
|
+
<div>
|
|
387
|
+
<h4 style={{ margin: '0 0 8px', fontSize: '12px', color: '#94a3b8', textTransform: 'uppercase', letterSpacing: '0.5px' }}>
|
|
388
|
+
Current State
|
|
389
|
+
</h4>
|
|
390
|
+
<div style={{
|
|
391
|
+
padding: '12px', backgroundColor: '#0f172a', borderRadius: '6px',
|
|
392
|
+
maxHeight: '400px', overflow: 'auto',
|
|
393
|
+
}}>
|
|
394
|
+
<JsonTree data={component.state} />
|
|
395
|
+
</div>
|
|
396
|
+
</div>
|
|
397
|
+
</div>
|
|
398
|
+
)
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
// ===== Helpers =====
|
|
402
|
+
|
|
403
|
+
function formatDuration(ms: number): string {
|
|
404
|
+
if (ms < 1000) return `${ms}ms`
|
|
405
|
+
const seconds = Math.floor(ms / 1000)
|
|
406
|
+
if (seconds < 60) return `${seconds}s`
|
|
407
|
+
const minutes = Math.floor(seconds / 60)
|
|
408
|
+
if (minutes < 60) return `${minutes}m ${seconds % 60}s`
|
|
409
|
+
const hours = Math.floor(minutes / 60)
|
|
410
|
+
return `${hours}h ${minutes % 60}m`
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
// ===== Filter Bar =====
|
|
414
|
+
|
|
415
|
+
const ALL_EVENT_TYPES: DebugEventType[] = [
|
|
416
|
+
'COMPONENT_MOUNT', 'COMPONENT_UNMOUNT', 'STATE_CHANGE',
|
|
417
|
+
'ACTION_CALL', 'ACTION_RESULT', 'ACTION_ERROR',
|
|
418
|
+
'ROOM_JOIN', 'ROOM_LEAVE', 'ROOM_EMIT',
|
|
419
|
+
'WS_CONNECT', 'WS_DISCONNECT', 'ERROR'
|
|
420
|
+
]
|
|
421
|
+
|
|
422
|
+
function FilterBar({
|
|
423
|
+
filter,
|
|
424
|
+
onFilterChange,
|
|
425
|
+
eventCount,
|
|
426
|
+
totalCount,
|
|
427
|
+
}: {
|
|
428
|
+
filter: DebugFilter
|
|
429
|
+
onFilterChange: (f: Partial<DebugFilter>) => void
|
|
430
|
+
eventCount: number
|
|
431
|
+
totalCount: number
|
|
432
|
+
}) {
|
|
433
|
+
const [showTypes, setShowTypes] = useState(false)
|
|
434
|
+
|
|
435
|
+
return (
|
|
436
|
+
<div style={{
|
|
437
|
+
display: 'flex', alignItems: 'center', gap: '8px',
|
|
438
|
+
padding: '8px 12px', borderBottom: '1px solid #1e293b',
|
|
439
|
+
fontSize: '12px',
|
|
440
|
+
}}>
|
|
441
|
+
{/* Search */}
|
|
442
|
+
<input
|
|
443
|
+
type="text"
|
|
444
|
+
placeholder="Search events..."
|
|
445
|
+
value={filter.search || ''}
|
|
446
|
+
onChange={e => onFilterChange({ search: e.target.value || undefined })}
|
|
447
|
+
style={{
|
|
448
|
+
flex: 1, padding: '4px 8px', borderRadius: '4px',
|
|
449
|
+
border: '1px solid #334155', backgroundColor: '#0f172a',
|
|
450
|
+
color: '#e2e8f0', fontSize: '12px', fontFamily: 'monospace',
|
|
451
|
+
outline: 'none',
|
|
452
|
+
}}
|
|
453
|
+
/>
|
|
454
|
+
|
|
455
|
+
{/* Type filter toggle */}
|
|
456
|
+
<button
|
|
457
|
+
onClick={() => setShowTypes(!showTypes)}
|
|
458
|
+
style={{
|
|
459
|
+
padding: '4px 8px', borderRadius: '4px', cursor: 'pointer',
|
|
460
|
+
border: '1px solid #334155', backgroundColor: filter.types?.size ? '#1e3a5f' : '#0f172a',
|
|
461
|
+
color: '#94a3b8', fontSize: '11px', fontFamily: 'monospace',
|
|
462
|
+
}}
|
|
463
|
+
>
|
|
464
|
+
Types {filter.types?.size ? `(${filter.types.size})` : ''}
|
|
465
|
+
</button>
|
|
466
|
+
|
|
467
|
+
{/* Event count */}
|
|
468
|
+
<span style={{ color: '#64748b', fontSize: '11px', fontFamily: 'monospace' }}>
|
|
469
|
+
{eventCount === totalCount ? totalCount : `${eventCount}/${totalCount}`}
|
|
470
|
+
</span>
|
|
471
|
+
|
|
472
|
+
{/* Type filter dropdown */}
|
|
473
|
+
{showTypes && (
|
|
474
|
+
<div style={{
|
|
475
|
+
position: 'absolute', top: '100%', right: '12px', zIndex: 10,
|
|
476
|
+
padding: '8px', backgroundColor: '#1e293b', borderRadius: '6px',
|
|
477
|
+
border: '1px solid #334155', display: 'flex', flexWrap: 'wrap', gap: '4px',
|
|
478
|
+
maxWidth: '400px',
|
|
479
|
+
}}>
|
|
480
|
+
{ALL_EVENT_TYPES.map(type => {
|
|
481
|
+
const active = !filter.types || filter.types.has(type)
|
|
482
|
+
return (
|
|
483
|
+
<button
|
|
484
|
+
key={type}
|
|
485
|
+
onClick={() => {
|
|
486
|
+
const types = new Set(filter.types || ALL_EVENT_TYPES)
|
|
487
|
+
if (active) types.delete(type)
|
|
488
|
+
else types.add(type)
|
|
489
|
+
onFilterChange({ types: types.size === ALL_EVENT_TYPES.length ? undefined : types })
|
|
490
|
+
}}
|
|
491
|
+
style={{
|
|
492
|
+
padding: '2px 6px', borderRadius: '3px', cursor: 'pointer',
|
|
493
|
+
border: 'none', fontSize: '10px', fontFamily: 'monospace',
|
|
494
|
+
backgroundColor: active ? EVENT_COLORS[type] : '#0f172a',
|
|
495
|
+
color: active ? '#fff' : '#64748b',
|
|
496
|
+
opacity: active ? 1 : 0.5,
|
|
497
|
+
}}
|
|
498
|
+
>
|
|
499
|
+
{EVENT_LABELS[type]}
|
|
500
|
+
</button>
|
|
501
|
+
)
|
|
502
|
+
})}
|
|
503
|
+
</div>
|
|
504
|
+
)}
|
|
505
|
+
</div>
|
|
506
|
+
)
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
// ===== Main Panel =====
|
|
510
|
+
|
|
511
|
+
export function LiveDebuggerPanel() {
|
|
512
|
+
const [settings, setSettingsState] = useState<DebuggerSettings>(loadSettings)
|
|
513
|
+
const updateSettings = useCallback((patch: Partial<DebuggerSettings>) => {
|
|
514
|
+
setSettingsState(prev => {
|
|
515
|
+
const next = { ...prev, ...patch }
|
|
516
|
+
saveSettings(next)
|
|
517
|
+
return next
|
|
518
|
+
})
|
|
519
|
+
}, [])
|
|
520
|
+
const fs = FONT_SIZES[settings.fontSize]
|
|
521
|
+
const [showSettings, setShowSettings] = useState(false)
|
|
522
|
+
|
|
523
|
+
const dbg = useLiveDebugger({ maxEvents: settings.maxEvents })
|
|
524
|
+
const eventsEndRef = useRef<HTMLDivElement>(null)
|
|
525
|
+
const [autoScroll, setAutoScroll] = useState(true)
|
|
526
|
+
|
|
527
|
+
// Auto-scroll to bottom
|
|
528
|
+
useEffect(() => {
|
|
529
|
+
if (autoScroll && eventsEndRef.current) {
|
|
530
|
+
eventsEndRef.current.scrollIntoView({ behavior: 'smooth' })
|
|
531
|
+
}
|
|
532
|
+
}, [dbg.filteredEvents.length, autoScroll])
|
|
533
|
+
|
|
534
|
+
return (
|
|
535
|
+
<div style={{
|
|
536
|
+
display: 'flex', flexDirection: 'column',
|
|
537
|
+
height: '100vh', backgroundColor: '#0f172a', color: '#e2e8f0',
|
|
538
|
+
fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif',
|
|
539
|
+
}}>
|
|
540
|
+
{/* Header */}
|
|
541
|
+
<div style={{
|
|
542
|
+
display: 'flex', alignItems: 'center', gap: '12px',
|
|
543
|
+
padding: '12px 16px', borderBottom: '1px solid #1e293b',
|
|
544
|
+
backgroundColor: '#020617',
|
|
545
|
+
}}>
|
|
546
|
+
<span style={{ fontSize: '16px' }}>🔍</span>
|
|
547
|
+
<h1 style={{ margin: 0, fontSize: '14px', fontWeight: 700, letterSpacing: '0.5px' }}>
|
|
548
|
+
LIVE DEBUGGER
|
|
549
|
+
</h1>
|
|
550
|
+
|
|
551
|
+
{/* Connection status */}
|
|
552
|
+
<span style={{
|
|
553
|
+
display: 'flex', alignItems: 'center', gap: '4px',
|
|
554
|
+
fontSize: '11px', fontFamily: 'monospace',
|
|
555
|
+
color: dbg.connected ? '#22c55e' : '#ef4444',
|
|
556
|
+
}}>
|
|
557
|
+
<span style={{ fontSize: '8px' }}>●</span>
|
|
558
|
+
{dbg.connecting ? 'connecting...' : dbg.connected ? 'connected' : 'disconnected'}
|
|
559
|
+
</span>
|
|
560
|
+
|
|
561
|
+
<div style={{ flex: 1 }} />
|
|
562
|
+
|
|
563
|
+
{/* Controls */}
|
|
564
|
+
<button
|
|
565
|
+
onClick={dbg.togglePause}
|
|
566
|
+
style={{
|
|
567
|
+
padding: '4px 10px', borderRadius: '4px', cursor: 'pointer',
|
|
568
|
+
border: '1px solid #334155',
|
|
569
|
+
backgroundColor: dbg.paused ? '#7c2d12' : '#0f172a',
|
|
570
|
+
color: dbg.paused ? '#fdba74' : '#94a3b8',
|
|
571
|
+
fontSize: '11px', fontFamily: 'monospace',
|
|
572
|
+
}}
|
|
573
|
+
>
|
|
574
|
+
{dbg.paused ? '▶ Resume' : '⏸ Pause'}
|
|
575
|
+
</button>
|
|
576
|
+
|
|
577
|
+
<button
|
|
578
|
+
onClick={dbg.clearEvents}
|
|
579
|
+
style={{
|
|
580
|
+
padding: '4px 10px', borderRadius: '4px', cursor: 'pointer',
|
|
581
|
+
border: '1px solid #334155', backgroundColor: '#0f172a',
|
|
582
|
+
color: '#94a3b8', fontSize: '11px', fontFamily: 'monospace',
|
|
583
|
+
}}
|
|
584
|
+
>
|
|
585
|
+
Clear
|
|
586
|
+
</button>
|
|
587
|
+
|
|
588
|
+
<button
|
|
589
|
+
onClick={() => setAutoScroll(!autoScroll)}
|
|
590
|
+
style={{
|
|
591
|
+
padding: '4px 10px', borderRadius: '4px', cursor: 'pointer',
|
|
592
|
+
border: '1px solid #334155',
|
|
593
|
+
backgroundColor: autoScroll ? '#1e3a5f' : '#0f172a',
|
|
594
|
+
color: autoScroll ? '#60a5fa' : '#94a3b8',
|
|
595
|
+
fontSize: '11px', fontFamily: 'monospace',
|
|
596
|
+
}}
|
|
597
|
+
>
|
|
598
|
+
Auto-scroll
|
|
599
|
+
</button>
|
|
600
|
+
|
|
601
|
+
<button
|
|
602
|
+
onClick={() => setShowSettings(v => !v)}
|
|
603
|
+
title="Settings"
|
|
604
|
+
style={{
|
|
605
|
+
padding: '4px 10px', borderRadius: '4px', cursor: 'pointer',
|
|
606
|
+
border: '1px solid #334155',
|
|
607
|
+
backgroundColor: showSettings ? '#78350f' : '#0f172a',
|
|
608
|
+
color: showSettings ? '#f59e0b' : '#94a3b8',
|
|
609
|
+
fontSize: '13px', fontFamily: 'monospace',
|
|
610
|
+
}}
|
|
611
|
+
>
|
|
612
|
+
{'\u2699'}
|
|
613
|
+
</button>
|
|
614
|
+
|
|
615
|
+
{/* Stats */}
|
|
616
|
+
<span style={{ fontSize: '11px', fontFamily: 'monospace', color: '#64748b' }}>
|
|
617
|
+
{dbg.componentCount} components | {dbg.eventCount} events
|
|
618
|
+
</span>
|
|
619
|
+
</div>
|
|
620
|
+
|
|
621
|
+
{/* Settings drawer */}
|
|
622
|
+
{showSettings && (
|
|
623
|
+
<div style={{
|
|
624
|
+
display: 'flex', gap: '16px', padding: '12px 16px',
|
|
625
|
+
borderBottom: '1px solid #1e293b', backgroundColor: '#020617',
|
|
626
|
+
flexWrap: 'wrap', alignItems: 'center',
|
|
627
|
+
}}>
|
|
628
|
+
{/* Font size */}
|
|
629
|
+
<div style={{ display: 'flex', alignItems: 'center', gap: '6px' }}>
|
|
630
|
+
<span style={{ fontFamily: 'monospace', fontSize: '10px', color: '#64748b', textTransform: 'uppercase' }}>Font</span>
|
|
631
|
+
{(['xs', 'sm', 'md', 'lg'] as const).map(size => (
|
|
632
|
+
<button
|
|
633
|
+
key={size}
|
|
634
|
+
onClick={() => updateSettings({ fontSize: size })}
|
|
635
|
+
style={{
|
|
636
|
+
padding: '3px 8px', borderRadius: '3px', border: 'none', cursor: 'pointer',
|
|
637
|
+
fontFamily: 'monospace', fontSize: FONT_SIZES[size], fontWeight: 600,
|
|
638
|
+
background: settings.fontSize === size ? '#1e3a5f' : '#1e293b',
|
|
639
|
+
color: settings.fontSize === size ? '#60a5fa' : '#64748b',
|
|
640
|
+
}}
|
|
641
|
+
>
|
|
642
|
+
{size.toUpperCase()}
|
|
643
|
+
</button>
|
|
644
|
+
))}
|
|
645
|
+
</div>
|
|
646
|
+
|
|
647
|
+
{/* Max events */}
|
|
648
|
+
<div style={{ display: 'flex', alignItems: 'center', gap: '6px' }}>
|
|
649
|
+
<span style={{ fontFamily: 'monospace', fontSize: '10px', color: '#64748b', textTransform: 'uppercase' }}>Buffer</span>
|
|
650
|
+
{[100, 300, 500, 1000].map(n => (
|
|
651
|
+
<button
|
|
652
|
+
key={n}
|
|
653
|
+
onClick={() => updateSettings({ maxEvents: n })}
|
|
654
|
+
style={{
|
|
655
|
+
padding: '3px 8px', borderRadius: '3px', border: 'none', cursor: 'pointer',
|
|
656
|
+
fontFamily: 'monospace', fontSize: '10px', fontWeight: 600,
|
|
657
|
+
background: settings.maxEvents === n ? '#1e3a5f' : '#1e293b',
|
|
658
|
+
color: settings.maxEvents === n ? '#60a5fa' : '#64748b',
|
|
659
|
+
}}
|
|
660
|
+
>
|
|
661
|
+
{n}
|
|
662
|
+
</button>
|
|
663
|
+
))}
|
|
664
|
+
</div>
|
|
665
|
+
|
|
666
|
+
{/* Toggles */}
|
|
667
|
+
<label style={{ display: 'flex', alignItems: 'center', gap: '4px', cursor: 'pointer', fontFamily: 'monospace', fontSize: '10px', color: '#94a3b8' }}>
|
|
668
|
+
<input type="checkbox" checked={settings.showTimestamps} onChange={e => updateSettings({ showTimestamps: e.target.checked })} />
|
|
669
|
+
Timestamps
|
|
670
|
+
</label>
|
|
671
|
+
<label style={{ display: 'flex', alignItems: 'center', gap: '4px', cursor: 'pointer', fontFamily: 'monospace', fontSize: '10px', color: '#94a3b8' }}>
|
|
672
|
+
<input type="checkbox" checked={settings.compactMode} onChange={e => updateSettings({ compactMode: e.target.checked })} />
|
|
673
|
+
Compact
|
|
674
|
+
</label>
|
|
675
|
+
<label style={{ display: 'flex', alignItems: 'center', gap: '4px', cursor: 'pointer', fontFamily: 'monospace', fontSize: '10px', color: '#94a3b8' }}>
|
|
676
|
+
<input type="checkbox" checked={settings.wordWrap} onChange={e => updateSettings({ wordWrap: e.target.checked })} />
|
|
677
|
+
Word wrap
|
|
678
|
+
</label>
|
|
679
|
+
|
|
680
|
+
<button
|
|
681
|
+
onClick={() => updateSettings(DEFAULT_SETTINGS)}
|
|
682
|
+
style={{
|
|
683
|
+
padding: '3px 8px', borderRadius: '3px', border: '1px solid #1e293b', cursor: 'pointer',
|
|
684
|
+
fontFamily: 'monospace', fontSize: '10px', background: 'transparent', color: '#64748b',
|
|
685
|
+
}}
|
|
686
|
+
>
|
|
687
|
+
Reset
|
|
688
|
+
</button>
|
|
689
|
+
</div>
|
|
690
|
+
)}
|
|
691
|
+
|
|
692
|
+
{/* Body */}
|
|
693
|
+
<div style={{ display: 'flex', flex: 1, overflow: 'hidden' }}>
|
|
694
|
+
{/* Left sidebar - Components */}
|
|
695
|
+
<div style={{
|
|
696
|
+
width: '240px', borderRight: '1px solid #1e293b',
|
|
697
|
+
display: 'flex', flexDirection: 'column', overflow: 'hidden',
|
|
698
|
+
}}>
|
|
699
|
+
<div style={{
|
|
700
|
+
padding: '8px 12px', borderBottom: '1px solid #1e293b',
|
|
701
|
+
fontSize: '11px', fontWeight: 600, color: '#64748b',
|
|
702
|
+
textTransform: 'uppercase', letterSpacing: '0.5px',
|
|
703
|
+
}}>
|
|
704
|
+
Components
|
|
705
|
+
</div>
|
|
706
|
+
<div style={{ flex: 1, overflow: 'auto', padding: '4px' }}>
|
|
707
|
+
<ComponentList
|
|
708
|
+
components={dbg.components}
|
|
709
|
+
selectedId={dbg.selectedComponentId}
|
|
710
|
+
onSelect={(id) => {
|
|
711
|
+
dbg.selectComponent(id)
|
|
712
|
+
dbg.setFilter({ componentId: id ?? undefined })
|
|
713
|
+
}}
|
|
714
|
+
/>
|
|
715
|
+
</div>
|
|
716
|
+
</div>
|
|
717
|
+
|
|
718
|
+
{/* Center - Event Timeline */}
|
|
719
|
+
<div style={{
|
|
720
|
+
flex: 1, display: 'flex', flexDirection: 'column', overflow: 'hidden',
|
|
721
|
+
minWidth: 0,
|
|
722
|
+
}}>
|
|
723
|
+
{/* Filter bar */}
|
|
724
|
+
<div style={{ position: 'relative' }}>
|
|
725
|
+
<FilterBar
|
|
726
|
+
filter={dbg.filter}
|
|
727
|
+
onFilterChange={dbg.setFilter}
|
|
728
|
+
eventCount={dbg.filteredEvents.length}
|
|
729
|
+
totalCount={dbg.events.length}
|
|
730
|
+
/>
|
|
731
|
+
</div>
|
|
732
|
+
|
|
733
|
+
{/* Events */}
|
|
734
|
+
<div style={{ flex: 1, overflow: 'auto' }}>
|
|
735
|
+
{dbg.filteredEvents.map(event => (
|
|
736
|
+
<EventRow
|
|
737
|
+
key={event.id}
|
|
738
|
+
event={event}
|
|
739
|
+
isSelected={event.componentId === dbg.selectedComponentId}
|
|
740
|
+
settings={settings}
|
|
741
|
+
/>
|
|
742
|
+
))}
|
|
743
|
+
|
|
744
|
+
{dbg.filteredEvents.length === 0 && (
|
|
745
|
+
<div style={{
|
|
746
|
+
display: 'flex', flexDirection: 'column', alignItems: 'center',
|
|
747
|
+
justifyContent: 'center', height: '200px', color: '#475569',
|
|
748
|
+
}}>
|
|
749
|
+
<div style={{ fontSize: '24px', marginBottom: '8px' }}>🔍</div>
|
|
750
|
+
<div style={{ fontSize: '13px' }}>
|
|
751
|
+
{dbg.connected
|
|
752
|
+
? dbg.paused
|
|
753
|
+
? 'Paused - click Resume to continue'
|
|
754
|
+
: 'Waiting for events...'
|
|
755
|
+
: 'Connecting to debug server...'}
|
|
756
|
+
</div>
|
|
757
|
+
<div style={{ fontSize: '11px', marginTop: '4px', color: '#334155' }}>
|
|
758
|
+
Use your app to generate Live Component events
|
|
759
|
+
</div>
|
|
760
|
+
</div>
|
|
761
|
+
)}
|
|
762
|
+
|
|
763
|
+
<div ref={eventsEndRef} />
|
|
764
|
+
</div>
|
|
765
|
+
</div>
|
|
766
|
+
|
|
767
|
+
{/* Right sidebar - State Inspector */}
|
|
768
|
+
{dbg.selectedComponent && (
|
|
769
|
+
<div style={{
|
|
770
|
+
width: '350px', borderLeft: '1px solid #1e293b',
|
|
771
|
+
overflow: 'auto',
|
|
772
|
+
}}>
|
|
773
|
+
<StateInspector component={dbg.selectedComponent} settings={settings} />
|
|
774
|
+
</div>
|
|
775
|
+
)}
|
|
776
|
+
</div>
|
|
777
|
+
</div>
|
|
778
|
+
)
|
|
779
|
+
}
|