@quazardous/qdadm 2.8.0 → 2.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/README.md CHANGED
@@ -2,11 +2,11 @@
2
2
 
3
3
  **Vue 3 admin framework. PrimeVue. Zero boilerplate.**
4
4
 
5
- Quick start: [../../README.md](../../README.md) (5 min tutorial)
5
+ Quick start: [README](https://github.com/quazardous/qdadm/blob/main/README.md) (5 min tutorial)
6
6
 
7
- Concepts & patterns: [QDADM_CREDO.md](QDADM_CREDO.md)
7
+ Concepts & patterns: [QDADM_CREDO](https://github.com/quazardous/qdadm/blob/main/packages/qdadm/QDADM_CREDO.md)
8
8
 
9
- Changelog: [../../CHANGELOG.md](../../CHANGELOG.md)
9
+ Changelog: [CHANGELOG](https://github.com/quazardous/qdadm/blob/main/CHANGELOG.md)
10
10
 
11
11
  ## Installation
12
12
 
@@ -95,20 +95,20 @@ Options: `transformRequest`, `transformResponse`, `responseFormat`, `clientSideP
95
95
 
96
96
  | Doc | Purpose |
97
97
  |-----|---------|
98
- | [QDADM_CREDO](QDADM_CREDO.md) | Philosophy, patterns, concepts |
99
- | [Page compositions](../../docs/page-compositions.md) | **Start here** — which composition for which need |
100
- | [CRUD Pages](../../docs/crud.md) | List / form / show / child pages reference |
101
- | [Navigation](../../docs/navigation.md) | Breadcrumb, sibling navlinks, View↔Edit toggle |
102
- | [Architecture](../../docs/architecture.md) | PAC pattern, layers |
103
- | [Extension](../../docs/extension.md) | Decorators, bundles, multi-storage |
104
- | [Hooks](../../docs/hooks.md) | Alter / invoke registry |
105
- | [Signals](../../docs/signals.md) | Event bus, SSE bridge |
106
- | [Zones](../../docs/zones.md) | Extensible UI blocks |
107
- | [Security](../../docs/security.md) | Permissions, roles |
108
- | [i18n](../../docs/i18n.md) | Messages, providers, strategies |
109
- | [Codegen](../../docs/gen.md) | Generate managers (OpenAPI / manual) |
110
- | [Deferred](../../docs/deferred.md) | Deferred values & warmup |
111
- | [AGENT_GUIDE](../../docs/AGENT_GUIDE.md) | Quick index for AI agents |
98
+ | [QDADM_CREDO](https://github.com/quazardous/qdadm/blob/main/packages/qdadm/QDADM_CREDO.md) | Philosophy, patterns, concepts |
99
+ | [Page compositions](https://github.com/quazardous/qdadm/blob/main/docs/page-compositions.md) | **Start here** — which composition for which need |
100
+ | [CRUD Pages](https://github.com/quazardous/qdadm/blob/main/docs/crud.md) | List / form / show / child pages reference |
101
+ | [Navigation](https://github.com/quazardous/qdadm/blob/main/docs/navigation.md) | Breadcrumb, sibling navlinks, View↔Edit toggle |
102
+ | [Architecture](https://github.com/quazardous/qdadm/blob/main/docs/architecture.md) | PAC pattern, layers |
103
+ | [Extension](https://github.com/quazardous/qdadm/blob/main/docs/extension.md) | Decorators, bundles, multi-storage |
104
+ | [Hooks](https://github.com/quazardous/qdadm/blob/main/docs/hooks.md) | Alter / invoke registry |
105
+ | [Signals](https://github.com/quazardous/qdadm/blob/main/docs/signals.md) | Event bus, SSE bridge |
106
+ | [Zones](https://github.com/quazardous/qdadm/blob/main/docs/zones.md) | Extensible UI blocks |
107
+ | [Security](https://github.com/quazardous/qdadm/blob/main/docs/security.md) | Permissions, roles |
108
+ | [i18n](https://github.com/quazardous/qdadm/blob/main/docs/i18n.md) | Messages, providers, strategies |
109
+ | [Codegen](https://github.com/quazardous/qdadm/blob/main/docs/gen.md) | Generate managers (OpenAPI / manual) |
110
+ | [Deferred](https://github.com/quazardous/qdadm/blob/main/docs/deferred.md) | Deferred values & warmup |
111
+ | [AGENT_GUIDE](https://github.com/quazardous/qdadm/blob/main/docs/AGENT_GUIDE.md) | Quick index for AI agents |
112
112
 
113
113
  ## Peer Dependencies
114
114
 
package/package.json CHANGED
@@ -1,12 +1,13 @@
1
1
  {
2
2
  "name": "@quazardous/qdadm",
3
- "version": "2.8.0",
3
+ "version": "2.9.0",
4
4
  "description": "Vue 3 framework for admin dashboards with PrimeVue",
5
5
  "author": "quazardous",
6
6
  "license": "MIT",
7
7
  "repository": {
8
8
  "type": "git",
9
- "url": "git+https://github.com/quazardous/qdadm.git"
9
+ "url": "git+https://github.com/quazardous/qdadm.git",
10
+ "directory": "packages/qdadm"
10
11
  },
11
12
  "homepage": "https://github.com/quazardous/qdadm#readme",
12
13
  "bugs": {
@@ -11,6 +11,7 @@ export { default as Breadcrumb } from './layout/Breadcrumb.vue'
11
11
  export { default as PageNav } from './layout/PageNav.vue'
12
12
  export { default as Zone } from './layout/Zone.vue'
13
13
  export { default as SidebarBox } from './layout/SidebarBox.vue'
14
+ export { default as NavlinksGroup } from './layout/NavlinksGroup.vue'
14
15
 
15
16
  // Default zone components
16
17
  export { default as DefaultHeader } from './layout/defaults/DefaultHeader.vue'
@@ -25,6 +25,7 @@ import Breadcrumb from 'primevue/breadcrumb'
25
25
  import UnsavedChangesDialog from '../dialogs/UnsavedChangesDialog.vue'
26
26
  import SidebarBox from './SidebarBox.vue'
27
27
  import Zone from './Zone.vue'
28
+ import NavlinksGroup from './NavlinksGroup.vue'
28
29
  import qdadmLogo from '../../assets/logo.svg'
29
30
  import { version as qdadmVersion } from '../../../package.json'
30
31
 
@@ -261,12 +262,6 @@ const showBreadcrumb = computed<boolean>(() => {
261
262
  return true
262
263
  })
263
264
 
264
- // View↔Edit mode links (#1332/#1341/#1353) — same opt-in contract as
265
- // DefaultBreadcrumb; single opposite-mode link on item pages, the parent's
266
- // View/Edit pair on child pages
267
- const shownModeLinks = computed(() =>
268
- features.breadcrumbModeToggle === true ? modeLinks.value : []
269
- )
270
265
  </script>
271
266
 
272
267
  <template>
@@ -392,30 +387,8 @@ const shownModeLinks = computed(() =>
392
387
  </Breadcrumb>
393
388
 
394
389
  <!-- Navlinks (provided by PageNav for child routes) + View↔Edit mode
395
- links (#1332/#1341/#1353) — plain navigation; the form page's own
396
- route-leave guard covers unsaved changes on edit→show -->
397
- <div v-if="navlinks.length > 0 || shownModeLinks.length > 0" class="layout-navlinks">
398
- <!-- mode links lead the group (#j7udkh), sibling tabs follow -->
399
- <template v-for="(mode, index) in shownModeLinks" :key="`mode-${mode.target}`">
400
- <span v-if="index > 0" class="layout-navlinks-separator">|</span>
401
- <RouterLink
402
- :to="mode.to"
403
- class="layout-navlink breadcrumb-mode-toggle"
404
- >
405
- {{ mode.label }}
406
- </RouterLink>
407
- </template>
408
- <template v-for="(link, index) in navlinks" :key="link.to?.name || index">
409
- <span v-if="shownModeLinks.length > 0 || index > 0" class="layout-navlinks-separator">|</span>
410
- <RouterLink
411
- :to="link.to"
412
- class="layout-navlink"
413
- :class="{ 'layout-navlink--active': link.active }"
414
- >
415
- {{ link.label }}
416
- </RouterLink>
417
- </template>
418
- </div>
390
+ links (#1332/#1341/#1353) — shared rendering + dedup (#1357) -->
391
+ <NavlinksGroup :navlinks="navlinks" :mode-links="modeLinks" variant="layout" />
419
392
  </div>
420
393
 
421
394
  <div class="page-content">
@@ -723,33 +696,7 @@ const shownModeLinks = computed(() =>
723
696
  padding-bottom: 0;
724
697
  }
725
698
 
726
- .layout-navlinks {
727
- display: flex;
728
- align-items: center;
729
- gap: 0.375rem;
730
- font-size: var(--fad-font-size-sm);
731
- }
732
-
733
- .layout-navlinks-separator {
734
- color: var(--p-surface-400, #94a3b8);
735
- opacity: 0.5;
736
- }
737
-
738
- .layout-navlink {
739
- color: var(--p-primary-500, #3b82f6);
740
- text-decoration: none;
741
- transition: color var(--fad-transition-fast);
742
- }
743
-
744
- .layout-navlink:hover {
745
- color: var(--p-primary-600, #2563eb);
746
- }
747
-
748
- .layout-navlink--active {
749
- color: var(--p-surface-600, #475569);
750
- font-weight: var(--fad-font-weight-medium);
751
- pointer-events: none;
752
- }
699
+ /* Navlinks styles moved to NavlinksGroup.vue (#1357) */
753
700
 
754
701
  /* Override PrimeVue Breadcrumb styles for flat look */
755
702
  .layout-nav-bar :deep(.p-breadcrumb) {
@@ -0,0 +1,153 @@
1
+ <script setup lang="ts">
2
+ /**
3
+ * NavlinksGroup - the right-side navigation group of a breadcrumb bar (#1357)
4
+ *
5
+ * Single rendering of the contract previously copy-pasted between AppLayout's
6
+ * inline breadcrumb and DefaultBreadcrumb: View↔Edit mode links LEAD the
7
+ * group (#1332/#1341/#1353, order #j7udkh), sibling navlinks follow, pipe
8
+ * separators between every pair, plain text (no icons).
9
+ *
10
+ * Owns the two behaviors both call sites need:
11
+ * - the `qdadmFeatures.breadcrumbModeToggle` opt-in gate on mode links
12
+ * - dedup (#1357): a navlink whose target route is already covered by a
13
+ * shown mode link is dropped — e.g. the auto "Details" link resolves to
14
+ * the parent's edit route and would render twice ("Details" + "Edit").
15
+ * With the feature off, no mode links are shown and every navlink stays
16
+ * (back-compat).
17
+ *
18
+ * `variant` keeps the historical CSS hooks of each call site so consumer
19
+ * overrides keep working: 'layout' = AppLayout classes, 'breadcrumb' =
20
+ * DefaultBreadcrumb classes.
21
+ */
22
+ import { computed, inject } from 'vue'
23
+ import { RouterLink } from 'vue-router'
24
+ import type { NavLinkItem, BreadcrumbModeToggle } from '../../composables/useNavContext'
25
+
26
+ interface FeaturesConfig {
27
+ breadcrumbModeToggle?: boolean
28
+ [key: string]: unknown
29
+ }
30
+
31
+ const props = withDefaults(
32
+ defineProps<{
33
+ navlinks?: NavLinkItem[]
34
+ modeLinks?: BreadcrumbModeToggle[]
35
+ variant?: 'layout' | 'breadcrumb'
36
+ }>(),
37
+ {
38
+ navlinks: () => [],
39
+ modeLinks: () => [],
40
+ variant: 'breadcrumb',
41
+ }
42
+ )
43
+
44
+ const features = inject<FeaturesConfig>('qdadmFeatures', {})
45
+
46
+ // Opt-in (#1332): mode links render only when the feature flag is on
47
+ const shownModeLinks = computed<BreadcrumbModeToggle[]>(() =>
48
+ features.breadcrumbModeToggle === true ? props.modeLinks : []
49
+ )
50
+
51
+ // Dedup (#1357): drop navlinks targeting a route a mode link already covers
52
+ const shownNavlinks = computed<NavLinkItem[]>(() => {
53
+ if (shownModeLinks.value.length === 0) return props.navlinks
54
+ const covered = new Set(shownModeLinks.value.map((m) => m.to.name))
55
+ return props.navlinks.filter((link) => !link.to?.name || !covered.has(link.to.name))
56
+ })
57
+
58
+ const cls = computed(() =>
59
+ props.variant === 'layout'
60
+ ? {
61
+ container: 'layout-navlinks',
62
+ separator: 'layout-navlinks-separator',
63
+ link: 'layout-navlink',
64
+ active: 'layout-navlink--active',
65
+ }
66
+ : {
67
+ container: 'breadcrumb-navlinks',
68
+ separator: 'navlinks-separator',
69
+ link: 'navlink',
70
+ active: 'navlink--active',
71
+ }
72
+ )
73
+ </script>
74
+
75
+ <template>
76
+ <!-- Plain navigation; the form page's own route-leave guard covers
77
+ unsaved changes on edit→show -->
78
+ <div v-if="shownNavlinks.length > 0 || shownModeLinks.length > 0" :class="cls.container">
79
+ <!-- mode links lead the group (#j7udkh), sibling tabs follow -->
80
+ <template v-for="(mode, index) in shownModeLinks" :key="`mode-${mode.target}`">
81
+ <span v-if="index > 0" :class="cls.separator">|</span>
82
+ <RouterLink :to="mode.to" class="breadcrumb-mode-toggle" :class="cls.link">
83
+ {{ mode.label }}
84
+ </RouterLink>
85
+ </template>
86
+ <template v-for="(link, index) in shownNavlinks" :key="link.to?.name || index">
87
+ <span v-if="shownModeLinks.length > 0 || index > 0" :class="cls.separator">|</span>
88
+ <RouterLink :to="link.to" :class="[cls.link, { [cls.active]: link.active }]">
89
+ {{ link.label }}
90
+ </RouterLink>
91
+ </template>
92
+ </div>
93
+ </template>
94
+
95
+ <style scoped>
96
+ /* Historical styles of each call site, keyed by variant container so the
97
+ look of both bars is preserved exactly (AppLayout was compact). */
98
+ .layout-navlinks {
99
+ display: flex;
100
+ align-items: center;
101
+ gap: 0.375rem;
102
+ font-size: var(--fad-font-size-sm);
103
+ }
104
+
105
+ .layout-navlinks-separator {
106
+ color: var(--p-surface-400, #94a3b8);
107
+ opacity: 0.5;
108
+ }
109
+
110
+ .layout-navlink {
111
+ color: var(--p-primary-500, #3b82f6);
112
+ text-decoration: none;
113
+ transition: color var(--fad-transition-fast);
114
+ }
115
+
116
+ .layout-navlink:hover {
117
+ color: var(--p-primary-600, #2563eb);
118
+ }
119
+
120
+ .layout-navlink--active {
121
+ color: var(--p-surface-600, #475569);
122
+ font-weight: var(--fad-font-weight-medium);
123
+ pointer-events: none;
124
+ }
125
+
126
+ .breadcrumb-navlinks {
127
+ display: flex;
128
+ align-items: center;
129
+ gap: 0.5rem;
130
+ font-size: 0.875rem;
131
+ }
132
+
133
+ .navlinks-separator {
134
+ color: var(--p-surface-400, #94a3b8);
135
+ }
136
+
137
+ .navlink {
138
+ color: var(--p-primary-500, #3b82f6);
139
+ text-decoration: none;
140
+ transition: color 0.15s;
141
+ }
142
+
143
+ .navlink:hover {
144
+ color: var(--p-primary-700, #1d4ed8);
145
+ text-decoration: underline;
146
+ }
147
+
148
+ .navlink--active {
149
+ color: var(--p-surface-700, #334155);
150
+ font-weight: 500;
151
+ pointer-events: none;
152
+ }
153
+ </style>
@@ -12,11 +12,10 @@ import { computed, inject, ref, type Ref } from 'vue'
12
12
  import { RouterLink, useRoute } from 'vue-router'
13
13
  import { useNavContext, type BreadcrumbItem, type NavLinkItem } from '../../../composables/useNavContext'
14
14
  import Breadcrumb from 'primevue/breadcrumb'
15
+ import NavlinksGroup from '../NavlinksGroup.vue'
15
16
 
16
17
  interface FeaturesConfig {
17
18
  breadcrumb?: boolean
18
- /** Opt-in (#1332): render a View↔Edit toggle next to the terminal crumb */
19
- breadcrumbModeToggle?: boolean
20
19
  [key: string]: unknown
21
20
  }
22
21
 
@@ -42,11 +41,6 @@ const showBreadcrumb = computed<boolean>(() => {
42
41
  return true
43
42
  })
44
43
 
45
- // View↔Edit mode links (#1332/#1353) — opt-in; single opposite-mode link on
46
- // item pages, the parent's View/Edit pair on child pages
47
- const shownModeLinks = computed(() =>
48
- features.breadcrumbModeToggle === true ? modeLinks.value : []
49
- )
50
44
  </script>
51
45
 
52
46
  <template>
@@ -65,30 +59,8 @@ const shownModeLinks = computed(() =>
65
59
  </Breadcrumb>
66
60
 
67
61
  <!-- Navlinks (provided by PageNav for child routes) + View↔Edit mode
68
- links (#1332/#1341/#1353) — plain navigation; the form page's own
69
- route-leave guard covers unsaved changes on edit→show -->
70
- <div v-if="navlinks.length > 0 || shownModeLinks.length > 0" class="breadcrumb-navlinks">
71
- <!-- mode links lead the group (#j7udkh), sibling tabs follow -->
72
- <template v-for="(mode, index) in shownModeLinks" :key="`mode-${mode.target}`">
73
- <span v-if="index > 0" class="navlinks-separator">|</span>
74
- <RouterLink
75
- :to="mode.to"
76
- class="navlink breadcrumb-mode-toggle"
77
- >
78
- {{ mode.label }}
79
- </RouterLink>
80
- </template>
81
- <template v-for="(link, index) in navlinks" :key="link.to?.name || index">
82
- <span v-if="shownModeLinks.length > 0 || index > 0" class="navlinks-separator">|</span>
83
- <RouterLink
84
- :to="link.to"
85
- class="navlink"
86
- :class="{ 'navlink--active': link.active }"
87
- >
88
- {{ link.label }}
89
- </RouterLink>
90
- </template>
91
- </div>
62
+ links (#1332/#1341/#1353) — shared rendering + dedup (#1357) -->
63
+ <NavlinksGroup :navlinks="navlinks" :mode-links="modeLinks" variant="breadcrumb" />
92
64
  </div>
93
65
  </template>
94
66
 
@@ -105,33 +77,7 @@ const shownModeLinks = computed(() =>
105
77
  padding-bottom: 0;
106
78
  }
107
79
 
108
- .breadcrumb-navlinks {
109
- display: flex;
110
- align-items: center;
111
- gap: 0.5rem;
112
- font-size: 0.875rem;
113
- }
114
-
115
- .navlinks-separator {
116
- color: var(--p-surface-400, #94a3b8);
117
- }
118
-
119
- .navlink {
120
- color: var(--p-primary-500, #3b82f6);
121
- text-decoration: none;
122
- transition: color 0.15s;
123
- }
124
-
125
- .navlink:hover {
126
- color: var(--p-primary-700, #1d4ed8);
127
- text-decoration: underline;
128
- }
129
-
130
- .navlink--active {
131
- color: var(--p-surface-700, #334155);
132
- font-weight: 500;
133
- pointer-events: none;
134
- }
80
+ /* Navlinks styles moved to NavlinksGroup.vue (#1357) */
135
81
 
136
82
  /* Override PrimeVue Breadcrumb styles for flat look */
137
83
  .default-breadcrumb :deep(.p-breadcrumb) {
@@ -377,6 +377,8 @@ export function useNavContext(_options: UseNavContextOptions = {}): UseNavContex
377
377
  })
378
378
 
379
379
  const navlinks = computed(() => {
380
+ void i18nLocale.value // "Details" label re-resolves on locale change
381
+
380
382
  if (!parentConfig.value) return []
381
383
 
382
384
  const { entity: parentEntity, param, itemRoute } = parentConfig.value
@@ -391,7 +393,7 @@ export function useNavContext(_options: UseNavContextOptions = {}): UseNavContex
391
393
  // Details link - use manager's idField for param name
392
394
  const links: NavLinkItem[] = [
393
395
  {
394
- label: 'Details',
396
+ label: breadcrumbLabel('details', 'Details'),
395
397
  to: {
396
398
  name: parentRouteName!,
397
399
  params: { [parentManager.idField!]: parentId.value as string },
@@ -448,10 +450,17 @@ export function useNavContext(_options: UseNavContextOptions = {}): UseNavContex
448
450
  // MODE TOGGLE / MODE LINKS (#1332, #1353)
449
451
  // ============================================================================
450
452
 
451
- function toggleLabel(target: 'show' | 'edit'): string {
452
- const trace = kernelI18n?.resolve(target === 'edit' ? 'breadcrumb.edit' : 'breadcrumb.view')
453
+ /** Resolve a `breadcrumb.*` label from the i18n catalog, with fallback. */
454
+ function breadcrumbLabel(key: string, fallback: string): string {
455
+ const trace = kernelI18n?.resolve(`breadcrumb.${key}`)
453
456
  if (trace?.hit) return trace.result
454
- return target === 'edit' ? 'Edit' : 'View'
457
+ return fallback
458
+ }
459
+
460
+ function toggleLabel(target: 'show' | 'edit'): string {
461
+ return target === 'edit'
462
+ ? breadcrumbLabel('edit', 'Edit')
463
+ : breadcrumbLabel('view', 'View')
455
464
  }
456
465
 
457
466
  /** Build one mode link if its route exists and (for edit) permission allows. */
@@ -89,10 +89,15 @@ export function computeSemanticBreadcrumb(
89
89
  items.push({ kind: actionKind, entity: lastEntity, id: pendingId })
90
90
  pendingId = null
91
91
  } else {
92
- // No pending ID - update last item's kind (e.g., /books/create)
92
+ // No pending ID and the last item is an id-less entity level
93
+ // (e.g. /books/create): append the action as its own item so the
94
+ // entity-list crumb survives — mutating the list item into
95
+ // entity-create ate the "Books" level (#1356). When the last item
96
+ // already carries an id (/books/1/edit) the action was consumed
97
+ // at the id segment; swallow it as before.
93
98
  const lastItem = items[items.length - 1]
94
99
  if (lastItem && lastItem.kind.startsWith('entity-') && !lastItem.id) {
95
- lastItem.kind = actionKind
100
+ items.push({ kind: actionKind, entity: lastEntity })
96
101
  }
97
102
  }
98
103
  continue