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.
Files changed (61) hide show
  1. package/LLMD/patterns/anti-patterns.md +100 -0
  2. package/LLMD/reference/routing.md +39 -39
  3. package/LLMD/resources/live-auth.md +20 -2
  4. package/LLMD/resources/live-components.md +94 -10
  5. package/LLMD/resources/live-logging.md +95 -33
  6. package/LLMD/resources/live-upload.md +59 -8
  7. package/app/client/index.html +2 -2
  8. package/app/client/public/favicon.svg +46 -0
  9. package/app/client/src/App.tsx +2 -1
  10. package/app/client/src/assets/fluxstack-static.svg +46 -0
  11. package/app/client/src/assets/fluxstack.svg +183 -0
  12. package/app/client/src/components/AppLayout.tsx +138 -9
  13. package/app/client/src/components/BackButton.tsx +13 -13
  14. package/app/client/src/components/DemoPage.tsx +4 -4
  15. package/app/client/src/live/AuthDemo.tsx +23 -21
  16. package/app/client/src/live/ChatDemo.tsx +2 -2
  17. package/app/client/src/live/CounterDemo.tsx +12 -12
  18. package/app/client/src/live/FormDemo.tsx +2 -2
  19. package/app/client/src/live/LiveDebuggerPanel.tsx +779 -0
  20. package/app/client/src/live/RoomChatDemo.tsx +24 -16
  21. package/app/client/src/main.tsx +13 -13
  22. package/app/client/src/pages/ApiTestPage.tsx +6 -6
  23. package/app/client/src/pages/HomePage.tsx +80 -52
  24. package/app/server/live/LiveAdminPanel.ts +1 -0
  25. package/app/server/live/LiveChat.ts +78 -77
  26. package/app/server/live/LiveCounter.ts +1 -1
  27. package/app/server/live/LiveForm.ts +1 -0
  28. package/app/server/live/LiveLocalCounter.ts +38 -37
  29. package/app/server/live/LiveProtectedChat.ts +1 -0
  30. package/app/server/live/LiveRoomChat.ts +1 -0
  31. package/app/server/live/LiveUpload.ts +1 -0
  32. package/app/server/live/register-components.ts +19 -19
  33. package/config/system/runtime.config.ts +4 -0
  34. package/core/build/optimizer.ts +235 -235
  35. package/core/client/components/Live.tsx +17 -11
  36. package/core/client/components/LiveDebugger.tsx +1324 -0
  37. package/core/client/hooks/AdaptiveChunkSizer.ts +215 -215
  38. package/core/client/hooks/useLiveComponent.ts +11 -1
  39. package/core/client/hooks/useLiveDebugger.ts +392 -0
  40. package/core/client/index.ts +14 -0
  41. package/core/plugins/built-in/index.ts +134 -134
  42. package/core/plugins/built-in/live-components/commands/create-live-component.ts +4 -0
  43. package/core/plugins/built-in/vite/index.ts +75 -21
  44. package/core/server/index.ts +15 -15
  45. package/core/server/live/ComponentRegistry.ts +55 -26
  46. package/core/server/live/FileUploadManager.ts +188 -24
  47. package/core/server/live/LiveDebugger.ts +462 -0
  48. package/core/server/live/LiveLogger.ts +38 -5
  49. package/core/server/live/LiveRoomManager.ts +17 -1
  50. package/core/server/live/StateSignature.ts +87 -27
  51. package/core/server/live/WebSocketConnectionManager.ts +11 -10
  52. package/core/server/live/auto-generated-components.ts +1 -1
  53. package/core/server/live/websocket-plugin.ts +233 -8
  54. package/core/server/plugins/static-files-plugin.ts +179 -69
  55. package/core/types/build.ts +219 -219
  56. package/core/types/plugin.ts +107 -107
  57. package/core/types/types.ts +145 -9
  58. package/core/utils/logger/startup-banner.ts +82 -82
  59. package/core/utils/version.ts +6 -6
  60. package/package.json +1 -1
  61. 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
+ }