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.
Files changed (49) hide show
  1. package/package.json +11 -2
  2. package/src/components/forms/FormPage.vue +1 -1
  3. package/src/components/index.js +5 -3
  4. package/src/components/layout/AppLayout.vue +13 -1
  5. package/src/components/layout/Zone.vue +40 -23
  6. package/src/composables/index.js +2 -1
  7. package/src/composables/useAuth.js +43 -4
  8. package/src/composables/useCurrentEntity.js +44 -0
  9. package/src/composables/useFormPageBuilder.js +3 -3
  10. package/src/composables/useNavContext.js +24 -8
  11. package/src/composables/useSSEBridge.js +118 -0
  12. package/src/debug/AuthCollector.js +254 -0
  13. package/src/debug/Collector.js +235 -0
  14. package/src/debug/DebugBridge.js +163 -0
  15. package/src/debug/DebugModule.js +215 -0
  16. package/src/debug/EntitiesCollector.js +376 -0
  17. package/src/debug/ErrorCollector.js +66 -0
  18. package/src/debug/LocalStorageAdapter.js +150 -0
  19. package/src/debug/SignalCollector.js +87 -0
  20. package/src/debug/ToastCollector.js +82 -0
  21. package/src/debug/ZonesCollector.js +300 -0
  22. package/src/debug/components/DebugBar.vue +1232 -0
  23. package/src/debug/components/ObjectTree.vue +194 -0
  24. package/src/debug/components/index.js +8 -0
  25. package/src/debug/components/panels/AuthPanel.vue +103 -0
  26. package/src/debug/components/panels/EntitiesPanel.vue +616 -0
  27. package/src/debug/components/panels/EntriesPanel.vue +188 -0
  28. package/src/debug/components/panels/ToastsPanel.vue +112 -0
  29. package/src/debug/components/panels/ZonesPanel.vue +232 -0
  30. package/src/debug/components/panels/index.js +8 -0
  31. package/src/debug/index.js +31 -0
  32. package/src/editors/index.js +12 -0
  33. package/src/entity/EntityManager.js +142 -20
  34. package/src/entity/storage/MockApiStorage.js +17 -1
  35. package/src/entity/storage/index.js +9 -2
  36. package/src/gen/index.js +3 -0
  37. package/src/index.js +7 -0
  38. package/src/kernel/Kernel.js +661 -48
  39. package/src/kernel/KernelContext.js +385 -0
  40. package/src/kernel/Module.js +111 -0
  41. package/src/kernel/ModuleLoader.js +573 -0
  42. package/src/kernel/SSEBridge.js +354 -0
  43. package/src/kernel/SignalBus.js +10 -7
  44. package/src/kernel/index.js +19 -0
  45. package/src/toast/ToastBridgeModule.js +70 -0
  46. package/src/toast/ToastListener.vue +47 -0
  47. package/src/toast/index.js +15 -0
  48. package/src/toast/useSignalToast.js +113 -0
  49. 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
+ }