qdadm 1.6.0 → 1.8.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.6.0",
3
+ "version": "1.8.0",
4
4
  "description": "Vue 3 framework for admin dashboards with PrimeVue",
5
5
  "author": "quazardous",
6
6
  "license": "MIT",
@@ -13,6 +13,7 @@
13
13
  */
14
14
 
15
15
  import { ref, watch, onMounted, onUnmounted, computed, inject, provide, useSlots } from 'vue'
16
+ import { SIDEBAR_COLLAPSED_KEY } from '../../composables/useSidebarState'
16
17
  import { RouterLink, RouterView, useRouter, useRoute } from 'vue-router'
17
18
  import { useNavigation, type NavSection } from '../../composables/useNavigation'
18
19
  import { useApp } from '../../composables/useApp'
@@ -69,8 +70,9 @@ const collapsedSections = ref<Record<string, boolean>>({})
69
70
  const sidebarOpen = ref<boolean>(false)
70
71
  const MOBILE_BREAKPOINT = 768
71
72
 
72
- // Desktop sidebar collapsed state
73
+ // Desktop sidebar collapsed state (provided for child components via useSidebarState)
73
74
  const sidebarCollapsed = ref<boolean>(false)
75
+ provide(SIDEBAR_COLLAPSED_KEY, sidebarCollapsed)
74
76
  const COLLAPSED_STORAGE_KEY = computed<string>(() => `${app.shortName.toLowerCase()}_sidebar_collapsed`)
75
77
 
