@quazardous/qdadm 2.28.0 → 2.29.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": "@quazardous/qdadm",
3
- "version": "2.28.0",
3
+ "version": "2.29.0",
4
4
  "description": "Vue 3 framework for admin dashboards with PrimeVue",
5
5
  "author": "quazardous",
6
6
  "license": "MIT",
@@ -340,7 +340,12 @@ export function useEntityItemShowPage<T = Record<string, unknown>>(
340
340
  keep: 'short',
341
341
  ...(route?.name ? { to: { name: String(route.name), params: { ...route.params } } } : {}),
342
342
  })
343
- }, { id: () => entityId.value })
343
+ }, {
344
+ id: () => entityId.value,
345
+ // The update has arrived: say so on the badge at once — before the reload is
346
+ // coalesced, let alone finished (#2679).
347
+ onEvent: () => notifications.flash(),
348
+ })
344
349
  // Cast orchestrator to include toast methods
345
350
  const orchestrator = baseOrchestrator as Orchestrator
346
351
 
@@ -27,6 +27,12 @@ export interface UseLiveEntityOptions {
27
27
  * own id: without it, changing one record reloads every open detail page.
28
28
  */
29
29
  id?: () => string | number | null | undefined
30
+ /**
31
+ * Called at once for every event that concerns this screen, before the
32
+ * reload is coalesced (#2679) — for a cue that must not wait, like the
33
+ * notification badge's flash.
34
+ */
35
+ onEvent?: () => void
30
36
  }
31
37
 
32
38
  interface LivePolicy {
@@ -129,6 +135,8 @@ export function useLiveEntity(
129
135
  }
130
136
  }
131
137
 
138
+ options.onEvent?.()
139
+
132
140
  // Coalesce: a backend replaying fifty rows must cost one reload, not fifty.
133
141
  if (coalesceMs <= 0) {
134
142
  fire()
@@ -7,7 +7,8 @@
7
7
  * - something needs attention: the parent layout blinks the logo via
8
8
  * :has(.notification-badge-zone--alert);
9
9
  * - how many entries are unread: a count;
10
- * - work is in progress: a ring, once tracked work has lasted `activityDelayMs`.
10
+ * - work is in progress: a ring, once tracked work has lasted `activityDelayMs`;
11
+ * - something just arrived: a short flash, immediately (#2679).
11
12
  *
12
13
  * Click toggles the notification panel open/close.
13
14
  */
@@ -29,6 +30,7 @@ const unreadLabel = computed(() => (unread.value > 99 ? '99+' : String(unread.va
29
30
  @click.stop.prevent="store.toggle()"
30
31
  >
31
32
  <span v-if="store.isBusy.value" class="notification-badge-ring" role="progressbar" aria-label="Loading" />
33
+ <span v-if="store.isFlashing.value" class="notification-badge-flash" aria-hidden="true" />
32
34
  <span v-if="unread > 0" class="notification-badge-count">{{ unreadLabel }}</span>
33
35
  </div>
34
36
  </template>
@@ -78,11 +80,30 @@ const unreadLabel = computed(() => (unread.value > 99 ? '99+' : String(unread.va
78
80
  to { transform: rotate(360deg); }
79
81
  }
80
82
 
83
+ /* Something just arrived: a halo that grows out of the logo and fades. */
84
+ .notification-badge-flash {
85
+ position: absolute;
86
+ inset: 0;
87
+ border-radius: 50%;
88
+ pointer-events: none;
89
+ animation: notification-badge-flash 0.9s ease-out 1;
90
+ }
91
+
92
+ @keyframes notification-badge-flash {
93
+ 0% { box-shadow: 0 0 0 0 color-mix(in srgb, var(--p-primary-color, #3b82f6) 70%, transparent); }
94
+ 100% { box-shadow: 0 0 0 14px color-mix(in srgb, var(--p-primary-color, #3b82f6) 0%, transparent); }
95
+ }
96
+
81
97
  @media (prefers-reduced-motion: reduce) {
82
98
  .notification-badge-ring {
83
99
  animation: none;
84
100
  border-color: var(--p-primary-color, #3b82f6);
85
101
  opacity: 0.5;
86
102
  }
103
+
104
+ .notification-badge-flash {
105
+ animation: none;
106
+ box-shadow: 0 0 0 3px var(--p-primary-color, #3b82f6);
107
+ }
87
108
  }
88
109
  </style>
@@ -118,6 +118,14 @@ export interface NotificationStore {
118
118
  track<T>(work: Promise<T>): Promise<T>
119
119
  /** True once tracked work has been in progress for `activityDelayMs`. */
120
120
  isBusy: ComputedRef<boolean>
121
+ /**
122
+ * Flash the badge: something just arrived — an update to the record on
123
+ * screen (#2679). Immediate, never delayed; a flash already running absorbs
124
+ * further calls, so a burst flashes once.
125
+ */
126
+ flash(): void
127
+ /** True while the badge flashes. */
128
+ isFlashing: ComputedRef<boolean>
121
129
 
122
130
  // Panel state
123
131
  isOpen: Ref<boolean>
@@ -288,6 +296,20 @@ export function createNotificationStore(config: NotificationStoreConfig = {}): N
288
296
  return work
289
297
  }
290
298
 
299
+ // ── Flash ──────────────────────────────────────────────────────────────
300
+
301
+ const FLASH_MS = 900
302
+ const flashing = ref(false)
303
+
304
+ function flash(): void {
305
+ // A burst of updates flashes once: the running flash already says it.
306
+ if (flashing.value) return
307
+ flashing.value = true
308
+ setTimeout(() => {
309
+ flashing.value = false
310
+ }, FLASH_MS)
311
+ }
312
+
291
313
  // ── Panel state ────────────────────────────────────────────────────────
292
314
 
293
315
  function open(): void {
@@ -338,6 +360,8 @@ export function createNotificationStore(config: NotificationStoreConfig = {}): N
338
360
  removeStatus,
339
361
  track,
340
362
  isBusy: computed(() => busy.value),
363
+ flash,
364
+ isFlashing: computed(() => flashing.value),
341
365
  isOpen,
342
366
  open,
343
367
  close,
@@ -383,6 +407,8 @@ export function useNotifications(): NotificationStore {
383
407
  removeStatus: () => {},
384
408
  track: (work) => work,
385
409
  isBusy: computed(() => false),
410
+ flash: () => {},
411
+ isFlashing: computed(() => false),
386
412
  isOpen: ref(false),
387
413
  open: () => {},
388
414
  close: () => {},