qdadm 1.5.0 → 1.6.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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "qdadm",
3
- "version": "1.5.0",
3
+ "version": "1.6.0",
4
4
  "description": "Vue 3 framework for admin dashboards with PrimeVue",
5
5
  "author": "quazardous",
6
6
  "license": "MIT",
@@ -23,6 +23,7 @@ import Button from 'primevue/button'
23
23
  import Breadcrumb from 'primevue/breadcrumb'
24
24
  import UnsavedChangesDialog from '../dialogs/UnsavedChangesDialog.vue'
25
25
  import SidebarBox from './SidebarBox.vue'
26
+ import Zone from './Zone.vue'
26
27
  import qdadmLogo from '../../assets/logo.svg'
27
28
  import { version as qdadmVersion } from '../../../package.json'
28
29
 
@@ -204,10 +205,10 @@ const userSubtitle = computed<string>(() => {
204
205
  return userData?.email || userData?.role || ''
205
206
  })
206
207
 
207
- function handleLogout(): void {
208
+ async function handleLogout(): Promise<void> {
208
209
  logout()
210
+ await router.push({ name: 'login' })
209
211
  signals?.emit('auth:logout', { reason: 'user' })
210
- router.push({ name: 'login' })
211
212
  }
212
213
 
213
214
  /**
@@ -332,7 +333,11 @@ const showBreadcrumb = computed<boolean>(() => {
332
333
 
333
334
  <SidebarBox v-if="features.poweredBy" id="powered-by">
334
335
  <template #icon>
335
- <img :src="qdadmLogo" alt="qdadm" />
336
+ <div class="footer-logo-wrapper">
337
+ <img :src="qdadmLogo" alt="qdadm" />
338
+ <!-- Notification badge overlay on logo -->
339
+ <Zone name="_app:notification-badge" />
340
+ </div>
336
341
  </template>
337
342
  <template #subtitle-content>
338
343
  <span class="sidebar-box-subtitle">
@@ -340,6 +345,9 @@ const showBreadcrumb = computed<boolean>(() => {
340
345
  </span>
341
346
  </template>
342
347
  </SidebarBox>
348
+
349
+ <!-- Always-visible notification status (hidden when collapsed) -->
350
+ <Zone name="_app:notification-status" class="sidebar-notification-status" />
343
351
  </aside>
344
352
 
345
353
  <!-- Main content -->
@@ -394,6 +402,9 @@ const showBreadcrumb = computed<boolean>(() => {
394
402
  </div>
395
403
  </main>
396
404
 
405
+ <!-- Notification panel (rendered outside sidebar, follows sidebar position) -->
406
+ <Zone name="_app:notifications" />
407
+
397
408
  <!-- Unsaved Changes Dialog (auto-rendered when a form registers guardDialog) -->
398
409
  <UnsavedChangesDialog
399
410
  v-if="guardDialog"
@@ -652,6 +663,19 @@ const showBreadcrumb = computed<boolean>(() => {
652
663
  height: 100%;
653
664
  }
654
665
 
666
+ .footer-logo-wrapper {
667
+ position: relative;
668
+ display: inline-flex;
669
+ width: 100%;
670
+ height: 100%;
671
+ align-items: center;
672
+ justify-content: center;
673
+ }
674
+
675
+ .footer-logo-wrapper > :deep(.qdadm-zone) {
676
+ display: contents;
677
+ }
678
+
655
679
  .powered-by-link {
656
680
  color: var(--p-surface-300, #cbd5e1);
657
681
  text-decoration: none;
@@ -988,3 +1012,15 @@ const showBreadcrumb = computed<boolean>(() => {
988
1012
  }
989
1013
  }
990
1014
  </style>
1015
+
1016
+ <style>
1017
+ /* Non-scoped: logo alpha blink triggered by notification badge alert state */
1018
+ .footer-logo-wrapper:has(.notification-badge-zone--alert) {
1019
+ animation: logo-alpha-blink 1s ease-in-out infinite;
1020
+ }
1021
+
1022
+ @keyframes logo-alpha-blink {
1023
+ 0%, 100% { opacity: 1; filter: none; }
1024
+ 50% { opacity: 0.4; filter: brightness(1.2) sepia(1) hue-rotate(-30deg) saturate(3); }
1025
+ }
1026
+ </style>
@@ -84,6 +84,9 @@ provide('qdadmNavlinksOverride', navlinksOverride)
84
84
  :name="LAYOUT_ZONES.FOOTER"
85
85
  :default-component="DefaultFooter"
86
86
  />
87
+
88
+ <!-- Always-visible notification status (hidden when collapsed) -->
89
+ <Zone name="_app:notification-status" class="sidebar-notification-status" />
87
90
  </aside>
88
91
 
89
92
  <!-- Main content area -->
@@ -113,6 +116,9 @@ provide('qdadmNavlinksOverride', navlinksOverride)
113
116
 
114
117
  <!-- Toast is now provided by Kernel at root level -->
115
118
 
119
+ <!-- Notification panel (rendered outside sidebar, follows sidebar position) -->
120
+ <Zone name="_app:notifications" />
121
+
116
122
  <!-- Confirm dialog (global) -->
117
123
  <ConfirmDialog />
118
124
 
@@ -9,6 +9,7 @@
9
9
  * when no blocks are registered.
10
10
  */
11
11
  import { inject } from 'vue'
12
+ import Zone from '../Zone.vue'
12
13
  import qdadmLogo from '../../../assets/logo.svg'
13
14
  import { version as qdadmVersion } from '../../../../package.json'
14
15
 
@@ -21,18 +22,24 @@ const features = inject<Features>('qdadmFeatures', { poweredBy: true })
21
22
  </script>
22
23
 
23
24
  <template>
24
- <a
25
- v-if="features.poweredBy"
26
- href="https://github.com/quazardous/qdadm"
27
- target="_blank"
28
- rel="noopener noreferrer"
29
- class="default-footer"
30
- >
31
- <img :src="qdadmLogo" alt="qdadm" class="footer-logo" />
32
- <span class="footer-text">
33
- powered by <strong>qdadm</strong> v{{ qdadmVersion }}
34
- </span>
35
- </a>
25
+ <div class="default-footer-wrapper">
26
+ <a
27
+ v-if="features.poweredBy"
28
+ href="https://github.com/quazardous/qdadm"
29
+ target="_blank"
30
+ rel="noopener noreferrer"
31
+ class="default-footer"
32
+ >
33
+ <div class="footer-logo-wrapper">
34
+ <img :src="qdadmLogo" alt="qdadm" class="footer-logo" />
35
+ <!-- Notification badge overlay on logo -->
36
+ <Zone name="_app:notification-badge" />
37
+ </div>
38
+ <span class="footer-text">
39
+ powered by <strong>qdadm</strong> v{{ qdadmVersion }}
40
+ </span>
41
+ </a>
42
+ </div>
36
43
  </template>
37
44
 
38
45
  <style scoped>
@@ -52,6 +59,15 @@ const features = inject<Features>('qdadmFeatures', { poweredBy: true })
52
59
  opacity: 1;
53
60
  }
