qdadm 0.30.0 → 0.31.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 (43) hide show
  1. package/package.json +2 -1
  2. package/src/components/forms/FormPage.vue +1 -1
  3. package/src/components/layout/AppLayout.vue +13 -1
  4. package/src/components/layout/Zone.vue +40 -23
  5. package/src/composables/index.js +1 -0
  6. package/src/composables/useAuth.js +43 -4
  7. package/src/composables/useCurrentEntity.js +44 -0
  8. package/src/composables/useFormPageBuilder.js +3 -3
  9. package/src/composables/useNavContext.js +24 -8
  10. package/src/debug/AuthCollector.js +254 -0
  11. package/src/debug/Collector.js +235 -0
  12. package/src/debug/DebugBridge.js +163 -0
  13. package/src/debug/DebugModule.js +215 -0
  14. package/src/debug/EntitiesCollector.js +376 -0
  15. package/src/debug/ErrorCollector.js +66 -0
  16. package/src/debug/LocalStorageAdapter.js +150 -0
  17. package/src/debug/SignalCollector.js +87 -0
  18. package/src/debug/ToastCollector.js +82 -0
  19. package/src/debug/ZonesCollector.js +300 -0
  20. package/src/debug/components/DebugBar.vue +1232 -0
  21. package/src/debug/components/ObjectTree.vue +194 -0
  22. package/src/debug/components/index.js +8 -0
  23. package/src/debug/components/panels/AuthPanel.vue +103 -0
  24. package/src/debug/components/panels/EntitiesPanel.vue +616 -0
  25. package/src/debug/components/panels/EntriesPanel.vue +188 -0
  26. package/src/debug/components/panels/ToastsPanel.vue +112 -0
  27. package/src/debug/components/panels/ZonesPanel.vue +232 -0
  28. package/src/debug/components/panels/index.js +8 -0
  29. package/src/debug/index.js +31 -0
  30. package/src/entity/EntityManager.js +142 -20
  31. package/src/entity/storage/MockApiStorage.js +17 -1
  32. package/src/entity/storage/index.js +9 -2
  33. package/src/index.js +7 -0
  34. package/src/kernel/Kernel.js +436 -48
  35. package/src/kernel/KernelContext.js +385 -0
  36. package/src/kernel/Module.js +111 -0
  37. package/src/kernel/ModuleLoader.js +573 -0
  38. package/src/kernel/SignalBus.js +2 -7
  39. package/src/kernel/index.js +14 -0
  40. package/src/toast/ToastBridgeModule.js +70 -0
  41. package/src/toast/ToastListener.vue +47 -0
  42. package/src/toast/index.js +15 -0
  43. package/src/toast/useSignalToast.js +113 -0
