qdadm 1.6.1 → 1.9.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.1",
3
+ "version": "1.9.0",
4
4
  "description": "Vue 3 framework for admin dashboards with PrimeVue",
5
5
  "author": "quazardous",
6
6
  "license": "MIT",
@@ -2,6 +2,10 @@
2
2
  /**
3
3
  * SeverityTag - Auto-discovers severity from EntityManager
4
4
  *
5
+ * Renders a PrimeVue Tag for simple severity strings, or a rich badge
6
+ * with icon (including animated spinners) when the severity map provides
7
+ * a descriptor with an `icon` field.
8
+ *
5
9
  * Entity can be specified explicitly or auto-discovered from list context.
6
10
  *
7
11
  * @example
@@ -50,22 +54,33 @@ const manager = computed<EntityManager | null>(() => {
50
54
  return orchestrator?.get(resolvedEntity.value) ?? null
51
55
  })
52
56
 
53
- const severity = computed(() => {
54
- if (!manager.value) return props.defaultSeverity
55
- // getSeverity expects string | number, but value can be boolean | null too
56
- const fieldValue = props.value === null || props.value === undefined
57
- ? ''
58
- : typeof props.value === 'boolean'
59
- ? String(props.value)
60
- : props.value
61
- return manager.value.getSeverity(props.field, fieldValue, props.defaultSeverity)
57
+ const fieldValue = computed(() => {
58
+ if (props.value === null || props.value === undefined) return ''
59
+ return typeof props.value === 'boolean' ? String(props.value) : props.value
60
+ })
61
+
62
+ const descriptor = computed(() => {
63
+ if (!manager.value?.getSeverityDescriptor) {
64
+ return { severity: manager.value?.getSeverity?.(props.field, fieldValue.value as string | number, props.defaultSeverity) ?? props.defaultSeverity }
65
+ }
66
+ return manager.value.getSeverityDescriptor(props.field, fieldValue.value as string | number, props.defaultSeverity)
62
67
  })
63
68
 
69
+ const severity = computed(() => descriptor.value.severity)
70
+
64
71
  const displayLabel = computed(() => {
65
- return props.label ?? props.value
72
+ return props.label ?? descriptor.value.label ?? props.value
66
73
  })
74
+
75
+ const hasIcon = computed(() => !!descriptor.value.icon)
67
76
  </script>
68
77
 
69
78
  <template>
70
- <Tag :value="displayLabel" :severity="severity" />
79
+ <!-- Rich badge with icon -->
80
+ <span v-if="hasIcon" class="p-tag p-component" :class="`p-tag-${severity}`">
81
+ <i :class="descriptor.icon" />
82
+ <span class="p-tag-label">{{ displayLabel }}</span>
83
+ </span>
84
+ <!-- Simple PrimeVue Tag -->
85
+ <Tag v-else :value="displayLabel" :severity="severity" />
71
86
  </template>
@@ -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 {
@@ -71,7 +71,7 @@ interface FieldConfig {
71
71
  optionLabel?: string
72
72
  optionValue?: string
73
73
  // Badge options
74
- severity?: string | ((value: unknown) => string)
74
+ severity?: string | { severity: string; icon?: string; label?: string } | ((value: unknown) => string | { severity: string; icon?: string; label?: string })
75
75
  // Image options
76
76
  imageWidth?: string
77
77
  imageHeight?: string
@@ -200,14 +200,18 @@ const badgeValue = computed(() => {
200
200
  return String(props.value)
201
201
  })
202
202
 
203
- // Badge severity
204
- const badgeSeverity = computed(() => {
205
- if (typeof props.field.severity === 'function') {
206
- return props.field.severity(props.value)
207
- }
208
- return props.field.severity || 'info'
203
+ // Badge descriptor (normalized to { severity, icon? })
204
+ const badgeDescriptor = computed(() => {
205
+ const raw = typeof props.field.severity === 'function'
206
+ ? props.field.severity(props.value)
207
+ : props.field.severity
208
+ if (!raw) return { severity: 'info' }
209
+ return typeof raw === 'string' ? { severity: raw } : raw
209
210
  })
210
211
 
212
+ const badgeSeverity = computed(() => badgeDescriptor.value.severity)
213
+ const badgeIcon = computed(() => badgeDescriptor.value.icon)
214
+
211
215
  // JSON formatted
212
216
  const jsonValue = computed(() => {
213
217
  if (isEmpty.value) return '-'
@@ -316,7 +320,15 @@ const imageStyle = computed(() => ({
316
320
  <!-- JSON -->
317
321
  <pre v-else-if="displayType === 'json'" class="show-display show-display--json">{{ jsonValue }}</pre>
318
322
 
319
- <!-- Badge -->
323
+ <!-- Badge (with optional icon) -->
324
+ <span
325
+ v-else-if="displayType === 'badge' && badgeIcon"
326
+ class="show-display show-display--badge p-tag p-component"
327
+ :class="`p-tag-${badgeSeverity}`"
328
+ >
329
+ <i :class="badgeIcon" />
330
+ <span class="p-tag-label">{{ badgeValue }}</span>
331
+ </span>
320
332
  <Tag
321
333
  v-else-if="displayType === 'badge'"
322
334
  :value="badgeValue"
@@ -335,6 +347,11 @@ const imageStyle = computed(() => ({
335
347
  display: inline;
336
348
  }
337
349
 
350
+ .show-display--badge {
351
+ display: inline-flex;
352
+ align-items: center;
353
+ }
354
+
338
355
  .show-display--empty {
339
356
  color: var(--p-text-muted-color, #6c757d);
340
357
  }
@@ -78,6 +78,9 @@ 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
83
+ getSeverityDescriptor?: (field: string, value: string | number, defaultSeverity?: string) => { severity: string; icon?: string; label?: string }
81
84
  }
82
85
 
83
86
  /**
@@ -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,25 @@ 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 = manager.getSeverityDescriptor
410
+ ? (value: unknown) => manager.getSeverityDescriptor!(name, value as string | number, 'secondary')
411
+ : (value: unknown) => manager.getSeverity!(name, value as string | number, 'secondary')
412
+ if (resolvedDisplayType === 'text') {
413
+ resolvedDisplayType = 'badge'
414
+ }
415
+ }
416
+
405
417
  return {
406
418
  name,
407
- type: displayType,
419
+ type: resolvedDisplayType,
408
420
  schemaType,
409
421
  label: resolvedLabel,
410
422
  reference,
411
423
  referenceRoute,
424
+ severity: resolvedSeverity,
412
425
  ...rest,
413
426
  }
414
427
  }
@@ -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
+ }
@@ -158,6 +158,22 @@ export interface EntityBadge {
158
158
  severity?: 'secondary' | 'info' | 'success' | 'warn' | 'danger' | 'contrast'
159
159
  }
160
160
 
161
+ /**
162
+ * Rich severity descriptor for a field value.
163
+ * Extends beyond a simple severity string to support icons and label overrides.
164
+ */
165
+ export interface SeverityDescriptor {
166
+ severity: string
167
+ icon?: string
168
+ label?: string
169
+ }
170
+
171
+ /** A severity map value: plain string (backward compat) or rich descriptor */
172
+ export type SeverityMapValue = string | SeverityDescriptor
173
+
174
+ /** Severity map: field value → severity string or descriptor */
175
+ export type SeverityMap = Record<string, SeverityMapValue>
176
+
161
177
  /**
162
178
  * EntityManager constructor options
163
179
  */
