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,188 @@
1
+ <script setup>
2
+ /**
3
+ * EntriesPanel - Default entries display (horizontal/vertical)
4
+ */
5
+ import { ref } from 'vue'
6
+ import ObjectTree from '../ObjectTree.vue'
7
+
8
+ defineProps({
9
+ entries: { type: Array, required: true },
10
+ mode: { type: String, default: 'vertical' }, // 'horizontal' or 'vertical'
11
+ maxEntries: { type: Number, default: 10 }
12
+ })
13
+
14
+ const copiedIdx = ref(null)
15
+
16
+ function formatTime(ts) {
17
+ return new Date(ts).toLocaleTimeString('en-US', {
18
+ hour12: false, hour: '2-digit', minute: '2-digit', second: '2-digit'
19
+ })
20
+ }
21
+
22
+ function getEntryData(entry) {
23
+ const { timestamp, _isNew, ...rest } = entry
24
+ return rest
25
+ }
26
+
27
+ async function copyEntry(entry, idx) {
28
+ try {
29
+ const data = getEntryData(entry)
30
+ await navigator.clipboard.writeText(JSON.stringify(data, null, 2))
31
+ copiedIdx.value = idx
32
+ setTimeout(() => { copiedIdx.value = null }, 1500)
33
+ } catch (e) {
34
+ console.error('Failed to copy:', e)
35
+ }
36
+ }
37
+ </script>
38
+
39
+ <template>
40
+ <!-- Horizontal layout -->
41
+ <div v-if="mode === 'horizontal'" class="entries-h">
42
+ <div
43
+ v-for="(entry, idx) in entries.slice().reverse().slice(0, maxEntries)"
44
+ :key="idx"
45
+ class="entry-h"
46
+ :class="{ 'entry-new': entry._isNew }"
47
+ >
48
+ <div class="entry-meta">
49
+ <span class="entry-time">{{ formatTime(entry.timestamp) }}</span>
50
+ <span v-if="entry.name" class="entry-name">{{ entry.name }}</span>
51
+ </div>
52
+ <ObjectTree :data="getEntryData(entry)" :maxDepth="3" />
53
+ </div>
54
+ </div>
55
+
56
+ <!-- Vertical layout -->
57
+ <div v-else class="entries-v">
58
+ <div
59
+ v-for="(entry, idx) in entries.slice().reverse()"
60
+ :key="idx"
61
+ class="entry-v"
62
+ :class="{ 'entry-new': entry._isNew }"
63
+ >
64
+ <div class="entry-header">
65
+ <span class="entry-time">{{ formatTime(entry.timestamp) }}</span>
66
+ <span v-if="entry.name" class="entry-name">{{ entry.name }}</span>
67
+ <span v-if="entry.message" class="entry-message">{{ entry.message }}</span>
68
+ <button
69
+ class="entry-copy"
70
+ :class="{ 'entry-copied': copiedIdx === idx }"
71
+ @click="copyEntry(entry, idx)"
72
+ :title="copiedIdx === idx ? 'Copied!' : 'Copy to clipboard'"
73
+ >
74
+ <i :class="['pi', copiedIdx === idx ? 'pi-check' : 'pi-copy']" />
75
+ </button>
76
+ </div>
77
+ <ObjectTree :data="getEntryData(entry)" :maxDepth="6" />
78
+ </div>
79
+ </div>
80
+ </template>
81
+
82
+ <style scoped>
83
+ /* Horizontal entries (bottom mode) */
84
+ .entries-h {
85
+ display: flex;
86
+ gap: 1px;
87
+ height: 100%;
88
+ overflow-x: auto;
89
+ }
90
+ .entry-h {
91
+ flex: 0 0 auto;
92
+ min-width: 200px;
93
+ max-width: 320px;
94
+ padding: 8px 12px;
95
+ border-right: 1px solid #3f3f46;
96
+ overflow: hidden;
97
+ }
98
+ .entry-h:last-child {
99
+ border-right: none;
100
+ }
101
+ .entry-h.entry-new {
102
+ border-left: 3px solid #8b5cf6;
103
+ }
104
+
105
+ /* Vertical entries (right/fullscreen mode) */
106
+ .entries-v {
107
+ display: flex;
108
+ flex-direction: column;
109
+ }
110
+ .entry-v {
111
+ padding: 8px 12px;
112
+ border-bottom: 1px solid #27272a;
113
+ }
114
+ .entry-v:hover {
115
+ background: #1f1f23;
116
+ }
117
+
118
+ /* New entry indicator */
119
+ .entry-new {
120
+ position: relative;
121
+ background: rgba(139, 92, 246, 0.08);
122
+ }
123
+ .entry-v.entry-new::before {
124
+ content: '';
125
+ position: absolute;
126
+ left: 0;
127
+ top: 0;
128
+ bottom: 0;
129
+ width: 3px;
130
+ background: #8b5cf6;
131
+ }
132
+
133
+ /* Entry parts */
134
+ .entry-meta, .entry-header {
135
+ display: flex;
136
+ align-items: center;
137
+ gap: 6px;
138
+ margin-bottom: 4px;
139
+ }
140
+ .entry-time {
141
+ font-family: 'JetBrains Mono', monospace;
142
+ font-size: 10px;
143
+ color: #71717a;
144
+ background: #27272a;
145
+ padding: 1px 4px;
146
+ border-radius: 2px;
147
+ }
148
+ .entry-name {
149
+ color: #a78bfa;
150
+ font-weight: 500;
151
+ font-size: 12px;
152
+ }
153
+ .entry-message {
154
+ color: #f87171;
155
+ font-size: 12px;
156
+ }
157
+ /* Copy button */
158
+ .entry-copy {
159
+ margin-left: auto;
160
+ display: flex;
161
+ align-items: center;
162
+ justify-content: center;
163
+ width: 22px;
164
+ height: 22px;
165
+ padding: 0;
166
+ background: transparent;
167
+ border: none;
168
+ border-radius: 3px;
169
+ color: #71717a;
170
+ cursor: pointer;
171
+ opacity: 0;
172
+ transition: opacity 0.15s, background 0.15s, color 0.15s;
173
+ }
174
+ .entry-v:hover .entry-copy {
175
+ opacity: 1;
176
+ }
177
+ .entry-copy:hover {
178
+ background: #3f3f46;
179
+ color: #d4d4d8;
180
+ }
181
+ .entry-copied {
182
+ opacity: 1;
183
+ color: #22c55e;
184
+ }
185
+ .entry-copy .pi {
186
+ font-size: 12px;
187
+ }
188
+ </style>
@@ -0,0 +1,112 @@
1
+ <script setup>
2
+ /**
3
+ * ToastsPanel - Toasts collector display (vertical mode)
4
+ */
5
+ defineProps({
6
+ entries: { type: Array, required: true }
7
+ })
8
+
9
+ function formatTime(ts) {
10
+ return new Date(ts).toLocaleTimeString('en-US', {
11
+ hour12: false, hour: '2-digit', minute: '2-digit', second: '2-digit'
12
+ })
13
+ }
14
+ </script>
15
+
16
+ <template>
17
+ <div class="toasts-panel">
18
+ <div
19
+ v-for="(entry, idx) in entries.slice().reverse()"
20
+ :key="idx"
21
+ class="toast-entry"
22
+ :class="[`toast-${entry.severity}`, { 'entry-new': entry._isNew }]"
23
+ >
24
+ <div class="toast-header">
25
+ <span class="toast-time">{{ formatTime(entry.timestamp) }}</span>
26
+ <span class="toast-severity" :class="`toast-severity-${entry.severity}`">{{ entry.severity }}</span>
27
+ <span v-if="entry.summary" class="toast-summary">{{ entry.summary }}</span>
28
+ <span v-if="entry.emitter" class="toast-emitter">{{ entry.emitter }}</span>
29
+ </div>
30
+ <div v-if="entry.detail" class="toast-detail">{{ entry.detail }}</div>
31
+ </div>
32
+ </div>
33
+ </template>
34
+
35
+ <style scoped>
36
+ .toasts-panel {
37
+ display: flex;
38
+ flex-direction: column;
39
+ }
40
+ .toast-entry {
41
+ padding: 8px 12px;
42
+ border-bottom: 1px solid #27272a;
43
+ border-left: 3px solid;
44
+ }
45
+ .toast-entry:hover {
46
+ background: #1f1f23;
47
+ }
48
+ .toast-success { border-left-color: #22c55e; }
49
+ .toast-info { border-left-color: #3b82f6; }
50
+ .toast-warn { border-left-color: #f59e0b; }
51
+ .toast-error { border-left-color: #ef4444; }
52
+
53
+ .toast-header {
54
+ display: flex;
55
+ align-items: center;
56
+ gap: 6px;
57
+ margin-bottom: 4px;
58
+ }
59
+ .toast-time {
60
+ font-family: 'JetBrains Mono', monospace;
61
+ font-size: 10px;
62
+ color: #71717a;
63
+ background: #27272a;
64
+ padding: 1px 4px;
65
+ border-radius: 2px;
66
+ }
67
+ .toast-severity {
68
+ padding: 1px 6px;
69
+ border-radius: 3px;
70
+ font-size: 10px;
71
+ font-weight: 600;
72
+ text-transform: uppercase;
73
+ }
74
+ .toast-severity-success { background: #22c55e; color: white; }
75
+ .toast-severity-info { background: #3b82f6; color: white; }
76
+ .toast-severity-warn { background: #f59e0b; color: white; }
77
+ .toast-severity-error { background: #ef4444; color: white; }
78
+
79
+ .toast-summary {
80
+ font-weight: 500;
81
+ color: #f4f4f5;
82
+ }
83
+ .toast-detail {
84
+ color: #a1a1aa;
85
+ font-size: 11px;
86
+ margin-top: 4px;
87
+ }
88
+ .toast-emitter {
89
+ margin-left: auto;
90
+ padding: 1px 6px;
91
+ background: #3f3f46;
92
+ border-radius: 3px;
93
+ font-size: 10px;
94
+ color: #a1a1aa;
95
+ font-family: 'JetBrains Mono', monospace;
96
+ }
97
+
98
+ /* New entry indicator */
99
+ .entry-new {
100
+ position: relative;
101
+ background: rgba(139, 92, 246, 0.08);
102
+ }
103
+ .entry-new::before {
104
+ content: '';
105
+ position: absolute;
106
+ left: 0;
107
+ top: 0;
108
+ bottom: 0;
109
+ width: 3px;
110
+ background: #8b5cf6;
111
+ }
112
+ </style>
@@ -0,0 +1,232 @@
1
+ <script setup>
2
+ /**
3
+ * ZonesPanel - Zones collector display
4
+ */
5
+ const props = defineProps({
6
+ collector: { type: Object, required: true },
7
+ entries: { type: Array, required: true }
8
+ })
9
+
10
+ const emit = defineEmits(['update'])
11
+
12
+ function toggleFilter() {
13
+ if (props.collector.toggleFilter) {
14
+ props.collector.toggleFilter()
15
+ emit('update')
16
+ }
17
+ }
18
+
19
+ function isFilterActive() {
20
+ return props.collector.showCurrentPageOnly ?? true
21
+ }
22
+
23
+ function toggleInternalFilter() {
24
+ if (props.collector.toggleInternalFilter) {
25
+ props.collector.toggleInternalFilter()
26
+ emit('update')
27
+ }
28
+ }
29
+
30
+ function showInternalZones() {
31
+ return props.collector.showInternalZones ?? false
32
+ }
33
+
34
+ function highlightZone(zoneName) {
35
+ if (props.collector.toggleHighlight) {
36
+ props.collector.toggleHighlight(zoneName)
37
+ emit('update')
38
+ }
39
+ }
40
+
41
+ function isHighlighted(zoneName) {
42
+ return props.collector.getHighlightedZone?.() === zoneName
43
+ }
44
+ </script>
45
+
46
+ <template>
47
+ <div class="zones-panel">
48
+ <div class="zones-toolbar">
49
+ <button
50
+ class="zones-toggle"
51
+ :class="{ 'zones-toggle-on': !isFilterActive() }"
52
+ :title="isFilterActive() ? 'Click to show all zones' : 'Click to show current page only'"
53
+ @click="toggleFilter"
54
+ >
55
+ <i :class="['pi', isFilterActive() ? 'pi-filter-slash' : 'pi-filter']" />
56
+ All Pages
57
+ </button>
58
+ <button
59
+ class="zones-toggle"
60
+ :class="{ 'zones-toggle-internal': showInternalZones() }"
61
+ :title="showInternalZones() ? 'Click to hide internal zones' : 'Click to show internal zones (prefixed with _)'"
62
+ @click="toggleInternalFilter"
63
+ >
64
+ <i :class="['pi', showInternalZones() ? 'pi-lock-open' : 'pi-lock']" />
65
+ Internal
66
+ </button>
67
+ </div>
68
+ <div v-if="entries.length === 0" class="zones-empty">
69
+ <i class="pi pi-inbox" />
70
+ <span>No zones</span>
71
+ </div>
72
+ <div v-for="zone in entries" :key="zone.name" class="zone-item">
73
+ <div class="zone-header">
74
+ <button
75
+ class="zone-highlight"
76
+ :class="{ 'zone-highlight-active': isHighlighted(zone.name) }"
77
+ :title="isHighlighted(zone.name) ? 'Hide highlight' : 'Highlight zone'"
78
+ @click="highlightZone(zone.name)"
79
+ >
80
+ <i class="pi pi-eye" />
81
+ </button>
82
+ <span class="zone-name">{{ zone.name }}</span>
83
+ <span class="zone-count">{{ zone.blocksCount }} block(s)</span>
84
+ </div>
85
+ <div v-if="zone.blocks.length > 0" class="zone-blocks">
86
+ <div v-for="block in zone.blocks" :key="block.id" class="zone-block">
87
+ <span class="block-id">{{ block.id }}</span>
88
+ <span class="block-component">{{ block.component }}</span>
89
+ <span class="block-weight">w:{{ block.weight }}</span>
90
+ <span v-if="block.hasWrappers" class="block-wrappers">
91
+ <i class="pi pi-sitemap" :title="`${block.wrappersCount} wrapper(s)`" />
92
+ </span>
93
+ </div>
94
+ </div>
95
+ <div v-else-if="zone.hasDefault" class="zone-default">
96
+ Default: {{ zone.defaultName || '(component)' }}
97
+ </div>
98
+ </div>
99
+ </div>
100
+ </template>
101
+
102
+ <style scoped>
103
+ .zones-panel {
104
+ padding: 8px;
105
+ display: flex;
106
+ flex-direction: column;
107
+ gap: 8px;
108
+ }
109
+ .zones-toolbar {
110
+ display: flex;
111
+ align-items: center;
112
+ gap: 8px;
113
+ margin-bottom: 4px;
114
+ }
115
+ .zones-empty {
116
+ display: flex;
117
+ flex-direction: column;
118
+ align-items: center;
119
+ justify-content: center;
120
+ gap: 8px;
121
+ padding: 32px;
122
+ color: #71717a;
123
+ font-size: 12px;
124
+ }
125
+ .zones-empty i {
126
+ font-size: 24px;
127
+ }
128
+ .zones-toggle {
129
+ display: flex;
130
+ align-items: center;
131
+ gap: 6px;
132
+ padding: 4px 10px;
133
+ background: #3f3f46;
134
+ border: none;
135
+ border-radius: 4px;
136
+ color: #a1a1aa;
137
+ cursor: pointer;
138
+ font-size: 11px;
139
+ }
140
+ .zones-toggle:hover {
141
+ background: #52525b;
142
+ color: #f4f4f5;
143
+ }
144
+ .zones-toggle-on {
145
+ background: #06b6d4;
146
+ color: white;
147
+ }
148
+ .zones-toggle-on:hover {
149
+ background: #0891b2;
150
+ }
151
+ .zones-toggle-internal {
152
+ background: #f59e0b;
153
+ color: white;
154
+ }
155
+ .zones-toggle-internal:hover {
156
+ background: #d97706;
157
+ }
158
+ .zone-item {
159
+ background: #27272a;
160
+ border-radius: 4px;
161
+ padding: 8px;
162
+ }
163
+ .zone-header {
164
+ display: flex;
165
+ align-items: center;
166
+ gap: 8px;
167
+ margin-bottom: 6px;
168
+ }
169
+ .zone-highlight {
170
+ display: flex;
171
+ align-items: center;
172
+ justify-content: center;
173
+ width: 22px;
174
+ height: 22px;
175
+ padding: 0;
176
+ background: #3f3f46;
177
+ border: none;
178
+ border-radius: 4px;
179
+ color: #71717a;
180
+ cursor: pointer;
181
+ }
182
+ .zone-highlight:hover {
183
+ background: #52525b;
184
+ color: #06b6d4;
185
+ }
186
+ .zone-highlight-active {
187
+ background: #06b6d4;
188
+ color: white;
189
+ }
190
+ .zone-name {
191
+ font-weight: 600;
192
+ color: #06b6d4;
193
+ }
194
+ .zone-count {
195
+ color: #71717a;
196
+ font-size: 11px;
197
+ }
198
+ .zone-blocks {
199
+ display: flex;
200
+ flex-direction: column;
201
+ gap: 4px;
202
+ margin-left: 30px;
203
+ }
204
+ .zone-block {
205
+ display: flex;
206
+ align-items: center;
207
+ gap: 8px;
208
+ font-size: 11px;
209
+ padding: 2px 6px;
210
+ background: #1f1f23;
211
+ border-radius: 2px;
212
+ }
213
+ .block-id {
214
+ color: #a78bfa;
215
+ font-weight: 500;
216
+ }
217
+ .block-component {
218
+ color: #d4d4d8;
219
+ }
220
+ .block-weight {
221
+ color: #71717a;
222
+ font-size: 10px;
223
+ }
224
+ .block-wrappers {
225
+ color: #f59e0b;
226
+ }
227
+ .zone-default {
228
+ color: #71717a;
229
+ font-size: 11px;
230
+ margin-left: 30px;
231
+ }
232
+ </style>
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Debug Panel Components
3
+ */
4
+ export { default as ZonesPanel } from './ZonesPanel.vue'
5
+ export { default as AuthPanel } from './AuthPanel.vue'
6
+ export { default as EntitiesPanel } from './EntitiesPanel.vue'
7
+ export { default as ToastsPanel } from './ToastsPanel.vue'
8
+ export { default as EntriesPanel } from './EntriesPanel.vue'
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Debug Module Exports
3
+ *
4
+ * Provides base classes and utilities for debug collectors
5
+ * used by the debug panel UI.
6
+ */
7
+
8
+ export { Collector } from './Collector.js'
9
+ export { SignalCollector } from './SignalCollector.js'
10
+ export { DebugBridge, createDebugBridge } from './DebugBridge.js'
11
+ export { ErrorCollector } from './ErrorCollector.js'
12
+ export { ToastCollector } from './ToastCollector.js'
13
+ export { ZonesCollector } from './ZonesCollector.js'
14
+ export { AuthCollector } from './AuthCollector.js'
15
+ export { EntitiesCollector } from './EntitiesCollector.js'
16
+ export { LocalStorageAdapter, createLocalStorageAdapter } from './LocalStorageAdapter.js'
17
+
18
+ // Module System v2 integration
19
+ export { DebugModule, DEBUG_BRIDGE_KEY, DEBUG_ZONE, QdadmDebugBar } from './DebugModule.js'
20
+
21
+ // Components
22
+ export { DebugBar } from './components/index.js'
23
+
24
+ // Convenience export for Kernel debugBar option
25
+ // Usage: import { debugBar } from '@qdadm/core/debug'
26
+ // new Kernel({ debugBar })
27
+ import { DebugModule as _DebugModule, QdadmDebugBar as _QdadmDebugBar } from './DebugModule.js'
28
+ export const debugBar = {
29
+ module: _DebugModule,
30
+ component: _QdadmDebugBar
31
+ }