54
61
 
62
+ .default-footer-wrapper {
63
+ position: relative;
64
+ }
65
+
66
+ .footer-logo-wrapper {
67
+ position: relative;
68
+ flex-shrink: 0;
69
+ }
70
+
55
71
  .footer-logo {
56
72
  width: 1.25rem;
57
73
  height: 1.25rem;
package/src/index.ts CHANGED
@@ -22,6 +22,7 @@ export type {
22
22
  SecurityConfig,
23
23
  SSEConfig,
24
24
  DebugBarConfig,
25
+ NotificationsConfig,
25
26
  HomeRoute,
26
27
  KernelOptions,
27
28
  } from './kernel/Kernel'
@@ -387,6 +388,11 @@ export * from './utils/index'
387
388
  // ════════════════════════════════════════════════════════════════════════════
388
389
  export * from './toast/index'
389
390
 
391
+ // ════════════════════════════════════════════════════════════════════════════
392
+ // NOTIFICATIONS (optional notification panel)
393
+ // ════════════════════════════════════════════════════════════════════════════
394
+ export * from './notifications/index'
395
+
390
396
  // ════════════════════════════════════════════════════════════════════════════
391
397
  // DEBUG - NOT exported here to enable tree-shaking.
392
398
  // Import from 'qdadm/debug' separately when needed:
@@ -66,6 +66,8 @@ import { StackHydrator } from '../chain/StackHydrator.js'
66
66
  import type { EntityManager } from '../entity/EntityManager'
67
67
  import type { RoleProvider } from '../security/RolesProvider'
68
68
  import type { EntityAuthAdapter } from '../entity/auth/EntityAuthAdapter'
69
+ import { NotificationModule } from '../notifications/NotificationModule'
70
+ import { createNotificationStore, NOTIFICATION_KEY, type NotificationStore } from '../notifications/NotificationStore'
69
71
 
70
72
  // ─────────────────────────────────────────────────────────────────────────────
71
73
  // Types
@@ -178,6 +180,14 @@ export interface DebugBarConfig {
178
180
  [key: string]: unknown
179
181
  }
180
182
 
183
+ /**
184
+ * Notifications configuration
185
+ */
186
+ export interface NotificationsConfig {
187
+ enabled?: boolean
188
+ maxNotifications?: number
189
+ }
190
+
181
191
  /**
182
192
  * Home route configuration
183
193
  */
@@ -215,6 +225,7 @@ export interface KernelOptions {
215
225
  eventRouter?: RoutesConfig
216
226
  sse?: SSEConfig
217
227
  debugBar?: DebugBarConfig
228
+ notifications?: NotificationsConfig
218
229
  toast?: Record<string, unknown>
219
230
  debug?: boolean
220
231
  onAuthExpired?: (payload: unknown) => void
@@ -282,6 +293,7 @@ export class Kernel {
282
293
  moduleLoader: ModuleLoader | null = null
283
294
  activeStack: ActiveStack | null = null
284
295
  stackHydrator: StackHydrator | null = null
296
+ notificationStore: NotificationStore | null = null
285
297
 
286
298
  /** Pending provides from modules (applied after vueApp creation) */
287
299
  _pendingProvides: Map<string | symbol, unknown> = new Map()
@@ -322,6 +334,13 @@ export class Kernel {
322
334
  }
323
335
  }
324
336
 
337
+ // Auto-inject NotificationModule if notifications config is provided
338
+ if (options.notifications?.enabled) {
339
+ const notificationModule = new NotificationModule()
340
+ options.moduleDefs = options.moduleDefs || []
341
+ options.moduleDefs.push(notificationModule)
342
+ }
343
+
325
344
  this.options = options
326
345
  }
327
346
 
@@ -1262,6 +1281,14 @@ export class Kernel {
1262
1281
  app.provide('qdadmPermissionRegistry', this.permissionRegistry)
1263
1282
  }
1264
1283
 
1284
+ // Create and provide notification store if notifications are enabled
1285
+ if (this.options.notifications?.enabled) {
1286
+ this.notificationStore = createNotificationStore({
1287
+ maxNotifications: this.options.notifications.maxNotifications,
1288
+ })
1289
+ app.provide(NOTIFICATION_KEY, this.notificationStore)
1290
+ }
1291
+
1265
1292
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
1266
1293
  const qdadmOptions: any = {
1267
1294
  orchestrator: this.orchestrator,
@@ -1349,6 +1376,13 @@ export class Kernel {
1349
1376
  return this.sseBridge
1350
1377
  }
1351
1378
 
1379
+ /**
1380
+ * Get the NotificationStore instance
1381
+ */
1382
+ getNotificationStore(): NotificationStore | null {
1383
+ return this.notificationStore
1384
+ }
1385
+
1352
1386
  /**
1353
1387
  * Shorthand accessor for SSE bridge
1354
1388
  */
@@ -119,6 +119,14 @@ export interface BlockConfig {
119
119
  props?: Record<string, unknown>
120
120
  id?: string
121
121
  operation?: 'add' | 'replace' | 'extend' | 'wrap'
122
+ /** Block ID to replace (required if operation='replace') */
123
+ replaces?: string
124
+ /** Block ID to insert before (for operation='extend') */
125
+ before?: string
126
+ /** Block ID to insert after (for operation='extend') */
127
+ after?: string
128
+ /** Block ID to wrap (required if operation='wrap') */
129
+ wraps?: string
122
130
  }
123
131
 
124
132
  /**
@@ -0,0 +1,31 @@
1
+ <script setup lang="ts">
2
+ /**
3
+ * NotificationBadge - Clickable overlay for the sidebar footer logo
4
+ *
5
+ * Always rendered as a transparent click zone over the logo.
6
+ * When there are alerts, the parent layout applies an opacity blink
7
+ * on the logo wrapper via :has(.notification-badge-zone--alert).
8
+ *
9
+ * Click toggles the notification panel open/close.
10
+ */
11
+ import { useNotifications } from './NotificationStore'
12
+
13
+ const store = useNotifications()
14
+ </script>
15
+
16
+ <template>
17
+ <div
18
+ class="notification-badge-zone"
19
+ :class="{ 'notification-badge-zone--alert': store.hasAlert.value || store.unreadCount.value > 0 }"
20
+ @click.stop.prevent="store.toggle()"
21
+ />
22
+ </template>
23
+
24
+ <style scoped>
25
+ .notification-badge-zone {
26
+ position: absolute;
27
+ inset: 0;
28
+ cursor: pointer;
29
+ z-index: 1;
30
+ }
31
+ </style>
@@ -0,0 +1,71 @@
1
+ <script setup lang="ts">
2
+ /**
3
+ * NotificationListener - Captures toast signals to notification store
4
+ *
5
+ * Replaces ToastListener when NotificationModule is active.
6
+ * Intercepts toast:* signals and:
7
+ * - Always captures to NotificationStore
8
+ * - If data.forceToast === true, also shows a classic PrimeVue toast
9
+ */
10
+ import { onMounted, onUnmounted, inject } from 'vue'
11
+ import { useToast } from 'primevue/usetoast'
12
+ import type { SignalBus } from '../kernel/SignalBus'
13
+ import { useNotifications } from './NotificationStore'
14
+ import type { NotificationSeverity } from './NotificationStore'
15
+
16
+ interface ToastEventData {
17
+ summary?: string
18
+ detail?: string
19
+ life?: number
20
+ emitter?: string
21
+ forceToast?: boolean
22
+ }
23
+
24
+ const toast = useToast()
25
+ const signals = inject<SignalBus | null>('qdadmSignals', null)
26
+ const store = useNotifications()
27
+
28
+ let unsubscribe: (() => void) | null = null
29
+
30
+ onMounted(() => {
31
+ if (!signals) {
32
+ console.warn('[NotificationListener] No signals bus injected')
33
+ return
34
+ }
35
+
36
+ unsubscribe = signals.on('toast:*', (event) => {
37
+ const data = event.data as ToastEventData | undefined
38
+ const severity = event.name.split(':')[1] as NotificationSeverity
39
+
40
+ // Always capture to notification store
41
+ store.addNotification({
42
+ severity,
43
+ summary: data?.summary || '',
44
+ detail: data?.detail,
45
+ emitter: data?.emitter,
46
+ })
47
+
48
+ // If forceToast is set, also show classic PrimeVue toast
49
+ if (data?.forceToast) {
50
+ toast.add({
51
+ severity,
52
+ summary: data?.summary,
53
+ detail: data?.detail,
54
+ life: data?.life ?? 3000,
55
+ })
56
+ }
57
+ })
58
+ })
59
+
60
+ onUnmounted(() => {
61
+ if (unsubscribe) {
62
+ unsubscribe()
63
+ unsubscribe = null
64
+ }
65
+ })
66
+ </script>
67
+
68
+ <template>
69
+ <!-- Invisible listener component -->
70
+ <span style="display: none" />
71
+ </template>
@@ -0,0 +1,76 @@
1
+ /**
2
+ * NotificationModule - Optional notification panel system
3
+ *
4
+ * When loaded, this module:
5
+ * - Replaces the default ToastListener with NotificationListener
6
+ * (captures toasts to notification store instead of ephemeral PrimeVue toasts)
7
+ * - Registers NotificationBadge for the sidebar footer logo overlay
8
+ * - Registers NotificationPanel zone for the panel component
9
+ *
10
+ * Without this module, classic toast behavior is unchanged (ToastBridgeModule).
11
+ * With this module, toasts are captured. Use `forceToast: true` in signal data
12
+ * to also show a classic PrimeVue toast.
13
+ *
14
+ * @example
15
+ * // In kernel config
16
+ * const kernel = new Kernel({
17
+ * notifications: { enabled: true, maxNotifications: 100 }
18
+ * })
19
+ */
20
+
21
+ import { Module } from '../kernel/Module'
22
+ import type { KernelContext } from '../kernel/KernelContext'
23
+ import { TOAST_ZONE } from '../toast/ToastBridgeModule'
24
+ import NotificationListener from './NotificationListener.vue'
25
+ import NotificationBadge from './NotificationBadge.vue'
26
+ import NotificationPanel from './NotificationPanel.vue'
27
+
28
+ /**
29
+ * Zone names for notification components
30
+ */
31
+ export const NOTIFICATION_ZONE = '_app:notifications'
32
+ export const NOTIFICATION_BADGE_ZONE = '_app:notification-badge'
33
+ export const NOTIFICATION_STATUS_ZONE = '_app:notification-status'
34
+
35
+ export class NotificationModule extends Module {
36
+ static override moduleName = 'notifications'
37
+ static override requires: string[] = []
38
+ static override priority = 5 // Before toast-bridge (10) to intercept first
39
+
40
+ static override styles = () => import('./styles.scss')
41
+
42
+ async connect(ctx: KernelContext): Promise<void> {
43
+ // Define notification zones
44
+ ctx.zone(NOTIFICATION_ZONE)
45
+ ctx.zone(NOTIFICATION_BADGE_ZONE)
46
+ ctx.zone(NOTIFICATION_STATUS_ZONE)
47
+
48
+ // Replace the toast-listener block in the toast zone with our NotificationListener.
49
+ // This intercepts toast signals and captures them to the notification store.
50
+ // The toast zone is defined by ToastBridgeModule - we replace its listener block.
51
+ ctx.zone(TOAST_ZONE) // Ensure the zone exists
52
+ ctx.block(TOAST_ZONE, {
53
+ id: 'toast-listener',
54
+ component: NotificationListener,
55
+ weight: 0,
56
+ operation: 'replace',
57
+ replaces: 'toast-listener',
58
+ })
59
+
60
+ // Register badge component for sidebar footer overlay
61
+ ctx.block(NOTIFICATION_BADGE_ZONE, {
62
+ id: 'notification-badge',
63
+ component: NotificationBadge,
64
+ weight: 0,
65
+ })
66
+
67
+ // Register panel component
68
+ ctx.block(NOTIFICATION_ZONE, {
69
+ id: 'notification-panel',
70
+ component: NotificationPanel,
71
+ weight: 0,
72
+ })
73
+ }
74
+ }
75
+
76
+ export default NotificationModule