@@ -243,7 +259,7 @@ export class EntityManager<T extends EntityRecord = EntityRecord> {
243
259
  protected _nav: NavConfig
244
260
 
245
261
  protected _hooks: HookRegistry | null = null
246
- protected _severityMaps: Record<string, Record<string, string>> = {}
262
+ protected _severityMaps: Record<string, SeverityMap> = {}
247
263
 
248
264
  protected _cache: CacheState<T> = {
249
265
  items: [],
@@ -939,24 +955,40 @@ export class EntityManager<T extends EntityRecord = EntityRecord> {
939
955
  // ============ SEVERITY MAPS ============
940
956
 
941
957
  /**
942
- * Set severity map for a field
958
+ * Set severity map for a field.
959
+ * Values can be plain severity strings or rich descriptors with icon/label.
943
960
  */
944
- setSeverityMap(field: string, map: Record<string, string>): this {
961
+ setSeverityMap(field: string, map: SeverityMap): this {
945
962
  this._severityMaps[field] = map
946
963
  return this
947
964
  }
948
965
 
949
966
  /**
950
- * Get severity for a field value
967
+ * Get severity string for a field value (backward compat).
968
+ * Extracts .severity from descriptors automatically.
951
969
  */
952
970
  getSeverity(
953
971
  field: string,
954
972
  value: string | number,
955
973
  defaultSeverity: string = 'secondary'
956
974
  ): string {
957
- const map = this._severityMaps[field]
958
- if (!map) return defaultSeverity
959
- return map[String(value)] ?? defaultSeverity
975
+ const entry = this._severityMaps[field]?.[String(value)]
976
+ if (!entry) return defaultSeverity
977
+ return typeof entry === 'string' ? entry : entry.severity
978
+ }
979
+
980
+ /**
981
+ * Get full severity descriptor for a field value.
982
+ * Returns normalized descriptor (plain strings wrapped as { severity }).
983
+ */
984
+ getSeverityDescriptor(
985
+ field: string,
986
+ value: string | number,
987
+ defaultSeverity: string = 'secondary'
988
+ ): SeverityDescriptor {
989
+ const entry = this._severityMaps[field]?.[String(value)]
990
+ if (!entry) return { severity: defaultSeverity }
991
+ return typeof entry === 'string' ? { severity: entry } : entry
960
992
  }
961
993
 
962
994
  /**
package/src/index.ts CHANGED
@@ -92,6 +92,9 @@ export {
92
92
  createEntityManager,
93
93
  type EntityManagerOptions,
94
94
  type EntityBadge,
95
+ type SeverityDescriptor,
96
+ type SeverityMapValue,
97
+ type SeverityMap,
95
98
  type RoutingContext,
96
99
  type PresaveContext,
97
100
  type PostsaveContext,
@@ -291,6 +294,7 @@ export {
291
294
  } from './composables/useUserImpersonator'
292
295
  export { useCurrentEntity, type UseCurrentEntityReturn } from './composables/useCurrentEntity'
293
296
  export { useChildPage, type UseChildPageReturn } from './composables/useChildPage'
297
+ export { useSidebarState, SIDEBAR_COLLAPSED_KEY } from './composables/useSidebarState'
294
298
 
295
299
  // ════════════════════════════════════════════════════════════════════════════
296
300
  // COMPONENTS
@@ -156,6 +156,7 @@ function formatTime(timestamp: number): string {
156
156
  left: calc(var(--fad-sidebar-width, 15rem) + 0.375rem);
157
157
  bottom: 0.5rem;
158
158
  width: 22rem;
159
+ min-height: 6.6rem;
159
160
  max-height: 50vh;
160
161
  background: var(--p-surface-0, #ffffff);
161
162
  border: 1px solid var(--p-surface-200, #e2e8f0);
@@ -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
  }