76
78
  function toggleSidebar(): void {
@@ -78,6 +78,8 @@ export interface EntityManager {
78
78
  canCreate: () => boolean
79
79
  canUpdate: (data?: unknown) => boolean
80
80
  canDelete: (data?: unknown) => boolean
81
+ hasSeverityMap?: (field: string) => boolean
82
+ getSeverity?: (field: string, value: string | number, defaultSeverity?: string) => string
81
83
  }
82
84
 
83
85
  /**
@@ -384,10 +384,11 @@ export function useEntityItemShowPage(
384
384
  schemaType = type,
385
385
  label = '',
386
386
  reference = '',
387
+ severity,
387
388
  ...rest
388
389
  } = fieldConfig
389
390
 
390
- const displayType = TYPE_MAPPINGS[type] || TYPE_MAPPINGS[schemaType] || 'text'
391
+ let resolvedDisplayType = TYPE_MAPPINGS[type] || TYPE_MAPPINGS[schemaType] || 'text'
391
392
  const resolvedLabel = label || snakeCaseToTitle(name)
392
393
 
393
394
  // Auto-set reference route if reference entity is specified
@@ -402,13 +403,23 @@ export function useEntityItemShowPage(
402
403
  }
403
404
  }
404
405
 
406
+ // Auto-inject severity from manager's severity maps
407
+ let resolvedSeverity = severity
408
+ if (!resolvedSeverity && manager.hasSeverityMap?.(name)) {
409
+ resolvedSeverity = (value: unknown) => manager.getSeverity!(name, value as string | number, 'secondary')
410
+ if (resolvedDisplayType === 'text') {
411
+ resolvedDisplayType = 'badge'
412
+ }
413
+ }
414
+
405
415
  return {
406
416
  name,
407
- type: displayType,
417
+ type: resolvedDisplayType,
408
418
  schemaType,
409
419
  label: resolvedLabel,
410
420
  reference,
411
421
  referenceRoute,
422
+ severity: resolvedSeverity,
412
423
  ...rest,
413
424
  }
414
425
  }
@@ -0,0 +1,24 @@
1
+ /**
2
+ * useSidebarState - Access sidebar collapsed state from any component
3
+ *
4
+ * The sidebar collapsed ref is provided by AppLayout via Vue's provide/inject.
5
+ * This composable gives blocks rendered inside the sidebar (including zone blocks)
6
+ * programmatic access to the collapsed state.
7
+ *
8
+ * @example
9
+ * ```ts
10
+ * import { useSidebarState } from 'qdadm'
11
+ *
12
+ * const { collapsed } = useSidebarState()
13
+ * // collapsed.value === true when sidebar is in icon-only mode
14
+ * ```
15
+ */
16
+
17
+ import { inject, ref, type Ref } from 'vue'
18
+
19
+ export const SIDEBAR_COLLAPSED_KEY = Symbol('qdadm-sidebar-collapsed') as symbol & { __type: Ref<boolean> }
20
+
21
+ export function useSidebarState(): { collapsed: Ref<boolean> } {
22
+ const collapsed = inject<Ref<boolean>>(SIDEBAR_COLLAPSED_KEY, ref(false))
23
+ return { collapsed }
24
+ }
package/src/index.ts CHANGED
@@ -291,6 +291,7 @@ export {
291
291
  } from './composables/useUserImpersonator'
292
292
  export { useCurrentEntity, type UseCurrentEntityReturn } from './composables/useCurrentEntity'
293
293
  export { useChildPage, type UseChildPageReturn } from './composables/useChildPage'
294
+ export { useSidebarState, SIDEBAR_COLLAPSED_KEY } from './composables/useSidebarState'
294
295
 
295
296
  // ════════════════════════════════════════════════════════════════════════════
296
297
  // COMPONENTS
@@ -8,11 +8,10 @@
8
8
  * - Mobile: Full width overlay at bottom of screen
9
9
  *
10
10
  * Features:
11
- * - Header with title, unread count, mark all read, close button
11
+ * - Inline action bar (mark read, clear, close) at top right
12
12
  * - Status items section (custom module items)
13
13
  * - Notification list (most recent first)
14
14
  * - Empty state
15
- * - Closable
16
15
  */
17
16
  import { RouterLink } from 'vue-router'
18
17
  import { useNotifications } from './NotificationStore'
@@ -55,28 +54,26 @@ function formatTime(timestamp: number): string {
55
54
  v-if="store.isOpen.value"
56
55
  class="notification-panel"
57
56
  >
58
- <!-- Header -->
59
- <div class="notification-panel-header">
60
- <div class="notification-panel-actions">
61
- <button
62
- v-if="store.unreadCount.value > 0"
63
- class="notification-panel-btn"
64
- title="Mark all read"
65
- @click="store.markAllRead()"
66
- >
67
- <i class="pi pi-check-circle" />
68
- </button>
69
- <button
70
- v-if="store.notifications.value.length > 0"
71
- class="notification-panel-btn"
72
- title="Clear all"
73
- @click="store.clearNotifications()"
74
- >
75
- <i class="pi pi-trash" />
76
- </button>
77
- </div>
57
+ <!-- Inline toolbar -->
58
+ <div class="notification-toolbar">
78
59
  <button
79
- class="notification-panel-btn"
60
+ v-if="store.unreadCount.value > 0"
61
+ class="notification-toolbar-btn"
62
+ title="Mark all read"
63
+ @click="store.markAllRead()"
64
+ >
65
+ <i class="pi pi-check-circle" />
66
+ </button>
67
+ <button
68
+ v-if="store.notifications.value.length > 0"
69
+ class="notification-toolbar-btn"
70
+ title="Clear all"
71
+ @click="store.clearNotifications()"
72
+ >
73
+ <i class="pi pi-trash" />
74
+ </button>
75
+ <button
76
+ class="notification-toolbar-btn notification-toolbar-close"
80
77
  title="Close"
81
78
  @click="store.close()"
82
79
  >
@@ -159,11 +156,12 @@ function formatTime(timestamp: number): string {
159
156
  left: calc(var(--fad-sidebar-width, 15rem) + 0.375rem);
160
157
  bottom: 0.5rem;
161
158
  width: 22rem;
159
+ min-height: 6.6rem;
162
160
  max-height: 50vh;
163
161
  background: var(--p-surface-0, #ffffff);
164
162
  border: 1px solid var(--p-surface-200, #e2e8f0);
165
- border-radius: 0.1875rem;
166
- box-shadow: 0 2px 8px rgba(0, 0, 0, 0.12);
163
+ border-radius: 2px;
164
+ box-shadow: 0 1px 4px rgba(0, 0, 0, 0.1);
167
165
  display: flex;
168
166
  flex-direction: column;
169
167
  z-index: 200;
@@ -171,51 +169,51 @@ function formatTime(timestamp: number): string {
171
169
  }
172
170
 
173
171
  /* Collapsed sidebar */
174
- .sidebar--collapsed ~ .main-area .notification-panel,
175
172
  :root:has(.sidebar--collapsed) .notification-panel {
176
173
  left: calc(var(--fad-sidebar-width-collapsed, 2.5rem) + 0.375rem);
177
174
  }
178
175
 
179
- /* Header */
180
- .notification-panel-header {
176
+ /* Inline toolbar - floats top-right inside the panel */
177
+ .notification-toolbar {
178
+ position: absolute;
179
+ top: 0;
180
+ right: 0;
181
181
  display: flex;
182
182
  align-items: center;
183
- justify-content: space-between;
184
- padding: 0.375rem 0.5rem;
185
- border-bottom: 1px solid var(--p-surface-200, #e2e8f0);
186
- background: var(--p-surface-50, #f8fafc);
187
- flex-shrink: 0;
183
+ gap: 1px;
184
+ padding: 0.25rem;
185
+ z-index: 1;
188
186
  }
189
187
 
190
- .notification-panel-actions {
191
- display: flex;
192
- align-items: center;
193
- gap: 0.125rem;
194
- }
195
-
196
- .notification-panel-btn {
188
+ .notification-toolbar-btn {
197
189
  display: inline-flex;
198
190
  align-items: center;
199
191
  justify-content: center;
200
- width: 1.5rem;
201
- height: 1.5rem;
192
+ width: 1.375rem;
193
+ height: 1.375rem;
202
194
  border: none;
203
195
  background: none;
204
- border-radius: 0.125rem;
205
- color: var(--p-surface-500, #64748b);
196
+ border-radius: 2px;
197
+ color: var(--p-surface-400, #94a3b8);
206
198
  cursor: pointer;
207
- font-size: 0.75rem;
199
+ font-size: 0.6875rem;
200
+ transition: color 0.1s, background 0.1s;
208
201
  }
209
202
 
210
- .notification-panel-btn:hover {
211
- background: var(--p-surface-200, #e2e8f0);
203
+ .notification-toolbar-btn:hover {
204
+ background: var(--p-surface-100, #f1f5f9);
205
+ color: var(--p-surface-600, #475569);
206
+ }
207
+
208
+ .notification-toolbar-close:hover {
212
209
  color: var(--p-surface-700, #334155);
213
210
  }
214
211
 
215
212
  /* Status items */
216
213
  .notification-panel-status {
217
- padding: 0.5rem 0.75rem;
218
- border-bottom: 1px solid var(--p-surface-200, #e2e8f0);
214
+ padding: 0.5rem 0.625rem;
215
+ padding-right: 2.5rem;
216
+ border-bottom: 1px solid var(--p-surface-100, #f1f5f9);
219
217
  flex-shrink: 0;
220
218
  }
221
219
 
@@ -223,13 +221,13 @@ function formatTime(timestamp: number): string {
223
221
  display: flex;
224
222
  align-items: center;
225
223
  gap: 0.375rem;
226
- padding: 0.25rem 0;
224
+ padding: 0.1875rem 0;
227
225
  font-size: 0.75rem;
228
226
  color: var(--p-surface-600, #475569);
229
227
  }
230
228
 
231
229
  .notification-status-item i {
232
- font-size: 0.75rem;
230
+ font-size: 0.6875rem;
233
231
  }
234
232
 
235
233
  .notification-status-item--nominal i {
@@ -250,22 +248,24 @@ function formatTime(timestamp: number): string {
250
248
 
251
249
  .notification-status-count {
252
250
  font-weight: 600;
251
+ font-size: 0.6875rem;
253
252
  }
254
253
 
255
254
  .notification-status-item--link {
256
255
  text-decoration: none;
257
256
  cursor: pointer;
258
- border-radius: 0.25rem;
259
- padding: 0.25rem 0.375rem;
257
+ border-radius: 2px;
258
+ padding: 0.1875rem 0.375rem;
260
259
  margin: 0 -0.375rem;
260
+ transition: background 0.1s;
261
261
  }
262
262
 
263
263
  .notification-status-item--link:hover {
264
- background: var(--p-surface-100, #f1f5f9);
264
+ background: var(--p-surface-50, #f8fafc);
265
265
  }
266
266
 
267
267
  .notification-status-arrow {
268
- font-size: 0.625rem;
268
+ font-size: 0.5625rem;
269
269
  opacity: 0;
270
270
  transition: opacity 0.1s;
271
271
  color: var(--p-surface-400, #94a3b8);
@@ -287,12 +287,16 @@ function formatTime(timestamp: number): string {
287
287
  display: flex;
288
288
  align-items: flex-start;
289
289
  gap: 0.5rem;
290
- padding: 0.5rem 0.75rem;
291
- border-bottom: 1px solid var(--p-surface-100, #f1f5f9);
290
+ padding: 0.5rem 0.625rem;
291
+ border-bottom: 1px solid var(--p-surface-50, #f8fafc);
292
292
  cursor: pointer;
293
293
  transition: background 0.1s;
294
294
  }
295
295
 
296
+ .notification-item:first-child {
297
+ padding-right: 2.5rem;
298
+ }
299
+
296
300
  .notification-item:hover {
297
301
  background: var(--p-surface-50, #f8fafc);
298
302
  }
@@ -311,16 +315,16 @@ function formatTime(timestamp: number): string {
311
315
 
312
316
  .notification-item-icon {
313
317
  flex-shrink: 0;
314
- width: 1.25rem;
315
- height: 1.25rem;
318
+ width: 1.125rem;
319
+ height: 1.125rem;
316
320
  display: flex;
317
321
  align-items: center;
318
322
  justify-content: center;
319
- margin-top: 0.125rem;
323
+ margin-top: 0.0625rem;
320
324
  }
321
325
 
322
326
  .notification-item-icon i {
323
- font-size: 0.8125rem;
327
+ font-size: 0.75rem;
324
328
  }
325
329
 
326
330
  .notification-item--success .notification-item-icon i {
@@ -345,16 +349,16 @@ function formatTime(timestamp: number): string {
345
349
  }
346
350
 
347
351
  .notification-item-summary {
348
- font-size: 0.8125rem;
352
+ font-size: 0.75rem;
349
353
  font-weight: 500;
350
354
  color: var(--p-surface-700, #334155);
351
355
  line-height: 1.3;
352
356
  }
353
357
 
354
358
  .notification-item-detail {
355
- font-size: 0.75rem;
359
+ font-size: 0.6875rem;
356
360
  color: var(--p-surface-500, #64748b);
357
- margin-top: 0.125rem;
361
+ margin-top: 0.0625rem;
358
362
  line-height: 1.3;
359
363
  }
360
364
 
@@ -362,8 +366,8 @@ function formatTime(timestamp: number): string {
362
366
  display: flex;
363
367
  align-items: center;
364
368
  gap: 0.375rem;
365
- margin-top: 0.25rem;
366
- font-size: 0.6875rem;
369
+ margin-top: 0.125rem;
370
+ font-size: 0.625rem;
367
371
  color: var(--p-surface-400, #94a3b8);
368
372
  }
369
373
 
@@ -377,14 +381,14 @@ function formatTime(timestamp: number): string {
377
381
  display: inline-flex;
378
382
  align-items: center;
379
383
  justify-content: center;
380
- width: 1.25rem;
381
- height: 1.25rem;
384
+ width: 1.125rem;
385
+ height: 1.125rem;
382
386
  border: none;
383
387
  background: none;
384
- border-radius: 0.25rem;
388
+ border-radius: 2px;
385
389
  color: var(--p-surface-400, #94a3b8);
386
390
  cursor: pointer;
387
- font-size: 0.625rem;
391
+ font-size: 0.5625rem;
388
392
  opacity: 0;
389
393
  transition: opacity 0.1s;
390
394
  }
@@ -404,17 +408,17 @@ function formatTime(timestamp: number): string {
404
408
  flex-direction: column;
405
409
  align-items: center;
406
410
  justify-content: center;
407
- padding: 2rem 1rem;
408
- color: var(--p-surface-400, #94a3b8);
409
- gap: 0.5rem;
411
+ padding: 1.5rem 1rem;
412
+ color: var(--p-surface-300, #cbd5e1);
413
+ gap: 0.375rem;
410
414
  }
411
415
 
412
416
  .notification-panel-empty i {
413
- font-size: 1.5rem;
417
+ font-size: 1.25rem;
414
418
  }
415
419
 
416
420
  .notification-panel-empty span {
417
- font-size: 0.8125rem;
421
+ font-size: 0.75rem;
418
422
  }
419
423
 
420
424
  /* Transition */
@@ -435,12 +439,12 @@ function formatTime(timestamp: number): string {
435
439
  /* Mobile: full width bottom overlay */
436
440
  @media (max-width: 767px) {
437
441
  .notification-panel {
438
- left: 0;
442
+ left: 0 !important;
439
443
  right: 0;
440
444
  bottom: 0;
441
445
  width: auto;
442
446
  max-height: 60vh;
443
- border-radius: 0.5rem 0.5rem 0 0;
447
+ border-radius: 2px 2px 0 0;
444
448
  border-bottom: none;
445
449
  }
446
450
  }
@@ -451,13 +455,9 @@ function formatTime(timestamp: number): string {
451
455
  border-color: var(--p-surface-700, #334155);
452
456
  }
453
457
 
454
- .dark-mode .notification-panel-header {
455
- background: var(--p-surface-900, #0f172a);
456
- border-color: var(--p-surface-700, #334155);
457
- }
458
-
459
- .dark-mode .notification-panel-title {
460
- color: var(--p-surface-200, #e2e8f0);
458
+ .dark-mode .notification-toolbar-btn:hover {
459
+ background: var(--p-surface-700, #334155);
460
+ color: var(--p-surface-300, #cbd5e1);
461
461
  }
462
462
 
463
463
  .dark-mode .notification-item--unread {
@@ -18,10 +18,15 @@ $_content-duration: 0.1s;
18
18
  transition: opacity $_content-duration ease $_content-delay;
19
19
  }
20
20
 
21
- .sidebar--collapsed .sidebar-notification-status {
21
+ /* Status label: delayed fade-in on expand, instant hide on collapse.
22
+ Matches SidebarBox content behavior (.sidebar-box-content). */
23
+ .sidebar-notification-status .status-label {
24
+ opacity: 1;
25
+ transition: opacity $_content-duration ease $_content-delay;
26
+ }
27
+
28
+ .sidebar--collapsed .sidebar-notification-status .status-label {
22
29
  opacity: 0;
23
30
  visibility: hidden;
24
- height: 0;
25
- overflow: hidden;
26
- transition: opacity 0s, visibility 0s, height 0s;
31
+ transition: opacity 0s, visibility 0s;
27
32
  }