create-fluxstack 1.15.0 → 1.16.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LLMD/INDEX.md +4 -3
- package/LLMD/resources/live-binary-delta.md +507 -0
- package/LLMD/resources/live-components.md +1 -0
- package/LLMD/resources/live-rooms.md +731 -333
- package/app/client/.live-stubs/LivePingPong.js +10 -0
- package/app/client/.live-stubs/LiveRoomChat.js +3 -2
- package/app/client/.live-stubs/LiveSharedCounter.js +10 -0
- package/app/client/src/App.tsx +15 -14
- package/app/client/src/components/AppLayout.tsx +4 -4
- package/app/client/src/live/PingPongDemo.tsx +199 -0
- package/app/client/src/live/RoomChatDemo.tsx +187 -22
- package/app/client/src/live/SharedCounterDemo.tsx +142 -0
- package/app/server/live/LivePingPong.ts +61 -0
- package/app/server/live/LiveRoomChat.ts +106 -38
- package/app/server/live/LiveSharedCounter.ts +73 -0
- package/app/server/live/rooms/ChatRoom.ts +68 -0
- package/app/server/live/rooms/CounterRoom.ts +51 -0
- package/app/server/live/rooms/DirectoryRoom.ts +42 -0
- package/app/server/live/rooms/PingRoom.ts +40 -0
- package/core/build/live-components-generator.ts +10 -1
- package/core/client/index.ts +0 -16
- package/core/server/live/auto-generated-components.ts +3 -9
- package/core/server/live/index.ts +0 -5
- package/core/server/live/websocket-plugin.ts +37 -2
- package/core/utils/version.ts +1 -1
- package/package.json +100 -99
- package/tsconfig.json +4 -1
- package/app/client/.live-stubs/LiveChat.js +0 -7
- package/app/client/.live-stubs/LiveTodoList.js +0 -9
- package/app/client/src/live/ChatDemo.tsx +0 -107
- package/app/client/src/live/LiveDebuggerPanel.tsx +0 -779
- package/app/client/src/live/TodoListDemo.tsx +0 -158
- package/app/server/live/LiveChat.ts +0 -78
- package/app/server/live/LiveTodoList.ts +0 -110
- package/core/client/components/LiveDebugger.tsx +0 -1324
|
@@ -1,1324 +0,0 @@
|
|
|
1
|
-
// FluxStack Live Debugger - Draggable Floating Window
|
|
2
|
-
//
|
|
3
|
-
// A floating, draggable, resizable debug panel for inspecting Live Components.
|
|
4
|
-
// Toggle with Ctrl+Shift+D or click the small badge in the corner.
|
|
5
|
-
//
|
|
6
|
-
// Usage:
|
|
7
|
-
// import { LiveDebugger } from '@/core/client'
|
|
8
|
-
// <LiveDebugger />
|
|
9
|
-
|
|
10
|
-
import React, { useState, useRef, useEffect, useCallback, useMemo } from 'react'
|
|
11
|
-
import { useLiveDebugger, type DebugEvent, type DebugEventType, type ComponentSnapshot } from '@fluxstack/live-react'
|
|
12
|
-
|
|
13
|
-
// ===== Debugger Settings =====
|
|
14
|
-
|
|
15
|
-
export interface DebuggerSettings {
|
|
16
|
-
fontSize: 'xs' | 'sm' | 'md' | 'lg'
|
|
17
|
-
showTimestamps: boolean
|
|
18
|
-
compactMode: boolean
|
|
19
|
-
wordWrap: boolean
|
|
20
|
-
maxEvents: number
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
const FONT_SIZES: Record<DebuggerSettings['fontSize'], number> = {
|
|
24
|
-
xs: 9,
|
|
25
|
-
sm: 10,
|
|
26
|
-
md: 11,
|
|
27
|
-
lg: 13,
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
const DEFAULT_SETTINGS: DebuggerSettings = {
|
|
31
|
-
fontSize: 'sm',
|
|
32
|
-
showTimestamps: true,
|
|
33
|
-
compactMode: false,
|
|
34
|
-
wordWrap: false,
|
|
35
|
-
maxEvents: 300,
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
const SETTINGS_KEY = 'fluxstack-debugger-settings'
|
|
39
|
-
|
|
40
|
-
function loadSettings(): DebuggerSettings {
|
|
41
|
-
try {
|
|
42
|
-
const stored = localStorage.getItem(SETTINGS_KEY)
|
|
43
|
-
if (stored) return { ...DEFAULT_SETTINGS, ...JSON.parse(stored) }
|
|
44
|
-
} catch { /* ignore */ }
|
|
45
|
-
return { ...DEFAULT_SETTINGS }
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
function saveSettings(settings: DebuggerSettings) {
|
|
49
|
-
try {
|
|
50
|
-
localStorage.setItem(SETTINGS_KEY, JSON.stringify(settings))
|
|
51
|
-
} catch { /* ignore */ }
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
// ===== Event Type Groups =====
|
|
55
|
-
|
|
56
|
-
type EventGroup = 'lifecycle' | 'state' | 'actions' | 'rooms' | 'connection' | 'errors'
|
|
57
|
-
|
|
58
|
-
const EVENT_GROUPS: Record<EventGroup, { label: string; color: string; types: DebugEventType[] }> = {
|
|
59
|
-
lifecycle: {
|
|
60
|
-
label: 'Lifecycle',
|
|
61
|
-
color: '#22c55e',
|
|
62
|
-
types: ['COMPONENT_MOUNT', 'COMPONENT_UNMOUNT', 'COMPONENT_REHYDRATE'],
|
|
63
|
-
},
|
|
64
|
-
state: {
|
|
65
|
-
label: 'State',
|
|
66
|
-
color: '#3b82f6',
|
|
67
|
-
types: ['STATE_CHANGE'],
|
|
68
|
-
},
|
|
69
|
-
actions: {
|
|
70
|
-
label: 'Actions',
|
|
71
|
-
color: '#8b5cf6',
|
|
72
|
-
types: ['ACTION_CALL', 'ACTION_RESULT', 'ACTION_ERROR'],
|
|
73
|
-
},
|
|
74
|
-
rooms: {
|
|
75
|
-
label: 'Rooms',
|
|
76
|
-
color: '#10b981',
|
|
77
|
-
types: ['ROOM_JOIN', 'ROOM_LEAVE', 'ROOM_EMIT', 'ROOM_EVENT_RECEIVED'],
|
|
78
|
-
},
|
|
79
|
-
connection: {
|
|
80
|
-
label: 'WS',
|
|
81
|
-
color: '#06b6d4',
|
|
82
|
-
types: ['WS_CONNECT', 'WS_DISCONNECT'],
|
|
83
|
-
},
|
|
84
|
-
errors: {
|
|
85
|
-
label: 'Errors',
|
|
86
|
-
color: '#ef4444',
|
|
87
|
-
types: ['ERROR'],
|
|
88
|
-
},
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
const ALL_GROUPS = Object.keys(EVENT_GROUPS) as EventGroup[]
|
|
92
|
-
|
|
93
|
-
const COLORS: Record<string, string> = {
|
|
94
|
-
COMPONENT_MOUNT: '#22c55e',
|
|
95
|
-
COMPONENT_UNMOUNT: '#ef4444',
|
|
96
|
-
COMPONENT_REHYDRATE: '#f59e0b',
|
|
97
|
-
STATE_CHANGE: '#3b82f6',
|
|
98
|
-
ACTION_CALL: '#8b5cf6',
|
|
99
|
-
ACTION_RESULT: '#06b6d4',
|
|
100
|
-
ACTION_ERROR: '#ef4444',
|
|
101
|
-
ROOM_JOIN: '#10b981',
|
|
102
|
-
ROOM_LEAVE: '#f97316',
|
|
103
|
-
ROOM_EMIT: '#6366f1',
|
|
104
|
-
ROOM_EVENT_RECEIVED: '#6366f1',
|
|
105
|
-
WS_CONNECT: '#22c55e',
|
|
106
|
-
WS_DISCONNECT: '#ef4444',
|
|
107
|
-
ERROR: '#dc2626',
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
const LABELS: Record<string, string> = {
|
|
111
|
-
COMPONENT_MOUNT: 'MOUNT',
|
|
112
|
-
COMPONENT_UNMOUNT: 'UNMOUNT',
|
|
113
|
-
COMPONENT_REHYDRATE: 'REHYDRATE',
|
|
114
|
-
STATE_CHANGE: 'STATE',
|
|
115
|
-
ACTION_CALL: 'ACTION',
|
|
116
|
-
ACTION_RESULT: 'RESULT',
|
|
117
|
-
ACTION_ERROR: 'ERR',
|
|
118
|
-
ROOM_JOIN: 'JOIN',
|
|
119
|
-
ROOM_LEAVE: 'LEAVE',
|
|
120
|
-
ROOM_EMIT: 'EMIT',
|
|
121
|
-
ROOM_EVENT_RECEIVED: 'ROOM_EVT',
|
|
122
|
-
WS_CONNECT: 'CONNECT',
|
|
123
|
-
WS_DISCONNECT: 'DISCONNECT',
|
|
124
|
-
ERROR: 'ERROR',
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
// ===== Collapsible JSON Tree Viewer =====
|
|
128
|
-
|
|
129
|
-
function jsonPreview(data: unknown): string {
|
|
130
|
-
if (data === null || data === undefined) return String(data)
|
|
131
|
-
if (typeof data !== 'object') return JSON.stringify(data)
|
|
132
|
-
if (Array.isArray(data)) {
|
|
133
|
-
if (data.length === 0) return '[]'
|
|
134
|
-
const items = data.slice(0, 3).map(jsonPreview).join(', ')
|
|
135
|
-
return data.length <= 3 ? `[${items}]` : `[${items}, ...+${data.length - 3}]`
|
|
136
|
-
}
|
|
137
|
-
const entries = Object.entries(data as Record<string, unknown>)
|
|
138
|
-
if (entries.length === 0) return '{}'
|
|
139
|
-
const items = entries.slice(0, 3).map(([k, v]) => {
|
|
140
|
-
const val = typeof v === 'object' && v !== null
|
|
141
|
-
? (Array.isArray(v) ? `[${v.length}]` : `{${Object.keys(v).length}}`)
|
|
142
|
-
: JSON.stringify(v)
|
|
143
|
-
return `${k}: ${val}`
|
|
144
|
-
}).join(', ')
|
|
145
|
-
return entries.length <= 3 ? `{ ${items} }` : `{ ${items}, ...+${entries.length - 3} }`
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
function isExpandable(data: unknown): boolean {
|
|
149
|
-
return data !== null && typeof data === 'object' && (
|
|
150
|
-
Array.isArray(data) ? data.length > 0 : Object.keys(data as object).length > 0
|
|
151
|
-
)
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
function JsonNode({ label, data, defaultOpen = false }: {
|
|
155
|
-
label?: string | number
|
|
156
|
-
data: unknown
|
|
157
|
-
defaultOpen?: boolean
|
|
158
|
-
}) {
|
|
159
|
-
const [open, setOpen] = useState(defaultOpen)
|
|
160
|
-
const expandable = isExpandable(data)
|
|
161
|
-
|
|
162
|
-
// Primitives
|
|
163
|
-
if (!expandable) {
|
|
164
|
-
let rendered: React.ReactNode
|
|
165
|
-
if (data === null || data === undefined) rendered = <span style={{ color: '#6b7280' }}>{String(data)}</span>
|
|
166
|
-
else if (typeof data === 'boolean') rendered = <span style={{ color: '#f59e0b' }}>{String(data)}</span>
|
|
167
|
-
else if (typeof data === 'number') rendered = <span style={{ color: '#60a5fa' }}>{data}</span>
|
|
168
|
-
else if (typeof data === 'string') {
|
|
169
|
-
const display = data.length > 120 ? data.slice(0, 120) + '...' : data
|
|
170
|
-
rendered = <span style={{ color: '#34d399' }}>"{display}"</span>
|
|
171
|
-
} else {
|
|
172
|
-
rendered = <span>{String(data)}</span>
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
return (
|
|
176
|
-
<div style={{ lineHeight: '1.6', paddingLeft: 2 }}>
|
|
177
|
-
{label !== undefined && (
|
|
178
|
-
<>
|
|
179
|
-
<span style={{ color: typeof label === 'number' ? '#60a5fa' : '#c084fc' }}>{label}</span>
|
|
180
|
-
<span style={{ color: '#6b7280' }}>: </span>
|
|
181
|
-
</>
|
|
182
|
-
)}
|
|
183
|
-
{rendered}
|
|
184
|
-
</div>
|
|
185
|
-
)
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
// Expandable (object / array)
|
|
189
|
-
const isArray = Array.isArray(data)
|
|
190
|
-
const entries = isArray
|
|
191
|
-
? (data as unknown[]).map((v, i) => [i, v] as [number, unknown])
|
|
192
|
-
: Object.entries(data as Record<string, unknown>)
|
|
193
|
-
const bracketOpen = isArray ? '[' : '{'
|
|
194
|
-
const bracketClose = isArray ? ']' : '}'
|
|
195
|
-
|
|
196
|
-
return (
|
|
197
|
-
<div style={{ lineHeight: '1.6' }}>
|
|
198
|
-
<div
|
|
199
|
-
onClick={() => setOpen(!open)}
|
|
200
|
-
style={{ cursor: 'pointer', display: 'flex', alignItems: 'flex-start', gap: 2, paddingLeft: 2 }}
|
|
201
|
-
>
|
|
202
|
-
<span style={{
|
|
203
|
-
color: '#475569', fontSize: 8, width: 10, flexShrink: 0,
|
|
204
|
-
display: 'inline-flex', alignItems: 'center', justifyContent: 'center',
|
|
205
|
-
paddingTop: 4, userSelect: 'none',
|
|
206
|
-
}}>
|
|
207
|
-
{open ? '\u25BC' : '\u25B6'}
|
|
208
|
-
</span>
|
|
209
|
-
<span>
|
|
210
|
-
{label !== undefined && (
|
|
211
|
-
<>
|
|
212
|
-
<span style={{ color: typeof label === 'number' ? '#60a5fa' : '#c084fc' }}>{label}</span>
|
|
213
|
-
<span style={{ color: '#6b7280' }}>: </span>
|
|
214
|
-
</>
|
|
215
|
-
)}
|
|
216
|
-
{!open && (
|
|
217
|
-
<span style={{ color: '#64748b' }}>
|
|
218
|
-
{bracketOpen} {jsonPreview(data).slice(1, -1)} {bracketClose}
|
|
219
|
-
</span>
|
|
220
|
-
)}
|
|
221
|
-
{open && (
|
|
222
|
-
<span style={{ color: '#64748b' }}>
|
|
223
|
-
{bracketOpen}
|
|
224
|
-
<span style={{ color: '#475569', fontSize: 9, marginLeft: 4 }}>
|
|
225
|
-
{entries.length} {isArray ? (entries.length === 1 ? 'item' : 'items') : (entries.length === 1 ? 'key' : 'keys')}
|
|
226
|
-
</span>
|
|
227
|
-
</span>
|
|
228
|
-
)}
|
|
229
|
-
</span>
|
|
230
|
-
</div>
|
|
231
|
-
{open && (
|
|
232
|
-
<div style={{ paddingLeft: 14 }}>
|
|
233
|
-
{entries.map(([key, val]) => (
|
|
234
|
-
<JsonNode
|
|
235
|
-
key={String(key)}
|
|
236
|
-
label={key}
|
|
237
|
-
data={val}
|
|
238
|
-
defaultOpen={false}
|
|
239
|
-
/>
|
|
240
|
-
))}
|
|
241
|
-
<div style={{ color: '#64748b', paddingLeft: 2 }}>{bracketClose}</div>
|
|
242
|
-
</div>
|
|
243
|
-
)}
|
|
244
|
-
</div>
|
|
245
|
-
)
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
function Json({ data, depth = 0 }: { data: unknown; depth?: number }) {
|
|
249
|
-
return <JsonNode data={data} defaultOpen={depth === 0} />
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
// ===== Event Summary =====
|
|
253
|
-
|
|
254
|
-
function eventSummary(e: DebugEvent): string {
|
|
255
|
-
switch (e.type) {
|
|
256
|
-
case 'STATE_CHANGE': {
|
|
257
|
-
const delta = e.data.delta as Record<string, unknown> | undefined
|
|
258
|
-
if (!delta) return ''
|
|
259
|
-
const keys = Object.keys(delta)
|
|
260
|
-
if (keys.length <= 2) return keys.map(k => `${k}=${JSON.stringify(delta[k])}`).join(' ')
|
|
261
|
-
return `${keys.length} props`
|
|
262
|
-
}
|
|
263
|
-
case 'ACTION_CALL':
|
|
264
|
-
return String(e.data.action || '')
|
|
265
|
-
case 'ACTION_RESULT':
|
|
266
|
-
return `${e.data.action} ${e.data.duration}ms`
|
|
267
|
-
case 'ACTION_ERROR':
|
|
268
|
-
return `${e.data.action}: ${e.data.error}`
|
|
269
|
-
case 'ROOM_JOIN':
|
|
270
|
-
case 'ROOM_LEAVE':
|
|
271
|
-
return String(e.data.roomId || '')
|
|
272
|
-
case 'ROOM_EMIT':
|
|
273
|
-
return `${e.data.event} -> ${e.data.roomId}`
|
|
274
|
-
case 'ERROR':
|
|
275
|
-
return String(e.data.error || '')
|
|
276
|
-
default:
|
|
277
|
-
return ''
|
|
278
|
-
}
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
function displayName(comp: ComponentSnapshot): string {
|
|
282
|
-
return comp.debugLabel || comp.componentName
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
// ===== Drag Hook =====
|
|
286
|
-
|
|
287
|
-
function useDrag(
|
|
288
|
-
initialPos: { x: number; y: number },
|
|
289
|
-
onDragEnd?: (pos: { x: number; y: number }) => void
|
|
290
|
-
) {
|
|
291
|
-
const [pos, setPos] = useState(initialPos)
|
|
292
|
-
const dragging = useRef(false)
|
|
293
|
-
const offset = useRef({ x: 0, y: 0 })
|
|
294
|
-
|
|
295
|
-
const onMouseDown = useCallback((e: React.MouseEvent) => {
|
|
296
|
-
// Only drag from title bar, not from buttons
|
|
297
|
-
if ((e.target as HTMLElement).closest('button, input, select')) return
|
|
298
|
-
dragging.current = true
|
|
299
|
-
offset.current = { x: e.clientX - pos.x, y: e.clientY - pos.y }
|
|
300
|
-
e.preventDefault()
|
|
301
|
-
}, [pos])
|
|
302
|
-
|
|
303
|
-
useEffect(() => {
|
|
304
|
-
const onMouseMove = (e: MouseEvent) => {
|
|
305
|
-
if (!dragging.current) return
|
|
306
|
-
const newX = Math.max(0, Math.min(window.innerWidth - 100, e.clientX - offset.current.x))
|
|
307
|
-
const newY = Math.max(0, Math.min(window.innerHeight - 40, e.clientY - offset.current.y))
|
|
308
|
-
setPos({ x: newX, y: newY })
|
|
309
|
-
}
|
|
310
|
-
const onMouseUp = () => {
|
|
311
|
-
if (dragging.current) {
|
|
312
|
-
dragging.current = false
|
|
313
|
-
onDragEnd?.(pos)
|
|
314
|
-
}
|
|
315
|
-
}
|
|
316
|
-
window.addEventListener('mousemove', onMouseMove)
|
|
317
|
-
window.addEventListener('mouseup', onMouseUp)
|
|
318
|
-
return () => {
|
|
319
|
-
window.removeEventListener('mousemove', onMouseMove)
|
|
320
|
-
window.removeEventListener('mouseup', onMouseUp)
|
|
321
|
-
}
|
|
322
|
-
}, [pos, onDragEnd])
|
|
323
|
-
|
|
324
|
-
return { pos, setPos, onMouseDown }
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
// ===== Resize Hook =====
|
|
328
|
-
|
|
329
|
-
function useResize(
|
|
330
|
-
initialSize: { w: number; h: number },
|
|
331
|
-
minSize = { w: 420, h: 300 }
|
|
332
|
-
) {
|
|
333
|
-
const [size, setSize] = useState(initialSize)
|
|
334
|
-
const resizing = useRef(false)
|
|
335
|
-
const startData = useRef({ mouseX: 0, mouseY: 0, w: 0, h: 0 })
|
|
336
|
-
|
|
337
|
-
const onResizeStart = useCallback((e: React.MouseEvent) => {
|
|
338
|
-
resizing.current = true
|
|
339
|
-
startData.current = { mouseX: e.clientX, mouseY: e.clientY, w: size.w, h: size.h }
|
|
340
|
-
e.preventDefault()
|
|
341
|
-
e.stopPropagation()
|
|
342
|
-
}, [size])
|
|
343
|
-
|
|
344
|
-
useEffect(() => {
|
|
345
|
-
const onMouseMove = (e: MouseEvent) => {
|
|
346
|
-
if (!resizing.current) return
|
|
347
|
-
const dw = e.clientX - startData.current.mouseX
|
|
348
|
-
const dh = e.clientY - startData.current.mouseY
|
|
349
|
-
setSize({
|
|
350
|
-
w: Math.max(minSize.w, startData.current.w + dw),
|
|
351
|
-
h: Math.max(minSize.h, startData.current.h + dh),
|
|
352
|
-
})
|
|
353
|
-
}
|
|
354
|
-
const onMouseUp = () => { resizing.current = false }
|
|
355
|
-
window.addEventListener('mousemove', onMouseMove)
|
|
356
|
-
window.addEventListener('mouseup', onMouseUp)
|
|
357
|
-
return () => {
|
|
358
|
-
window.removeEventListener('mousemove', onMouseMove)
|
|
359
|
-
window.removeEventListener('mouseup', onMouseUp)
|
|
360
|
-
}
|
|
361
|
-
}, [minSize.w, minSize.h])
|
|
362
|
-
|
|
363
|
-
return { size, onResizeStart }
|
|
364
|
-
}
|
|
365
|
-
|
|
366
|
-
// ===== Component Card =====
|
|
367
|
-
|
|
368
|
-
function ComponentCard({
|
|
369
|
-
comp,
|
|
370
|
-
isSelected,
|
|
371
|
-
onSelect,
|
|
372
|
-
}: {
|
|
373
|
-
comp: ComponentSnapshot
|
|
374
|
-
isSelected: boolean
|
|
375
|
-
onSelect: () => void
|
|
376
|
-
}) {
|
|
377
|
-
return (
|
|
378
|
-
<button
|
|
379
|
-
onClick={onSelect}
|
|
380
|
-
style={{
|
|
381
|
-
width: '100%', textAlign: 'left', cursor: 'pointer',
|
|
382
|
-
padding: '6px 8px', borderRadius: 4, border: 'none',
|
|
383
|
-
background: isSelected ? '#1e293b' : 'transparent',
|
|
384
|
-
color: '#e2e8f0', fontFamily: 'monospace', fontSize: 11,
|
|
385
|
-
display: 'flex', flexDirection: 'column', gap: 1,
|
|
386
|
-
transition: 'background 0.15s',
|
|
387
|
-
}}
|
|
388
|
-
>
|
|
389
|
-
<div style={{ display: 'flex', alignItems: 'center', gap: 5 }}>
|
|
390
|
-
<span style={{ color: '#22c55e', fontSize: 6 }}>●</span>
|
|
391
|
-
<strong style={{ fontSize: 11 }}>{displayName(comp)}</strong>
|
|
392
|
-
</div>
|
|
393
|
-
{comp.debugLabel && (
|
|
394
|
-
<div style={{ fontSize: 9, color: '#64748b', paddingLeft: 11 }}>{comp.componentName}</div>
|
|
395
|
-
)}
|
|
396
|
-
<div style={{ display: 'flex', gap: 8, color: '#64748b', fontSize: 9, paddingLeft: 11 }}>
|
|
397
|
-
<span>S:{comp.stateChangeCount}</span>
|
|
398
|
-
<span>A:{comp.actionCount}</span>
|
|
399
|
-
{comp.errorCount > 0 && <span style={{ color: '#f87171' }}>E:{comp.errorCount}</span>}
|
|
400
|
-
</div>
|
|
401
|
-
</button>
|
|
402
|
-
)
|
|
403
|
-
}
|
|
404
|
-
|
|
405
|
-
// ===== Filter Bar =====
|
|
406
|
-
|
|
407
|
-
function FilterBar({
|
|
408
|
-
activeGroups,
|
|
409
|
-
toggleGroup,
|
|
410
|
-
search,
|
|
411
|
-
setSearch,
|
|
412
|
-
groupCounts,
|
|
413
|
-
}: {
|
|
414
|
-
activeGroups: Set<EventGroup>
|
|
415
|
-
toggleGroup: (g: EventGroup) => void
|
|
416
|
-
search: string
|
|
417
|
-
setSearch: (s: string) => void
|
|
418
|
-
groupCounts: Record<EventGroup, number>
|
|
419
|
-
}) {
|
|
420
|
-
return (
|
|
421
|
-
<div style={{
|
|
422
|
-
display: 'flex', alignItems: 'center', gap: 4,
|
|
423
|
-
padding: '4px 8px', borderBottom: '1px solid #1e293b',
|
|
424
|
-
flexShrink: 0, flexWrap: 'wrap',
|
|
425
|
-
}}>
|
|
426
|
-
{ALL_GROUPS.map(g => {
|
|
427
|
-
const group = EVENT_GROUPS[g]
|
|
428
|
-
const active = activeGroups.has(g)
|
|
429
|
-
const count = groupCounts[g] || 0
|
|
430
|
-
return (
|
|
431
|
-
<button
|
|
432
|
-
key={g}
|
|
433
|
-
onClick={() => toggleGroup(g)}
|
|
434
|
-
style={{
|
|
435
|
-
padding: '2px 6px', borderRadius: 3,
|
|
436
|
-
border: `1px solid ${active ? group.color + '60' : '#1e293b'}`,
|
|
437
|
-
cursor: 'pointer', fontFamily: 'monospace', fontSize: 9,
|
|
438
|
-
background: active ? group.color + '20' : 'transparent',
|
|
439
|
-
color: active ? group.color : '#475569',
|
|
440
|
-
transition: 'all 0.15s',
|
|
441
|
-
display: 'flex', alignItems: 'center', gap: 3,
|
|
442
|
-
}}
|
|
443
|
-
>
|
|
444
|
-
{group.label}
|
|
445
|
-
{count > 0 && (
|
|
446
|
-
<span style={{
|
|
447
|
-
fontSize: 8, color: active ? group.color : '#374151',
|
|
448
|
-
fontWeight: 600,
|
|
449
|
-
}}>
|
|
450
|
-
{count}
|
|
451
|
-
</span>
|
|
452
|
-
)}
|
|
453
|
-
</button>
|
|
454
|
-
)
|
|
455
|
-
})}
|
|
456
|
-
<div style={{ flex: 1 }} />
|
|
457
|
-
<input
|
|
458
|
-
type="text"
|
|
459
|
-
value={search}
|
|
460
|
-
onChange={e => setSearch(e.target.value)}
|
|
461
|
-
placeholder="Search..."
|
|
462
|
-
style={{
|
|
463
|
-
width: 120, padding: '2px 6px', borderRadius: 3,
|
|
464
|
-
border: '1px solid #1e293b', background: '#0f172a',
|
|
465
|
-
color: '#e2e8f0', fontFamily: 'monospace', fontSize: 10,
|
|
466
|
-
outline: 'none',
|
|
467
|
-
}}
|
|
468
|
-
onFocus={e => { e.target.style.borderColor = '#334155' }}
|
|
469
|
-
onBlur={e => { e.target.style.borderColor = '#1e293b' }}
|
|
470
|
-
/>
|
|
471
|
-
</div>
|
|
472
|
-
)
|
|
473
|
-
}
|
|
474
|
-
|
|
475
|
-
// ===== Settings Panel =====
|
|
476
|
-
|
|
477
|
-
function SettingsPanel({
|
|
478
|
-
settings,
|
|
479
|
-
onChange,
|
|
480
|
-
}: {
|
|
481
|
-
settings: DebuggerSettings
|
|
482
|
-
onChange: (patch: Partial<DebuggerSettings>) => void
|
|
483
|
-
}) {
|
|
484
|
-
const sectionStyle: React.CSSProperties = {
|
|
485
|
-
marginBottom: 14,
|
|
486
|
-
}
|
|
487
|
-
const labelStyle: React.CSSProperties = {
|
|
488
|
-
fontFamily: 'monospace', fontSize: 9, color: '#64748b',
|
|
489
|
-
textTransform: 'uppercase', letterSpacing: 0.5, marginBottom: 6,
|
|
490
|
-
display: 'block',
|
|
491
|
-
}
|
|
492
|
-
const rowStyle: React.CSSProperties = {
|
|
493
|
-
display: 'flex', alignItems: 'center', justifyContent: 'space-between',
|
|
494
|
-
padding: '5px 0',
|
|
495
|
-
}
|
|
496
|
-
const descStyle: React.CSSProperties = {
|
|
497
|
-
fontFamily: 'monospace', fontSize: 10, color: '#94a3b8',
|
|
498
|
-
}
|
|
499
|
-
|
|
500
|
-
return (
|
|
501
|
-
<div style={{ flex: 1, overflow: 'auto', padding: 10 }}>
|
|
502
|
-
{/* Font Size */}
|
|
503
|
-
<div style={sectionStyle}>
|
|
504
|
-
<span style={labelStyle}>Font Size</span>
|
|
505
|
-
<div style={{ display: 'flex', gap: 4 }}>
|
|
506
|
-
{(['xs', 'sm', 'md', 'lg'] as const).map(size => (
|
|
507
|
-
<button
|
|
508
|
-
key={size}
|
|
509
|
-
onClick={() => onChange({ fontSize: size })}
|
|
510
|
-
style={{
|
|
511
|
-
flex: 1, padding: '5px 0', borderRadius: 4, border: 'none', cursor: 'pointer',
|
|
512
|
-
fontFamily: 'monospace', fontSize: FONT_SIZES[size], fontWeight: 600,
|
|
513
|
-
background: settings.fontSize === size ? '#1e3a5f' : '#1e293b',
|
|
514
|
-
color: settings.fontSize === size ? '#60a5fa' : '#64748b',
|
|
515
|
-
transition: 'all 0.15s',
|
|
516
|
-
}}
|
|
517
|
-
>
|
|
518
|
-
{size.toUpperCase()}
|
|
519
|
-
</button>
|
|
520
|
-
))}
|
|
521
|
-
</div>
|
|
522
|
-
<div style={{
|
|
523
|
-
fontFamily: 'monospace', fontSize: 9, color: '#475569',
|
|
524
|
-
marginTop: 4, textAlign: 'center',
|
|
525
|
-
}}>
|
|
526
|
-
Preview: {FONT_SIZES[settings.fontSize]}px
|
|
527
|
-
</div>
|
|
528
|
-
</div>
|
|
529
|
-
|
|
530
|
-
{/* Max Events */}
|
|
531
|
-
<div style={sectionStyle}>
|
|
532
|
-
<span style={labelStyle}>Max Events in Buffer</span>
|
|
533
|
-
<div style={{ display: 'flex', gap: 4 }}>
|
|
534
|
-
{[100, 300, 500, 1000].map(n => (
|
|
535
|
-
<button
|
|
536
|
-
key={n}
|
|
537
|
-
onClick={() => onChange({ maxEvents: n })}
|
|
538
|
-
style={{
|
|
539
|
-
flex: 1, padding: '5px 0', borderRadius: 4, border: 'none', cursor: 'pointer',
|
|
540
|
-
fontFamily: 'monospace', fontSize: 10, fontWeight: 600,
|
|
541
|
-
background: settings.maxEvents === n ? '#1e3a5f' : '#1e293b',
|
|
542
|
-
color: settings.maxEvents === n ? '#60a5fa' : '#64748b',
|
|
543
|
-
transition: 'all 0.15s',
|
|
544
|
-
}}
|
|
545
|
-
>
|
|
546
|
-
{n}
|
|
547
|
-
</button>
|
|
548
|
-
))}
|
|
549
|
-
</div>
|
|
550
|
-
</div>
|
|
551
|
-
|
|
552
|
-
{/* Toggles */}
|
|
553
|
-
<div style={sectionStyle}>
|
|
554
|
-
<span style={labelStyle}>Display</span>
|
|
555
|
-
|
|
556
|
-
<div style={rowStyle}>
|
|
557
|
-
<span style={descStyle}>Show timestamps</span>
|
|
558
|
-
<ToggleSwitch
|
|
559
|
-
checked={settings.showTimestamps}
|
|
560
|
-
onChange={v => onChange({ showTimestamps: v })}
|
|
561
|
-
/>
|
|
562
|
-
</div>
|
|
563
|
-
|
|
564
|
-
<div style={rowStyle}>
|
|
565
|
-
<span style={descStyle}>Compact mode</span>
|
|
566
|
-
<ToggleSwitch
|
|
567
|
-
checked={settings.compactMode}
|
|
568
|
-
onChange={v => onChange({ compactMode: v })}
|
|
569
|
-
/>
|
|
570
|
-
</div>
|
|
571
|
-
|
|
572
|
-
<div style={rowStyle}>
|
|
573
|
-
<span style={descStyle}>Word wrap in data</span>
|
|
574
|
-
<ToggleSwitch
|
|
575
|
-
checked={settings.wordWrap}
|
|
576
|
-
onChange={v => onChange({ wordWrap: v })}
|
|
577
|
-
/>
|
|
578
|
-
</div>
|
|
579
|
-
</div>
|
|
580
|
-
|
|
581
|
-
{/* Reset */}
|
|
582
|
-
<button
|
|
583
|
-
onClick={() => onChange(DEFAULT_SETTINGS)}
|
|
584
|
-
style={{
|
|
585
|
-
width: '100%', padding: '6px 0', borderRadius: 4, border: '1px solid #1e293b',
|
|
586
|
-
cursor: 'pointer', fontFamily: 'monospace', fontSize: 10,
|
|
587
|
-
background: 'transparent', color: '#64748b',
|
|
588
|
-
transition: 'all 0.15s',
|
|
589
|
-
}}
|
|
590
|
-
>
|
|
591
|
-
Reset to defaults
|
|
592
|
-
</button>
|
|
593
|
-
</div>
|
|
594
|
-
)
|
|
595
|
-
}
|
|
596
|
-
|
|
597
|
-
function ToggleSwitch({ checked, onChange }: { checked: boolean; onChange: (v: boolean) => void }) {
|
|
598
|
-
return (
|
|
599
|
-
<button
|
|
600
|
-
onClick={() => onChange(!checked)}
|
|
601
|
-
style={{
|
|
602
|
-
width: 32, height: 18, borderRadius: 9, border: 'none', cursor: 'pointer',
|
|
603
|
-
background: checked ? '#2563eb' : '#334155',
|
|
604
|
-
position: 'relative', padding: 0, transition: 'background 0.2s',
|
|
605
|
-
flexShrink: 0,
|
|
606
|
-
}}
|
|
607
|
-
>
|
|
608
|
-
<span style={{
|
|
609
|
-
position: 'absolute', top: 2, left: checked ? 16 : 2,
|
|
610
|
-
width: 14, height: 14, borderRadius: '50%',
|
|
611
|
-
background: '#fff', transition: 'left 0.2s',
|
|
612
|
-
boxShadow: '0 1px 3px rgba(0,0,0,0.3)',
|
|
613
|
-
}} />
|
|
614
|
-
</button>
|
|
615
|
-
)
|
|
616
|
-
}
|
|
617
|
-
|
|
618
|
-
// ===== Main Component =====
|
|
619
|
-
|
|
620
|
-
export interface LiveDebuggerProps {
|
|
621
|
-
/** Start open. Default: false */
|
|
622
|
-
defaultOpen?: boolean
|
|
623
|
-
/** Initial position. Default: bottom-right corner */
|
|
624
|
-
defaultPosition?: { x: number; y: number }
|
|
625
|
-
/** Initial size. Default: 680x420 */
|
|
626
|
-
defaultSize?: { w: number; h: number }
|
|
627
|
-
/** Force enable even in production. Default: false */
|
|
628
|
-
force?: boolean
|
|
629
|
-
}
|
|
630
|
-
|
|
631
|
-
export function LiveDebugger({
|
|
632
|
-
defaultOpen = false,
|
|
633
|
-
defaultPosition,
|
|
634
|
-
defaultSize = { w: 680, h: 420 },
|
|
635
|
-
force = false,
|
|
636
|
-
}: LiveDebuggerProps) {
|
|
637
|
-
const [settings, setSettingsState] = useState<DebuggerSettings>(loadSettings)
|
|
638
|
-
const updateSettings = useCallback((patch: Partial<DebuggerSettings>) => {
|
|
639
|
-
setSettingsState(prev => {
|
|
640
|
-
const next = { ...prev, ...patch }
|
|
641
|
-
saveSettings(next)
|
|
642
|
-
return next
|
|
643
|
-
})
|
|
644
|
-
}, [])
|
|
645
|
-
const fs = FONT_SIZES[settings.fontSize]
|
|
646
|
-
|
|
647
|
-
const dbg = useLiveDebugger({ maxEvents: settings.maxEvents })
|
|
648
|
-
const [open, setOpen] = useState(defaultOpen)
|
|
649
|
-
const [tab, setTab] = useState<'events' | 'state' | 'rooms' | 'settings'>('events')
|
|
650
|
-
const [selectedId, setSelectedId] = useState<string | null>(null)
|
|
651
|
-
const [expandedEvents, setExpandedEvents] = useState<Set<string>>(new Set())
|
|
652
|
-
const [activeGroups, setActiveGroups] = useState<Set<EventGroup>>(new Set(ALL_GROUPS))
|
|
653
|
-
const [search, setSearch] = useState('')
|
|
654
|
-
const feedRef = useRef<HTMLDivElement>(null)
|
|
655
|
-
const [autoScroll, setAutoScroll] = useState(true)
|
|
656
|
-
|
|
657
|
-
// Default position: bottom-right with padding
|
|
658
|
-
const initPos = defaultPosition ?? {
|
|
659
|
-
x: typeof window !== 'undefined' ? window.innerWidth - defaultSize.w - 16 : 100,
|
|
660
|
-
y: typeof window !== 'undefined' ? window.innerHeight - defaultSize.h - 16 : 100,
|
|
661
|
-
}
|
|
662
|
-
|
|
663
|
-
const { pos, onMouseDown } = useDrag(initPos)
|
|
664
|
-
const { size, onResizeStart } = useResize(defaultSize)
|
|
665
|
-
|
|
666
|
-
// Keyboard shortcut: Ctrl+Shift+D
|
|
667
|
-
useEffect(() => {
|
|
668
|
-
const handler = (e: KeyboardEvent) => {
|
|
669
|
-
if (e.ctrlKey && e.shiftKey && e.key === 'D') {
|
|
670
|
-
e.preventDefault()
|
|
671
|
-
setOpen(prev => !prev)
|
|
672
|
-
}
|
|
673
|
-
}
|
|
674
|
-
window.addEventListener('keydown', handler)
|
|
675
|
-
return () => window.removeEventListener('keydown', handler)
|
|
676
|
-
}, [])
|
|
677
|
-
|
|
678
|
-
const selectedComp = selectedId
|
|
679
|
-
? dbg.components.find(c => c.componentId === selectedId) ?? null
|
|
680
|
-
: null
|
|
681
|
-
|
|
682
|
-
// Compute allowed event types from active groups
|
|
683
|
-
const allowedTypes = useMemo(() => {
|
|
684
|
-
const types = new Set<DebugEventType>()
|
|
685
|
-
for (const g of activeGroups) {
|
|
686
|
-
for (const t of EVENT_GROUPS[g].types) types.add(t)
|
|
687
|
-
}
|
|
688
|
-
return types
|
|
689
|
-
}, [activeGroups])
|
|
690
|
-
|
|
691
|
-
// Filter events
|
|
692
|
-
const visibleEvents = useMemo(() => {
|
|
693
|
-
return dbg.events.filter(e => {
|
|
694
|
-
if (!allowedTypes.has(e.type)) return false
|
|
695
|
-
if (selectedId && e.componentId !== selectedId) return false
|
|
696
|
-
if (search) {
|
|
697
|
-
const s = search.toLowerCase()
|
|
698
|
-
const inData = JSON.stringify(e.data).toLowerCase().includes(s)
|
|
699
|
-
const inName = e.componentName?.toLowerCase().includes(s)
|
|
700
|
-
const inType = e.type.toLowerCase().includes(s)
|
|
701
|
-
if (!inData && !inName && !inType) return false
|
|
702
|
-
}
|
|
703
|
-
return true
|
|
704
|
-
})
|
|
705
|
-
}, [dbg.events, allowedTypes, selectedId, search])
|
|
706
|
-
|
|
707
|
-
// Count events per group (unfiltered by group, but filtered by component)
|
|
708
|
-
const groupCounts = useMemo(() => {
|
|
709
|
-
const counts = {} as Record<EventGroup, number>
|
|
710
|
-
for (const g of ALL_GROUPS) counts[g] = 0
|
|
711
|
-
const baseEvents = selectedId
|
|
712
|
-
? dbg.events.filter(e => e.componentId === selectedId)
|
|
713
|
-
: dbg.events
|
|
714
|
-
for (const e of baseEvents) {
|
|
715
|
-
for (const g of ALL_GROUPS) {
|
|
716
|
-
if (EVENT_GROUPS[g].types.includes(e.type)) {
|
|
717
|
-
counts[g]++
|
|
718
|
-
break
|
|
719
|
-
}
|
|
720
|
-
}
|
|
721
|
-
}
|
|
722
|
-
return counts
|
|
723
|
-
}, [dbg.events, selectedId])
|
|
724
|
-
|
|
725
|
-
// Build rooms map: roomId -> list of components in that room
|
|
726
|
-
const roomsMap = useMemo(() => {
|
|
727
|
-
const map = new Map<string, ComponentSnapshot[]>()
|
|
728
|
-
for (const comp of dbg.components) {
|
|
729
|
-
for (const roomId of comp.rooms) {
|
|
730
|
-
if (!map.has(roomId)) map.set(roomId, [])
|
|
731
|
-
map.get(roomId)!.push(comp)
|
|
732
|
-
}
|
|
733
|
-
}
|
|
734
|
-
return map
|
|
735
|
-
}, [dbg.components])
|
|
736
|
-
|
|
737
|
-
// Room events (filtered to room-related types)
|
|
738
|
-
const roomEvents = useMemo(() => {
|
|
739
|
-
return dbg.events.filter(e =>
|
|
740
|
-
e.type === 'ROOM_JOIN' || e.type === 'ROOM_LEAVE' ||
|
|
741
|
-
e.type === 'ROOM_EMIT' || e.type === 'ROOM_EVENT_RECEIVED'
|
|
742
|
-
).slice(-50)
|
|
743
|
-
}, [dbg.events])
|
|
744
|
-
|
|
745
|
-
// Auto-scroll feed
|
|
746
|
-
useEffect(() => {
|
|
747
|
-
if (feedRef.current && autoScroll && !dbg.paused) {
|
|
748
|
-
feedRef.current.scrollTop = feedRef.current.scrollHeight
|
|
749
|
-
}
|
|
750
|
-
}, [visibleEvents.length, dbg.paused, autoScroll])
|
|
751
|
-
|
|
752
|
-
// Detect manual scroll to disable auto-scroll
|
|
753
|
-
const handleFeedScroll = useCallback(() => {
|
|
754
|
-
if (!feedRef.current) return
|
|
755
|
-
const { scrollTop, scrollHeight, clientHeight } = feedRef.current
|
|
756
|
-
const atBottom = scrollHeight - scrollTop - clientHeight < 30
|
|
757
|
-
setAutoScroll(atBottom)
|
|
758
|
-
}, [])
|
|
759
|
-
|
|
760
|
-
const toggleEvent = useCallback((id: string) => {
|
|
761
|
-
setExpandedEvents(prev => {
|
|
762
|
-
const next = new Set(prev)
|
|
763
|
-
if (next.has(id)) next.delete(id)
|
|
764
|
-
else next.add(id)
|
|
765
|
-
return next
|
|
766
|
-
})
|
|
767
|
-
}, [])
|
|
768
|
-
|
|
769
|
-
const toggleGroup = useCallback((g: EventGroup) => {
|
|
770
|
-
setActiveGroups(prev => {
|
|
771
|
-
const next = new Set(prev)
|
|
772
|
-
if (next.has(g)) next.delete(g)
|
|
773
|
-
else next.add(g)
|
|
774
|
-
return next
|
|
775
|
-
})
|
|
776
|
-
}, [])
|
|
777
|
-
|
|
778
|
-
// Server has debugging disabled — render nothing, no resources used
|
|
779
|
-
if (dbg.serverDisabled && !force) return null
|
|
780
|
-
|
|
781
|
-
// Badge (when closed) - small floating circle in bottom-right
|
|
782
|
-
if (!open) {
|
|
783
|
-
const hasErrors = dbg.events.some(e => e.type === 'ERROR' || e.type === 'ACTION_ERROR')
|
|
784
|
-
return (
|
|
785
|
-
<button
|
|
786
|
-
onClick={() => setOpen(true)}
|
|
787
|
-
title="Live Debugger (Ctrl+Shift+D)"
|
|
788
|
-
style={{
|
|
789
|
-
position: 'fixed',
|
|
790
|
-
bottom: 16,
|
|
791
|
-
right: 16,
|
|
792
|
-
zIndex: 99999,
|
|
793
|
-
width: 40,
|
|
794
|
-
height: 40,
|
|
795
|
-
borderRadius: '50%',
|
|
796
|
-
border: `2px solid ${dbg.connected ? '#22c55e50' : '#ef444450'}`,
|
|
797
|
-
background: '#020617e0',
|
|
798
|
-
color: dbg.connected ? '#22c55e' : '#ef4444',
|
|
799
|
-
cursor: 'pointer',
|
|
800
|
-
display: 'flex',
|
|
801
|
-
alignItems: 'center',
|
|
802
|
-
justifyContent: 'center',
|
|
803
|
-
fontSize: 16,
|
|
804
|
-
boxShadow: '0 2px 12px rgba(0,0,0,0.4)',
|
|
805
|
-
transition: 'all 0.2s',
|
|
806
|
-
fontFamily: 'monospace',
|
|
807
|
-
padding: 0,
|
|
808
|
-
}}
|
|
809
|
-
>
|
|
810
|
-
{hasErrors ? (
|
|
811
|
-
<span style={{ color: '#ef4444', fontSize: 14, fontWeight: 700 }}>!</span>
|
|
812
|
-
) : (
|
|
813
|
-
<span style={{ fontSize: 14 }}>{dbg.componentCount}</span>
|
|
814
|
-
)}
|
|
815
|
-
</button>
|
|
816
|
-
)
|
|
817
|
-
}
|
|
818
|
-
|
|
819
|
-
// Floating window
|
|
820
|
-
return (
|
|
821
|
-
<div
|
|
822
|
-
style={{
|
|
823
|
-
position: 'fixed',
|
|
824
|
-
left: pos.x,
|
|
825
|
-
top: pos.y,
|
|
826
|
-
width: size.w,
|
|
827
|
-
height: size.h,
|
|
828
|
-
zIndex: 99999,
|
|
829
|
-
display: 'flex',
|
|
830
|
-
flexDirection: 'column',
|
|
831
|
-
background: '#020617',
|
|
832
|
-
border: '1px solid #1e293b',
|
|
833
|
-
borderRadius: 8,
|
|
834
|
-
boxShadow: '0 8px 32px rgba(0,0,0,0.6)',
|
|
835
|
-
fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif',
|
|
836
|
-
color: '#e2e8f0',
|
|
837
|
-
overflow: 'hidden',
|
|
838
|
-
}}
|
|
839
|
-
>
|
|
840
|
-
{/* Title bar - draggable */}
|
|
841
|
-
<div
|
|
842
|
-
onMouseDown={onMouseDown}
|
|
843
|
-
style={{
|
|
844
|
-
display: 'flex', alignItems: 'center', gap: 8,
|
|
845
|
-
padding: '5px 10px', borderBottom: '1px solid #1e293b',
|
|
846
|
-
flexShrink: 0, cursor: 'grab', userSelect: 'none',
|
|
847
|
-
background: '#0f172a',
|
|
848
|
-
}}
|
|
849
|
-
>
|
|
850
|
-
{/* Status dot */}
|
|
851
|
-
<span style={{
|
|
852
|
-
width: 7, height: 7, borderRadius: '50%', flexShrink: 0,
|
|
853
|
-
background: dbg.connected ? '#22c55e' : '#ef4444',
|
|
854
|
-
}} />
|
|
855
|
-
|
|
856
|
-
<span style={{
|
|
857
|
-
fontFamily: 'monospace', fontSize: 11, fontWeight: 700,
|
|
858
|
-
letterSpacing: 0.5, color: '#94a3b8',
|
|
859
|
-
}}>
|
|
860
|
-
LIVE DEBUGGER
|
|
861
|
-
</span>
|
|
862
|
-
|
|
863
|
-
{/* Tabs */}
|
|
864
|
-
<div style={{ display: 'flex', gap: 1, marginLeft: 4 }}>
|
|
865
|
-
{(['events', 'state', 'rooms', 'settings'] as const).map(t => (
|
|
866
|
-
<button
|
|
867
|
-
key={t}
|
|
868
|
-
onClick={() => setTab(t)}
|
|
869
|
-
style={{
|
|
870
|
-
padding: '2px 8px', borderRadius: 3, border: 'none', cursor: 'pointer',
|
|
871
|
-
fontFamily: 'monospace', fontSize: 10, textTransform: 'uppercase',
|
|
872
|
-
background: tab === t ? '#1e293b' : 'transparent',
|
|
873
|
-
color: tab === t
|
|
874
|
-
? (t === 'settings' ? '#f59e0b' : '#e2e8f0')
|
|
875
|
-
: '#475569',
|
|
876
|
-
}}
|
|
877
|
-
>
|
|
878
|
-
{t === 'settings' ? '\u2699' : t}
|
|
879
|
-
</button>
|
|
880
|
-
))}
|
|
881
|
-
</div>
|
|
882
|
-
|
|
883
|
-
<div style={{ flex: 1 }} />
|
|
884
|
-
|
|
885
|
-
{/* Controls */}
|
|
886
|
-
<button
|
|
887
|
-
onClick={dbg.togglePause}
|
|
888
|
-
title={dbg.paused ? 'Resume' : 'Pause'}
|
|
889
|
-
style={{
|
|
890
|
-
padding: '2px 6px', borderRadius: 3, border: 'none', cursor: 'pointer',
|
|
891
|
-
fontFamily: 'monospace', fontSize: 10,
|
|
892
|
-
background: dbg.paused ? '#7c2d12' : '#1e293b',
|
|
893
|
-
color: dbg.paused ? '#fdba74' : '#94a3b8',
|
|
894
|
-
}}
|
|
895
|
-
>
|
|
896
|
-
{dbg.paused ? '\u25B6' : '\u23F8'}
|
|
897
|
-
</button>
|
|
898
|
-
<button
|
|
899
|
-
onClick={dbg.clearEvents}
|
|
900
|
-
title="Clear events"
|
|
901
|
-
style={{
|
|
902
|
-
padding: '2px 6px', borderRadius: 3, border: 'none', cursor: 'pointer',
|
|
903
|
-
fontFamily: 'monospace', fontSize: 10,
|
|
904
|
-
background: '#1e293b', color: '#94a3b8',
|
|
905
|
-
}}
|
|
906
|
-
>
|
|
907
|
-
Clear
|
|
908
|
-
</button>
|
|
909
|
-
<span style={{ fontFamily: 'monospace', fontSize: 9, color: '#475569' }}>
|
|
910
|
-
{dbg.componentCount}C {visibleEvents.length}/{dbg.eventCount}E
|
|
911
|
-
</span>
|
|
912
|
-
<button
|
|
913
|
-
onClick={() => setOpen(false)}
|
|
914
|
-
title="Close (Ctrl+Shift+D)"
|
|
915
|
-
style={{
|
|
916
|
-
width: 18, height: 18, borderRadius: 3, border: 'none', cursor: 'pointer',
|
|
917
|
-
fontFamily: 'monospace', fontSize: 12, lineHeight: '18px',
|
|
918
|
-
background: 'transparent', color: '#64748b',
|
|
919
|
-
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
|
920
|
-
padding: 0,
|
|
921
|
-
}}
|
|
922
|
-
>
|
|
923
|
-
✕
|
|
924
|
-
</button>
|
|
925
|
-
</div>
|
|
926
|
-
|
|
927
|
-
{/* Body */}
|
|
928
|
-
<div style={{ display: 'flex', flex: 1, overflow: 'hidden', minHeight: 0 }}>
|
|
929
|
-
{/* Component sidebar */}
|
|
930
|
-
<div style={{
|
|
931
|
-
width: 160, borderRight: '1px solid #1e293b',
|
|
932
|
-
overflow: 'auto', flexShrink: 0, padding: '3px',
|
|
933
|
-
}}>
|
|
934
|
-
<button
|
|
935
|
-
onClick={() => setSelectedId(null)}
|
|
936
|
-
style={{
|
|
937
|
-
width: '100%', textAlign: 'left', cursor: 'pointer',
|
|
938
|
-
padding: '5px 8px', borderRadius: 4, border: 'none',
|
|
939
|
-
background: selectedId === null ? '#1e293b' : 'transparent',
|
|
940
|
-
color: '#94a3b8', fontFamily: 'monospace', fontSize: 10,
|
|
941
|
-
}}
|
|
942
|
-
>
|
|
943
|
-
All ({dbg.componentCount})
|
|
944
|
-
</button>
|
|
945
|
-
|
|
946
|
-
{dbg.components.map(comp => (
|
|
947
|
-
<ComponentCard
|
|
948
|
-
key={comp.componentId}
|
|
949
|
-
comp={comp}
|
|
950
|
-
isSelected={selectedId === comp.componentId}
|
|
951
|
-
onSelect={() => {
|
|
952
|
-
setSelectedId(selectedId === comp.componentId ? null : comp.componentId)
|
|
953
|
-
}}
|
|
954
|
-
/>
|
|
955
|
-
))}
|
|
956
|
-
|
|
957
|
-
{dbg.components.length === 0 && (
|
|
958
|
-
<div style={{
|
|
959
|
-
padding: 10, textAlign: 'center', color: '#475569',
|
|
960
|
-
fontFamily: 'monospace', fontSize: 10,
|
|
961
|
-
}}>
|
|
962
|
-
No components
|
|
963
|
-
</div>
|
|
964
|
-
)}
|
|
965
|
-
</div>
|
|
966
|
-
|
|
967
|
-
{/* Main content */}
|
|
968
|
-
<div style={{ flex: 1, display: 'flex', flexDirection: 'column', overflow: 'hidden', minWidth: 0 }}>
|
|
969
|
-
{/* === Events Tab === */}
|
|
970
|
-
{tab === 'events' && (
|
|
971
|
-
<>
|
|
972
|
-
<FilterBar
|
|
973
|
-
activeGroups={activeGroups}
|
|
974
|
-
toggleGroup={toggleGroup}
|
|
975
|
-
search={search}
|
|
976
|
-
setSearch={setSearch}
|
|
977
|
-
groupCounts={groupCounts}
|
|
978
|
-
/>
|
|
979
|
-
<div
|
|
980
|
-
ref={feedRef}
|
|
981
|
-
onScroll={handleFeedScroll}
|
|
982
|
-
style={{ flex: 1, overflow: 'auto' }}
|
|
983
|
-
>
|
|
984
|
-
{visibleEvents.length === 0 ? (
|
|
985
|
-
<div style={{
|
|
986
|
-
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
|
987
|
-
height: '100%', color: '#475569', fontFamily: 'monospace', fontSize: 11,
|
|
988
|
-
}}>
|
|
989
|
-
{dbg.connected
|
|
990
|
-
? dbg.paused ? 'Paused' : 'Waiting for events...'
|
|
991
|
-
: 'Connecting...'}
|
|
992
|
-
</div>
|
|
993
|
-
) : (
|
|
994
|
-
visibleEvents.map(event => {
|
|
995
|
-
const color = COLORS[event.type] || '#6b7280'
|
|
996
|
-
const label = LABELS[event.type] || event.type
|
|
997
|
-
const summary = eventSummary(event)
|
|
998
|
-
const isExpanded = expandedEvents.has(event.id)
|
|
999
|
-
const time = new Date(event.timestamp)
|
|
1000
|
-
const ts = `${time.toLocaleTimeString('en-US', { hour12: false })}.${String(time.getMilliseconds()).padStart(3, '0')}`
|
|
1001
|
-
const py = settings.compactMode ? 1 : 3
|
|
1002
|
-
|
|
1003
|
-
return (
|
|
1004
|
-
<div
|
|
1005
|
-
key={event.id}
|
|
1006
|
-
style={{ borderBottom: '1px solid #0f172a', cursor: 'pointer' }}
|
|
1007
|
-
onClick={() => toggleEvent(event.id)}
|
|
1008
|
-
>
|
|
1009
|
-
<div style={{
|
|
1010
|
-
display: 'flex', alignItems: 'center', gap: 5,
|
|
1011
|
-
padding: `${py}px 8px`, fontSize: fs, fontFamily: 'monospace',
|
|
1012
|
-
background: isExpanded ? '#0f172a' : 'transparent',
|
|
1013
|
-
}}>
|
|
1014
|
-
{settings.showTimestamps && (
|
|
1015
|
-
<span style={{ color: '#4b5563', fontSize: fs - 1, flexShrink: 0 }}>{ts}</span>
|
|
1016
|
-
)}
|
|
1017
|
-
<span style={{
|
|
1018
|
-
display: 'inline-block', padding: '0 4px', borderRadius: 2,
|
|
1019
|
-
fontSize: Math.max(8, fs - 2), fontWeight: 700, color: '#fff',
|
|
1020
|
-
background: color, flexShrink: 0, lineHeight: `${fs + 4}px`,
|
|
1021
|
-
}}>
|
|
1022
|
-
{label}
|
|
1023
|
-
</span>
|
|
1024
|
-
{!selectedId && event.componentName && (() => {
|
|
1025
|
-
const comp = dbg.components.find(c => c.componentId === event.componentId)
|
|
1026
|
-
const name = comp?.debugLabel || event.componentName
|
|
1027
|
-
return (
|
|
1028
|
-
<span style={{ color: '#64748b', flexShrink: 0, fontSize: fs - 1 }}>
|
|
1029
|
-
{name}
|
|
1030
|
-
</span>
|
|
1031
|
-
)
|
|
1032
|
-
})()}
|
|
1033
|
-
<span style={{
|
|
1034
|
-
color: '#94a3b8', fontSize: fs - 1, overflow: 'hidden',
|
|
1035
|
-
textOverflow: 'ellipsis',
|
|
1036
|
-
whiteSpace: settings.wordWrap ? 'normal' : 'nowrap',
|
|
1037
|
-
flex: 1,
|
|
1038
|
-
}}>
|
|
1039
|
-
{summary}
|
|
1040
|
-
</span>
|
|
1041
|
-
</div>
|
|
1042
|
-
{isExpanded && (
|
|
1043
|
-
<div
|
|
1044
|
-
onClick={e => e.stopPropagation()}
|
|
1045
|
-
style={{
|
|
1046
|
-
padding: '3px 8px 6px 42px', fontSize: fs - 1,
|
|
1047
|
-
fontFamily: 'monospace', color: '#cbd5e1',
|
|
1048
|
-
background: '#0f172a',
|
|
1049
|
-
wordBreak: settings.wordWrap ? 'break-all' : undefined,
|
|
1050
|
-
}}>
|
|
1051
|
-
<Json data={event.data} />
|
|
1052
|
-
</div>
|
|
1053
|
-
)}
|
|
1054
|
-
</div>
|
|
1055
|
-
)
|
|
1056
|
-
})
|
|
1057
|
-
)}
|
|
1058
|
-
{!autoScroll && visibleEvents.length > 0 && (
|
|
1059
|
-
<button
|
|
1060
|
-
onClick={() => {
|
|
1061
|
-
setAutoScroll(true)
|
|
1062
|
-
if (feedRef.current) feedRef.current.scrollTop = feedRef.current.scrollHeight
|
|
1063
|
-
}}
|
|
1064
|
-
style={{
|
|
1065
|
-
position: 'sticky', bottom: 4, left: '50%',
|
|
1066
|
-
transform: 'translateX(-50%)',
|
|
1067
|
-
padding: '3px 10px', borderRadius: 10,
|
|
1068
|
-
border: '1px solid #1e293b',
|
|
1069
|
-
background: '#0f172ae0', color: '#94a3b8',
|
|
1070
|
-
fontFamily: 'monospace', fontSize: 9, cursor: 'pointer',
|
|
1071
|
-
}}
|
|
1072
|
-
>
|
|
1073
|
-
↓ Scroll to bottom
|
|
1074
|
-
</button>
|
|
1075
|
-
)}
|
|
1076
|
-
</div>
|
|
1077
|
-
</>
|
|
1078
|
-
)}
|
|
1079
|
-
|
|
1080
|
-
{/* === State Tab === */}
|
|
1081
|
-
{tab === 'state' && (
|
|
1082
|
-
<div style={{ flex: 1, overflow: 'auto', padding: 10 }}>
|
|
1083
|
-
{selectedComp ? (
|
|
1084
|
-
<div>
|
|
1085
|
-
<div style={{
|
|
1086
|
-
fontFamily: 'monospace', fontSize: 12, fontWeight: 700,
|
|
1087
|
-
marginBottom: 2, color: '#f1f5f9',
|
|
1088
|
-
}}>
|
|
1089
|
-
{displayName(selectedComp)}
|
|
1090
|
-
</div>
|
|
1091
|
-
{selectedComp.debugLabel && (
|
|
1092
|
-
<div style={{
|
|
1093
|
-
fontFamily: 'monospace', fontSize: 9, color: '#64748b',
|
|
1094
|
-
marginBottom: 6,
|
|
1095
|
-
}}>
|
|
1096
|
-
{selectedComp.componentName}
|
|
1097
|
-
</div>
|
|
1098
|
-
)}
|
|
1099
|
-
|
|
1100
|
-
<div style={{
|
|
1101
|
-
display: 'flex', gap: 4, marginBottom: 10, flexWrap: 'wrap',
|
|
1102
|
-
fontFamily: 'monospace', fontSize: 9,
|
|
1103
|
-
}}>
|
|
1104
|
-
{[
|
|
1105
|
-
{ label: 'state', value: selectedComp.stateChangeCount, color: '#3b82f6' },
|
|
1106
|
-
{ label: 'actions', value: selectedComp.actionCount, color: '#8b5cf6' },
|
|
1107
|
-
{ label: 'errors', value: selectedComp.errorCount, color: selectedComp.errorCount > 0 ? '#ef4444' : '#22c55e' },
|
|
1108
|
-
].map(s => (
|
|
1109
|
-
<span key={s.label} style={{
|
|
1110
|
-
padding: '2px 6px', borderRadius: 3, background: '#1e293b',
|
|
1111
|
-
}}>
|
|
1112
|
-
<span style={{ color: s.color }}>{s.value}</span>
|
|
1113
|
-
<span style={{ color: '#64748b' }}> {s.label}</span>
|
|
1114
|
-
</span>
|
|
1115
|
-
))}
|
|
1116
|
-
{selectedComp.rooms.length > 0 && (
|
|
1117
|
-
<span style={{
|
|
1118
|
-
padding: '2px 6px', borderRadius: 3, background: '#1e293b', color: '#64748b',
|
|
1119
|
-
}}>
|
|
1120
|
-
rooms: {selectedComp.rooms.join(', ')}
|
|
1121
|
-
</span>
|
|
1122
|
-
)}
|
|
1123
|
-
</div>
|
|
1124
|
-
|
|
1125
|
-
<div style={{
|
|
1126
|
-
fontFamily: 'monospace', fontSize: 9, color: '#475569', marginBottom: 8,
|
|
1127
|
-
}}>
|
|
1128
|
-
{selectedComp.componentId}
|
|
1129
|
-
</div>
|
|
1130
|
-
|
|
1131
|
-
<div style={{
|
|
1132
|
-
fontFamily: 'monospace', fontSize: 9, color: '#64748b',
|
|
1133
|
-
textTransform: 'uppercase', letterSpacing: 0.5, marginBottom: 4,
|
|
1134
|
-
}}>
|
|
1135
|
-
Current State
|
|
1136
|
-
</div>
|
|
1137
|
-
<div style={{
|
|
1138
|
-
padding: 8, background: '#0f172a', borderRadius: 4,
|
|
1139
|
-
fontFamily: 'monospace', fontSize: fs, color: '#e2e8f0',
|
|
1140
|
-
overflow: 'auto',
|
|
1141
|
-
wordBreak: settings.wordWrap ? 'break-all' : undefined,
|
|
1142
|
-
}}>
|
|
1143
|
-
<Json data={selectedComp.state} />
|
|
1144
|
-
</div>
|
|
1145
|
-
</div>
|
|
1146
|
-
) : (
|
|
1147
|
-
<div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
|
|
1148
|
-
{dbg.components.length === 0 ? (
|
|
1149
|
-
<div style={{
|
|
1150
|
-
textAlign: 'center', color: '#475569',
|
|
1151
|
-
fontFamily: 'monospace', fontSize: 11, padding: 20,
|
|
1152
|
-
}}>
|
|
1153
|
-
No active components
|
|
1154
|
-
</div>
|
|
1155
|
-
) : (
|
|
1156
|
-
dbg.components.map(comp => (
|
|
1157
|
-
<div key={comp.componentId} style={{
|
|
1158
|
-
padding: 8, background: '#0f172a', borderRadius: 4,
|
|
1159
|
-
}}>
|
|
1160
|
-
<div style={{
|
|
1161
|
-
display: 'flex', alignItems: 'center', gap: 6, marginBottom: 4,
|
|
1162
|
-
}}>
|
|
1163
|
-
<span style={{
|
|
1164
|
-
fontFamily: 'monospace', fontSize: 11, fontWeight: 700, color: '#f1f5f9',
|
|
1165
|
-
}}>
|
|
1166
|
-
{displayName(comp)}
|
|
1167
|
-
</span>
|
|
1168
|
-
<span style={{ fontFamily: 'monospace', fontSize: 9, color: '#475569' }}>
|
|
1169
|
-
S:{comp.stateChangeCount} A:{comp.actionCount}
|
|
1170
|
-
{comp.errorCount > 0 && <span style={{ color: '#f87171' }}> E:{comp.errorCount}</span>}
|
|
1171
|
-
</span>
|
|
1172
|
-
</div>
|
|
1173
|
-
<div style={{ fontFamily: 'monospace', fontSize: fs, color: '#e2e8f0' }}>
|
|
1174
|
-
<Json data={comp.state} />
|
|
1175
|
-
</div>
|
|
1176
|
-
</div>
|
|
1177
|
-
))
|
|
1178
|
-
)}
|
|
1179
|
-
</div>
|
|
1180
|
-
)}
|
|
1181
|
-
</div>
|
|
1182
|
-
)}
|
|
1183
|
-
|
|
1184
|
-
{/* === Rooms Tab === */}
|
|
1185
|
-
{tab === 'rooms' && (
|
|
1186
|
-
<div style={{ flex: 1, overflow: 'auto', padding: 10, display: 'flex', flexDirection: 'column', gap: 8 }}>
|
|
1187
|
-
{roomsMap.size === 0 ? (
|
|
1188
|
-
<div style={{
|
|
1189
|
-
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
|
1190
|
-
height: '100%', color: '#475569', fontFamily: 'monospace', fontSize: 11,
|
|
1191
|
-
}}>
|
|
1192
|
-
No active rooms
|
|
1193
|
-
</div>
|
|
1194
|
-
) : (
|
|
1195
|
-
<>
|
|
1196
|
-
{/* Room cards */}
|
|
1197
|
-
{Array.from(roomsMap.entries()).map(([roomId, members]) => (
|
|
1198
|
-
<div key={roomId} style={{
|
|
1199
|
-
padding: 10, background: '#0f172a', borderRadius: 6,
|
|
1200
|
-
border: '1px solid #1e293b',
|
|
1201
|
-
}}>
|
|
1202
|
-
{/* Room header */}
|
|
1203
|
-
<div style={{
|
|
1204
|
-
display: 'flex', alignItems: 'center', gap: 8, marginBottom: 8,
|
|
1205
|
-
}}>
|
|
1206
|
-
<span style={{
|
|
1207
|
-
width: 8, height: 8, borderRadius: '50%',
|
|
1208
|
-
background: '#10b981', flexShrink: 0,
|
|
1209
|
-
}} />
|
|
1210
|
-
<span style={{
|
|
1211
|
-
fontFamily: 'monospace', fontSize: 12, fontWeight: 700, color: '#f1f5f9',
|
|
1212
|
-
}}>
|
|
1213
|
-
{roomId}
|
|
1214
|
-
</span>
|
|
1215
|
-
<span style={{
|
|
1216
|
-
fontFamily: 'monospace', fontSize: 9, color: '#64748b',
|
|
1217
|
-
padding: '1px 6px', borderRadius: 3, background: '#1e293b',
|
|
1218
|
-
}}>
|
|
1219
|
-
{members.length} {members.length === 1 ? 'member' : 'members'}
|
|
1220
|
-
</span>
|
|
1221
|
-
</div>
|
|
1222
|
-
|
|
1223
|
-
{/* Members list */}
|
|
1224
|
-
<div style={{
|
|
1225
|
-
display: 'flex', flexDirection: 'column', gap: 4,
|
|
1226
|
-
paddingLeft: 16,
|
|
1227
|
-
}}>
|
|
1228
|
-
{members.map(comp => (
|
|
1229
|
-
<div key={comp.componentId} style={{
|
|
1230
|
-
display: 'flex', alignItems: 'center', gap: 6,
|
|
1231
|
-
fontFamily: 'monospace', fontSize: 10,
|
|
1232
|
-
}}>
|
|
1233
|
-
<span style={{ color: '#22c55e', fontSize: 5 }}>●</span>
|
|
1234
|
-
<span style={{ color: '#e2e8f0' }}>{displayName(comp)}</span>
|
|
1235
|
-
{comp.debugLabel && (
|
|
1236
|
-
<span style={{ color: '#475569', fontSize: 9 }}>({comp.componentName})</span>
|
|
1237
|
-
)}
|
|
1238
|
-
<span style={{ color: '#475569', fontSize: 9 }}>
|
|
1239
|
-
S:{comp.stateChangeCount} A:{comp.actionCount}
|
|
1240
|
-
</span>
|
|
1241
|
-
</div>
|
|
1242
|
-
))}
|
|
1243
|
-
</div>
|
|
1244
|
-
</div>
|
|
1245
|
-
))}
|
|
1246
|
-
|
|
1247
|
-
{/* Recent room events */}
|
|
1248
|
-
{roomEvents.length > 0 && (
|
|
1249
|
-
<div style={{ marginTop: 4 }}>
|
|
1250
|
-
<div style={{
|
|
1251
|
-
fontFamily: 'monospace', fontSize: 9, color: '#64748b',
|
|
1252
|
-
textTransform: 'uppercase', letterSpacing: 0.5, marginBottom: 6,
|
|
1253
|
-
}}>
|
|
1254
|
-
Recent Room Activity
|
|
1255
|
-
</div>
|
|
1256
|
-
<div style={{
|
|
1257
|
-
background: '#0f172a', borderRadius: 4, border: '1px solid #1e293b',
|
|
1258
|
-
overflow: 'auto', maxHeight: 160,
|
|
1259
|
-
}}>
|
|
1260
|
-
{roomEvents.map(event => {
|
|
1261
|
-
const color = COLORS[event.type] || '#6b7280'
|
|
1262
|
-
const label = LABELS[event.type] || event.type
|
|
1263
|
-
const summary = eventSummary(event)
|
|
1264
|
-
const time = new Date(event.timestamp)
|
|
1265
|
-
const ts = `${time.toLocaleTimeString('en-US', { hour12: false })}.${String(time.getMilliseconds()).padStart(3, '0')}`
|
|
1266
|
-
const comp = dbg.components.find(c => c.componentId === event.componentId)
|
|
1267
|
-
const name = comp?.debugLabel || event.componentName
|
|
1268
|
-
|
|
1269
|
-
return (
|
|
1270
|
-
<div key={event.id} style={{
|
|
1271
|
-
display: 'flex', alignItems: 'center', gap: 5,
|
|
1272
|
-
padding: '2px 8px', fontSize: 10, fontFamily: 'monospace',
|
|
1273
|
-
borderBottom: '1px solid #020617',
|
|
1274
|
-
}}>
|
|
1275
|
-
<span style={{ color: '#4b5563', fontSize: 9, flexShrink: 0 }}>{ts}</span>
|
|
1276
|
-
<span style={{
|
|
1277
|
-
display: 'inline-block', padding: '0 4px', borderRadius: 2,
|
|
1278
|
-
fontSize: 8, fontWeight: 700, color: '#fff',
|
|
1279
|
-
background: color, flexShrink: 0, lineHeight: '14px',
|
|
1280
|
-
}}>
|
|
1281
|
-
{label}
|
|
1282
|
-
</span>
|
|
1283
|
-
{name && <span style={{ color: '#64748b', fontSize: 9 }}>{name}</span>}
|
|
1284
|
-
<span style={{
|
|
1285
|
-
color: '#94a3b8', fontSize: 9, overflow: 'hidden',
|
|
1286
|
-
textOverflow: 'ellipsis', whiteSpace: 'nowrap', flex: 1,
|
|
1287
|
-
}}>
|
|
1288
|
-
{summary}
|
|
1289
|
-
</span>
|
|
1290
|
-
</div>
|
|
1291
|
-
)
|
|
1292
|
-
})}
|
|
1293
|
-
</div>
|
|
1294
|
-
</div>
|
|
1295
|
-
)}
|
|
1296
|
-
</>
|
|
1297
|
-
)}
|
|
1298
|
-
</div>
|
|
1299
|
-
)}
|
|
1300
|
-
|
|
1301
|
-
{/* === Settings Tab === */}
|
|
1302
|
-
{tab === 'settings' && (
|
|
1303
|
-
<SettingsPanel settings={settings} onChange={updateSettings} />
|
|
1304
|
-
)}
|
|
1305
|
-
</div>
|
|
1306
|
-
</div>
|
|
1307
|
-
|
|
1308
|
-
{/* Resize handle - bottom-right corner */}
|
|
1309
|
-
<div
|
|
1310
|
-
onMouseDown={onResizeStart}
|
|
1311
|
-
style={{
|
|
1312
|
-
position: 'absolute', bottom: 0, right: 0,
|
|
1313
|
-
width: 14, height: 14, cursor: 'nwse-resize',
|
|
1314
|
-
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
|
1315
|
-
}}
|
|
1316
|
-
>
|
|
1317
|
-
<svg width="8" height="8" viewBox="0 0 8 8" style={{ opacity: 0.3 }}>
|
|
1318
|
-
<line x1="7" y1="1" x2="1" y2="7" stroke="#94a3b8" strokeWidth="1" />
|
|
1319
|
-
<line x1="7" y1="4" x2="4" y2="7" stroke="#94a3b8" strokeWidth="1" />
|
|
1320
|
-
</svg>
|
|
1321
|
-
</div>
|
|
1322
|
-
</div>
|
|
1323
|
-
)
|
|
1324
|
-
}
|