@@ -0,0 +1,1232 @@
1
+ <script setup>
2
+ /**
3
+ * DebugBar - Floating debug toolbar for qdadm
4
+ *
5
+ * Display modes:
6
+ * - bottom: wide horizontal layout (entries side by side)
7
+ * - right: vertical sidebar (entries stacked)
8
+ * - window: floating draggable/resizable window
9
+ * - fullscreen: covers entire viewport
10
+ * - minimized: small corner button
11
+ */
12
+ import { ref, computed, watch, onMounted, onUnmounted } from 'vue'
13
+ import Badge from 'primevue/badge'
14
+ import Button from 'primevue/button'
15
+ import { ZonesPanel, AuthPanel, EntitiesPanel, ToastsPanel, EntriesPanel } from './panels'
16
+
17
+ const props = defineProps({
18
+ bridge: { type: Object, required: true },
19
+ zIndex: { type: Number, default: 9999 }
20
+ })
21
+
22
+ // Persistence
23
+ const STORAGE_KEY = 'qdadm-debug-bar'
24
+
25
+ const defaultWindowBounds = { x: 100, y: 100, width: 500, height: 400 }
26
+ const defaultPanelSizes = { bottomHeight: 180, rightWidth: 420 }
27
+
28
+ function loadState() {
29
+ try {
30
+ const saved = localStorage.getItem(STORAGE_KEY)
31
+ if (saved) {
32
+ const state = JSON.parse(saved)
33
+ // Validate windowBounds (min 280x200)
34
+ if (state.windowBounds) {
35
+ const b = state.windowBounds
36
+ if (typeof b.x !== 'number' || typeof b.y !== 'number' ||
37
+ typeof b.width !== 'number' || typeof b.height !== 'number' ||
38
+ b.width < 280 || b.height < 200) {
39
+ state.windowBounds = { ...defaultWindowBounds }
40
+ }
41
+ }
42
+ // Validate panelSizes
43
+ if (state.panelSizes) {
44
+ const p = state.panelSizes
45
+ if (typeof p.bottomHeight !== 'number' || p.bottomHeight < 100) {
46
+ p.bottomHeight = defaultPanelSizes.bottomHeight
47
+ }
48
+ if (typeof p.rightWidth !== 'number' || p.rightWidth < 200) {
49
+ p.rightWidth = defaultPanelSizes.rightWidth
50
+ }
51
+ } else {
52
+ state.panelSizes = { ...defaultPanelSizes }
53
+ }
54
+ return state
55
+ }
56
+ } catch (e) { /* ignore */ }
57
+ return { minimized: true, expanded: false, mode: 'bottom', fullscreen: false, windowBounds: { ...defaultWindowBounds }, panelSizes: { ...defaultPanelSizes } }
58
+ }
59
+
60
+ function saveState() {
61
+ try {
62
+ localStorage.setItem(STORAGE_KEY, JSON.stringify({
63
+ minimized: minimized.value,
64
+ expanded: expanded.value,
65
+ mode: displayMode.value,
66
+ fullscreen: fullscreen.value,
67
+ windowBounds: windowBounds.value,
68
+ panelSizes: panelSizes.value,
69
+ countAllBadges: countAllBadges.value
70
+ }))
71
+ } catch (e) { /* ignore */ }
72
+ }
73
+
74
+ const savedState = loadState()
75
+
76
+ // State
77
+ const minimized = ref(savedState.minimized)
78
+ const expanded = ref(savedState.expanded)
79
+ const displayMode = ref(savedState.mode) // 'bottom', 'right', or 'window'
80
+ const fullscreen = ref(savedState.fullscreen || false)
81
+ const activeCollector = ref(0)
82
+ const countAllBadges = ref(savedState.countAllBadges ?? false) // false = show unseen only
83
+
84
+ // Reactive tick from bridge - collectors notify when they have new data
85
+ const bridgeTick = computed(() => props.bridge?.tick?.value ?? 0)
86
+
87
+ // Window mode state
88
+ const windowBounds = ref(savedState.windowBounds || defaultWindowBounds)
89
+ const isDragging = ref(false)
90
+ const isResizing = ref(false)
91
+ const dragOffset = ref({ x: 0, y: 0 })
92
+ const resizeStart = ref({ x: 0, y: 0, width: 0, height: 0 })
93
+
94
+ // Docked panel sizes (resizable)
95
+ const panelSizes = ref(savedState.panelSizes || defaultPanelSizes)
96
+ const isPanelResizing = ref(false)
97
+ const panelResizeStart = ref({ y: 0, x: 0, height: 0, width: 0 })
98
+
99
+ // Responsive tabs state (applies to bottom and window modes with horizontal header)
100
+ const headerRef = ref(null)
101
+ const headerWidth = ref(1000)
102
+ const isHorizontalHeader = computed(() => (displayMode.value === 'bottom' || displayMode.value === 'window' || fullscreen.value) && displayMode.value !== 'right')
103
+ const tabsCompact = computed(() => isHorizontalHeader.value && headerWidth.value < 600)
104
+ const tabsDropdown = computed(() => isHorizontalHeader.value && headerWidth.value < 400)
105
+ const showTabsDropdown = ref(false)
106
+
107
+ // Close dropdown when exiting dropdown mode
108
+ watch(tabsDropdown, (isDropdown) => {
109
+ if (!isDropdown) showTabsDropdown.value = false
110
+ })
111
+
112
+ let resizeObserver = null
113
+ onMounted(() => {
114
+ resizeObserver = new ResizeObserver((entries) => {
115
+ for (const entry of entries) {
116
+ headerWidth.value = entry.contentRect.width
117
+ }
118
+ })
119
+ // Close dropdown on outside click
120
+ document.addEventListener('click', onDocumentClick)
121
+ })
122
+ onUnmounted(() => {
123
+ resizeObserver?.disconnect()
124
+ document.removeEventListener('click', onDocumentClick)
125
+ })
126
+ watch(headerRef, (el) => {
127
+ resizeObserver?.disconnect()
128
+ if (el) resizeObserver?.observe(el)
129
+ }, { immediate: true })
130
+
131
+ function onDocumentClick(e) {
132
+ if (showTabsDropdown.value && !e.target.closest('.debug-tabs-dropdown')) {
133
+ showTabsDropdown.value = false
134
+ }
135
+ }
136
+
137
+ // Computed
138
+ const isEnabled = computed(() => props.bridge?.enabled?.value ?? false)
139
+
140
+ const collectors = computed(() => {
141
+ bridgeTick.value // Reactive dependency on bridge tick
142
+ const result = []
143
+ if (props.bridge?.collectors) {
144
+ for (const [name, collector] of props.bridge.collectors) {
145
+ result.push({
146
+ name,
147
+ collector,
148
+ badge: collector.getBadge(countAllBadges.value),
149
+ totalBadge: collector.getTotalCount?.() ?? collector.getBadge(true),
150
+ entries: collector.getEntries(),
151
+ records: collector.records, // Does this collector record events?
152
+ enabled: collector.enabled // Per-collector enabled state
153
+ })
154
+ }
155
+ }
156
+ return result
157
+ })
158
+
159
+ const currentCollector = computed(() => collectors.value[activeCollector.value])
160
+ const totalBadge = computed(() => {
161
+ bridgeTick.value // Reactive dependency
162
+ return props.bridge?.getTotalBadge?.(countAllBadges.value) ?? 0
163
+ })
164
+
165
+ // Separate badges for minimized state (always show unseen)
166
+ const errorBadge = computed(() => {
167
+ bridgeTick.value // Reactive dependency
168
+ const c = collectors.value.find(c => c.name === 'ErrorCollector' || c.name === 'errors')
169
+ return c?.badge || 0
170
+ })
171
+ const signalBadge = computed(() => {
172
+ bridgeTick.value // Reactive dependency
173
+ const c = collectors.value.find(c => c.name === 'SignalCollector' || c.name === 'signals')
174
+ return c?.badge || 0
175
+ })
176
+
177
+ // Mark previous collector as seen when leaving tab
178
+ watch(activeCollector, (newIdx, oldIdx) => {
179
+ if (oldIdx !== undefined && collectors.value[oldIdx]?.collector?.markAsSeen) {
180
+ collectors.value[oldIdx].collector.markAsSeen()
181
+ props.bridge?.notify()
182
+ }
183
+ })
184
+
185
+ // Mark current collector as seen when collapsing panel
186
+ watch(expanded, (isExpanded, wasExpanded) => {
187
+ if (wasExpanded && !isExpanded) {
188
+ const idx = activeCollector.value
189
+ if (collectors.value[idx]?.collector?.markAsSeen) {
190
+ collectors.value[idx].collector.markAsSeen()
191
+ props.bridge?.notify()
192
+ }
193
+ }
194
+ })
195
+
196
+ function toggleCountMode() {
197
+ countAllBadges.value = !countAllBadges.value
198
+ saveState()
199
+ props.bridge?.notify()
200
+ }
201
+
202
+ // Actions
203
+ function toggle() {
204
+ expanded.value = !expanded.value
205
+ saveState()
206
+ }
207
+
208
+ function expand() {
209
+ minimized.value = false
210
+ displayMode.value = 'right'
211
+ expanded.value = true
212
+ saveState()
213
+ }
214
+
215
+ function minimize() {
216
+ minimized.value = true
217
+ // Keep expanded state - will be restored when un-minimizing
218
+ fullscreen.value = false
219
+ saveState()
220
+ }
221
+
222
+ function toggleMode() {
223
+ // Toggle between bottom and right (not window)
224
+ if (displayMode.value === 'window') {
225
+ displayMode.value = 'bottom'
226
+ } else {
227
+ displayMode.value = displayMode.value === 'bottom' ? 'right' : 'bottom'
228
+ }
229
+ saveState()
230
+ }
231
+
232
+ function enterWindowMode() {
233
+ displayMode.value = 'window'
234
+ expanded.value = true
235
+ // Ensure window is visible within viewport with min size
236
+ const vw = window.innerWidth
237
+ const vh = window.innerHeight
238
+ const bounds = windowBounds.value
239
+ const width = Math.max(280, Math.min(bounds.width, vw - 40))
240
+ const height = Math.max(200, Math.min(bounds.height, vh - 40))
241
+ windowBounds.value = {
242
+ x: Math.max(20, Math.min(vw - width - 20, bounds.x)),
243
+ y: Math.max(20, Math.min(vh - height - 20, bounds.y)),
244
+ width,
245
+ height
246
+ }
247
+ saveState()
248
+ }
249
+
250
+ function exitWindowMode() {
251
+ displayMode.value = 'bottom'
252
+ saveState()
253
+ }
254
+
255
+ function getModeIcon() {
256
+ if (displayMode.value === 'window') return 'pi pi-window-maximize'
257
+ return null // Using custom SVG icons instead
258
+ }
259
+
260
+ function getModeTitle() {
261
+ if (displayMode.value === 'window') return 'Docked mode'
262
+ return displayMode.value === 'bottom' ? 'Dock to right side' : 'Dock to bottom'
263
+ }
264
+
265
+ // Window drag handlers
266
+ function startDrag(e) {
267
+ if (displayMode.value !== 'window') return
268
+ isDragging.value = true
269
+ dragOffset.value = {
270
+ x: e.clientX - windowBounds.value.x,
271
+ y: e.clientY - windowBounds.value.y
272
+ }
273
+ document.addEventListener('mousemove', onDrag)
274
+ document.addEventListener('mouseup', stopDrag)
275
+ e.preventDefault()
276
+ }
277
+
278
+ function onDrag(e) {
279
+ if (!isDragging.value) return
280
+ windowBounds.value = {
281
+ ...windowBounds.value,
282
+ x: Math.max(0, Math.min(window.innerWidth - 100, e.clientX - dragOffset.value.x)),
283
+ y: Math.max(0, Math.min(window.innerHeight - 50, e.clientY - dragOffset.value.y))
284
+ }
285
+ }
286
+
287
+ function stopDrag() {
288
+ if (isDragging.value) {
289
+ isDragging.value = false
290
+ document.removeEventListener('mousemove', onDrag)
291
+ document.removeEventListener('mouseup', stopDrag)
292
+ saveState()
293
+ }
294
+ }
295
+
296
+ // Window resize handlers
297
+ function startResize(e) {
298
+ if (displayMode.value !== 'window') return
299
+ isResizing.value = true
300
+ resizeStart.value = {
301
+ x: e.clientX,
302
+ y: e.clientY,
303
+ width: windowBounds.value.width,
304
+ height: windowBounds.value.height
305
+ }
306
+ document.addEventListener('mousemove', onResize)
307
+ document.addEventListener('mouseup', stopResize)
308
+ e.preventDefault()
309
+ e.stopPropagation()
310
+ }
311
+
312
+ const MIN_WINDOW_WIDTH = 280
313
+ const MIN_WINDOW_HEIGHT = 200
314
+
315
+ function onResize(e) {
316
+ if (!isResizing.value) return
317
+ const deltaX = e.clientX - resizeStart.value.x
318
+ const deltaY = e.clientY - resizeStart.value.y
319
+ windowBounds.value = {
320
+ ...windowBounds.value,
321
+ width: Math.max(MIN_WINDOW_WIDTH, resizeStart.value.width + deltaX),
322
+ height: Math.max(MIN_WINDOW_HEIGHT, resizeStart.value.height + deltaY)
323
+ }
324
+ }
325
+
326
+ function stopResize() {
327
+ if (isResizing.value) {
328
+ isResizing.value = false
329
+ document.removeEventListener('mousemove', onResize)
330
+ document.removeEventListener('mouseup', stopResize)
331
+ saveState()
332
+ }
333
+ }
334
+
335
+ // Docked panel resize handlers
336
+ const MIN_PANEL_HEIGHT = 100
337
+ const MAX_PANEL_HEIGHT = 600
338
+ const MIN_PANEL_WIDTH = 200
339
+ const MAX_PANEL_WIDTH = 800
340
+
341
+ function startPanelResize(e, direction) {
342
+ if (displayMode.value === 'window' || fullscreen.value) return
343
+ isPanelResizing.value = true
344
+ panelResizeStart.value = {
345
+ y: e.clientY,
346
+ x: e.clientX,
347
+ height: panelSizes.value.bottomHeight,
348
+ width: panelSizes.value.rightWidth
349
+ }
350
+ const handler = direction === 'vertical' ? onPanelResizeVertical : onPanelResizeHorizontal
351
+ const stop = () => stopPanelResize(handler, stop)
352
+ document.addEventListener('mousemove', handler)
353
+ document.addEventListener('mouseup', stop)
354
+ e.preventDefault()
355
+ }
356
+
357
+ function onPanelResizeVertical(e) {
358
+ if (!isPanelResizing.value) return
359
+ const deltaY = panelResizeStart.value.y - e.clientY
360
+ panelSizes.value = {
361
+ ...panelSizes.value,
362
+ bottomHeight: Math.max(MIN_PANEL_HEIGHT, Math.min(MAX_PANEL_HEIGHT, panelResizeStart.value.height + deltaY))
363
+ }
364
+ }
365
+
366
+ function onPanelResizeHorizontal(e) {
367
+ if (!isPanelResizing.value) return
368
+ const deltaX = panelResizeStart.value.x - e.clientX
369
+ panelSizes.value = {
370
+ ...panelSizes.value,
371
+ rightWidth: Math.max(MIN_PANEL_WIDTH, Math.min(MAX_PANEL_WIDTH, panelResizeStart.value.width + deltaX))
372
+ }
373
+ }
374
+
375
+ function stopPanelResize(moveHandler, upHandler) {
376
+ if (isPanelResizing.value) {
377
+ isPanelResizing.value = false
378
+ document.removeEventListener('mousemove', moveHandler)
379
+ document.removeEventListener('mouseup', upHandler)
380
+ saveState()
381
+ }
382
+ }
383
+
384
+ // State before entering fullscreen (to restore on exit)
385
+ let preFullscreenExpanded = false
386
+
387
+ function toggleFullscreen() {
388
+ if (!fullscreen.value) {
389
+ // Entering fullscreen - save current state
390
+ preFullscreenExpanded = expanded.value
391
+ fullscreen.value = true
392
+ expanded.value = true
393
+ } else {
394
+ // Exiting fullscreen - restore previous state
395
+ fullscreen.value = false
396
+ expanded.value = preFullscreenExpanded
397
+ }
398
+ saveState()
399
+ }
400
+
401
+ function toggleEnabled() {
402
+ props.bridge?.toggle()
403
+ }
404
+
405
+ function clearAll() {
406
+ props.bridge?.clearAll()
407
+ props.bridge?.notify()
408
+ }
409
+
410
+ function clearCollector(collector) {
411
+ collector.clear()
412
+ props.bridge?.notify()
413
+ }
414
+
415
+ function toggleCollector(collector) {
416
+ collector.toggle()
417
+ props.bridge?.notify()
418
+ }
419
+
420
+ function notifyBridge() {
421
+ props.bridge?.notify()
422
+ }
423
+
424
+ function getCollectorIcon(name) {
425
+ const icons = {
426
+ errors: 'pi-exclamation-triangle',
427
+ signals: 'pi-bolt',
428
+ toasts: 'pi-bell',
429
+ zones: 'pi-th-large',
430
+ auth: 'pi-user',
431
+ entities: 'pi-database',
432
+ ErrorCollector: 'pi-exclamation-triangle',
433
+ SignalCollector: 'pi-bolt',
434
+ ToastCollector: 'pi-bell',
435
+ ZonesCollector: 'pi-th-large',
436
+ AuthCollector: 'pi-user',
437
+ EntitiesCollector: 'pi-database'
438
+ }
439
+ return icons[name] || 'pi-database'
440
+ }
441
+
442
+ function getCollectorLabel(name) {
443
+ const labels = {
444
+ errors: 'Errors',
445
+ signals: 'Signals',
446
+ toasts: 'Toasts',
447
+ zones: 'Zones',
448
+ auth: 'Auth',
449
+ entities: 'Entities',
450
+ ErrorCollector: 'Errors',
451
+ SignalCollector: 'Signals',
452
+ ToastCollector: 'Toasts',
453
+ ZonesCollector: 'Zones',
454
+ AuthCollector: 'Auth',
455
+ EntitiesCollector: 'Entities'
456
+ }
457
+ return labels[name] || name
458
+ }
459
+
460
+ function getCollectorColor(name) {
461
+ const colors = {
462
+ errors: '#ef4444',
463
+ toasts: '#f59e0b',
464
+ signals: '#8b5cf6',
465
+ zones: '#06b6d4',
466
+ auth: '#10b981',
467
+ entities: '#3b82f6'
468
+ }
469
+ return colors[name] || '#6b7280'
470
+ }
471
+ </script>
472
+
473
+ <template>
474
+ <Teleport to="body">
475
+ <!-- Minimized: corner button with separate badges -->
476
+ <div v-if="minimized" class="debug-minimized" :style="{ zIndex }" @click="expand">
477
+ <svg viewBox="0 0 100 100" width="28" height="28">
478
+ <polygon points="50,5 93,27.5 93,72.5 50,95 7,72.5 7,27.5" fill="#1E3A8A"/>
479
+ <text x="48" y="50" text-anchor="middle" dominant-baseline="central" font-family="system-ui" font-size="58" font-weight="800" letter-spacing="-4">
480
+ <tspan fill="#60A5FA">Q</tspan><tspan fill="#93C5FD">D</tspan>
481
+ </text>
482
+ </svg>
483
+ <div v-if="errorBadge > 0 || signalBadge > 0" class="debug-badges">
484
+ <span v-if="errorBadge > 0" class="debug-badge debug-badge-error" title="Errors">
485
+ <i class="pi pi-exclamation-triangle" />{{ errorBadge }}
486
+ </span>
487
+ <span v-if="signalBadge > 0" class="debug-badge debug-badge-signal" title="Signals">
488
+ <i class="pi pi-bolt" />{{ signalBadge }}
489
+ </span>
490
+ </div>
491
+ </div>
492
+
493
+ <!-- Full panel -->
494
+ <div v-else class="debug-panel" :class="[
495
+ fullscreen ? 'debug-fullscreen' : `debug-${displayMode}`,
496
+ { 'debug-expanded': expanded }
497
+ ]" :style="displayMode === 'window' && !fullscreen ? {
498
+ zIndex,
499
+ left: windowBounds.x + 'px',
500
+ top: windowBounds.y + 'px',
501
+ width: windowBounds.width + 'px',
502
+ height: windowBounds.height + 'px'
503
+ } : displayMode === 'bottom' && expanded && !fullscreen ? {
504
+ zIndex,
505
+ height: panelSizes.bottomHeight + 'px'
506
+ } : displayMode === 'right' && expanded && !fullscreen ? {
507
+ zIndex,
508
+ width: panelSizes.rightWidth + 'px'
509
+ } : { zIndex }">
510
+ <!-- Header -->
511
+ <div ref="headerRef" class="debug-header" @mousedown="displayMode === 'window' && !fullscreen ? startDrag($event) : null" :class="{ 'debug-header-draggable': displayMode === 'window' && !fullscreen }">
512
+ <div class="debug-title" :class="{ 'debug-title-draggable': displayMode === 'window' && !fullscreen }">
513
+ <svg viewBox="0 0 100 100" width="18" height="18">
514
+ <polygon points="50,5 93,27.5 93,72.5 50,95 7,72.5 7,27.5" fill="#1E3A8A"/>
515
+ <text x="48" y="50" text-anchor="middle" dominant-baseline="central" font-family="system-ui" font-size="58" font-weight="800" letter-spacing="-4">
516
+ <tspan fill="#60A5FA">Q</tspan><tspan fill="#93C5FD">D</tspan>
517
+ </text>
518
+ </svg>
519
+ <span>Debug</span>
520
+ <span v-if="errorBadge > 0" class="debug-header-badge debug-header-badge-error" title="Errors">
521
+ <i class="pi pi-exclamation-triangle" />{{ errorBadge }}
522
+ </span>
523
+ <span v-if="signalBadge > 0" class="debug-header-badge debug-header-badge-signal" title="Signals">
524
+ <i class="pi pi-bolt" />{{ signalBadge }}
525
+ </span>
526
+ </div>
527
+
528
+ <!-- Collector tabs - dropdown mode when very narrow -->
529
+ <div v-if="expanded && tabsDropdown" class="debug-tabs-dropdown">
530
+ <button class="debug-dropdown-trigger" @click="showTabsDropdown = !showTabsDropdown">
531
+ <i :class="['pi', getCollectorIcon(currentCollector?.name)]" :style="{ color: getCollectorColor(currentCollector?.name) }" />
532
+ <span>{{ getCollectorLabel(currentCollector?.name) }}</span>
533
+ <Badge v-if="currentCollector?.badge > 0" :value="currentCollector.badge" severity="secondary" />
534
+ <i class="pi pi-chevron-down" />
535
+ </button>
536
+ <div v-if="showTabsDropdown" class="debug-dropdown-menu" @click="showTabsDropdown = false">
537
+ <button
538
+ v-for="(c, idx) in collectors"
539
+ :key="c.name"
540
+ class="debug-dropdown-item"
541
+ :class="{ 'debug-dropdown-item-active': activeCollector === idx }"
542
+ @click="activeCollector = idx"
543
+ >
544
+ <i :class="['pi', getCollectorIcon(c.name)]" :style="{ color: getCollectorColor(c.name) }" />
545
+ <span>{{ getCollectorLabel(c.name) }}</span>
546
+ <Badge v-if="c.badge > 0" :value="c.badge" severity="secondary" />
547
+ </button>
548
+ </div>
549
+ </div>
550
+
551
+ <!-- Collector tabs - normal/compact mode -->
552
+ <div v-else-if="expanded" class="debug-tabs" :class="{ 'debug-tabs-compact': tabsCompact }">
553
+ <button
554
+ v-for="(c, idx) in collectors"
555
+ :key="c.name"
556
+ class="debug-tab"
557
+ :class="{ 'debug-tab-active': activeCollector === idx }"
558
+ :title="getCollectorLabel(c.name)"
559
+ @click="activeCollector = idx"
560
+ >
561
+ <i :class="['pi', getCollectorIcon(c.name)]" :style="{ color: getCollectorColor(c.name) }" />
562
+ <span v-if="!tabsCompact" class="debug-tab-label">{{ getCollectorLabel(c.name) }}</span>
563
+ <Badge v-if="c.badge > 0" :value="c.badge" severity="secondary" />
564
+ <!-- Per-collector play/pause for recording collectors (bottom mode only, not compact) -->
565
+ <button
566
+ v-if="c.records && displayMode === 'bottom' && !tabsCompact"
567
+ class="debug-tab-toggle"
568
+ :class="{ 'debug-tab-toggle-paused': !c.enabled }"
569
+ :title="c.enabled ? 'Pause recording' : 'Resume recording'"
570
+ @click.stop="toggleCollector(c.collector)"
571
+ >
572
+ <i :class="['pi', c.enabled ? 'pi-pause' : 'pi-play']" />
573
+ </button>
574
+ <!-- Per-collector clear button for recording collectors (bottom mode only, not compact) -->
575
+ <button
576
+ v-if="c.records && c.badge > 0 && displayMode === 'bottom' && !tabsCompact"
577
+ class="debug-tab-clear"
578
+ title="Clear entries"
579
+ @click.stop="clearCollector(c.collector)"
580
+ >
581
+ <i class="pi pi-trash" />
582
+ </button>
583
+ </button>
584
+ </div>
585
+
586
+ <div class="debug-actions">
587
+ <Button :icon="isEnabled ? 'pi pi-pause' : 'pi pi-play'" :severity="isEnabled ? 'success' : 'secondary'" size="small" text rounded :title="isEnabled ? 'Pause' : 'Resume'" @click="toggleEnabled" />
588
+ <Button icon="pi pi-trash" severity="secondary" size="small" text rounded title="Clear all" @click="clearAll" />
589
+ <!-- Badge count mode toggle -->
590
+ <Button :icon="countAllBadges ? 'pi pi-hashtag' : 'pi pi-eye'" :severity="countAllBadges ? 'info' : 'secondary'" size="small" text rounded :title="countAllBadges ? 'Showing all (click for unseen only)' : 'Showing unseen (click for all)'" @click="toggleCountMode" />
591
+ <!-- Mode toggle (bottom/right) - not shown in window/fullscreen mode -->
592
+ <button
593
+ v-if="displayMode !== 'window' && !fullscreen"
594
+ class="debug-mode-btn"
595
+ :title="getModeTitle()"
596
+ @click="toggleMode"
597
+ >{{ displayMode === 'bottom' ? '|' : '―' }}</button>
598
+ <!-- Enter window mode button - not in fullscreen -->
599
+ <Button v-if="displayMode !== 'window' && !fullscreen" icon="pi pi-external-link" severity="secondary" size="small" text rounded title="Detach window" @click="enterWindowMode" />
600
+ <!-- Exit window mode button (dock back) -->
601
+ <Button v-if="displayMode === 'window'" icon="pi pi-link" severity="secondary" size="small" text rounded title="Dock panel" @click="exitWindowMode" />
602
+ <!-- Fullscreen (not in window mode) -->
603
+ <Button v-if="displayMode !== 'window'" :icon="fullscreen ? 'pi pi-window-minimize' : 'pi pi-window-maximize'" severity="secondary" size="small" text rounded :title="fullscreen ? 'Exit fullscreen' : 'Fullscreen'" @click="toggleFullscreen" />
604
+ <!-- Expand/collapse (not in window/fullscreen mode) -->
605
+ <Button v-if="displayMode !== 'window' && !fullscreen" :icon="displayMode === 'right' ? (expanded ? 'pi pi-chevron-right' : 'pi pi-chevron-left') : (expanded ? 'pi pi-chevron-down' : 'pi pi-chevron-up')" severity="secondary" size="small" text rounded @click="toggle" />
606
+ <!-- Minimize - not in fullscreen -->
607
+ <Button v-if="!fullscreen" icon="pi pi-arrow-down-right" severity="secondary" size="small" text rounded title="Minimize" @click="minimize" />
608
+ </div>
609
+ </div>
610
+
611
+ <!-- Resize handle for window mode (not in fullscreen) -->
612
+ <div v-if="displayMode === 'window' && !fullscreen" class="debug-resize-handle" @mousedown="startResize" />
613
+
614
+ <!-- Resize handle for bottom mode (drag to resize height) -->
615
+ <div
616
+ v-if="displayMode === 'bottom' && expanded && !fullscreen"
617
+ class="debug-panel-resize debug-panel-resize-top"
618
+ @mousedown="startPanelResize($event, 'vertical')"
619
+ />
620
+
621
+ <!-- Resize handle for right mode (drag to resize width) -->
622
+ <div
623
+ v-if="displayMode === 'right' && expanded && !fullscreen"
624
+ class="debug-panel-resize debug-panel-resize-left"
625
+ @mousedown="startPanelResize($event, 'horizontal')"
626
+ />
627
+
628
+ <!-- Content -->
629
+ <div v-if="expanded && currentCollector" class="debug-content">
630
+ <!-- Content header with controls (right/window/fullscreen mode, for recording collectors) -->
631
+ <div v-if="(displayMode === 'right' || displayMode === 'window' || fullscreen) && currentCollector.records" class="debug-content-header">
632
+ <span class="debug-content-title">{{ getCollectorLabel(currentCollector.name) }}</span>
633
+ <div class="debug-content-controls">
634
+ <button
635
+ class="debug-tab-toggle"
636
+ :class="{ 'debug-tab-toggle-paused': !currentCollector.enabled }"
637
+ :title="currentCollector.enabled ? 'Pause recording' : 'Resume recording'"
638
+ @click="toggleCollector(currentCollector.collector)"
639
+ >
640
+ <i :class="['pi', currentCollector.enabled ? 'pi-pause' : 'pi-play']" />
641
+ </button>
642
+ <button
643
+ v-if="currentCollector.badge > 0"
644
+ class="debug-tab-clear"
645
+ title="Clear entries"
646
+ @click="clearCollector(currentCollector.collector)"
647
+ >
648
+ <i class="pi pi-trash" />
649
+ </button>
650
+ </div>
651
+ </div>
652
+
653
+ <!-- Zones collector - always render for toolbar access -->
654
+ <ZonesPanel
655
+ v-if="currentCollector.name === 'zones' || currentCollector.name === 'ZonesCollector'"
656
+ :collector="currentCollector.collector"
657
+ :entries="currentCollector.entries"
658
+ @update="notifyBridge"
659
+ />
660
+
661
+ <div v-else-if="currentCollector.entries.length === 0" class="debug-empty">
662
+ <i class="pi pi-inbox" />
663
+ <span>No entries</span>
664
+ </div>
665
+
666
+ <!-- Auth collector -->
667
+ <AuthPanel
668
+ v-else-if="currentCollector.name === 'auth' || currentCollector.name === 'AuthCollector'"
669
+ :collector="currentCollector.collector"
670
+ :entries="currentCollector.entries"
671
+ />
672
+
673
+ <!-- Entities collector -->
674
+ <EntitiesPanel
675
+ v-else-if="currentCollector.name === 'entities' || currentCollector.name === 'EntitiesCollector'"
676
+ :collector="currentCollector.collector"
677
+ :entries="currentCollector.entries"
678
+ @update="notifyBridge"
679
+ />
680
+
681
+ <!-- Toasts collector (vertical modes) -->
682
+ <ToastsPanel
683
+ v-else-if="(currentCollector.name === 'toasts' || currentCollector.name === 'ToastCollector') && (displayMode !== 'bottom' || fullscreen)"
684
+ :entries="currentCollector.entries"
685
+ />
686
+
687
+ <!-- Default entries - horizontal (bottom mode) -->
688
+ <EntriesPanel
689
+ v-else-if="displayMode === 'bottom' && !fullscreen"
690
+ :entries="currentCollector.entries"
691
+ mode="horizontal"
692
+ />
693
+
694
+ <!-- Default entries - vertical (right/fullscreen mode) -->
695
+ <EntriesPanel
696
+ v-else
697
+ :entries="currentCollector.entries"
698
+ mode="vertical"
699
+ />
700
+ </div>
701
+ </div>
702
+ </Teleport>
703
+ </template>
704
+
705
+ <style scoped>
706
+ /* Minimized button */
707
+ .debug-minimized {
708
+ position: fixed;
709
+ bottom: 1rem;
710
+ right: 1rem;
711
+ width: 44px;
712
+ height: 44px;
713
+ border-radius: 8px;
714
+ background: #18181b;
715
+ border: 1px solid #3f3f46;
716
+ box-shadow: 0 4px 12px rgba(0,0,0,0.4);
717
+ display: flex;
718
+ align-items: center;
719
+ justify-content: center;
720
+ cursor: pointer;
721
+ transition: transform 0.15s;
722
+ }
723
+ .debug-minimized:hover {
724
+ transform: scale(1.08);
725
+ border-color: #60a5fa;
726
+ }
727
+
728
+ /* Separate badges */
729
+ .debug-badge {
730
+ position: absolute;
731
+ min-width: 16px;
732
+ height: 16px;
733
+ padding: 0 4px;
734
+ border-radius: 8px;
735
+ font-size: 10px;
736
+ font-weight: 600;
737
+ display: flex;
738
+ align-items: center;
739
+ justify-content: center;
740
+ gap: 2px;
741
+ }
742
+ .debug-badge .pi {
743
+ font-size: 8px;
744
+ }
745
+
746
+ /* Header inline badges */
747
+ .debug-header-badge {
748
+ display: inline-flex;
749
+ align-items: center;
750
+ gap: 3px;
751
+ padding: 2px 6px;
752
+ border-radius: 10px;
753
+ font-size: 11px;
754
+ font-weight: 600;
755
+ }
756
+ .debug-header-badge .pi {
757
+ font-size: 10px;
758
+ }
759
+ .debug-header-badge-error {
760
+ background: #ef4444;
761
+ color: white;
762
+ }
763
+ .debug-header-badge-signal {
764
+ background: #8b5cf6;
765
+ color: white;
766
+ }
767
+ .debug-badge-error {
768
+ top: -4px;
769
+ right: -4px;
770
+ background: #ef4444;
771
+ color: white;
772
+ }
773
+ .debug-badge-signal {
774
+ bottom: -4px;
775
+ right: -4px;
776
+ background: #8b5cf6;
777
+ color: white;
778
+ }
779
+
780
+ /* Panel base */
781
+ .debug-panel {
782
+ position: fixed;
783
+ background: #18181b;
784
+ border: 1px solid #3f3f46;
785
+ font-family: system-ui, sans-serif;
786
+ font-size: 13px;
787
+ color: #f4f4f5;
788
+ display: flex;
789
+ flex-direction: column;
790
+ }
791
+
792
+ /* Bottom mode */
793
+ .debug-bottom {
794
+ bottom: 0; left: 0; right: 0;
795
+ border-top: 1px solid #3f3f46;
796
+ border-left: none; border-right: none; border-bottom: none;
797
+ }
798
+ .debug-bottom.debug-expanded {
799
+ min-height: 100px;
800
+ max-height: 600px;
801
+ }
802
+
803
+ /* Right mode - vertical header on left, horizontal slide */
804
+ .debug-right {
805
+ top: 0; right: 0; bottom: 0;
806
+ flex-direction: row;
807
+ border-left: 1px solid #3f3f46;
808
+ border-top: none; border-right: none; border-bottom: none;
809
+ transition: transform 0.2s ease-out;
810
+ }
811
+ .debug-right:not(.debug-expanded) {
812
+ transform: translateX(calc(100% - 44px));
813
+ }
814
+ .debug-right.debug-expanded {
815
+ min-width: 200px;
816
+ max-width: 800px;
817
+ transform: translateX(0);
818
+ }
819
+
820
+ /* Window mode - floating draggable/resizable */
821
+ .debug-window {
822
+ /* Window appearance */
823
+ position: fixed;
824
+ border-radius: 8px;
825
+ box-shadow: 0 8px 32px rgba(0,0,0,0.5);
826
+ border: 1px solid #3f3f46;
827
+ min-width: 280px;
828
+ min-height: 200px;
829
+ }
830
+ .debug-window .debug-header {
831
+ cursor: move;
832
+ user-select: none;
833
+ flex-wrap: nowrap;
834
+ }
835
+ .debug-window .debug-tabs {
836
+ flex: 1;
837
+ min-width: 0;
838
+ overflow: hidden;
839
+ }
840
+ .debug-window .debug-actions {
841
+ flex-shrink: 0;
842
+ }
843
+ .debug-window .debug-content {
844
+ flex: 1;
845
+ min-height: 0;
846
+ overflow: auto;
847
+ }
848
+ .debug-resize-handle {
849
+ position: absolute;
850
+ bottom: 0;
851
+ right: 0;
852
+ width: 16px;
853
+ height: 16px;
854
+ cursor: nwse-resize;
855
+ background: linear-gradient(135deg, transparent 50%, #3f3f46 50%);
856
+ border-bottom-right-radius: 8px;
857
+ z-index: 10;
858
+ }
859
+ .debug-resize-handle:hover {
860
+ background: linear-gradient(135deg, transparent 50%, #60a5fa 50%);
861
+ }
862
+
863
+ /* Panel resize handles for docked modes */
864
+ .debug-panel-resize {
865
+ position: absolute;
866
+ z-index: 10;
867
+ background: transparent;
868
+ }
869
+ .debug-panel-resize-top {
870
+ top: 0;
871
+ left: 0;
872
+ right: 0;
873
+ height: 4px;
874
+ cursor: ns-resize;
875
+ }
876
+ .debug-panel-resize-top:hover {
877
+ background: linear-gradient(180deg, #60a5fa 0%, transparent 100%);
878
+ }
879
+ .debug-panel-resize-left {
880
+ top: 0;
881
+ left: 0;
882
+ bottom: 0;
883
+ width: 4px;
884
+ cursor: ew-resize;
885
+ }
886
+ .debug-panel-resize-left:hover {
887
+ background: linear-gradient(90deg, #60a5fa 0%, transparent 100%);
888
+ }
889
+
890
+ .debug-right .debug-header {
891
+ flex-direction: column;
892
+ width: 44px;
893
+ padding: 8px 6px;
894
+ border-bottom: none;
895
+ border-right: 1px solid #3f3f46;
896
+ align-items: center;
897
+ gap: 4px;
898
+ }
899
+ .debug-right .debug-title {
900
+ flex-direction: column;
901
+ gap: 4px;
902
+ }
903
+ .debug-right .debug-title span:not(.debug-header-badge) {
904
+ display: none;
905
+ }
906
+ .debug-right .debug-tabs {
907
+ flex-direction: column;
908
+ margin-left: 0;
909
+ margin-top: 8px;
910
+ gap: 2px;
911
+ }
912
+ .debug-right .debug-tab {
913
+ width: 32px;
914
+ height: 32px;
915
+ padding: 0;
916
+ justify-content: center;
917
+ position: relative;
918
+ }
919
+ .debug-right .debug-tab-label {
920
+ display: none;
921
+ }
922
+ .debug-right .debug-tab .p-badge {
923
+ position: absolute;
924
+ top: -2px;
925
+ right: -2px;
926
+ min-width: 14px;
927
+ height: 14px;
928
+ font-size: 9px;
929
+ padding: 0 3px;
930
+ }
931
+ /* Content header for right mode */
932
+ .debug-content-header {
933
+ display: flex;
934
+ align-items: center;
935
+ justify-content: space-between;
936
+ padding: 6px 12px;
937
+ background: #27272a;
938
+ border-bottom: 1px solid #3f3f46;
939
+ flex-shrink: 0;
940
+ }
941
+ .debug-content-title {
942
+ font-weight: 600;
943
+ font-size: 12px;
944
+ color: #a1a1aa;
945
+ }
946
+ .debug-content-controls {
947
+ display: flex;
948
+ gap: 4px;
949
+ }
950
+ .debug-content-controls .debug-tab-toggle,
951
+ .debug-content-controls .debug-tab-clear {
952
+ margin-left: 0;
953
+ width: 22px;
954
+ height: 22px;
955
+ }
956
+ .debug-right .debug-actions {
957
+ flex-direction: column;
958
+ margin-left: 0;
959
+ margin-top: auto;
960
+ gap: 2px;
961
+ }
962
+ .debug-right .debug-actions .p-button {
963
+ width: 28px;
964
+ height: 28px;
965
+ }
966
+ .debug-right .debug-content {
967
+ flex: 1;
968
+ min-width: 0;
969
+ }
970
+
971
+ /* Fullscreen - standalone mode like window but full viewport */
972
+ .debug-fullscreen {
973
+ position: fixed;
974
+ top: 0;
975
+ left: 0;
976
+ right: 0;
977
+ bottom: 0;
978
+ width: auto;
979
+ height: auto;
980
+ border: none;
981
+ border-radius: 0;
982
+ flex-direction: column;
983
+ }
984
+ .debug-fullscreen .debug-header {
985
+ flex-direction: row;
986
+ width: auto;
987
+ height: auto;
988
+ padding: 8px 16px;
989
+ border-bottom: 1px solid #3f3f46;
990
+ border-right: none;
991
+ }
992
+ .debug-fullscreen .debug-title {
993
+ flex-direction: row;
994
+ }
995
+ .debug-fullscreen .debug-title span:not(.debug-header-badge) {
996
+ display: inline;
997
+ }
998
+ .debug-fullscreen .debug-tabs {
999
+ flex-direction: row;
1000
+ margin-left: 12px;
1001
+ margin-top: 0;
1002
+ }
1003
+ .debug-fullscreen .debug-tab {
1004
+ width: auto;
1005
+ height: auto;
1006
+ padding: 4px 10px;
1007
+ }
1008
+ .debug-fullscreen .debug-tab-label {
1009
+ display: inline;
1010
+ }
1011
+ .debug-fullscreen .debug-tab .p-badge {
1012
+ position: static;
1013
+ }
1014
+ .debug-fullscreen .debug-actions {
1015
+ flex-direction: row;
1016
+ margin-left: auto;
1017
+ margin-top: 0;
1018
+ }
1019
+ .debug-fullscreen .debug-actions .p-button {
1020
+ width: auto;
1021
+ height: auto;
1022
+ }
1023
+ .debug-fullscreen .debug-content {
1024
+ flex: 1;
1025
+ min-height: 0;
1026
+ overflow: auto;
1027
+ }
1028
+
1029
+ /* Header */
1030
+ .debug-header {
1031
+ display: flex;
1032
+ align-items: center;
1033
+ gap: 8px;
1034
+ padding: 6px 12px;
1035
+ background: #27272a;
1036
+ border-bottom: 1px solid #3f3f46;
1037
+ flex-shrink: 0;
1038
+ }
1039
+ .debug-title {
1040
+ display: flex;
1041
+ align-items: center;
1042
+ gap: 6px;
1043
+ font-weight: 600;
1044
+ }
1045
+ .debug-title-draggable {
1046
+ cursor: grab;
1047
+ }
1048
+ .debug-title-draggable:active {
1049
+ cursor: grabbing;
1050
+ }
1051
+ .debug-tabs {
1052
+ display: flex;
1053
+ gap: 2px;
1054
+ margin-left: 12px;
1055
+ }
1056
+ .debug-tabs-compact {
1057
+ gap: 1px;
1058
+ }
1059
+ .debug-tabs-compact .debug-tab {
1060
+ padding: 4px 6px;
1061
+ }
1062
+ .debug-tab {
1063
+ display: flex;
1064
+ align-items: center;
1065
+ gap: 4px;
1066
+ padding: 4px 10px;
1067
+ background: transparent;
1068
+ border: none;
1069
+ border-radius: 4px;
1070
+ color: #a1a1aa;
1071
+ cursor: pointer;
1072
+ font-size: 12px;
1073
+ }
1074
+ .debug-tab:hover {
1075
+ background: #3f3f46;
1076
+ color: #f4f4f5;
1077
+ }
1078
+ .debug-tab-active {
1079
+ background: #3f3f46;
1080
+ color: #a78bfa;
1081
+ }
1082
+
1083
+ /* Tabs dropdown mode */
1084
+ .debug-tabs-dropdown {
1085
+ position: relative;
1086
+ margin-left: 12px;
1087
+ overflow: visible;
1088
+ }
1089
+ .debug-dropdown-trigger {
1090
+ display: flex;
1091
+ align-items: center;
1092
+ gap: 6px;
1093
+ padding: 4px 10px;
1094
+ background: #3f3f46;
1095
+ border: none;
1096
+ border-radius: 4px;
1097
+ color: #f4f4f5;
1098
+ cursor: pointer;
1099
+ font-size: 12px;
1100
+ }
1101
+ .debug-dropdown-trigger:hover {
1102
+ background: #52525b;
1103
+ }
1104
+ .debug-dropdown-trigger .pi-chevron-down {
1105
+ font-size: 10px;
1106
+ color: #71717a;
1107
+ margin-left: 4px;
1108
+ }
1109
+ .debug-dropdown-menu {
1110
+ position: absolute;
1111
+ top: 100%;
1112
+ left: 0;
1113
+ margin-top: 4px;
1114
+ background: #27272a;
1115
+ border: 1px solid #3f3f46;
1116
+ border-radius: 6px;
1117
+ box-shadow: 0 4px 12px rgba(0,0,0,0.4);
1118
+ min-width: 160px;
1119
+ z-index: 10001;
1120
+ padding: 4px;
1121
+ }
1122
+ .debug-dropdown-item {
1123
+ display: flex;
1124
+ align-items: center;
1125
+ gap: 8px;
1126
+ width: 100%;
1127
+ padding: 6px 10px;
1128
+ background: transparent;
1129
+ border: none;
1130
+ border-radius: 4px;
1131
+ color: #a1a1aa;
1132
+ cursor: pointer;
1133
+ font-size: 12px;
1134
+ text-align: left;
1135
+ }
1136
+ .debug-dropdown-item:hover {
1137
+ background: #3f3f46;
1138
+ color: #f4f4f5;
1139
+ }
1140
+ .debug-dropdown-item-active {
1141
+ background: #3f3f46;
1142
+ color: #a78bfa;
1143
+ }
1144
+ .debug-actions {
1145
+ display: flex;
1146
+ gap: 2px;
1147
+ margin-left: auto;
1148
+ }
1149
+
1150
+ /* Content */
1151
+ .debug-content {
1152
+ flex: 1;
1153
+ overflow: auto;
1154
+ background: #18181b;
1155
+ }
1156
+ .debug-empty {
1157
+ display: flex;
1158
+ flex-direction: column;
1159
+ align-items: center;
1160
+ justify-content: center;
1161
+ height: 100%;
1162
+ color: #71717a;
1163
+ gap: 8px;
1164
+ }
1165
+
1166
+ /* Per-collector toggle button */
1167
+ .debug-tab-toggle {
1168
+ display: flex;
1169
+ align-items: center;
1170
+ justify-content: center;
1171
+ width: 18px;
1172
+ height: 18px;
1173
+ margin-left: 4px;
1174
+ padding: 0;
1175
+ background: transparent;
1176
+ border: none;
1177
+ border-radius: 3px;
1178
+ color: #71717a;
1179
+ cursor: pointer;
1180
+ font-size: 9px;
1181
+ }
1182
+ .debug-tab-toggle:hover {
1183
+ background: #52525b;
1184
+ color: #f4f4f5;
1185
+ }
1186
+ .debug-tab-toggle-paused {
1187
+ color: #f59e0b;
1188
+ }
1189
+ .debug-tab-clear {
1190
+ display: flex;
1191
+ align-items: center;
1192
+ justify-content: center;
1193
+ width: 18px;
1194
+ height: 18px;
1195
+ margin-left: 2px;
1196
+ padding: 0;
1197
+ background: transparent;
1198
+ border: none;
1199
+ border-radius: 3px;
1200
+ color: #71717a;
1201
+ cursor: pointer;
1202
+ font-size: 9px;
1203
+ }
1204
+ .debug-tab-clear:hover {
1205
+ background: #ef4444;
1206
+ color: white;
1207
+ }
1208
+
1209
+ /* Custom mode toggle button */
1210
+ .debug-mode-btn {
1211
+ display: flex;
1212
+ align-items: center;
1213
+ justify-content: center;
1214
+ width: 2rem;
1215
+ height: 2rem;
1216
+ padding: 0;
1217
+ background: transparent;
1218
+ border: none;
1219
+ border-radius: 50%;
1220
+ color: #a1a1aa;
1221
+ cursor: pointer;
1222
+ font-size: 18px;
1223
+ font-weight: bold;
1224
+ font-family: monospace;
1225
+ line-height: 1;
1226
+ transition: background-color 0.15s, color 0.15s;
1227
+ }
1228
+ .debug-mode-btn:hover {
1229
+ background: rgba(255, 255, 255, 0.1);
1230
+ color: #f4f4f5;
1231
+ }
1232
+ </style>