qdadm 1.13.0 → 1.19.2

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 (86) hide show
  1. package/package.json +8 -4
  2. package/src/chain/ActiveStack.ts +79 -98
  3. package/src/chain/StackHydrator.ts +3 -2
  4. package/src/chain/index.ts +7 -1
  5. package/src/components/QdadmRoot.vue +52 -0
  6. package/src/components/edit/FormActions.vue +9 -6
  7. package/src/components/edit/LookupPickerDialog.vue +6 -3
  8. package/src/components/index.ts +6 -0
  9. package/src/composables/useEntityItemFormPage.ts +1 -0
  10. package/src/composables/useEntityItemShowPage.ts +1 -0
  11. package/src/composables/useFieldManager.ts +100 -3
  12. package/src/composables/useListPage.ts +50 -59
  13. package/src/composables/useListPage.utils.ts +101 -0
  14. package/src/composables/useNavigation.ts +26 -3
  15. package/src/composables/useOptionsLookup.ts +5 -1
  16. package/src/gen/generateManagers.test.js +27 -0
  17. package/src/gen/generateManagers.ts +12 -0
  18. package/src/hooks/HookRegistry.ts +14 -435
  19. package/src/i18n/I18n.ts +344 -0
  20. package/src/i18n/IncrementalDomainProvider.ts +153 -0
  21. package/src/i18n/InlineTranslationProvider.ts +4 -0
  22. package/src/i18n/LazyTranslationProvider.ts +102 -0
  23. package/src/i18n/MessagesRegistry.ts +4 -0
  24. package/src/i18n/Resolver.ts +4 -0
  25. package/src/i18n/__tests__/I18n.test.ts +169 -0
  26. package/src/i18n/__tests__/IncrementalDomainProvider.test.ts +146 -0
  27. package/src/i18n/__tests__/LazyTranslationProvider.test.ts +100 -0
  28. package/src/i18n/__tests__/Resolver.test.ts +271 -0
  29. package/src/i18n/defaults/DefaultCoreProvider.ts +28 -0
  30. package/src/i18n/defaults/core.en.yml +55 -0
  31. package/src/i18n/defaults/core.fr.yml +55 -0
  32. package/src/i18n/index.ts +55 -0
  33. package/src/i18n/loaders/raw-modules.d.ts +15 -0
  34. package/src/i18n/loaders/yaml.ts +35 -0
  35. package/src/i18n/strategies.ts +4 -0
  36. package/src/i18n/types.ts +34 -0
  37. package/src/i18n/useI18n.ts +34 -0
  38. package/src/index.ts +37 -0
  39. package/src/kernel/EventRouter.ts +17 -300
  40. package/src/kernel/Kernel.i18n.ts +29 -0
  41. package/src/kernel/Kernel.modules.ts +6 -0
  42. package/src/kernel/Kernel.registries.ts +10 -2
  43. package/src/kernel/Kernel.routing.ts +43 -1
  44. package/src/kernel/Kernel.ts +43 -0
  45. package/src/kernel/Kernel.types.ts +52 -1
  46. package/src/kernel/Kernel.vue.ts +121 -15
  47. package/src/kernel/KernelContext.entities.ts +80 -0
  48. package/src/kernel/KernelContext.events.ts +57 -0
  49. package/src/kernel/KernelContext.i18n.ts +37 -0
  50. package/src/kernel/KernelContext.permissions.ts +38 -0
  51. package/src/kernel/KernelContext.routing.ts +280 -0
  52. package/src/kernel/KernelContext.ts +125 -834
  53. package/src/kernel/KernelContext.types.ts +173 -0
  54. package/src/kernel/KernelContext.zones.ts +54 -0
  55. package/src/kernel/SSEBridge.ts +7 -362
  56. package/src/kernel/SignalBus.ts +24 -148
  57. package/src/modules/debug/AuthCollector.ts +48 -1
  58. package/src/modules/debug/Collector.ts +16 -302
  59. package/src/modules/debug/DebugBridge.ts +10 -171
  60. package/src/modules/debug/DebugModule.ts +35 -5
  61. package/src/modules/debug/EntitiesCollector.ts +97 -1
  62. package/src/modules/debug/ErrorCollector.ts +2 -77
  63. package/src/modules/debug/I18nCollector.ts +9 -0
  64. package/src/modules/debug/LocalStorageAdapter.ts +3 -147
  65. package/src/modules/debug/RouterCollector.ts +101 -1
  66. package/src/modules/debug/SignalCollector.ts +2 -150
  67. package/src/modules/debug/ToastCollector.ts +2 -91
  68. package/src/modules/debug/ZonesCollector.ts +93 -1
  69. package/src/modules/debug/components/DebugBar.vue +19 -775
  70. package/src/modules/debug/components/adminPanelsConfig.ts +42 -0
  71. package/src/modules/debug/components/index.ts +4 -3
  72. package/src/modules/debug/components/panels/AuthPanel.vue +1 -1
  73. package/src/modules/debug/components/panels/EntitiesPanel.vue +5 -5
  74. package/src/modules/debug/components/panels/I18nPanel.vue +738 -0
  75. package/src/modules/debug/components/panels/RouterPanel.vue +1 -1
  76. package/src/modules/debug/components/panels/index.ts +10 -4
  77. package/src/modules/debug/index.ts +15 -0
  78. package/src/modules/debug/styles.scss +22 -18
  79. package/src/utils/index.ts +0 -3
  80. package/src/vite/qdadmDebugPlugin.ts +401 -0
  81. package/src/vite-env.d.ts +16 -0
  82. package/src/modules/debug/components/ObjectTree.vue +0 -123
  83. package/src/modules/debug/components/panels/EntriesPanel.vue +0 -100
  84. package/src/modules/debug/components/panels/SignalsPanel.vue +0 -188
  85. package/src/modules/debug/components/panels/ToastsPanel.vue +0 -45
  86. package/src/utils/debugInjector.ts +0 -306
