qdadm 0.29.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.
- package/package.json +11 -2
- package/src/components/forms/FormPage.vue +1 -1
- package/src/components/index.js +5 -3
- package/src/components/layout/AppLayout.vue +13 -1
- package/src/components/layout/Zone.vue +40 -23
- package/src/composables/index.js +2 -1
- package/src/composables/useAuth.js +43 -4
- package/src/composables/useCurrentEntity.js +44 -0
- package/src/composables/useFormPageBuilder.js +3 -3
- package/src/composables/useNavContext.js +24 -8
- package/src/composables/useSSEBridge.js +118 -0
- package/src/debug/AuthCollector.js +254 -0
- package/src/debug/Collector.js +235 -0
- package/src/debug/DebugBridge.js +163 -0
- package/src/debug/DebugModule.js +215 -0
- package/src/debug/EntitiesCollector.js +376 -0
- package/src/debug/ErrorCollector.js +66 -0
- package/src/debug/LocalStorageAdapter.js +150 -0
- package/src/debug/SignalCollector.js +87 -0
- package/src/debug/ToastCollector.js +82 -0
- package/src/debug/ZonesCollector.js +300 -0
- package/src/debug/components/DebugBar.vue +1232 -0
- package/src/debug/components/ObjectTree.vue +194 -0
- package/src/debug/components/index.js +8 -0
- package/src/debug/components/panels/AuthPanel.vue +103 -0
- package/src/debug/components/panels/EntitiesPanel.vue +616 -0
- package/src/debug/components/panels/EntriesPanel.vue +188 -0
- package/src/debug/components/panels/ToastsPanel.vue +112 -0
- package/src/debug/components/panels/ZonesPanel.vue +232 -0
- package/src/debug/components/panels/index.js +8 -0
- package/src/debug/index.js +31 -0
- package/src/editors/index.js +12 -0
- package/src/entity/EntityManager.js +142 -20
- package/src/entity/storage/MockApiStorage.js +17 -1
- package/src/entity/storage/index.js +9 -2
- package/src/gen/index.js +3 -0
- package/src/index.js +7 -0
- package/src/kernel/Kernel.js +661 -48
- package/src/kernel/KernelContext.js +385 -0
- package/src/kernel/Module.js +111 -0
- package/src/kernel/ModuleLoader.js +573 -0
- package/src/kernel/SSEBridge.js +354 -0
- package/src/kernel/SignalBus.js +10 -7
- package/src/kernel/index.js +19 -0
- package/src/toast/ToastBridgeModule.js +70 -0
- package/src/toast/ToastListener.vue +47 -0
- package/src/toast/index.js +15 -0
- package/src/toast/useSignalToast.js +113 -0
- package/src/composables/useSSE.js +0 -212
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LocalStorageAdapter - Debug settings persistence
|
|
3
|
+
*
|
|
4
|
+
* Persists debug bridge state (enabled, collector settings) to localStorage.
|
|
5
|
+
* Allows debug settings to survive page refreshes.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* import { createDebugBridge, LocalStorageAdapter } from '@qdadm/core/debug'
|
|
9
|
+
*
|
|
10
|
+
* const debug = createDebugBridge()
|
|
11
|
+
* const storage = new LocalStorageAdapter('qdadm-debug')
|
|
12
|
+
* storage.attach(debug) // Auto-saves on change, restores on load
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* LocalStorage adapter for debug settings persistence
|
|
17
|
+
*/
|
|
18
|
+
export class LocalStorageAdapter {
|
|
19
|
+
/**
|
|
20
|
+
* Create a new LocalStorageAdapter
|
|
21
|
+
* @param {string} key - localStorage key prefix
|
|
22
|
+
*/
|
|
23
|
+
constructor(key = 'qdadm-debug') {
|
|
24
|
+
this.key = key
|
|
25
|
+
this._bridge = null
|
|
26
|
+
this._unwatch = null
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Get the full key for a setting
|
|
31
|
+
* @param {string} name - Setting name
|
|
32
|
+
* @returns {string} Full localStorage key
|
|
33
|
+
*/
|
|
34
|
+
_getKey(name) {
|
|
35
|
+
return `${this.key}:${name}`
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Get a value from localStorage
|
|
40
|
+
* @param {string} name - Setting name
|
|
41
|
+
* @param {*} defaultValue - Default if not found
|
|
42
|
+
* @returns {*} The value
|
|
43
|
+
*/
|
|
44
|
+
get(name, defaultValue = null) {
|
|
45
|
+
try {
|
|
46
|
+
const stored = localStorage.getItem(this._getKey(name))
|
|
47
|
+
return stored !== null ? JSON.parse(stored) : defaultValue
|
|
48
|
+
} catch {
|
|
49
|
+
return defaultValue
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Set a value in localStorage
|
|
55
|
+
* @param {string} name - Setting name
|
|
56
|
+
* @param {*} value - Value to store
|
|
57
|
+
*/
|
|
58
|
+
set(name, value) {
|
|
59
|
+
try {
|
|
60
|
+
localStorage.setItem(this._getKey(name), JSON.stringify(value))
|
|
61
|
+
} catch {
|
|
62
|
+
// localStorage might be full or disabled
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Remove a value from localStorage
|
|
68
|
+
* @param {string} name - Setting name
|
|
69
|
+
*/
|
|
70
|
+
remove(name) {
|
|
71
|
+
localStorage.removeItem(this._getKey(name))
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Clear all debug settings
|
|
76
|
+
*/
|
|
77
|
+
clear() {
|
|
78
|
+
const prefix = this.key + ':'
|
|
79
|
+
const keys = []
|
|
80
|
+
for (let i = 0; i < localStorage.length; i++) {
|
|
81
|
+
const key = localStorage.key(i)
|
|
82
|
+
if (key && key.startsWith(prefix)) {
|
|
83
|
+
keys.push(key)
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
keys.forEach(key => localStorage.removeItem(key))
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Attach to a DebugBridge and sync state
|
|
91
|
+
* Restores saved state and watches for changes
|
|
92
|
+
* @param {DebugBridge} bridge - The debug bridge to attach to
|
|
93
|
+
* @returns {LocalStorageAdapter} this for chaining
|
|
94
|
+
*/
|
|
95
|
+
attach(bridge) {
|
|
96
|
+
this._bridge = bridge
|
|
97
|
+
|
|
98
|
+
// Restore saved enabled state
|
|
99
|
+
const savedEnabled = this.get('enabled')
|
|
100
|
+
if (savedEnabled !== null && bridge.enabled.value !== savedEnabled) {
|
|
101
|
+
bridge.enabled.value = savedEnabled
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Watch for enabled changes (if Vue's watch is available)
|
|
105
|
+
if (typeof window !== 'undefined' && window.__VUE_WATCH__) {
|
|
106
|
+
// In a real Vue app, use watch from vue
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return this
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Detach from the debug bridge
|
|
114
|
+
*/
|
|
115
|
+
detach() {
|
|
116
|
+
if (this._unwatch) {
|
|
117
|
+
this._unwatch()
|
|
118
|
+
this._unwatch = null
|
|
119
|
+
}
|
|
120
|
+
this._bridge = null
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Save current bridge state
|
|
125
|
+
*/
|
|
126
|
+
save() {
|
|
127
|
+
if (!this._bridge) return
|
|
128
|
+
this.set('enabled', this._bridge.enabled.value)
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Restore bridge state from localStorage
|
|
133
|
+
*/
|
|
134
|
+
restore() {
|
|
135
|
+
if (!this._bridge) return
|
|
136
|
+
const savedEnabled = this.get('enabled')
|
|
137
|
+
if (savedEnabled !== null) {
|
|
138
|
+
this._bridge.enabled.value = savedEnabled
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Factory function to create a LocalStorageAdapter
|
|
145
|
+
* @param {string} key - localStorage key prefix
|
|
146
|
+
* @returns {LocalStorageAdapter}
|
|
147
|
+
*/
|
|
148
|
+
export function createLocalStorageAdapter(key) {
|
|
149
|
+
return new LocalStorageAdapter(key)
|
|
150
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SignalCollector - Debug collector for SignalBus events
|
|
3
|
+
*
|
|
4
|
+
* Extends the base Collector to capture all signals emitted through the SignalBus.
|
|
5
|
+
* Uses wildcard subscription to capture all domain:action events.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* const collector = new SignalCollector({ maxEntries: 50 })
|
|
9
|
+
* collector.install(ctx) // ctx.signals is the SignalBus
|
|
10
|
+
*
|
|
11
|
+
* // Later, retrieve captured signals
|
|
12
|
+
* collector.getEntries() // [{ name, data, source, timestamp }, ...]
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { Collector } from './Collector.js'
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Collector for SignalBus events
|
|
19
|
+
*
|
|
20
|
+
* Records all signals with their name, data, and source for debugging.
|
|
21
|
+
* Uses the `*:*` wildcard pattern to capture all signals following the
|
|
22
|
+
* domain:action naming convention.
|
|
23
|
+
*/
|
|
24
|
+
export class SignalCollector extends Collector {
|
|
25
|
+
/**
|
|
26
|
+
* Collector name for identification
|
|
27
|
+
* @type {string}
|
|
28
|
+
*/
|
|
29
|
+
static name = 'signals'
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Internal install - subscribe to all signals
|
|
33
|
+
*
|
|
34
|
+
* @param {object} ctx - Context object from Kernel
|
|
35
|
+
* @param {import('../kernel/SignalBus.js').SignalBus} ctx.signals - SignalBus instance
|
|
36
|
+
* @protected
|
|
37
|
+
*/
|
|
38
|
+
_doInstall(ctx) {
|
|
39
|
+
if (!ctx?.signals) {
|
|
40
|
+
console.warn('[SignalCollector] No signals bus found in context')
|
|
41
|
+
return
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Subscribe to all signals using wildcard pattern
|
|
45
|
+
// QuarKernel supports wildcards with the configured delimiter (:)
|
|
46
|
+
// '*:*' matches any domain:action signal
|
|
47
|
+
this._unsubscribe = ctx.signals.on('*:*', (event) => {
|
|
48
|
+
this.record({
|
|
49
|
+
name: event.name,
|
|
50
|
+
data: event.data,
|
|
51
|
+
source: event.data?.source ?? null
|
|
52
|
+
})
|
|
53
|
+
})
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Internal uninstall - cleanup subscription
|
|
58
|
+
* @protected
|
|
59
|
+
*/
|
|
60
|
+
_doUninstall() {
|
|
61
|
+
if (this._unsubscribe) {
|
|
62
|
+
this._unsubscribe()
|
|
63
|
+
this._unsubscribe = null
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Get entries filtered by signal name pattern
|
|
69
|
+
*
|
|
70
|
+
* @param {string|RegExp} pattern - Pattern to match signal names
|
|
71
|
+
* @returns {Array<object>} Filtered entries
|
|
72
|
+
*/
|
|
73
|
+
getByPattern(pattern) {
|
|
74
|
+
const regex = pattern instanceof RegExp ? pattern : new RegExp(pattern)
|
|
75
|
+
return this.entries.filter((entry) => regex.test(entry.name))
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Get entries for a specific signal domain
|
|
80
|
+
*
|
|
81
|
+
* @param {string} domain - Domain prefix (e.g., 'entity', 'auth', 'books')
|
|
82
|
+
* @returns {Array<object>} Entries matching the domain
|
|
83
|
+
*/
|
|
84
|
+
getByDomain(domain) {
|
|
85
|
+
return this.entries.filter((entry) => entry.name.startsWith(`${domain}:`))
|
|
86
|
+
}
|
|
87
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ToastCollector - Captures toast notifications via signal bus
|
|
3
|
+
*
|
|
4
|
+
* This collector listens to toast signals emitted on the signal bus
|
|
5
|
+
* and records them for display in the debug panel.
|
|
6
|
+
*
|
|
7
|
+
* Toast signals should follow the pattern:
|
|
8
|
+
* - signals.emit('toast:success', { summary: '...', detail: '...' })
|
|
9
|
+
* - signals.emit('toast:error', { summary: '...', detail: '...' })
|
|
10
|
+
* - signals.emit('toast:info', { summary: '...', detail: '...' })
|
|
11
|
+
* - signals.emit('toast:warn', { summary: '...', detail: '...' })
|
|
12
|
+
*
|
|
13
|
+
* Use with ToastBridge module which handles displaying toasts via PrimeVue.
|
|
14
|
+
*
|
|
15
|
+
* @example
|
|
16
|
+
* const collector = new ToastCollector()
|
|
17
|
+
* collector.install(ctx)
|
|
18
|
+
* // Toasts emitted via signals are now automatically recorded
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import { Collector } from './Collector.js'
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Collector for toast notifications
|
|
25
|
+
*/
|
|
26
|
+
export class ToastCollector extends Collector {
|
|
27
|
+
/**
|
|
28
|
+
* Collector name identifier
|
|
29
|
+
* @type {string}
|
|
30
|
+
*/
|
|
31
|
+
static name = 'toasts'
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Internal install - subscribe to toast signals
|
|
35
|
+
* @param {object} ctx - Context object
|
|
36
|
+
* @protected
|
|
37
|
+
*/
|
|
38
|
+
_doInstall(ctx) {
|
|
39
|
+
// Listen for toast signals on the signal bus
|
|
40
|
+
if (ctx?.signals) {
|
|
41
|
+
this._unsubscribe = ctx.signals.on('toast:*', (event) => {
|
|
42
|
+
this.record({
|
|
43
|
+
severity: event.name.split(':')[1], // toast:success -> success
|
|
44
|
+
summary: event.data?.summary,
|
|
45
|
+
detail: event.data?.detail,
|
|
46
|
+
life: event.data?.life,
|
|
47
|
+
emitter: event.data?.emitter || 'unknown'
|
|
48
|
+
})
|
|
49
|
+
})
|
|
50
|
+
} else {
|
|
51
|
+
console.warn('[ToastCollector] No signals bus in context - toast recording disabled')
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Internal uninstall - cleanup
|
|
57
|
+
* @protected
|
|
58
|
+
*/
|
|
59
|
+
_doUninstall() {
|
|
60
|
+
if (this._unsubscribe) {
|
|
61
|
+
this._unsubscribe()
|
|
62
|
+
this._unsubscribe = null
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Get entries by severity
|
|
68
|
+
* @param {string} severity - Severity level (success, info, warn, error)
|
|
69
|
+
* @returns {Array<object>} Filtered entries
|
|
70
|
+
*/
|
|
71
|
+
getBySeverity(severity) {
|
|
72
|
+
return this.entries.filter((entry) => entry.severity === severity)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Get error toasts count for badge
|
|
77
|
+
* @returns {number}
|
|
78
|
+
*/
|
|
79
|
+
getErrorCount() {
|
|
80
|
+
return this.entries.filter((e) => e.severity === 'error').length
|
|
81
|
+
}
|
|
82
|
+
}
|
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ZonesCollector - Debug collector for Zone Registry visualization
|
|
3
|
+
*
|
|
4
|
+
* This collector provides real-time visibility into the zone system:
|
|
5
|
+
* - All defined zones
|
|
6
|
+
* - Blocks registered in each zone
|
|
7
|
+
* - Wrapper chains
|
|
8
|
+
* - Visual highlighting of zones on the page
|
|
9
|
+
*
|
|
10
|
+
* Unlike event collectors, this shows current state rather than historical events.
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* const collector = new ZonesCollector()
|
|
14
|
+
* collector.install(ctx)
|
|
15
|
+
* collector.getZoneInfo() // { zones: [...], totalBlocks: n }
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { Collector } from './Collector.js'
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Collector for Zone Registry state visualization
|
|
22
|
+
*/
|
|
23
|
+
export class ZonesCollector extends Collector {
|
|
24
|
+
/**
|
|
25
|
+
* Collector name identifier
|
|
26
|
+
* @type {string}
|
|
27
|
+
*/
|
|
28
|
+
static name = 'zones'
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* This collector shows state, not events
|
|
32
|
+
* @type {boolean}
|
|
33
|
+
*/
|
|
34
|
+
static records = false
|
|
35
|
+
|
|
36
|
+
constructor(options = {}) {
|
|
37
|
+
super(options)
|
|
38
|
+
this._registry = null
|
|
39
|
+
this._ctx = null
|
|
40
|
+
this._highlightedZone = null
|
|
41
|
+
this._overlays = new Map()
|
|
42
|
+
this._showCurrentPageOnly = options.showCurrentPageOnly ?? true
|
|
43
|
+
this._showInternalZones = options.showInternalZones ?? false
|
|
44
|
+
this._routerCleanup = null
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Internal install - get zone registry reference and subscribe to navigation
|
|
49
|
+
* @param {object} ctx - Context object
|
|
50
|
+
* @protected
|
|
51
|
+
*/
|
|
52
|
+
_doInstall(ctx) {
|
|
53
|
+
this._ctx = ctx
|
|
54
|
+
this._registry = ctx.zones
|
|
55
|
+
if (!this._registry) {
|
|
56
|
+
console.warn('[ZonesCollector] No zone registry found in context')
|
|
57
|
+
}
|
|
58
|
+
this._setupRouterListener()
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Setup router listener for navigation changes
|
|
63
|
+
* @private
|
|
64
|
+
*/
|
|
65
|
+
_setupRouterListener() {
|
|
66
|
+
const router = this._ctx?.router
|
|
67
|
+
if (!router) {
|
|
68
|
+
setTimeout(() => this._setupRouterListener(), 100)
|
|
69
|
+
return
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Listen to route changes - zones on page may differ
|
|
73
|
+
this._routerCleanup = router.afterEach(() => {
|
|
74
|
+
// Small delay to let DOM update
|
|
75
|
+
setTimeout(() => this.notifyChange(), 50)
|
|
76
|
+
})
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Internal uninstall - cleanup highlights and router listener
|
|
81
|
+
* @protected
|
|
82
|
+
*/
|
|
83
|
+
_doUninstall() {
|
|
84
|
+
this.clearHighlights()
|
|
85
|
+
if (this._routerCleanup) {
|
|
86
|
+
this._routerCleanup()
|
|
87
|
+
this._routerCleanup = null
|
|
88
|
+
}
|
|
89
|
+
this._registry = null
|
|
90
|
+
this._ctx = null
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Check if a zone is rendered on the current page
|
|
95
|
+
* @param {string} zoneName - Zone name to check
|
|
96
|
+
* @returns {boolean}
|
|
97
|
+
*
|
|
98
|
+
* Note: Internal zones (prefixed with _) are currently considered always on page
|
|
99
|
+
* since they're typically global (app:debug, app:toasts). If contextual internal
|
|
100
|
+
* zones are needed later, consider using a different prefix convention (e.g. `__`
|
|
101
|
+
* for global, `_` for contextual) or adding a `global` flag to zone config.
|
|
102
|
+
*/
|
|
103
|
+
isZoneOnPage(zoneName) {
|
|
104
|
+
// Internal zones are global, always considered "on page"
|
|
105
|
+
// TODO: revisit if contextual internal zones are needed
|
|
106
|
+
if (zoneName.startsWith('_')) {
|
|
107
|
+
return true
|
|
108
|
+
}
|
|
109
|
+
const escapedName = CSS.escape(zoneName)
|
|
110
|
+
return document.querySelector(`[data-zone="${zoneName}"], .zone-${escapedName}`) !== null
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Get/set filter for current page zones only
|
|
115
|
+
* @type {boolean}
|
|
116
|
+
*/
|
|
117
|
+
get showCurrentPageOnly() {
|
|
118
|
+
return this._showCurrentPageOnly
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
set showCurrentPageOnly(value) {
|
|
122
|
+
this._showCurrentPageOnly = value
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Toggle current page filter
|
|
127
|
+
* @returns {boolean} New filter state
|
|
128
|
+
*/
|
|
129
|
+
toggleFilter() {
|
|
130
|
+
this._showCurrentPageOnly = !this._showCurrentPageOnly
|
|
131
|
+
return this._showCurrentPageOnly
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Get/set filter for internal zones (prefixed with _)
|
|
136
|
+
* @type {boolean}
|
|
137
|
+
*/
|
|
138
|
+
get showInternalZones() {
|
|
139
|
+
return this._showInternalZones
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
set showInternalZones(value) {
|
|
143
|
+
this._showInternalZones = value
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Toggle internal zones filter
|
|
148
|
+
* @returns {boolean} New filter state
|
|
149
|
+
*/
|
|
150
|
+
toggleInternalFilter() {
|
|
151
|
+
this._showInternalZones = !this._showInternalZones
|
|
152
|
+
return this._showInternalZones
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Get badge - show number of zones (filtered if filter active)
|
|
157
|
+
* @returns {number}
|
|
158
|
+
*/
|
|
159
|
+
getBadge() {
|
|
160
|
+
if (!this._registry) return 0
|
|
161
|
+
let count = 0
|
|
162
|
+
for (const [name] of this._registry._zones) {
|
|
163
|
+
// Skip internal zones (prefixed with _) unless showing them
|
|
164
|
+
if (name.startsWith('_') && !this._showInternalZones) continue
|
|
165
|
+
// Apply page filter if enabled
|
|
166
|
+
if (this._showCurrentPageOnly && !this.isZoneOnPage(name)) continue
|
|
167
|
+
count++
|
|
168
|
+
}
|
|
169
|
+
return count
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Get all zone information for display
|
|
174
|
+
* @param {boolean} [forceAll=false] - If true, ignore filter and return all zones
|
|
175
|
+
* @returns {Array<object>} Zone info array
|
|
176
|
+
*/
|
|
177
|
+
getEntries(forceAll = false) {
|
|
178
|
+
if (!this._registry) return []
|
|
179
|
+
|
|
180
|
+
const zones = []
|
|
181
|
+
for (const [name, config] of this._registry._zones) {
|
|
182
|
+
// Skip internal zones (prefixed with _) unless showing them
|
|
183
|
+
if (name.startsWith('_') && !this._showInternalZones) {
|
|
184
|
+
continue
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// Apply filter if enabled
|
|
188
|
+
const isOnPage = this.isZoneOnPage(name)
|
|
189
|
+
if (!forceAll && this._showCurrentPageOnly && !isOnPage) {
|
|
190
|
+
continue
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const blocks = this._registry.getBlocks(name)
|
|
194
|
+
zones.push({
|
|
195
|
+
name,
|
|
196
|
+
isOnPage,
|
|
197
|
+
hasDefault: !!config.default,
|
|
198
|
+
defaultName: config.default?.name || config.default?.__name || null,
|
|
199
|
+
blocksCount: blocks.length,
|
|
200
|
+
blocks: blocks.map(b => ({
|
|
201
|
+
id: b.id || '(anonymous)',
|
|
202
|
+
weight: b.weight,
|
|
203
|
+
component: b.component?.name || b.component?.__name || 'Component',
|
|
204
|
+
hasWrappers: !!(b.wrappers && b.wrappers.length),
|
|
205
|
+
wrappersCount: b.wrappers?.length || 0,
|
|
206
|
+
wrappers: b.wrappers?.map(w => ({
|
|
207
|
+
component: w.component?.name || w.component?.__name || 'Wrapper',
|
|
208
|
+
weight: w.weight
|
|
209
|
+
})) || []
|
|
210
|
+
}))
|
|
211
|
+
})
|
|
212
|
+
}
|
|
213
|
+
return zones.sort((a, b) => a.name.localeCompare(b.name))
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Highlight a zone on the page
|
|
218
|
+
* Creates a visual overlay around zone elements
|
|
219
|
+
* @param {string} zoneName - Zone name to highlight
|
|
220
|
+
*/
|
|
221
|
+
highlightZone(zoneName) {
|
|
222
|
+
this.clearHighlights()
|
|
223
|
+
this._highlightedZone = zoneName
|
|
224
|
+
|
|
225
|
+
// Find zone elements by data attribute
|
|
226
|
+
// Note: CSS.escape() handles special characters like colons in zone names
|
|
227
|
+
const escapedName = CSS.escape(zoneName)
|
|
228
|
+
const elements = document.querySelectorAll(`[data-zone="${zoneName}"], .zone-${escapedName}`)
|
|
229
|
+
|
|
230
|
+
elements.forEach((el, idx) => {
|
|
231
|
+
const rect = el.getBoundingClientRect()
|
|
232
|
+
const overlay = document.createElement('div')
|
|
233
|
+
overlay.className = 'qdadm-zone-overlay'
|
|
234
|
+
overlay.style.cssText = `
|
|
235
|
+
position: fixed;
|
|
236
|
+
top: ${rect.top}px;
|
|
237
|
+
left: ${rect.left}px;
|
|
238
|
+
width: ${rect.width}px;
|
|
239
|
+
height: ${rect.height}px;
|
|
240
|
+
border: 2px dashed #8b5cf6;
|
|
241
|
+
background: rgba(139, 92, 246, 0.1);
|
|
242
|
+
pointer-events: none;
|
|
243
|
+
z-index: 99998;
|
|
244
|
+
transition: all 0.2s;
|
|
245
|
+
`
|
|
246
|
+
|
|
247
|
+
// Label
|
|
248
|
+
const label = document.createElement('div')
|
|
249
|
+
label.style.cssText = `
|
|
250
|
+
position: absolute;
|
|
251
|
+
top: -20px;
|
|
252
|
+
left: 0;
|
|
253
|
+
background: #8b5cf6;
|
|
254
|
+
color: white;
|
|
255
|
+
padding: 2px 6px;
|
|
256
|
+
font-size: 10px;
|
|
257
|
+
font-family: system-ui, sans-serif;
|
|
258
|
+
border-radius: 2px;
|
|
259
|
+
white-space: nowrap;
|
|
260
|
+
`
|
|
261
|
+
label.textContent = zoneName
|
|
262
|
+
overlay.appendChild(label)
|
|
263
|
+
|
|
264
|
+
document.body.appendChild(overlay)
|
|
265
|
+
this._overlays.set(`${zoneName}-${idx}`, overlay)
|
|
266
|
+
})
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Clear all zone highlights
|
|
271
|
+
*/
|
|
272
|
+
clearHighlights() {
|
|
273
|
+
this._overlays.forEach(overlay => overlay.remove())
|
|
274
|
+
this._overlays.clear()
|
|
275
|
+
this._highlightedZone = null
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* Get currently highlighted zone
|
|
280
|
+
* @returns {string|null}
|
|
281
|
+
*/
|
|
282
|
+
getHighlightedZone() {
|
|
283
|
+
return this._highlightedZone
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* Toggle zone highlight
|
|
288
|
+
* @param {string} zoneName - Zone name
|
|
289
|
+
* @returns {boolean} Whether zone is now highlighted
|
|
290
|
+
*/
|
|
291
|
+
toggleHighlight(zoneName) {
|
|
292
|
+
if (this._highlightedZone === zoneName) {
|
|
293
|
+
this.clearHighlights()
|
|
294
|
+
return false
|
|
295
|
+
} else {
|
|
296
|
+
this.highlightZone(zoneName)
|
|
297
|
+
return true
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
}
|