@quazardous/qdadm 1.19.3
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/LICENSE +21 -0
- package/README.md +111 -0
- package/package.json +101 -0
- package/src/assets/logo.svg +6 -0
- package/src/auth/SessionAuthAdapter.ts +459 -0
- package/src/auth/index.ts +15 -0
- package/src/chain/ActiveStack.ts +154 -0
- package/src/chain/StackHydrator.ts +292 -0
- package/src/chain/index.ts +25 -0
- package/src/chain/useActiveStack.ts +120 -0
- package/src/chain/useStackHydrator.ts +155 -0
- package/src/components/BoolCell.vue +34 -0
- package/src/components/InfoBanner.vue +73 -0
- package/src/components/QdadmRoot.vue +52 -0
- package/src/components/SeverityTag.vue +86 -0
- package/src/components/dialogs/BulkStatusDialog.vue +60 -0
- package/src/components/dialogs/MultiStepDialog.vue +331 -0
- package/src/components/dialogs/SimpleDialog.vue +122 -0
- package/src/components/dialogs/UnsavedChangesDialog.vue +80 -0
- package/src/components/display/BannerZone.vue +65 -0
- package/src/components/display/CardsGrid.vue +72 -0
- package/src/components/display/CopyableId.vue +84 -0
- package/src/components/display/EmptyState.vue +62 -0
- package/src/components/display/IntensityBar.vue +190 -0
- package/src/components/display/RichCardsGrid.vue +101 -0
- package/src/components/edit/FormActions.vue +78 -0
- package/src/components/edit/FormField.vue +82 -0
- package/src/components/edit/FormInput.vue +158 -0
- package/src/components/edit/FormPage.vue +260 -0
- package/src/components/edit/FormTab.vue +217 -0
- package/src/components/edit/FormTabs.vue +120 -0
- package/src/components/edit/LookupField.vue +406 -0
- package/src/components/edit/LookupPickerDialog.vue +230 -0
- package/src/components/editors/JsonEditorFoldable.vue +470 -0
- package/src/components/editors/JsonStructuredField.vue +264 -0
- package/src/components/editors/JsonViewer.vue +51 -0
- package/src/components/editors/KeyValueEditor.vue +314 -0
- package/src/components/editors/LanguageEditor.vue +259 -0
- package/src/components/editors/PermissionEditor.vue +558 -0
- package/src/components/editors/ScopeEditor.vue +372 -0
- package/src/components/editors/VanillaJsonEditor.vue +183 -0
- package/src/components/index.ts +83 -0
- package/src/components/item/FieldGroups.vue +392 -0
- package/src/components/layout/AppLayout.vue +1032 -0
- package/src/components/layout/BaseLayout.vue +190 -0
- package/src/components/layout/Breadcrumb.vue +55 -0
- package/src/components/layout/DashboardLayout.vue +104 -0
- package/src/components/layout/FormLayout.vue +256 -0
- package/src/components/layout/ListLayout.vue +278 -0
- package/src/components/layout/PageHeader.vue +156 -0
- package/src/components/layout/PageLayout.vue +126 -0
- package/src/components/layout/PageNav.vue +192 -0
- package/src/components/layout/SidebarBox.vue +87 -0
- package/src/components/layout/Zone.vue +182 -0
- package/src/components/layout/defaults/DefaultBreadcrumb.vue +149 -0
- package/src/components/layout/defaults/DefaultFooter.vue +89 -0
- package/src/components/layout/defaults/DefaultFormActions.vue +67 -0
- package/src/components/layout/defaults/DefaultHeader.vue +78 -0
- package/src/components/layout/defaults/DefaultMenu.vue +188 -0
- package/src/components/layout/defaults/DefaultPagination.vue +75 -0
- package/src/components/layout/defaults/DefaultTable.vue +137 -0
- package/src/components/layout/defaults/DefaultUserInfo.vue +111 -0
- package/src/components/layout/defaults/index.ts +16 -0
- package/src/components/lists/ActionButtons.vue +49 -0
- package/src/components/lists/ActionColumn.vue +40 -0
- package/src/components/lists/FilterBar.vue +54 -0
- package/src/components/lists/ListPage.vue +467 -0
- package/src/components/pages/LoginPage.vue +334 -0
- package/src/components/pages/NotFoundPage.vue +98 -0
- package/src/components/show/ShowDisplay.vue +343 -0
- package/src/components/show/ShowField.vue +67 -0
- package/src/components/show/ShowPage.vue +431 -0
- package/src/composables/index.ts +155 -0
- package/src/composables/useApp.ts +67 -0
- package/src/composables/useAuth.ts +98 -0
- package/src/composables/useBareForm.ts +319 -0
- package/src/composables/useBreadcrumb.ts +252 -0
- package/src/composables/useChildPage.ts +113 -0
- package/src/composables/useCurrentEntity.ts +60 -0
- package/src/composables/useDeferred.ts +94 -0
- package/src/composables/useDirtyState.ts +138 -0
- package/src/composables/useEntityItemFormPage.ts +942 -0
- package/src/composables/useEntityItemFormPage.types.ts +451 -0
- package/src/composables/useEntityItemPage.ts +438 -0
- package/src/composables/useEntityItemShowPage.ts +722 -0
- package/src/composables/useFieldManager.ts +692 -0
- package/src/composables/useForm.ts +544 -0
- package/src/composables/useGuardStore.ts +53 -0
- package/src/composables/useHooks.ts +56 -0
- package/src/composables/useInfoBanner.ts +217 -0
- package/src/composables/useJsonSyntax.ts +122 -0
- package/src/composables/useLayoutResolver.ts +329 -0
- package/src/composables/useListPage.ts +1535 -0
- package/src/composables/useListPage.types.ts +437 -0
- package/src/composables/useListPage.utils.ts +101 -0
- package/src/composables/useNavContext.ts +439 -0
- package/src/composables/useNavigation.ts +216 -0
- package/src/composables/useOptionsLookup.ts +323 -0
- package/src/composables/usePageTitle.ts +84 -0
- package/src/composables/useSSEBridge.ts +142 -0
- package/src/composables/useSemanticBreadcrumb.ts +222 -0
- package/src/composables/useSidebarState.ts +24 -0
- package/src/composables/useSignals.ts +52 -0
- package/src/composables/useUnsavedChangesGuard.ts +155 -0
- package/src/composables/useUserImpersonator.ts +317 -0
- package/src/composables/useZoneRegistry.ts +252 -0
- package/src/core/bundles.ts +558 -0
- package/src/core/decorator.ts +420 -0
- package/src/core/extension.ts +470 -0
- package/src/core/index.ts +59 -0
- package/src/deferred/DeferredRegistry.ts +353 -0
- package/src/deferred/index.ts +15 -0
- package/src/editors/index.ts +12 -0
- package/src/entity/EntityManager.cache.ts +530 -0
- package/src/entity/EntityManager.crud.ts +615 -0
- package/src/entity/EntityManager.interface.ts +78 -0
- package/src/entity/EntityManager.query.ts +221 -0
- package/src/entity/EntityManager.relations.ts +151 -0
- package/src/entity/EntityManager.ts +944 -0
- package/src/entity/EntityManager.types.ts +288 -0
- package/src/entity/auth/CompositeAuthAdapter.ts +194 -0
- package/src/entity/auth/EntityAuthAdapter.ts +119 -0
- package/src/entity/auth/PermissiveAdapter.ts +43 -0
- package/src/entity/auth/RoleHierarchy.ts +128 -0
- package/src/entity/auth/SecurityChecker.ts +202 -0
- package/src/entity/auth/factory.ts +203 -0
- package/src/entity/auth/index.ts +47 -0
- package/src/entity/factory.test.js +189 -0
- package/src/entity/factory.ts +146 -0
- package/src/entity/index.ts +49 -0
- package/src/entity/storage/ApiStorage.ts +228 -0
- package/src/entity/storage/IStorage.ts +100 -0
- package/src/entity/storage/LocalStorage.ts +200 -0
- package/src/entity/storage/MemoryStorage.ts +184 -0
- package/src/entity/storage/MockApiStorage.ts +318 -0
- package/src/entity/storage/SdkStorage.ts +377 -0
- package/src/entity/storage/factory.test.js +159 -0
- package/src/entity/storage/factory.ts +209 -0
- package/src/entity/storage/index.ts +75 -0
- package/src/gen/FieldMapper.ts +122 -0
- package/src/gen/StorageProfileFactory.ts +93 -0
- package/src/gen/connectors/BaseConnector.ts +150 -0
- package/src/gen/connectors/ManualConnector.test.js +499 -0
- package/src/gen/connectors/ManualConnector.ts +414 -0
- package/src/gen/connectors/OpenAPIConnector.test.js +836 -0
- package/src/gen/connectors/OpenAPIConnector.ts +693 -0
- package/src/gen/connectors/__fixtures__/sample-openapi.json +311 -0
- package/src/gen/connectors/index.ts +27 -0
- package/src/gen/createManagers.ts +245 -0
- package/src/gen/decorators.ts +134 -0
- package/src/gen/generateManagers.test.js +540 -0
- package/src/gen/generateManagers.ts +398 -0
- package/src/gen/index.ts +66 -0
- package/src/gen/schema.ts +248 -0
- package/src/gen/vite-plugin.ts +114 -0
- package/src/generated/managers/testManager.js +45 -0
- package/src/generated/managers/testManager.ts +46 -0
- package/src/hooks/HookRegistry.ts +16 -0
- package/src/hooks/index.ts +15 -0
- package/src/i18n/I18n.ts +344 -0
- package/src/i18n/IncrementalDomainProvider.ts +153 -0
- package/src/i18n/InlineTranslationProvider.ts +4 -0
- package/src/i18n/LazyTranslationProvider.ts +102 -0
- package/src/i18n/MessagesRegistry.ts +4 -0
- package/src/i18n/Resolver.ts +4 -0
- package/src/i18n/__tests__/I18n.test.ts +169 -0
- package/src/i18n/__tests__/IncrementalDomainProvider.test.ts +146 -0
- package/src/i18n/__tests__/LazyTranslationProvider.test.ts +100 -0
- package/src/i18n/__tests__/Resolver.test.ts +271 -0
- package/src/i18n/defaults/DefaultCoreProvider.ts +28 -0
- package/src/i18n/defaults/core.en.yml +55 -0
- package/src/i18n/defaults/core.fr.yml +55 -0
- package/src/i18n/index.ts +55 -0
- package/src/i18n/loaders/raw-modules.d.ts +15 -0
- package/src/i18n/loaders/yaml.ts +35 -0
- package/src/i18n/strategies.ts +4 -0
- package/src/i18n/types.ts +34 -0
- package/src/i18n/useI18n.ts +34 -0
- package/src/index.ts +453 -0
- package/src/kernel/EventRouter.ts +23 -0
- package/src/kernel/Kernel.api.ts +79 -0
- package/src/kernel/Kernel.auth.ts +120 -0
- package/src/kernel/Kernel.i18n.ts +29 -0
- package/src/kernel/Kernel.modules.ts +170 -0
- package/src/kernel/Kernel.registries.ts +237 -0
- package/src/kernel/Kernel.routing.ts +334 -0
- package/src/kernel/Kernel.ts +460 -0
- package/src/kernel/Kernel.types.ts +263 -0
- package/src/kernel/Kernel.vue.ts +304 -0
- package/src/kernel/KernelContext.entities.ts +80 -0
- package/src/kernel/KernelContext.events.ts +57 -0
- package/src/kernel/KernelContext.i18n.ts +37 -0
- package/src/kernel/KernelContext.permissions.ts +38 -0
- package/src/kernel/KernelContext.routing.ts +280 -0
- package/src/kernel/KernelContext.ts +252 -0
- package/src/kernel/KernelContext.types.ts +173 -0
- package/src/kernel/KernelContext.zones.ts +54 -0
- package/src/kernel/Module.ts +169 -0
- package/src/kernel/ModuleLoader.ts +645 -0
- package/src/kernel/SSEBridge.ts +11 -0
- package/src/kernel/SignalBus.ts +48 -0
- package/src/kernel/index.ts +56 -0
- package/src/module/index.ts +33 -0
- package/src/module/moduleRegistry.ts +492 -0
- package/src/modules/debug/AuthCollector.ts +501 -0
- package/src/modules/debug/Collector.ts +20 -0
- package/src/modules/debug/DebugBridge.ts +14 -0
- package/src/modules/debug/DebugModule.ts +220 -0
- package/src/modules/debug/EntitiesCollector.ts +839 -0
- package/src/modules/debug/ErrorCollector.ts +4 -0
- package/src/modules/debug/I18nCollector.ts +9 -0
- package/src/modules/debug/LocalStorageAdapter.ts +7 -0
- package/src/modules/debug/RouterCollector.ts +449 -0
- package/src/modules/debug/SignalCollector.ts +4 -0
- package/src/modules/debug/ToastCollector.ts +4 -0
- package/src/modules/debug/ZonesCollector.ts +447 -0
- package/src/modules/debug/components/DebugBar.vue +31 -0
- package/src/modules/debug/components/adminPanelsConfig.ts +42 -0
- package/src/modules/debug/components/index.ts +9 -0
- package/src/modules/debug/components/panels/AuthPanel.vue +267 -0
- package/src/modules/debug/components/panels/EntitiesPanel.vue +572 -0
- package/src/modules/debug/components/panels/I18nPanel.vue +738 -0
- package/src/modules/debug/components/panels/RouterPanel.vue +440 -0
- package/src/modules/debug/components/panels/ZonesPanel.vue +133 -0
- package/src/modules/debug/components/panels/index.ts +16 -0
- package/src/modules/debug/index.ts +58 -0
- package/src/modules/debug/styles.scss +2671 -0
- package/src/notifications/NotificationBadge.vue +35 -0
- package/src/notifications/NotificationListener.vue +71 -0
- package/src/notifications/NotificationModule.ts +76 -0
- package/src/notifications/NotificationPanel.vue +478 -0
- package/src/notifications/NotificationStore.ts +288 -0
- package/src/notifications/index.ts +42 -0
- package/src/notifications/styles.scss +32 -0
- package/src/orchestrator/Orchestrator.ts +427 -0
- package/src/orchestrator/index.ts +8 -0
- package/src/orchestrator/useOrchestrator.ts +80 -0
- package/src/plugin.ts +251 -0
- package/src/query/FilterQuery.ts +306 -0
- package/src/query/QueryExecutor.ts +341 -0
- package/src/query/index.ts +16 -0
- package/src/security/EntityRolesProvider.ts +385 -0
- package/src/security/PermissionMatcher.ts +152 -0
- package/src/security/PermissionRegistry.ts +273 -0
- package/src/security/PersistableRolesProvider.ts +584 -0
- package/src/security/RolesManager.ts +136 -0
- package/src/security/RolesProvider.ts +167 -0
- package/src/security/RolesProviderStorage.ts +211 -0
- package/src/security/SecurityModule.ts +94 -0
- package/src/security/StaticRolesProvider.ts +117 -0
- package/src/security/UsersManager.ts +163 -0
- package/src/security/index.ts +77 -0
- package/src/security/pages/RoleForm.vue +276 -0
- package/src/security/pages/RoleList.vue +123 -0
- package/src/styles/_alerts.scss +95 -0
- package/src/styles/_buttons.scss +391 -0
- package/src/styles/_cards.scss +241 -0
- package/src/styles/_code.scss +78 -0
- package/src/styles/_dialogs.scss +55 -0
- package/src/styles/_filter-bar.scss +70 -0
- package/src/styles/_forms.scss +434 -0
- package/src/styles/_helpers.scss +153 -0
- package/src/styles/_lists.scss +225 -0
- package/src/styles/_main.scss +356 -0
- package/src/styles/_markdown.scss +88 -0
- package/src/styles/_responsive.scss +255 -0
- package/src/styles/_show-pages.scss +186 -0
- package/src/styles/_sidebar.scss +380 -0
- package/src/styles/_states.scss +115 -0
- package/src/styles/_stats.scss +47 -0
- package/src/styles/_variables.scss +224 -0
- package/src/styles/index.scss +38 -0
- package/src/styles/theme/_components.scss +331 -0
- package/src/styles/theme/_index.scss +10 -0
- package/src/styles/theme/_tokens.scss +147 -0
- package/src/styles/theme/_utilities.scss +175 -0
- package/src/toast/ToastBridgeModule.ts +68 -0
- package/src/toast/ToastListener.vue +55 -0
- package/src/toast/index.ts +26 -0
- package/src/toast/useSignalToast.ts +154 -0
- package/src/types/index.ts +285 -0
- package/src/types/pluralize.d.ts +19 -0
- package/src/types/quarkernel.d.ts +50 -0
- package/src/utils/formatters.ts +174 -0
- package/src/utils/index.ts +35 -0
- package/src/utils/transformers.ts +131 -0
- package/src/vite/qdadmDebugPlugin.ts +401 -0
- package/src/vite-env.d.ts +50 -0
- package/src/zones/ZoneRegistry.ts +934 -0
- package/src/zones/index.ts +29 -0
- package/src/zones/zones.ts +191 -0
|
@@ -0,0 +1,439 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useNavContext - Route-aware navigation context for breadcrumb and navlinks
|
|
3
|
+
*
|
|
4
|
+
* Uses semantic breadcrumb as the source of truth for navigation structure.
|
|
5
|
+
* Entity data comes from activeStack (set by useEntityItemPage/useForm).
|
|
6
|
+
*
|
|
7
|
+
* Semantic breadcrumb kinds:
|
|
8
|
+
* - entity-list: Entity collection (e.g., /books)
|
|
9
|
+
* - entity-show: Entity instance view (e.g., /books/1)
|
|
10
|
+
* - entity-edit: Entity instance edit (e.g., /books/1/edit)
|
|
11
|
+
* - entity-create: Entity creation (e.g., /books/create)
|
|
12
|
+
* - route: Generic route (e.g., /settings)
|
|
13
|
+
*
|
|
14
|
+
* Examples:
|
|
15
|
+
* Path: /books → [{ kind: 'entity-list', entity: 'books' }]
|
|
16
|
+
* Path: /books/1/edit → [{ kind: 'entity-list', entity: 'books' }, { kind: 'entity-edit', entity: 'books', id: '1' }]
|
|
17
|
+
* Path: /books/stats → [{ kind: 'entity-list', entity: 'books' }, { kind: 'route', route: 'book-stats' }]
|
|
18
|
+
*/
|
|
19
|
+
import { ref, computed, watch, inject, type Ref, type ComputedRef } from 'vue'
|
|
20
|
+
import { useRoute, useRouter, type RouteParamsRawGeneric } from 'vue-router'
|
|
21
|
+
import { getSiblingRoutes } from '../module/moduleRegistry'
|
|
22
|
+
import { useSemanticBreadcrumb, type SemanticBreadcrumbItem } from './useSemanticBreadcrumb'
|
|
23
|
+
import { useStackHydrator, type HydratedLevel } from '../chain/useStackHydrator'
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Navigation chain segment types
|
|
27
|
+
*/
|
|
28
|
+
export type NavChainType = 'list' | 'item' | 'create' | 'route' | 'child-list'
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Navigation chain segment
|
|
32
|
+
*/
|
|
33
|
+
export interface NavChainSegment {
|
|
34
|
+
type: NavChainType
|
|
35
|
+
entity?: string
|
|
36
|
+
manager?: EntityManager | null
|
|
37
|
+
id?: string
|
|
38
|
+
label?: string
|
|
39
|
+
routeName?: string | null
|
|
40
|
+
navLabel?: string
|
|
41
|
+
route?: string
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Entity manager interface
|
|
46
|
+
*/
|
|
47
|
+
interface EntityManager {
|
|
48
|
+
routePrefix?: string
|
|
49
|
+
labelPlural?: string
|
|
50
|
+
idField?: string
|
|
51
|
+
readOnly?: boolean
|
|
52
|
+
getEntityLabel: (data: unknown) => string
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Orchestrator interface
|
|
57
|
+
*/
|
|
58
|
+
interface Orchestrator {
|
|
59
|
+
get: (entityName: string) => EntityManager | null
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Breadcrumb item
|
|
64
|
+
*/
|
|
65
|
+
export interface BreadcrumbItem {
|
|
66
|
+
label: string
|
|
67
|
+
to?: { name: string; params?: RouteParamsRawGeneric } | null
|
|
68
|
+
icon?: string
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Nav link item
|
|
73
|
+
*/
|
|
74
|
+
export interface NavLinkItem {
|
|
75
|
+
label: string
|
|
76
|
+
to: { name: string; params?: RouteParamsRawGeneric }
|
|
77
|
+
active: boolean
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Parent config from route meta
|
|
82
|
+
*/
|
|
83
|
+
interface ParentConfig {
|
|
84
|
+
entity: string
|
|
85
|
+
param: string
|
|
86
|
+
itemRoute?: string
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Options for useNavContext
|
|
91
|
+
*/
|
|
92
|
+
export interface UseNavContextOptions {
|
|
93
|
+
[key: string]: unknown
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Return type for useNavContext
|
|
98
|
+
*/
|
|
99
|
+
export interface UseNavContextReturn {
|
|
100
|
+
// Analysis
|
|
101
|
+
navChain: ComputedRef<NavChainSegment[]>
|
|
102
|
+
semanticBreadcrumb: ComputedRef<SemanticBreadcrumbItem[]>
|
|
103
|
+
|
|
104
|
+
// Data
|
|
105
|
+
entityData: ComputedRef<unknown | null>
|
|
106
|
+
parentData: ComputedRef<unknown | null>
|
|
107
|
+
chainData: Ref<Map<number, unknown>>
|
|
108
|
+
|
|
109
|
+
// Navigation
|
|
110
|
+
breadcrumb: ComputedRef<BreadcrumbItem[]>
|
|
111
|
+
navlinks: ComputedRef<NavLinkItem[]>
|
|
112
|
+
|
|
113
|
+
// Helpers
|
|
114
|
+
parentConfig: ComputedRef<ParentConfig | undefined>
|
|
115
|
+
parentId: ComputedRef<unknown | null>
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export function useNavContext(_options: UseNavContextOptions = {}): UseNavContextReturn {
|
|
119
|
+
const route = useRoute()
|
|
120
|
+
const router = useRouter()
|
|
121
|
+
|
|
122
|
+
// Semantic breadcrumb as source of truth
|
|
123
|
+
const { breadcrumb: semanticBreadcrumb } = useSemanticBreadcrumb()
|
|
124
|
+
|
|
125
|
+
// Injected dependencies
|
|
126
|
+
const orchestrator = inject<Orchestrator | null>('qdadmOrchestrator', null)
|
|
127
|
+
const homeRouteName = inject<string | null>('qdadmHomeRoute', null)
|
|
128
|
+
|
|
129
|
+
// Stack hydrator for entity data (has async-loaded data and labels)
|
|
130
|
+
const hydrator = useStackHydrator()
|
|
131
|
+
|
|
132
|
+
function getManager(entityName: string): EntityManager | null {
|
|
133
|
+
return orchestrator?.get(entityName) ?? null
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function routeExists(name: string): boolean {
|
|
137
|
+
return router.getRoutes().some((r) => r.name === name)
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Get default item route for an entity manager
|
|
142
|
+
* Checks which routes actually exist and returns the appropriate one:
|
|
143
|
+
* - Prefers -edit if it exists (editable entity)
|
|
144
|
+
* - Falls back to -show if -edit doesn't exist
|
|
145
|
+
* @param manager - Entity manager
|
|
146
|
+
* @returns Route name or null if neither exists
|
|
147
|
+
*/
|
|
148
|
+
function getDefaultItemRoute(manager: EntityManager | null): string | null {
|
|
149
|
+
if (!manager) return null
|
|
150
|
+
|
|
151
|
+
const editRoute = `${manager.routePrefix}-edit`
|
|
152
|
+
const showRoute = `${manager.routePrefix}-show`
|
|
153
|
+
|
|
154
|
+
// Check which routes actually exist and return the first available
|
|
155
|
+
if (router.hasRoute(editRoute)) return editRoute
|
|
156
|
+
if (router.hasRoute(showRoute)) return showRoute
|
|
157
|
+
|
|
158
|
+
return null
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Convert semantic breadcrumb to navigation chain format
|
|
163
|
+
* Maps semantic kinds to chain types for compatibility
|
|
164
|
+
*/
|
|
165
|
+
function semanticToNavChain(semantic: SemanticBreadcrumbItem[]): NavChainSegment[] {
|
|
166
|
+
const chain: NavChainSegment[] = []
|
|
167
|
+
|
|
168
|
+
for (const item of semantic) {
|
|
169
|
+
if (item.kind === 'entity-list') {
|
|
170
|
+
const manager = getManager(item.entity!)
|
|
171
|
+
// For child routes, prefer route's navLabel over manager's labelPlural
|
|
172
|
+
const routeRecord = item.route
|
|
173
|
+
? router.getRoutes().find((r) => r.name === item.route)
|
|
174
|
+
: null
|
|
175
|
+
const label =
|
|
176
|
+
(routeRecord?.meta?.navLabel as string) || manager?.labelPlural || item.entity!
|
|
177
|
+
chain.push({
|
|
178
|
+
type: 'list',
|
|
179
|
+
entity: item.entity,
|
|
180
|
+
manager,
|
|
181
|
+
label,
|
|
182
|
+
routeName: item.route || manager?.routePrefix || item.entity!.slice(0, -1),
|
|
183
|
+
})
|
|
184
|
+
} else if (item.kind?.startsWith('entity-') && item.id) {
|
|
185
|
+
// entity-show, entity-edit, entity-delete
|
|
186
|
+
const manager = getManager(item.entity!)
|
|
187
|
+
chain.push({
|
|
188
|
+
type: 'item',
|
|
189
|
+
entity: item.entity,
|
|
190
|
+
manager,
|
|
191
|
+
id: item.id,
|
|
192
|
+
routeName: getDefaultItemRoute(manager),
|
|
193
|
+
})
|
|
194
|
+
} else if (item.kind === 'entity-create') {
|
|
195
|
+
// Create page - treat as special list
|
|
196
|
+
const manager = getManager(item.entity!)
|
|
197
|
+
chain.push({
|
|
198
|
+
type: 'create',
|
|
199
|
+
entity: item.entity,
|
|
200
|
+
manager,
|
|
201
|
+
label: 'Create',
|
|
202
|
+
routeName: manager ? `${manager.routePrefix}-create` : null,
|
|
203
|
+
})
|
|
204
|
+
} else if (item.kind === 'route') {
|
|
205
|
+
// Generic route (like /books/stats)
|
|
206
|
+
// Use label from semantic item if provided (custom breadcrumb), else lookup route
|
|
207
|
+
const routeRecord = router.getRoutes().find((r) => r.name === item.route)
|
|
208
|
+
chain.push({
|
|
209
|
+
type: 'route',
|
|
210
|
+
route: item.route,
|
|
211
|
+
label:
|
|
212
|
+
item.label ||
|
|
213
|
+
(routeRecord?.meta?.navLabel as string) ||
|
|
214
|
+
(routeRecord?.meta?.title as string) ||
|
|
215
|
+
item.route,
|
|
216
|
+
routeName: item.route,
|
|
217
|
+
})
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
return chain
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// ============================================================================
|
|
225
|
+
// COMPUTED NAVIGATION CHAIN
|
|
226
|
+
// ============================================================================
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Navigation chain built from semantic breadcrumb
|
|
230
|
+
*/
|
|
231
|
+
const navChain = computed(() => {
|
|
232
|
+
return semanticToNavChain(semanticBreadcrumb.value)
|
|
233
|
+
})
|
|
234
|
+
|
|
235
|
+
// ============================================================================
|
|
236
|
+
// ENTITY DATA FROM ACTIVESTACK
|
|
237
|
+
// ============================================================================
|
|
238
|
+
|
|
239
|
+
const chainData = ref<Map<number, unknown>>(new Map()) // Map: chainIndex -> entityData
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Build chainData from stackHydrator levels
|
|
243
|
+
*
|
|
244
|
+
* StackHydrator fetches entity data asynchronously and provides labels.
|
|
245
|
+
* Each hydrated level has .data and .label fields when loaded.
|
|
246
|
+
*/
|
|
247
|
+
watch(
|
|
248
|
+
[navChain, hydrator.levels],
|
|
249
|
+
([chain, levels]: [NavChainSegment[], HydratedLevel[]]) => {
|
|
250
|
+
// Build new Map (reassignment triggers Vue reactivity)
|
|
251
|
+
const newChainData = new Map<number, unknown>()
|
|
252
|
+
|
|
253
|
+
for (let i = 0; i < chain.length; i++) {
|
|
254
|
+
const segment = chain[i]
|
|
255
|
+
if (!segment || segment.type !== 'item') continue
|
|
256
|
+
|
|
257
|
+
// Find matching data in hydrator by entity name and id
|
|
258
|
+
const hydratedLevel = levels.find(
|
|
259
|
+
(l) => l.entity === segment.entity && String(l.id) === String(segment.id)
|
|
260
|
+
)
|
|
261
|
+
if (hydratedLevel?.data) {
|
|
262
|
+
newChainData.set(i, hydratedLevel.data)
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// Assign new Map to trigger reactivity
|
|
267
|
+
chainData.value = newChainData
|
|
268
|
+
},
|
|
269
|
+
{ immediate: true, deep: true }
|
|
270
|
+
)
|
|
271
|
+
|
|
272
|
+
// ============================================================================
|
|
273
|
+
// BREADCRUMB
|
|
274
|
+
// ============================================================================
|
|
275
|
+
|
|
276
|
+
const homeItem = computed(() => {
|
|
277
|
+
if (!homeRouteName || !routeExists(homeRouteName)) return null
|
|
278
|
+
return {
|
|
279
|
+
label: homeRouteName === 'dashboard' ? 'Dashboard' : 'Home',
|
|
280
|
+
to: { name: homeRouteName },
|
|
281
|
+
icon: 'pi pi-home',
|
|
282
|
+
}
|
|
283
|
+
})
|
|
284
|
+
|
|
285
|
+
const breadcrumb = computed(() => {
|
|
286
|
+
const items: BreadcrumbItem[] = []
|
|
287
|
+
const chain = navChain.value
|
|
288
|
+
|
|
289
|
+
// Home
|
|
290
|
+
if (homeItem.value) {
|
|
291
|
+
items.push(homeItem.value)
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// Build from chain
|
|
295
|
+
for (let i = 0; i < chain.length; i++) {
|
|
296
|
+
const segment = chain[i]
|
|
297
|
+
if (!segment) continue
|
|
298
|
+
const isLast = i === chain.length - 1
|
|
299
|
+
|
|
300
|
+
if (segment.type === 'list') {
|
|
301
|
+
items.push({
|
|
302
|
+
label: segment.label!,
|
|
303
|
+
to:
|
|
304
|
+
isLast || !segment.routeName || !routeExists(segment.routeName)
|
|
305
|
+
? null
|
|
306
|
+
: { name: segment.routeName },
|
|
307
|
+
})
|
|
308
|
+
} else if (segment.type === 'item') {
|
|
309
|
+
const data = chainData.value.get(i)
|
|
310
|
+
const label = data && segment.manager ? segment.manager.getEntityLabel(data) : '...'
|
|
311
|
+
const idField = segment.manager?.idField || 'id'
|
|
312
|
+
|
|
313
|
+
// Only create link if we have a valid id
|
|
314
|
+
const canLink = !isLast && segment.id && segment.routeName && routeExists(segment.routeName)
|
|
315
|
+
items.push({
|
|
316
|
+
label,
|
|
317
|
+
to: canLink ? { name: segment.routeName!, params: { [idField]: segment.id } } : null,
|
|
318
|
+
})
|
|
319
|
+
} else if (segment.type === 'create') {
|
|
320
|
+
items.push({
|
|
321
|
+
label: segment.label!,
|
|
322
|
+
})
|
|
323
|
+
} else if (segment.type === 'route') {
|
|
324
|
+
// Generic route (e.g., /books/stats)
|
|
325
|
+
items.push({
|
|
326
|
+
label: segment.label!,
|
|
327
|
+
to:
|
|
328
|
+
isLast || !segment.routeName || !routeExists(segment.routeName)
|
|
329
|
+
? null
|
|
330
|
+
: { name: segment.routeName },
|
|
331
|
+
})
|
|
332
|
+
} else if (segment.type === 'child-list') {
|
|
333
|
+
items.push({
|
|
334
|
+
label: segment.navLabel || segment.label!,
|
|
335
|
+
})
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
return items
|
|
340
|
+
})
|
|
341
|
+
|
|
342
|
+
// ============================================================================
|
|
343
|
+
// NAVLINKS (for child routes)
|
|
344
|
+
// ============================================================================
|
|
345
|
+
|
|
346
|
+
const parentConfig = computed(() => route.meta?.parent as ParentConfig | undefined)
|
|
347
|
+
|
|
348
|
+
const parentId = computed(() => {
|
|
349
|
+
if (!parentConfig.value) return null
|
|
350
|
+
return route.params[parentConfig.value.param]
|
|
351
|
+
})
|
|
352
|
+
|
|
353
|
+
const navlinks = computed(() => {
|
|
354
|
+
if (!parentConfig.value) return []
|
|
355
|
+
|
|
356
|
+
const { entity: parentEntity, param, itemRoute } = parentConfig.value
|
|
357
|
+
const parentManager = getManager(parentEntity)
|
|
358
|
+
|
|
359
|
+
// Guard: need valid manager and parentId to build links
|
|
360
|
+
if (!parentManager || !parentId.value) return []
|
|
361
|
+
|
|
362
|
+
const parentRouteName = itemRoute || getDefaultItemRoute(parentManager)
|
|
363
|
+
const isOnParent = route.name === parentRouteName
|
|
364
|
+
|
|
365
|
+
// Details link - use manager's idField for param name
|
|
366
|
+
const links: NavLinkItem[] = [
|
|
367
|
+
{
|
|
368
|
+
label: 'Details',
|
|
369
|
+
to: {
|
|
370
|
+
name: parentRouteName!,
|
|
371
|
+
params: { [parentManager.idField!]: parentId.value as string },
|
|
372
|
+
},
|
|
373
|
+
active: isOnParent,
|
|
374
|
+
},
|
|
375
|
+
]
|
|
376
|
+
|
|
377
|
+
// Sibling routes - only include list routes (not create/edit/show)
|
|
378
|
+
// List routes are navigable tabs, while create/edit/show are accessed via the list
|
|
379
|
+
const siblings = getSiblingRoutes(parentEntity, param).filter(
|
|
380
|
+
(sibling) => sibling.meta?.layout === 'list' || sibling.meta?.layout === 'page'
|
|
381
|
+
)
|
|
382
|
+
for (const sibling of siblings) {
|
|
383
|
+
const sibManager = sibling.meta?.entity ? getManager(sibling.meta.entity as string) : null
|
|
384
|
+
links.push({
|
|
385
|
+
label:
|
|
386
|
+
(sibling.meta?.navLabel as string) || sibManager?.labelPlural || (sibling.name as string),
|
|
387
|
+
to: { name: sibling.name as string, params: route.params as RouteParamsRawGeneric },
|
|
388
|
+
active: route.name === sibling.name,
|
|
389
|
+
})
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
return links
|
|
393
|
+
})
|
|
394
|
+
|
|
395
|
+
// ============================================================================
|
|
396
|
+
// CONVENIENCE ACCESSORS
|
|
397
|
+
// ============================================================================
|
|
398
|
+
|
|
399
|
+
const entityData = computed(() => {
|
|
400
|
+
const chain = navChain.value
|
|
401
|
+
for (let i = chain.length - 1; i >= 0; i--) {
|
|
402
|
+
if (chain[i]?.type === 'item') {
|
|
403
|
+
return chainData.value.get(i) || null
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
return null
|
|
407
|
+
})
|
|
408
|
+
|
|
409
|
+
const parentData = computed(() => {
|
|
410
|
+
const chain = navChain.value
|
|
411
|
+
let foundCurrent = false
|
|
412
|
+
for (let i = chain.length - 1; i >= 0; i--) {
|
|
413
|
+
if (chain[i]?.type === 'item') {
|
|
414
|
+
if (foundCurrent) return chainData.value.get(i) || null
|
|
415
|
+
foundCurrent = true
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
return null
|
|
419
|
+
})
|
|
420
|
+
|
|
421
|
+
return {
|
|
422
|
+
// Analysis
|
|
423
|
+
navChain,
|
|
424
|
+
semanticBreadcrumb,
|
|
425
|
+
|
|
426
|
+
// Data
|
|
427
|
+
entityData,
|
|
428
|
+
parentData,
|
|
429
|
+
chainData,
|
|
430
|
+
|
|
431
|
+
// Navigation
|
|
432
|
+
breadcrumb,
|
|
433
|
+
navlinks,
|
|
434
|
+
|
|
435
|
+
// Helpers
|
|
436
|
+
parentConfig,
|
|
437
|
+
parentId,
|
|
438
|
+
}
|
|
439
|
+
}
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useNavigation - Navigation composable for AppLayout
|
|
3
|
+
*
|
|
4
|
+
* Provides reactive navigation state from moduleRegistry.
|
|
5
|
+
* All data comes from module init declarations.
|
|
6
|
+
* Nav items are filtered based on EntityManager.canRead() permissions.
|
|
7
|
+
*
|
|
8
|
+
* Invokes 'menu:alter' hook on first access to allow modules to modify
|
|
9
|
+
* the navigation structure dynamically.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { computed, inject, ref, onMounted, type ComputedRef, type Ref } from 'vue'
|
|
13
|
+
import { useRoute, useRouter } from 'vue-router'
|
|
14
|
+
import { getNavSections, alterMenuSections, isMenuAltered } from '../module/moduleRegistry'
|
|
15
|
+
import { useSemanticBreadcrumb } from './useSemanticBreadcrumb'
|
|
16
|
+
import { useI18n } from '../i18n/useI18n'
|
|
17
|
+
import type { HookRegistry } from '../hooks/HookRegistry'
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Navigation item interface
|
|
21
|
+
*/
|
|
22
|
+
export interface NavItem {
|
|
23
|
+
label: string
|
|
24
|
+
route: string
|
|
25
|
+
icon?: string
|
|
26
|
+
entity?: string
|
|
27
|
+
exact?: boolean
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Navigation section interface
|
|
32
|
+
*/
|
|
33
|
+
export interface NavSection {
|
|
34
|
+
label?: string
|
|
35
|
+
title?: string
|
|
36
|
+
items: NavItem[]
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Entity manager interface
|
|
41
|
+
*/
|
|
42
|
+
interface EntityManager {
|
|
43
|
+
canRead: () => boolean
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Orchestrator interface
|
|
48
|
+
*/
|
|
49
|
+
interface Orchestrator {
|
|
50
|
+
signals?: unknown
|
|
51
|
+
get: (entityName: string) => EntityManager | null
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Return type for useNavigation
|
|
56
|
+
*/
|
|
57
|
+
export interface UseNavigationReturn {
|
|
58
|
+
/** Navigation sections (filtered by permissions) */
|
|
59
|
+
navSections: ComputedRef<NavSection[]>
|
|
60
|
+
/** Whether menu:alter has completed */
|
|
61
|
+
isReady: Ref<boolean>
|
|
62
|
+
/** Check if nav item is active */
|
|
63
|
+
isNavActive: (item: NavItem) => boolean
|
|
64
|
+
/** Check if section has active item */
|
|
65
|
+
sectionHasActiveItem: (section: NavSection) => boolean
|
|
66
|
+
/** Handle nav item click */
|
|
67
|
+
handleNavClick: (event: Event, item: NavItem) => void
|
|
68
|
+
/** Current route name */
|
|
69
|
+
currentRouteName: ComputedRef<string | symbol | null | undefined>
|
|
70
|
+
/** Current route path */
|
|
71
|
+
currentRoutePath: ComputedRef<string>
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Navigation composable
|
|
76
|
+
*
|
|
77
|
+
* @returns Navigation state and helpers
|
|
78
|
+
*/
|
|
79
|
+
export function useNavigation(): UseNavigationReturn {
|
|
80
|
+
const route = useRoute()
|
|
81
|
+
const router = useRouter()
|
|
82
|
+
const orchestrator = inject<Orchestrator | null>('qdadmOrchestrator', null)
|
|
83
|
+
const hooks = inject<HookRegistry | null>('qdadmHooks', null)
|
|
84
|
+
|
|
85
|
+
// i18n integration — labels resolve through nav.sections.* / nav.routes.*
|
|
86
|
+
const { i18n: kernelI18n, locale: i18nLocale } = useI18n()
|
|
87
|
+
|
|
88
|
+
// Semantic breadcrumb for entity-based active detection
|
|
89
|
+
const { breadcrumb } = useSemanticBreadcrumb()
|
|
90
|
+
|
|
91
|
+
// Track whether menu:alter has completed
|
|
92
|
+
const isReady = ref(isMenuAltered())
|
|
93
|
+
|
|
94
|
+
// Trigger version to force reactivity after alteration or auth change
|
|
95
|
+
const alterVersion = ref(0)
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Check if user can access a nav item based on its entity's canRead()
|
|
99
|
+
*/
|
|
100
|
+
function canAccessNavItem(item: NavItem): boolean {
|
|
101
|
+
if (!item.entity || !orchestrator) return true
|
|
102
|
+
const manager = orchestrator.get(item.entity)
|
|
103
|
+
if (!manager) return true
|
|
104
|
+
return manager.canRead()
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Note: Auth signal listeners removed - Kernel.invalidateApp() remounts entire app
|
|
108
|
+
// on auth changes, so composable is re-initialized with fresh state
|
|
109
|
+
|
|
110
|
+
// Invoke menu:alter hook on mount
|
|
111
|
+
onMounted(async () => {
|
|
112
|
+
if (!isMenuAltered()) {
|
|
113
|
+
await alterMenuSections(hooks)
|
|
114
|
+
alterVersion.value++
|
|
115
|
+
isReady.value = true
|
|
116
|
+
}
|
|
117
|
+
})
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Resolve a label through i18n with the given convention key, falling back
|
|
121
|
+
* to the inline label when the bundle has no matching translation.
|
|
122
|
+
*/
|
|
123
|
+
function resolveNavLabel(key: string, fallback: string): string {
|
|
124
|
+
if (!kernelI18n) return fallback
|
|
125
|
+
const trace = kernelI18n.resolve(key)
|
|
126
|
+
return trace.hit ? trace.result : fallback
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Get nav sections from registry, filtering items based on permissions
|
|
130
|
+
// Depends on alterVersion + i18nLocale to trigger re-computation after
|
|
131
|
+
// alteration or locale change.
|
|
132
|
+
const navSections = computed(() => {
|
|
133
|
+
// Force dependencies for reactivity
|
|
134
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
|
|
135
|
+
alterVersion.value
|
|
136
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
|
|
137
|
+
i18nLocale.value
|
|
138
|
+
|
|
139
|
+
const sections = getNavSections()
|
|
140
|
+
return sections
|
|
141
|
+
.map((section) => ({
|
|
142
|
+
...section,
|
|
143
|
+
title: resolveNavLabel(`nav.sections.${section.title}`, section.title),
|
|
144
|
+
items: section.items
|
|
145
|
+
.filter((item) => canAccessNavItem(item))
|
|
146
|
+
.map((item) => ({
|
|
147
|
+
...item,
|
|
148
|
+
label: resolveNavLabel(`nav.routes.${item.route}`, item.label),
|
|
149
|
+
})),
|
|
150
|
+
}))
|
|
151
|
+
.filter((section) => section.items.length > 0) // Hide empty sections
|
|
152
|
+
})
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Check if a nav item is currently active
|
|
156
|
+
* Uses semantic breadcrumb only - no route segment deduction
|
|
157
|
+
*/
|
|
158
|
+
function isNavActive(item: NavItem): boolean {
|
|
159
|
+
// Exact match mode
|
|
160
|
+
if (item.exact) {
|
|
161
|
+
return route.name === item.route
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Check semantic breadcrumb for match
|
|
165
|
+
const bc = breadcrumb.value
|
|
166
|
+
|
|
167
|
+
// Entity-based menu item: match ONLY by entity (don't fall through)
|
|
168
|
+
if (item.entity) {
|
|
169
|
+
const firstEntityItem = bc.find((b) => b.kind.startsWith('entity-'))
|
|
170
|
+
return firstEntityItem ? firstEntityItem.entity === item.entity : false
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Route-based menu item: check if route is in breadcrumb
|
|
174
|
+
if (item.route) {
|
|
175
|
+
return bc.some((b) => b.kind === 'route' && b.route === item.route)
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
return false
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Check if section contains active item
|
|
183
|
+
*/
|
|
184
|
+
function sectionHasActiveItem(section: NavSection): boolean {
|
|
185
|
+
return section.items.some((item) => isNavActive(item))
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Handle nav click - force navigation if on same route with query params
|
|
190
|
+
*/
|
|
191
|
+
function handleNavClick(event: Event, item: NavItem): void {
|
|
192
|
+
if (route.name === item.route && Object.keys(route.query).length > 0) {
|
|
193
|
+
event.preventDefault()
|
|
194
|
+
router.push({ name: item.route })
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
return {
|
|
199
|
+
// Data (from moduleRegistry)
|
|
200
|
+
navSections,
|
|
201
|
+
|
|
202
|
+
// Ready state (menu:alter completed)
|
|
203
|
+
isReady,
|
|
204
|
+
|
|
205
|
+
// Active state
|
|
206
|
+
isNavActive,
|
|
207
|
+
sectionHasActiveItem,
|
|
208
|
+
|
|
209
|
+
// Event handlers
|
|
210
|
+
handleNavClick,
|
|
211
|
+
|
|
212
|
+
// Current route info (reactive)
|
|
213
|
+
currentRouteName: computed(() => route.name),
|
|
214
|
+
currentRoutePath: computed(() => route.path),
|
|
215
|
+
}
|
|
216
|
+
}
|