@@ -1,787 +1,31 @@
1
1
  <script setup lang="ts">
2
2
  /**
3
- * DebugBar - Floating debug toolbar for qdadm
3
+ * qdadm DebugBar wrapper.
4
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
5
+ * Wraps the panel-pluggable `@quazardous/qddebug` `DebugBar` with qdadm's
6
+ * admin-specific panels (auth, entities, router, zones, i18n) and the
7
+ * matching tab metadata (icons / labels / colors). Existing call sites that
8
+ * imported this file continue to work unchanged.
11
9
  */
12
- import { ref, computed, watch, onMounted, onUnmounted, type Ref } from 'vue'
13
- import Badge from 'primevue/badge'
14
- import Button from 'primevue/button'
15
- import { ZonesPanel, AuthPanel, EntitiesPanel, ToastsPanel, EntriesPanel, SignalsPanel, RouterPanel } from './panels'
10
+ import { DebugBar as QddebugBar } from '@quazardous/qddebug'
11
+ import { adminPanels, adminPanelsMeta } from './adminPanelsConfig'
16
12
 
17
- type DisplayMode = 'bottom' | 'right' | 'window'
18
-
19
- interface WindowBounds {
20
- x: number
21
- y: number
22
- width: number
23
- height: number
24
- }
25
-
26
- interface PanelSizes {
27
- bottomHeight: number
28
- rightWidth: number
29
- }
30
-
31
- interface SavedState {
32
- minimized: boolean
33
- expanded: boolean
34
- mode: DisplayMode
35
- fullscreen: boolean
36
- windowBounds: WindowBounds
37
- panelSizes: PanelSizes
38
- countAllBadges?: boolean
39
- }
40
-
41
- interface Collector {
42
- getBadge: (countAll?: boolean) => number
43
- getEntries: () => unknown[]
44
- records?: boolean
45
- enabled?: boolean
46
- clear: () => void
47
- toggle: () => void
48
- markAsSeen?: () => void
49
- [key: string]: unknown
50
- }
51
-
52
- interface CollectorEntry {
53
- name: string
54
- collector: Collector
55
- badge: number
56
- entries: unknown[]
57
- records?: boolean
58
- enabled?: boolean
59
- }
60
-
61
- interface DebugBridge {
62
- enabled?: Ref<boolean>
63
- tick?: Ref<number>
64
- collectors?: Map<string, Collector>
65
- toggle: () => void
66
- clearAll: () => void
67
- notify: () => void
68
- [key: string]: unknown
69
- }
70
-
71
- const props = withDefaults(defineProps<{
72
- bridge: DebugBridge
13
+ defineProps<{
14
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
15
+ bridge: any
73
16
  zIndex?: number
74
- }>(), {
75
- zIndex: 9999
76
- })
17
+ }>()
77
18
 
78
- // Persistence
19
+ // Preserve the legacy localStorage key so existing users keep their saved layout.
79
20
  const STORAGE_KEY = 'qdadm-debug-bar'
80
-
81
- const defaultWindowBounds: WindowBounds = { x: 100, y: 100, width: 500, height: 400 }
82
- const defaultPanelSizes: PanelSizes = { bottomHeight: 180, rightWidth: 420 }
83
-
84
- function loadState(): SavedState {
85
- try {
86
- const saved = localStorage.getItem(STORAGE_KEY)
87
- if (saved) {
88
- const state = JSON.parse(saved) as SavedState
89
- // Validate windowBounds (min 280x200)
90
- if (state.windowBounds) {
91
- const b = state.windowBounds
92
- if (typeof b.x !== 'number' || typeof b.y !== 'number' ||
93
- typeof b.width !== 'number' || typeof b.height !== 'number' ||
94
- b.width < 280 || b.height < 200) {
95
- state.windowBounds = { ...defaultWindowBounds }
96
- }
97
- }
98
- // Validate panelSizes
99
- if (state.panelSizes) {
100
- const p = state.panelSizes
101
- if (typeof p.bottomHeight !== 'number' || p.bottomHeight < 100) {
102
- p.bottomHeight = defaultPanelSizes.bottomHeight
103
- }
104
- if (typeof p.rightWidth !== 'number' || p.rightWidth < 200) {
105
- p.rightWidth = defaultPanelSizes.rightWidth
106
- }
107
- } else {
108
- state.panelSizes = { ...defaultPanelSizes }
109
- }
110
- return state
111
- }
112
- } catch { /* ignore */ }
113
- return { minimized: true, expanded: false, mode: 'bottom', fullscreen: false, windowBounds: { ...defaultWindowBounds }, panelSizes: { ...defaultPanelSizes } }
114
- }
115
-
116
- function saveState(): void {
117
- try {
118
- localStorage.setItem(STORAGE_KEY, JSON.stringify({
119
- minimized: minimized.value,
120
- expanded: expanded.value,
121
- mode: displayMode.value,
122
- fullscreen: fullscreen.value,
123
- windowBounds: windowBounds.value,
124
- panelSizes: panelSizes.value,
125
- countAllBadges: countAllBadges.value
126
- }))
127
- } catch { /* ignore */ }
128
- }
129
-
130
- const savedState = loadState()
131
-
132
- // State
133
- const minimized = ref<boolean>(savedState.minimized)
134
- const expanded = ref<boolean>(savedState.expanded)
135
- const displayMode = ref<DisplayMode>(savedState.mode) // 'bottom', 'right', or 'window'
136
- const fullscreen = ref<boolean>(savedState.fullscreen || false)
137
- const activeCollector = ref<number>(0)
138
- const countAllBadges = ref<boolean>(savedState.countAllBadges ?? false) // false = show unseen only
139
-
140
- // Reactive tick from bridge - collectors notify when they have new data
141
- const bridgeTick = computed<number>(() => props.bridge?.tick?.value ?? 0)
142
-
143
- // Window mode state
144
- const windowBounds = ref<WindowBounds>(savedState.windowBounds || defaultWindowBounds)
145
- const isDragging = ref<boolean>(false)
146
- const isResizing = ref<boolean>(false)
147
- const dragOffset = ref<{ x: number; y: number }>({ x: 0, y: 0 })
148
- const resizeStart = ref<{ x: number; y: number; width: number; height: number }>({ x: 0, y: 0, width: 0, height: 0 })
149
-
150
- // Docked panel sizes (resizable)
151
- const panelSizes = ref<PanelSizes>(savedState.panelSizes || defaultPanelSizes)
152
- const isPanelResizing = ref<boolean>(false)
153
- const panelResizeStart = ref<{ y: number; x: number; height: number; width: number }>({ y: 0, x: 0, height: 0, width: 0 })
154
-
155
- // Mobile detection
156
- const MOBILE_BREAKPOINT = 768
157
- const isMobile = ref<boolean>(window.innerWidth < MOBILE_BREAKPOINT)
158
-
159
- function updateMobileState(): void {
160
- isMobile.value = window.innerWidth < MOBILE_BREAKPOINT
161
- }
162
-
163
-
164
- // Responsive tabs state (applies to bottom and window modes with horizontal header, not mobile)
165
- const headerRef = ref<HTMLElement | null>(null)
166
- const headerWidth = ref<number>(1000)
167
- const isHorizontalHeader = computed<boolean>(() => !isMobile.value && (displayMode.value === 'bottom' || displayMode.value === 'window' || fullscreen.value) && displayMode.value !== 'right')
168
- const tabsCompact = computed<boolean>(() => isHorizontalHeader.value && headerWidth.value < 600)
169
- const tabsDropdown = computed<boolean>(() => isHorizontalHeader.value && headerWidth.value < 400)
170
- const showTabsDropdown = ref<boolean>(false)
171
-
172
- // Close dropdown when exiting dropdown mode
173
- watch(tabsDropdown, (isDropdown: boolean) => {
174
- if (!isDropdown) showTabsDropdown.value = false
175
- })
176
-
177
- let resizeObserver: ResizeObserver | null = null
178
- onMounted(() => {
179
- resizeObserver = new ResizeObserver((entries: ResizeObserverEntry[]) => {
180
- for (const entry of entries) {
181
- headerWidth.value = entry.contentRect.width
182
- }
183
- })
184
- // Close dropdown on outside click
185
- document.addEventListener('click', onDocumentClick)
186
- // Mobile detection on resize
187
- window.addEventListener('resize', updateMobileState)
188
- })
189
- onUnmounted(() => {
190
- resizeObserver?.disconnect()
191
- document.removeEventListener('click', onDocumentClick)
192
- window.removeEventListener('resize', updateMobileState)
193
- })
194
- watch(headerRef, (el: HTMLElement | null) => {
195
- resizeObserver?.disconnect()
196
- if (el) resizeObserver?.observe(el)
197
- }, { immediate: true })
198
-
199
- function onDocumentClick(e: MouseEvent): void {
200
- if (showTabsDropdown.value && !(e.target as Element).closest('.debug-tabs-dropdown')) {
201
- showTabsDropdown.value = false
202
- }
203
- }
204
-
205
- // Computed
206
- const isEnabled = computed<boolean>(() => props.bridge?.enabled?.value ?? false)
207
-
208
- const collectors = computed<CollectorEntry[]>(() => {
209
- void bridgeTick.value // Reactive dependency on bridge tick
210
- const result: CollectorEntry[] = []
211
- if (props.bridge?.collectors) {
212
- for (const [name, collector] of props.bridge.collectors) {
213
- result.push({
214
- name,
215
- collector,
216
- badge: collector.getBadge(countAllBadges.value),
217
- entries: collector.getEntries(),
218
- records: collector.records, // Does this collector record events?
219
- enabled: collector.enabled // Per-collector enabled state
220
- })
221
- }
222
- }
223
- return result
224
- })
225
-
226
- const currentCollector = computed<CollectorEntry | undefined>(() => collectors.value[activeCollector.value])
227
-
228
- // Separate badges for minimized state (always show unseen)
229
- const errorBadge = computed<number>(() => {
230
- void bridgeTick.value // Reactive dependency
231
- const c = collectors.value.find((c: CollectorEntry) => c.name === 'ErrorCollector' || c.name === 'errors')
232
- return c?.badge || 0
233
- })
234
- const signalBadge = computed<number>(() => {
235
- void bridgeTick.value // Reactive dependency
236
- const c = collectors.value.find((c: CollectorEntry) => c.name === 'SignalCollector' || c.name === 'signals')
237
- return c?.badge || 0
238
- })
239
-
240
- // Mark previous collector as seen when leaving tab
241
- watch(activeCollector, (newIdx: number, oldIdx: number | undefined) => {
242
- if (oldIdx !== undefined && collectors.value[oldIdx]?.collector?.markAsSeen) {
243
- collectors.value[oldIdx].collector.markAsSeen!()
244
- props.bridge?.notify()
245
- }
246
- })
247
-
248
- // Mark current collector as seen when collapsing panel
249
- watch(expanded, (isExpanded: boolean, wasExpanded: boolean) => {
250
- if (wasExpanded && !isExpanded) {
251
- const idx = activeCollector.value
252
- if (collectors.value[idx]?.collector?.markAsSeen) {
253
- collectors.value[idx].collector.markAsSeen!()
254
- props.bridge?.notify()
255
- }
256
- }
257
- })
258
-
259
- function toggleCountMode(): void {
260
- countAllBadges.value = !countAllBadges.value
261
- saveState()
262
- props.bridge?.notify()
263
- }
264
-
265
- // Actions
266
- function toggle(): void {
267
- expanded.value = !expanded.value
268
- saveState()
269
- }
270
-
271
- function expand(): void {
272
- minimized.value = false
273
- // On mobile, go directly to fullscreen
274
- if (isMobile.value) {
275
- fullscreen.value = true
276
- } else {
277
- displayMode.value = 'right'
278
- }
279
- expanded.value = true
280
- saveState()
281
- }
282
-
283
- function minimize(): void {
284
- minimized.value = true
285
- // Keep expanded state - will be restored when un-minimizing
286
- fullscreen.value = false
287
- saveState()
288
- }
289
-
290
- function toggleMode(): void {
291
- // Toggle between bottom and right (not window)
292
- if (displayMode.value === 'window') {
293
- displayMode.value = 'bottom'
294
- } else {
295
- displayMode.value = displayMode.value === 'bottom' ? 'right' : 'bottom'
296
- }
297
- saveState()
298
- }
299
-
300
- function enterWindowMode(): void {
301
- displayMode.value = 'window'
302
- expanded.value = true
303
- // Ensure window is visible within viewport with min size
304
- const vw = window.innerWidth
305
- const vh = window.innerHeight
306
- const bounds = windowBounds.value
307
- const width = Math.max(280, Math.min(bounds.width, vw - 40))
308
- const height = Math.max(200, Math.min(bounds.height, vh - 40))
309
- windowBounds.value = {
310
- x: Math.max(20, Math.min(vw - width - 20, bounds.x)),
311
- y: Math.max(20, Math.min(vh - height - 20, bounds.y)),
312
- width,
313
- height
314
- }
315
- saveState()
316
- }
317
-
318
- function exitWindowMode(): void {
319
- displayMode.value = 'bottom'
320
- saveState()
321
- }
322
-
323
- function getModeTitle(): string {
324
- if (displayMode.value === 'window') return 'Docked mode'
325
- return displayMode.value === 'bottom' ? 'Dock to right side' : 'Dock to bottom'
326
- }
327
-
328
- // Window drag handlers
329
- function startDrag(e: MouseEvent): void {
330
- if (displayMode.value !== 'window') return
331
- isDragging.value = true
332
- dragOffset.value = {
333
- x: e.clientX - windowBounds.value.x,
334
- y: e.clientY - windowBounds.value.y
335
- }
336
- document.addEventListener('mousemove', onDrag)
337
- document.addEventListener('mouseup', stopDrag)
338
- e.preventDefault()
339
- }
340
-
341
- function onDrag(e: MouseEvent): void {
342
- if (!isDragging.value) return
343
- windowBounds.value = {
344
- ...windowBounds.value,
345
- x: Math.max(0, Math.min(window.innerWidth - 100, e.clientX - dragOffset.value.x)),
346
- y: Math.max(0, Math.min(window.innerHeight - 50, e.clientY - dragOffset.value.y))
347
- }
348
- }
349
-
350
- function stopDrag(): void {
351
- if (isDragging.value) {
352
- isDragging.value = false
353
- document.removeEventListener('mousemove', onDrag)
354
- document.removeEventListener('mouseup', stopDrag)
355
- saveState()
356
- }
357
- }
358
-
359
- // Window resize handlers
360
- function startResize(e: MouseEvent): void {
361
- if (displayMode.value !== 'window') return
362
- isResizing.value = true
363
- resizeStart.value = {
364
- x: e.clientX,
365
- y: e.clientY,
366
- width: windowBounds.value.width,
367
- height: windowBounds.value.height
368
- }
369
- document.addEventListener('mousemove', onResize)
370
- document.addEventListener('mouseup', stopResize)
371
- e.preventDefault()
372
- e.stopPropagation()
373
- }
374
-
375
- const MIN_WINDOW_WIDTH = 280
376
- const MIN_WINDOW_HEIGHT = 200
377
-
378
- function onResize(e: MouseEvent): void {
379
- if (!isResizing.value) return
380
- const deltaX = e.clientX - resizeStart.value.x
381
- const deltaY = e.clientY - resizeStart.value.y
382
- windowBounds.value = {
383
- ...windowBounds.value,
384
- width: Math.max(MIN_WINDOW_WIDTH, resizeStart.value.width + deltaX),
385
- height: Math.max(MIN_WINDOW_HEIGHT, resizeStart.value.height + deltaY)
386
- }
387
- }
388
-
389
- function stopResize(): void {
390
- if (isResizing.value) {
391
- isResizing.value = false
392
- document.removeEventListener('mousemove', onResize)
393
- document.removeEventListener('mouseup', stopResize)
394
- saveState()
395
- }
396
- }
397
-
398
- // Docked panel resize handlers
399
- const MIN_PANEL_HEIGHT = 100
400
- const MAX_PANEL_HEIGHT = 600
401
- const MIN_PANEL_WIDTH = 200
402
- const MAX_PANEL_WIDTH = 800
403
-
404
- function startPanelResize(e: MouseEvent, direction: 'vertical' | 'horizontal'): void {
405
- if (displayMode.value === 'window' || fullscreen.value) return
406
- isPanelResizing.value = true
407
- panelResizeStart.value = {
408
- y: e.clientY,
409
- x: e.clientX,
410
- height: panelSizes.value.bottomHeight,
411
- width: panelSizes.value.rightWidth
412
- }
413
- const handler = direction === 'vertical' ? onPanelResizeVertical : onPanelResizeHorizontal
414
- const stop = () => stopPanelResize(handler, stop)
415
- document.addEventListener('mousemove', handler)
416
- document.addEventListener('mouseup', stop)
417
- e.preventDefault()
418
- }
419
-
420
- function onPanelResizeVertical(e: MouseEvent): void {
421
- if (!isPanelResizing.value) return
422
- const deltaY = panelResizeStart.value.y - e.clientY
423
- panelSizes.value = {
424
- ...panelSizes.value,
425
- bottomHeight: Math.max(MIN_PANEL_HEIGHT, Math.min(MAX_PANEL_HEIGHT, panelResizeStart.value.height + deltaY))
426
- }
427
- }
428
-
429
- function onPanelResizeHorizontal(e: MouseEvent): void {
430
- if (!isPanelResizing.value) return
431
- const deltaX = panelResizeStart.value.x - e.clientX
432
- panelSizes.value = {
433
- ...panelSizes.value,
434
- rightWidth: Math.max(MIN_PANEL_WIDTH, Math.min(MAX_PANEL_WIDTH, panelResizeStart.value.width + deltaX))
435
- }
436
- }
437
-
438
- function stopPanelResize(moveHandler: (e: MouseEvent) => void, upHandler: () => void): void {
439
- if (isPanelResizing.value) {
440
- isPanelResizing.value = false
441
- document.removeEventListener('mousemove', moveHandler)
442
- document.removeEventListener('mouseup', upHandler)
443
- saveState()
444
- }
445
- }
446
-
447
- // State before entering fullscreen (to restore on exit)
448
- let preFullscreenExpanded = false
449
-
450
- function toggleFullscreen(): void {
451
- if (!fullscreen.value) {
452
- // Entering fullscreen - save current state
453
- preFullscreenExpanded = expanded.value
454
- fullscreen.value = true
455
- expanded.value = true
456
- } else {
457
- // Exiting fullscreen - restore previous state
458
- fullscreen.value = false
459
- expanded.value = preFullscreenExpanded
460
- }
461
- saveState()
462
- }
463
-
464
- function toggleEnabled(): void {
465
- props.bridge?.toggle()
466
- }
467
-
468
- function clearAll(): void {
469
- props.bridge?.clearAll()
470
- props.bridge?.notify()
471
- }
472
-
473
- function clearCollector(collector: Collector): void {
474
- collector.clear()
475
- props.bridge?.notify()
476
- }
477
-
478
- function toggleCollector(collector: Collector): void {
479
- collector.toggle()
480
- props.bridge?.notify()
481
- }
482
-
483
- function notifyBridge(): void {
484
- props.bridge?.notify()
485
- }
486
-
487
- function getCollectorIcon(name: string | undefined): string {
488
- const icons: Record<string, string> = {
489
- errors: 'pi-exclamation-triangle',
490
- signals: 'pi-bolt',
491
- toasts: 'pi-bell',
492
- zones: 'pi-th-large',
493
- auth: 'pi-user',
494
- entities: 'pi-database',
495
- router: 'pi-directions',
496
- ErrorCollector: 'pi-exclamation-triangle',
497
- SignalCollector: 'pi-bolt',
498
- ToastCollector: 'pi-bell',
499
- ZonesCollector: 'pi-th-large',
500
- AuthCollector: 'pi-user',
501
- EntitiesCollector: 'pi-database',
502
- RouterCollector: 'pi-directions'
503
- }
504
- return (name && icons[name]) || 'pi-database'
505
- }
506
-
507
- function getCollectorLabel(name: string | undefined): string {
508
- const labels: Record<string, string> = {
509
- errors: 'Errors',
510
- signals: 'Signals',
511
- toasts: 'Toasts',
512
- zones: 'Zones',
513
- auth: 'Auth',
514
- entities: 'Entities',
515
- router: 'Router',
516
- ErrorCollector: 'Errors',
517
- SignalCollector: 'Signals',
518
- ToastCollector: 'Toasts',
519
- ZonesCollector: 'Zones',
520
- AuthCollector: 'Auth',
521
- EntitiesCollector: 'Entities',
522
- RouterCollector: 'Router'
523
- }
524
- return (name && labels[name]) || name || ''
525
- }
526
-
527
- function getCollectorColor(name: string | undefined): string {
528
- const colors: Record<string, string> = {
529
- errors: '#ef4444',
530
- toasts: '#f59e0b',
531
- signals: '#8b5cf6',
532
- zones: '#06b6d4',
533
- auth: '#10b981',
534
- entities: '#3b82f6',
535
- router: '#ec4899'
536
- }
537
- return (name && colors[name]) || '#6b7280'
538
- }
539
21
  </script>
540
22
 
541
23
  <template>
542
- <Teleport to="body">
543
- <!-- Minimized: corner button with separate badges -->
544
- <div v-if="minimized" class="debug-minimized" :style="{ zIndex }" @click="expand">
545
- <svg viewBox="0 0 100 100" width="28" height="28">
546
- <polygon points="50,5 93,27.5 93,72.5 50,95 7,72.5 7,27.5" fill="#1E3A8A"/>
547
- <text x="48" y="50" text-anchor="middle" dominant-baseline="central" font-family="system-ui" font-size="58" font-weight="800" letter-spacing="-4">
548
- <tspan fill="#60A5FA">Q</tspan><tspan fill="#93C5FD">D</tspan>
549
- </text>
550
- </svg>
551
- <div v-if="errorBadge > 0 || signalBadge > 0" class="debug-badges">
552
- <span v-if="errorBadge > 0" class="debug-badge debug-badge-error" title="Errors">
553
- <i class="pi pi-exclamation-triangle" />{{ errorBadge }}
554
- </span>
555
- <span v-if="signalBadge > 0" class="debug-badge debug-badge-signal" title="Signals">
556
- <i class="pi pi-bolt" />{{ signalBadge }}
557
- </span>
558
- </div>
559
- </div>
560
-
561
- <!-- Full panel -->
562
- <div v-else class="debug-panel" :class="[
563
- isMobile ? 'debug-mobile' : (fullscreen ? 'debug-fullscreen' : `debug-${displayMode}`),
564
- { 'debug-expanded': expanded }
565
- ]" :style="!isMobile && displayMode === 'window' && !fullscreen ? {
566
- zIndex,
567
- left: windowBounds.x + 'px',
568
- top: windowBounds.y + 'px',
569
- width: windowBounds.width + 'px',
570
- height: windowBounds.height + 'px'
571
- } : !isMobile && displayMode === 'bottom' && expanded && !fullscreen ? {
572
- zIndex,
573
- height: panelSizes.bottomHeight + 'px'
574
- } : !isMobile && displayMode === 'right' && expanded && !fullscreen ? {
575
- zIndex,
576
- width: panelSizes.rightWidth + 'px'
577
- } : { zIndex }">
578
- <!-- Header -->
579
- <div ref="headerRef" class="debug-header" @mousedown="displayMode === 'window' && !fullscreen ? startDrag($event) : null" :class="{ 'debug-header-draggable': displayMode === 'window' && !fullscreen }">
580
- <div class="debug-title" :class="{ 'debug-title-draggable': displayMode === 'window' && !fullscreen }">
581
- <svg viewBox="0 0 100 100" width="18" height="18">
582
- <polygon points="50,5 93,27.5 93,72.5 50,95 7,72.5 7,27.5" fill="#1E3A8A"/>
583
- <text x="48" y="50" text-anchor="middle" dominant-baseline="central" font-family="system-ui" font-size="58" font-weight="800" letter-spacing="-4">
584
- <tspan fill="#60A5FA">Q</tspan><tspan fill="#93C5FD">D</tspan>
585
- </text>
586
- </svg>
587
- <span>Debug</span>
588
- <span v-if="errorBadge > 0" class="debug-header-badge debug-header-badge-error" title="Errors">
589
- <i class="pi pi-exclamation-triangle" />{{ errorBadge }}
590
- </span>
591
- <span v-if="signalBadge > 0" class="debug-header-badge debug-header-badge-signal" title="Signals">
592
- <i class="pi pi-bolt" />{{ signalBadge }}
593
- </span>
594
- </div>
595
-
596
- <!-- Collector tabs - dropdown mode when very narrow -->
597
- <div v-if="expanded && tabsDropdown" class="debug-tabs-dropdown">
598
- <button class="debug-dropdown-trigger" @click="showTabsDropdown = !showTabsDropdown">
599
- <i :class="['pi', getCollectorIcon(currentCollector?.name)]" :style="{ color: getCollectorColor(currentCollector?.name) }" />
600
- <span>{{ getCollectorLabel(currentCollector?.name) }}</span>
601
- <Badge v-if="currentCollector && currentCollector.badge > 0" :value="currentCollector.badge" severity="secondary" />
602
- <i class="pi pi-chevron-down" />
603
- </button>
604
- <div v-if="showTabsDropdown" class="debug-dropdown-menu" @click="showTabsDropdown = false">
605
- <button
606
- v-for="(c, idx) in collectors"
607
- :key="c.name"
608
- class="debug-dropdown-item"
609
- :class="{ 'debug-dropdown-item-active': activeCollector === idx }"
610
- @click="activeCollector = idx"
611
- >
612
- <i :class="['pi', getCollectorIcon(c.name)]" :style="{ color: getCollectorColor(c.name) }" />
613
- <span>{{ getCollectorLabel(c.name) }}</span>
614
- <Badge v-if="c.badge > 0" :value="c.badge" severity="secondary" />
615
- </button>
616
- </div>
617
- </div>
618
-
619
- <!-- Collector tabs - normal/compact mode -->
620
- <div v-else-if="expanded" class="debug-tabs" :class="{ 'debug-tabs-compact': tabsCompact }">
621
- <button
622
- v-for="(c, idx) in collectors"
623
- :key="c.name"
624
- class="debug-tab"
625
- :class="{ 'debug-tab-active': activeCollector === idx }"
626
- :title="getCollectorLabel(c.name)"
627
- @click="activeCollector = idx"
628
- >
629
- <i :class="['pi', getCollectorIcon(c.name)]" :style="{ color: getCollectorColor(c.name) }" />
630
- <span v-if="!tabsCompact" class="debug-tab-label">{{ getCollectorLabel(c.name) }}</span>
631
- <Badge v-if="c.badge > 0" :value="c.badge" severity="secondary" />
632
- <!-- Per-collector play/pause for recording collectors (bottom mode only, not compact, not mobile) -->
633
- <button
634
- v-if="c.records && displayMode === 'bottom' && !tabsCompact && !isMobile"
635
- class="debug-tab-toggle"
636
- :class="{ 'debug-tab-toggle-paused': !c.enabled }"
637
- :title="c.enabled ? 'Pause recording' : 'Resume recording'"
638
- @click.stop="toggleCollector(c.collector)"
639
- >
640
- <i :class="['pi', c.enabled ? 'pi-pause' : 'pi-play']" />
641
- </button>
642
- <!-- Per-collector clear button for recording collectors (bottom mode only, not compact, not mobile) -->
643
- <button
644
- v-if="c.records && c.badge > 0 && displayMode === 'bottom' && !tabsCompact && !isMobile"
645
- class="debug-tab-clear"
646
- title="Clear entries"
647
- @click.stop="clearCollector(c.collector)"
648
- >
649
- <i class="pi pi-trash" />
650
- </button>
651
- </button>
652
- </div>
653
-
654
- <div class="debug-actions">
655
- <Button :icon="isEnabled ? 'pi pi-pause' : 'pi pi-play'" :severity="isEnabled ? 'success' : 'secondary'" size="small" text rounded :title="isEnabled ? 'Pause' : 'Resume'" @click="toggleEnabled" />
656
- <Button icon="pi pi-trash" severity="secondary" size="small" text rounded title="Clear all" @click="clearAll" />
657
- <!-- Badge count mode toggle -->
658
- <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" />
659
- <!-- Mode toggle (bottom/right) - not shown in window/fullscreen/mobile mode -->
660
- <button
661
- v-if="!isMobile && displayMode !== 'window' && !fullscreen"
662
- class="debug-mode-btn"
663
- :title="getModeTitle()"
664
- @click="toggleMode"
665
- >{{ displayMode === 'bottom' ? '|' : '―' }}</button>
666
- <!-- Enter window mode button - not in fullscreen/mobile -->
667
- <Button v-if="!isMobile && displayMode !== 'window' && !fullscreen" icon="pi pi-external-link" severity="secondary" size="small" text rounded title="Detach window" @click="enterWindowMode" />
668
- <!-- Exit window mode button (dock back) - not on mobile -->
669
- <Button v-if="!isMobile && displayMode === 'window'" icon="pi pi-link" severity="secondary" size="small" text rounded title="Dock panel" @click="exitWindowMode" />
670
- <!-- Fullscreen (not in window/mobile mode) -->
671
- <Button v-if="!isMobile && 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" />
672
- <!-- Expand/collapse (not in window/fullscreen/mobile mode) -->
673
- <Button v-if="!isMobile && 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" />
674
- <!-- Minimize - always show (on mobile, this is the only way to close) -->
675
- <Button icon="pi pi-times" severity="secondary" size="small" text rounded title="Close" @click="minimize" />
676
- </div>
677
- </div>
678
-
679
- <!-- Resize handle for window mode (not in fullscreen/mobile) -->
680
- <div v-if="!isMobile && displayMode === 'window' && !fullscreen" class="debug-resize-handle" @mousedown="startResize" />
681
-
682
- <!-- Resize handle for bottom mode (drag to resize height) - not on mobile -->
683
- <div
684
- v-if="!isMobile && displayMode === 'bottom' && expanded && !fullscreen"
685
- class="debug-panel-resize debug-panel-resize-top"
686
- @mousedown="startPanelResize($event, 'vertical')"
687
- />
688
-
689
- <!-- Resize handle for right mode (drag to resize width) - not on mobile -->
690
- <div
691
- v-if="!isMobile && displayMode === 'right' && expanded && !fullscreen"
692
- class="debug-panel-resize debug-panel-resize-left"
693
- @mousedown="startPanelResize($event, 'horizontal')"
694
- />
695
-
696
- <!-- Content -->
697
- <div v-if="expanded && currentCollector" class="debug-content">
698
- <!-- Content header with controls (right/window/fullscreen/mobile mode, for recording collectors) -->
699
- <div v-if="(isMobile || displayMode === 'right' || displayMode === 'window' || fullscreen) && currentCollector.records" class="debug-content-header">
700
- <span class="debug-content-title">{{ getCollectorLabel(currentCollector.name) }}</span>
701
- <div class="debug-content-controls">
702
- <button
703
- class="debug-tab-toggle"
704
- :class="{ 'debug-tab-toggle-paused': !currentCollector.enabled }"
705
- :title="currentCollector.enabled ? 'Pause recording' : 'Resume recording'"
706
- @click="toggleCollector(currentCollector.collector)"
707
- >
708
- <i :class="['pi', currentCollector.enabled ? 'pi-pause' : 'pi-play']" />
709
- </button>
710
- <button
711
- v-if="currentCollector.badge > 0"
712
- class="debug-tab-clear"
713
- title="Clear entries"
714
- @click="clearCollector(currentCollector.collector)"
715
- >
716
- <i class="pi pi-trash" />
717
- </button>
718
- </div>
719
- </div>
720
-
721
- <!-- Zones collector - always render for toolbar access -->
722
- <ZonesPanel
723
- v-if="currentCollector.name === 'zones' || currentCollector.name === 'ZonesCollector'"
724
- :collector="(currentCollector.collector as any)"
725
- :entries="(currentCollector.entries as any[])"
726
- @update="notifyBridge"
727
- />
728
-
729
- <!-- Signals collector -->
730
- <SignalsPanel
731
- v-else-if="currentCollector.name === 'signals' || currentCollector.name === 'SignalCollector'"
732
- :collector="(currentCollector.collector as any)"
733
- :entries="(currentCollector.entries as any[])"
734
- />
735
-
736
- <!-- Auth collector -->
737
- <AuthPanel
738
- v-else-if="currentCollector.name === 'auth' || currentCollector.name === 'AuthCollector'"
739
- :collector="(currentCollector.collector as any)"
740
- :entries="(currentCollector.entries as any[])"
741
- />
742
-
743
- <!-- Entities collector -->
744
- <EntitiesPanel
745
- v-else-if="currentCollector.name === 'entities' || currentCollector.name === 'EntitiesCollector'"
746
- :collector="(currentCollector.collector as any)"
747
- :entries="(currentCollector.entries as any[])"
748
- @update="notifyBridge"
749
- />
750
-
751
- <!-- Router collector -->
752
- <RouterPanel
753
- v-else-if="currentCollector.name === 'router' || currentCollector.name === 'RouterCollector'"
754
- :collector="(currentCollector.collector as any)"
755
- :entries="(currentCollector.entries as any[])"
756
- />
757
-
758
- <!-- Toasts collector (vertical modes) -->
759
- <ToastsPanel
760
- v-else-if="(currentCollector.name === 'toasts' || currentCollector.name === 'ToastCollector') && (isMobile || displayMode !== 'bottom' || fullscreen)"
761
- :entries="(currentCollector.entries as any[])"
762
- />
763
-
764
- <!-- Empty state for generic collectors without entries -->
765
- <div v-else-if="currentCollector.entries.length === 0" class="debug-empty">
766
- <i class="pi pi-inbox" />
767
- <span>No entries</span>
768
- </div>
769
-
770
- <!-- Default entries - horizontal (bottom mode, not mobile) -->
771
- <EntriesPanel
772
- v-else-if="!isMobile && displayMode === 'bottom' && !fullscreen"
773
- :entries="(currentCollector.entries as any[])"
774
- mode="horizontal"
775
- />
776
-
777
- <!-- Default entries - vertical (right/fullscreen/mobile mode) -->
778
- <EntriesPanel
779
- v-else
780
- :entries="(currentCollector.entries as any[])"
781
- mode="vertical"
782
- />
783
- </div>
784
- </div>
785
- </Teleport>
24
+ <QddebugBar
25
+ :bridge="bridge"
26
+ :z-index="zIndex"
27
+ :storage-key="STORAGE_KEY"
28
+ :panels="adminPanels"
29
+ :collector-meta="adminPanelsMeta"
30
+ />
786
31
  </template>
787
-