@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,192 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* PageNav - Navigation provider for navlinks (child/sibling routes)
|
|
4
|
+
*
|
|
5
|
+
* This component provides navlinks to AppLayout via provide/inject.
|
|
6
|
+
* Breadcrumb is now handled automatically by useNavContext + activeStack.
|
|
7
|
+
*
|
|
8
|
+
* Layout (rendered in AppLayout):
|
|
9
|
+
* Books > "Dune" Details | Loans | Reviews
|
|
10
|
+
* ↑ breadcrumb (from useNavContext) ↑ navlinks (from PageNav)
|
|
11
|
+
*
|
|
12
|
+
* Auto-detects from current route:
|
|
13
|
+
* - Navlinks: sibling routes (same parent entity + param) or children routes
|
|
14
|
+
*
|
|
15
|
+
* Props:
|
|
16
|
+
* - showDetailsLink: Show "Details" link in navlinks (default: false)
|
|
17
|
+
*/
|
|
18
|
+
import { computed, watch, inject, type Ref } from 'vue'
|
|
19
|
+
import { useRoute, useRouter } from 'vue-router'
|
|
20
|
+
import { getSiblingRoutes, getChildRoutes, type ModuleRoute, type ParentConfig } from '../../module/moduleRegistry'
|
|
21
|
+
import { useOrchestrator } from '../../orchestrator/useOrchestrator.js'
|
|
22
|
+
import type { EntityManager } from '../../entity/EntityManager'
|
|
23
|
+
import type { EntityRecord } from '../../types'
|
|
24
|
+
|
|
25
|
+
interface NavLink {
|
|
26
|
+
label: string
|
|
27
|
+
to: { name: string | undefined; params: Record<string, unknown> }
|
|
28
|
+
active: boolean
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Inject override ref from AppLayout
|
|
32
|
+
const navlinksOverride = inject<Ref<NavLink[] | null> | null>('qdadmNavlinksOverride', null)
|
|
33
|
+
|
|
34
|
+
const props = defineProps<{
|
|
35
|
+
// Show "Details" link in navlinks (default: false since breadcrumb shows current page)
|
|
36
|
+
showDetailsLink?: boolean
|
|
37
|
+
}>()
|
|
38
|
+
|
|
39
|
+
const route = useRoute()
|
|
40
|
+
const router = useRouter()
|
|
41
|
+
const { getManager } = useOrchestrator()
|
|
42
|
+
|
|
43
|
+
// Parent config from route meta
|
|
44
|
+
const parentConfig = computed<ParentConfig | undefined>(() => route.meta?.parent as ParentConfig | undefined)
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Get default item route for an entity manager
|
|
48
|
+
* Checks which routes actually exist and returns the appropriate one:
|
|
49
|
+
* - Prefers -edit if it exists (editable entity)
|
|
50
|
+
* - Falls back to -show if -edit doesn't exist
|
|
51
|
+
* - Returns null if neither exists
|
|
52
|
+
*/
|
|
53
|
+
function getDefaultItemRoute(manager: EntityManager<EntityRecord> | null): string | null {
|
|
54
|
+
if (!manager) return null
|
|
55
|
+
|
|
56
|
+
const editRoute = `${manager.routePrefix}-edit`
|
|
57
|
+
const showRoute = `${manager.routePrefix}-show`
|
|
58
|
+
|
|
59
|
+
// Check which routes actually exist and return the first available
|
|
60
|
+
if (router.hasRoute(editRoute)) return editRoute
|
|
61
|
+
if (router.hasRoute(showRoute)) return showRoute
|
|
62
|
+
|
|
63
|
+
return null
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Sibling navlinks (routes with same parent)
|
|
67
|
+
const siblingNavlinks = computed<NavLink[]>(() => {
|
|
68
|
+
if (!parentConfig.value) return []
|
|
69
|
+
|
|
70
|
+
const { entity: parentEntityName, param } = parentConfig.value
|
|
71
|
+
const siblings = getSiblingRoutes(parentEntityName, param)
|
|
72
|
+
|
|
73
|
+
// Filter out action routes (create, edit) - they're not navigation tabs
|
|
74
|
+
const navRoutes = siblings.filter((r: ModuleRoute) => {
|
|
75
|
+
const name = r.name || ''
|
|
76
|
+
const path = r.path || ''
|
|
77
|
+
// Exclude routes ending with -create, -edit, -new or paths with /create, /edit, /new
|
|
78
|
+
return !name.match(/-(create|edit|new)$/) && !path.match(/\/(create|edit|new)$/)
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
// Build navlinks with current route params
|
|
82
|
+
return navRoutes.map((siblingRoute: ModuleRoute): NavLink => {
|
|
83
|
+
const manager = siblingRoute.meta?.entity ? getManager(siblingRoute.meta.entity) : null
|
|
84
|
+
const label = (siblingRoute.meta?.navLabel as string) || manager?.labelPlural || siblingRoute.name || ''
|
|
85
|
+
|
|
86
|
+
return {
|
|
87
|
+
label,
|
|
88
|
+
to: { name: siblingRoute.name, params: route.params as Record<string, unknown> },
|
|
89
|
+
active: route.name === siblingRoute.name
|
|
90
|
+
}
|
|
91
|
+
})
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
// Child navlinks (when on parent detail page, show links to children)
|
|
95
|
+
const childNavlinks = computed<NavLink[]>(() => {
|
|
96
|
+
// Only when NOT on a child route (no parentConfig)
|
|
97
|
+
if (parentConfig.value) return []
|
|
98
|
+
|
|
99
|
+
const entityName = route.meta?.entity as string | undefined
|
|
100
|
+
if (!entityName) return []
|
|
101
|
+
|
|
102
|
+
// Get current entity's manager to determine idField
|
|
103
|
+
const currentManager = getManager(entityName)
|
|
104
|
+
const entityId = route.params[currentManager?.idField || 'id'] as string | undefined
|
|
105
|
+
|
|
106
|
+
// Only show children when we have an entity ID (not on create pages)
|
|
107
|
+
if (!entityId) return []
|
|
108
|
+
|
|
109
|
+
const children = getChildRoutes(entityName)
|
|
110
|
+
if (children.length === 0) return []
|
|
111
|
+
|
|
112
|
+
// Filter out action routes (create, edit) - they're not navigation tabs
|
|
113
|
+
const navRoutes = children.filter((r: ModuleRoute) => {
|
|
114
|
+
const name = r.name || ''
|
|
115
|
+
const path = r.path || ''
|
|
116
|
+
return !name.match(/-(create|edit|new)$/) && !path.match(/\/(create|edit|new)$/)
|
|
117
|
+
})
|
|
118
|
+
|
|
119
|
+
if (navRoutes.length === 0) return []
|
|
120
|
+
|
|
121
|
+
// Build navlinks to child routes
|
|
122
|
+
return navRoutes.map((childRoute: ModuleRoute): NavLink => {
|
|
123
|
+
const childManager = childRoute.meta?.entity ? getManager(childRoute.meta.entity) : null
|
|
124
|
+
const label = (childRoute.meta?.navLabel as string) || childManager?.labelPlural || childRoute.name || ''
|
|
125
|
+
const parentParam = (childRoute.meta?.parent as ParentConfig)?.param || currentManager?.idField || 'id'
|
|
126
|
+
|
|
127
|
+
return {
|
|
128
|
+
label,
|
|
129
|
+
to: { name: childRoute.name, params: { [parentParam]: entityId } },
|
|
130
|
+
active: false
|
|
131
|
+
}
|
|
132
|
+
})
|
|
133
|
+
})
|
|
134
|
+
|
|
135
|
+
// Combined navlinks with optional "Details" link
|
|
136
|
+
const allNavlinks = computed<NavLink[]>(() => {
|
|
137
|
+
// Case 1: On a child route - show siblings + optional Details link to parent
|
|
138
|
+
if (parentConfig.value) {
|
|
139
|
+
const { entity: parentEntityName, param, itemRoute } = parentConfig.value
|
|
140
|
+
const parentId = route.params[param] as string | undefined
|
|
141
|
+
const parentManager = getManager(parentEntityName)
|
|
142
|
+
|
|
143
|
+
// Guard: need valid manager and parentId to build links
|
|
144
|
+
if (!parentManager || !parentId) return []
|
|
145
|
+
|
|
146
|
+
// Details link is optional since breadcrumb already shows parent
|
|
147
|
+
if (props.showDetailsLink) {
|
|
148
|
+
const parentRouteName = itemRoute || getDefaultItemRoute(parentManager)
|
|
149
|
+
const isOnParentRoute = route.name === parentRouteName
|
|
150
|
+
|
|
151
|
+
// Details link to parent item page
|
|
152
|
+
const detailsLink: NavLink = {
|
|
153
|
+
label: 'Details',
|
|
154
|
+
to: { name: parentRouteName ?? undefined, params: { [parentManager.idField]: parentId } },
|
|
155
|
+
active: isOnParentRoute
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
return [detailsLink, ...siblingNavlinks.value]
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
return siblingNavlinks.value
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Case 2: On parent detail page - show children + optional Details (active)
|
|
165
|
+
if (childNavlinks.value.length > 0) {
|
|
166
|
+
// Details link is optional since breadcrumb already shows current page
|
|
167
|
+
if (props.showDetailsLink) {
|
|
168
|
+
const detailsLink: NavLink = {
|
|
169
|
+
label: 'Details',
|
|
170
|
+
to: { name: route.name as string | undefined, params: route.params as Record<string, unknown> },
|
|
171
|
+
active: true
|
|
172
|
+
}
|
|
173
|
+
return [detailsLink, ...childNavlinks.value]
|
|
174
|
+
}
|
|
175
|
+
return childNavlinks.value
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
return []
|
|
179
|
+
})
|
|
180
|
+
|
|
181
|
+
// Sync navlinks to AppLayout via provide/inject
|
|
182
|
+
watch([allNavlinks, () => route.fullPath], ([links]) => {
|
|
183
|
+
if (navlinksOverride) {
|
|
184
|
+
navlinksOverride.value = links as NavLink[]
|
|
185
|
+
}
|
|
186
|
+
}, { immediate: true })
|
|
187
|
+
</script>
|
|
188
|
+
|
|
189
|
+
<template>
|
|
190
|
+
<!-- PageNav provides navlinks to AppLayout via inject, renders nothing -->
|
|
191
|
+
<span v-if="false"></span>
|
|
192
|
+
</template>
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* SidebarBox - Reusable sidebar footer item component
|
|
4
|
+
*
|
|
5
|
+
* Provides consistent structure for icon + text boxes in the sidebar.
|
|
6
|
+
* Used for user info, impersonation, powered-by, etc.
|
|
7
|
+
*
|
|
8
|
+
* Props:
|
|
9
|
+
* - icon: PrimeIcons class (e.g., "pi-user") OR slot for custom content
|
|
10
|
+
* - title: Primary text
|
|
11
|
+
* - subtitle: Secondary text (optional)
|
|
12
|
+
* - variant: Color variant ('default' | 'impersonator')
|
|
13
|
+
*
|
|
14
|
+
* Slots:
|
|
15
|
+
* - icon: Custom icon content (overrides icon prop)
|
|
16
|
+
* - subtitle-content: Custom subtitle content (overrides subtitle prop)
|
|
17
|
+
* - default: Extra content after text (e.g., logout button)
|
|
18
|
+
* - full: Full-width content (bypasses icon+text structure, for controls/dropdowns)
|
|
19
|
+
*
|
|
20
|
+
* Usage:
|
|
21
|
+
* <SidebarBox icon="pi-user" title="John" subtitle="Admin" />
|
|
22
|
+
* <SidebarBox variant="impersonator" title="Impersonating" subtitle="bob" />
|
|
23
|
+
* <SidebarBox>
|
|
24
|
+
* <template #icon><img src="logo.svg" /></template>
|
|
25
|
+
* <template #subtitle-content>powered by qdadm</template>
|
|
26
|
+
* </SidebarBox>
|
|
27
|
+
* <SidebarBox>
|
|
28
|
+
* <template #full>
|
|
29
|
+
* <label>Impersonate</label>
|
|
30
|
+
* <AutoComplete ... />
|
|
31
|
+
* </template>
|
|
32
|
+
* </SidebarBox>
|
|
33
|
+
*/
|
|
34
|
+
|
|
35
|
+
import { useSlots, computed, type ComputedRef } from 'vue'
|
|
36
|
+
|
|
37
|
+
type SidebarBoxVariant = 'default' | 'impersonator'
|
|
38
|
+
|
|
39
|
+
interface Props {
|
|
40
|
+
icon?: string | null
|
|
41
|
+
title?: string
|
|
42
|
+
subtitle?: string
|
|
43
|
+
variant?: SidebarBoxVariant
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
withDefaults(defineProps<Props>(), {
|
|
47
|
+
icon: null,
|
|
48
|
+
title: '',
|
|
49
|
+
subtitle: '',
|
|
50
|
+
variant: 'default'
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
const slots = useSlots()
|
|
54
|
+
const hasFullSlot: ComputedRef<boolean> = computed(() => !!slots.full)
|
|
55
|
+
</script>
|
|
56
|
+
|
|
57
|
+
<template>
|
|
58
|
+
<div
|
|
59
|
+
class="sidebar-box"
|
|
60
|
+
:class="{
|
|
61
|
+
'sidebar-box--impersonator': variant === 'impersonator',
|
|
62
|
+
'sidebar-box--full': hasFullSlot
|
|
63
|
+
}"
|
|
64
|
+
>
|
|
65
|
+
<!-- Full-width mode (for controls/dropdowns) -->
|
|
66
|
+
<div v-if="hasFullSlot" class="sidebar-box-full">
|
|
67
|
+
<slot name="full"></slot>
|
|
68
|
+
</div>
|
|
69
|
+
|
|
70
|
+
<!-- Standard icon + text mode -->
|
|
71
|
+
<div v-else class="sidebar-box-inner">
|
|
72
|
+
<div class="sidebar-box-icon">
|
|
73
|
+
<slot name="icon">
|
|
74
|
+
<i v-if="icon" :class="['pi', icon]"></i>
|
|
75
|
+
</slot>
|
|
76
|
+
</div>
|
|
77
|
+
<div class="sidebar-box-content">
|
|
78
|
+
<div v-if="title" class="sidebar-box-title">{{ title }}</div>
|
|
79
|
+
<div v-if="subtitle" class="sidebar-box-subtitle">
|
|
80
|
+
<slot name="subtitle">{{ subtitle }}</slot>
|
|
81
|
+
</div>
|
|
82
|
+
<slot name="subtitle-content"></slot>
|
|
83
|
+
</div>
|
|
84
|
+
<slot></slot>
|
|
85
|
+
</div>
|
|
86
|
+
</div>
|
|
87
|
+
</template>
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* Zone - Renders blocks registered in a zone from ZoneRegistry
|
|
4
|
+
*
|
|
5
|
+
* Reads blocks from the injected ZoneRegistry and renders them
|
|
6
|
+
* sorted by weight. Supports default content when no blocks registered.
|
|
7
|
+
*
|
|
8
|
+
* **Reactivity**: The Zone component automatically re-renders when blocks
|
|
9
|
+
* are added, removed, or modified in the registry at runtime.
|
|
10
|
+
*
|
|
11
|
+
* Handles wrapped blocks by building nested component structures:
|
|
12
|
+
* - Outer wrappers (lower weight) wrap inner wrappers
|
|
13
|
+
* - Innermost component is the original block
|
|
14
|
+
*
|
|
15
|
+
* Usage:
|
|
16
|
+
* <Zone name="header" />
|
|
17
|
+
* <Zone name="sidebar" :default-component="DefaultSidebar" />
|
|
18
|
+
* <Zone name="main"><RouterView /></Zone>
|
|
19
|
+
*/
|
|
20
|
+
import { computed, inject, h, defineComponent, useSlots, type Component, type VNode, type PropType, type Slot } from 'vue'
|
|
21
|
+
import type { ZoneRegistry, BlockConfig } from '../../zones/ZoneRegistry'
|
|
22
|
+
|
|
23
|
+
const props = defineProps({
|
|
24
|
+
/**
|
|
25
|
+
* Zone name (required) - must match a zone in ZoneRegistry
|
|
26
|
+
*/
|
|
27
|
+
name: {
|
|
28
|
+
type: String,
|
|
29
|
+
required: true
|
|
30
|
+
},
|
|
31
|
+
/**
|
|
32
|
+
* Default component to render when no blocks are registered
|
|
33
|
+
*/
|
|
34
|
+
defaultComponent: {
|
|
35
|
+
type: Object as PropType<Component | null>,
|
|
36
|
+
default: null
|
|
37
|
+
},
|
|
38
|
+
/**
|
|
39
|
+
* Props to pass to all blocks
|
|
40
|
+
*/
|
|
41
|
+
blockProps: {
|
|
42
|
+
type: Object as PropType<Record<string, unknown>>,
|
|
43
|
+
default: () => ({})
|
|
44
|
+
}
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
const slots = useSlots()
|
|
48
|
+
const hasDefaultSlot = computed<boolean>(() => !!slots.default)
|
|
49
|
+
|
|
50
|
+
const registry = inject<ZoneRegistry | null>('qdadmZoneRegistry', null)
|
|
51
|
+
|
|
52
|
+
// Get the reactive version ref for tracking changes
|
|
53
|
+
const version = registry?.getVersionRef?.()
|
|
54
|
+
|
|
55
|
+
const blocks = computed<BlockConfig[]>(() => {
|
|
56
|
+
if (!registry) {
|
|
57
|
+
if ((import.meta as { env?: { DEV?: boolean } }).env?.DEV) {
|
|
58
|
+
console.warn(`[Zone] ZoneRegistry not injected. Zone "${props.name}" will render nothing.`)
|
|
59
|
+
}
|
|
60
|
+
return []
|
|
61
|
+
}
|
|
62
|
+
// Access version.value to create reactive dependency
|
|
63
|
+
// This ensures the computed re-evaluates when blocks change
|
|
64
|
+
if (version) {
|
|
65
|
+
void version.value
|
|
66
|
+
}
|
|
67
|
+
return registry.getBlocks(props.name)
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
const hasBlocks = computed<boolean>(() => blocks.value.length > 0)
|
|
71
|
+
|
|
72
|
+
const showDefault = computed<boolean>(() => {
|
|
73
|
+
return !hasBlocks.value && (props.defaultComponent !== null || registry?.getDefault(props.name) !== null)
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
const showSlot = computed<boolean>(() => {
|
|
77
|
+
return !hasBlocks.value && !showDefault.value && hasDefaultSlot.value
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
const defaultComp = computed<Component | null>(() => {
|
|
81
|
+
return props.defaultComponent || registry?.getDefault(props.name) || null
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Create a wrapper component that renders nested wrappers around inner content
|
|
86
|
+
*
|
|
87
|
+
* This creates a functional component that builds the nested structure
|
|
88
|
+
* once per block, avoiding the infinite re-render issue.
|
|
89
|
+
*
|
|
90
|
+
* Slot content from the Zone is passed to the innermost block component,
|
|
91
|
+
* allowing the block to render Zone's children (e.g., form fields).
|
|
92
|
+
*
|
|
93
|
+
* @param block - Block config with wrappers array
|
|
94
|
+
* @param mergedProps - Props to pass to the innermost component
|
|
95
|
+
* @param slotFn - Slot function to pass to innermost component
|
|
96
|
+
* @returns Vue component definition
|
|
97
|
+
*/
|
|
98
|
+
function createWrappedComponent(block: BlockConfig, mergedProps: Record<string, unknown>, slotFn: Slot | undefined): Component {
|
|
99
|
+
return defineComponent({
|
|
100
|
+
name: 'WrappedBlock',
|
|
101
|
+
render(): VNode {
|
|
102
|
+
// Start with the innermost component (the original block)
|
|
103
|
+
// Pass Zone's slot content to allow block to render it
|
|
104
|
+
const innerSlots = slotFn ? { default: slotFn } : {}
|
|
105
|
+
let current: VNode = h(block.component, mergedProps, innerSlots)
|
|
106
|
+
|
|
107
|
+
// Build from inside out: last wrapper in array wraps the innermost
|
|
108
|
+
// Wrappers are already sorted by weight (lower = outer)
|
|
109
|
+
// So we reverse to build from inside out
|
|
110
|
+
const reversedWrappers = [...(block.wrappers || [])].reverse()
|
|
111
|
+
|
|
112
|
+
for (const wrapper of reversedWrappers) {
|
|
113
|
+
const inner = current
|
|
114
|
+
current = h(wrapper.component, wrapper.props || {}, {
|
|
115
|
+
default: () => inner
|
|
116
|
+
})
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return current
|
|
120
|
+
}
|
|
121
|
+
})
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Computed map of wrapped block components
|
|
126
|
+
* Memoizes the wrapped components to avoid re-creating them on every render
|
|
127
|
+
*
|
|
128
|
+
* Note: Slot function is passed to allow Zone's children to be rendered
|
|
129
|
+
* inside the innermost block component.
|
|
130
|
+
*/
|
|
131
|
+
const wrappedComponents = computed<Map<string | number | null | undefined, Component>>(() => {
|
|
132
|
+
const map = new Map<string | number | null | undefined, Component>()
|
|
133
|
+
// Get slot function to pass to innermost block
|
|
134
|
+
const slotFn = slots.default
|
|
135
|
+
for (const block of blocks.value) {
|
|
136
|
+
if (block.wrappers && block.wrappers.length > 0) {
|
|
137
|
+
const key = block.id || block.weight
|
|
138
|
+
const mergedProps = { ...props.blockProps, ...block.props }
|
|
139
|
+
map.set(key, createWrappedComponent(block, mergedProps, slotFn))
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
return map
|
|
143
|
+
})
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Get the wrapped component for a block
|
|
147
|
+
*/
|
|
148
|
+
function getWrappedComponent(block: BlockConfig): Component | undefined {
|
|
149
|
+
const key = block.id || block.weight
|
|
150
|
+
return wrappedComponents.value.get(key)
|
|
151
|
+
}
|
|
152
|
+
</script>
|
|
153
|
+
|
|
154
|
+
<template>
|
|
155
|
+
<div :data-zone="name" class="qdadm-zone">
|
|
156
|
+
<template v-if="hasBlocks">
|
|
157
|
+
<template v-for="block in blocks" :key="block.id || block.weight">
|
|
158
|
+
<!-- Render wrapped blocks using memoized component -->
|
|
159
|
+
<component
|
|
160
|
+
v-if="block.wrappers"
|
|
161
|
+
:is="getWrappedComponent(block)"
|
|
162
|
+
/>
|
|
163
|
+
<!-- Render simple blocks directly, passing Zone's slot content -->
|
|
164
|
+
<component
|
|
165
|
+
v-else
|
|
166
|
+
:is="block.component"
|
|
167
|
+
v-bind="{ ...blockProps, ...block.props }"
|
|
168
|
+
>
|
|
169
|
+
<slot />
|
|
170
|
+
</component>
|
|
171
|
+
</template>
|
|
172
|
+
</template>
|
|
173
|
+
<component
|
|
174
|
+
v-else-if="showDefault"
|
|
175
|
+
:is="defaultComp"
|
|
176
|
+
v-bind="blockProps"
|
|
177
|
+
>
|
|
178
|
+
<slot />
|
|
179
|
+
</component>
|
|
180
|
+
<slot v-else-if="showSlot" />
|
|
181
|
+
</div>
|
|
182
|
+
</template>
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* DefaultBreadcrumb - Default breadcrumb component for BaseLayout
|
|
4
|
+
*
|
|
5
|
+
* Displays breadcrumb trail and optional navlinks.
|
|
6
|
+
* Extracted from AppLayout for zone-based architecture.
|
|
7
|
+
*
|
|
8
|
+
* This is the default component rendered in the "breadcrumb" zone
|
|
9
|
+
* when no blocks are registered.
|
|
10
|
+
*/
|
|
11
|
+
import { computed, inject, ref, type Ref } from 'vue'
|
|
12
|
+
import { RouterLink, useRoute } from 'vue-router'
|
|
13
|
+
import { useNavContext, type BreadcrumbItem, type NavLinkItem } from '../../../composables/useNavContext'
|
|
14
|
+
import Breadcrumb from 'primevue/breadcrumb'
|
|
15
|
+
|
|
16
|
+
interface FeaturesConfig {
|
|
17
|
+
breadcrumb?: boolean
|
|
18
|
+
[key: string]: unknown
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const route = useRoute()
|
|
22
|
+
const features = inject<FeaturesConfig>('qdadmFeatures', { breadcrumb: true })
|
|
23
|
+
|
|
24
|
+
// Navigation context (breadcrumb + navlinks from route config)
|
|
25
|
+
const { breadcrumb: defaultBreadcrumb, navlinks: defaultNavlinks } = useNavContext()
|
|
26
|
+
|
|
27
|
+
// Allow child pages to override breadcrumb/navlinks via provide/inject
|
|
28
|
+
const breadcrumbOverride = inject<Ref<BreadcrumbItem[] | null>>('qdadmBreadcrumbOverride', ref(null))
|
|
29
|
+
const navlinksOverride = inject<Ref<NavLinkItem[] | null>>('qdadmNavlinksOverride', ref(null))
|
|
30
|
+
|
|
31
|
+
// Use override if provided, otherwise default from useNavContext
|
|
32
|
+
const breadcrumbItems = computed<BreadcrumbItem[]>(() => breadcrumbOverride.value || defaultBreadcrumb.value)
|
|
33
|
+
const navlinks = computed<NavLinkItem[]>(() => navlinksOverride.value || defaultNavlinks.value)
|
|
34
|
+
|
|
35
|
+
// Show breadcrumb if enabled, has items, and not on home page
|
|
36
|
+
const showBreadcrumb = computed<boolean>(() => {
|
|
37
|
+
if (!features.breadcrumb || breadcrumbItems.value.length === 0) return false
|
|
38
|
+
// Don't show on home page (just "Dashboard" with no parents)
|
|
39
|
+
if (route.name === 'dashboard') return false
|
|
40
|
+
return true
|
|
41
|
+
})
|
|
42
|
+
</script>
|
|
43
|
+
|
|
44
|
+
<template>
|
|
45
|
+
<div v-if="showBreadcrumb" class="default-breadcrumb">
|
|
46
|
+
<Breadcrumb :model="breadcrumbItems" class="breadcrumb-component">
|
|
47
|
+
<template #item="{ item }">
|
|
48
|
+
<RouterLink v-if="item.to" :to="item.to" class="breadcrumb-link">
|
|
49
|
+
<i v-if="item.icon" :class="item.icon"></i>
|
|
50
|
+
<span>{{ item.label }}</span>
|
|
51
|
+
</RouterLink>
|
|
52
|
+
<span v-else class="breadcrumb-current">
|
|
53
|
+
<i v-if="item.icon" :class="item.icon"></i>
|
|
54
|
+
<span>{{ item.label }}</span>
|
|
55
|
+
</span>
|
|
56
|
+
</template>
|
|
57
|
+
</Breadcrumb>
|
|
58
|
+
|
|
59
|
+
<!-- Navlinks (provided by PageNav for child routes) -->
|
|
60
|
+
<div v-if="navlinks.length > 0" class="breadcrumb-navlinks">
|
|
61
|
+
<template v-for="(link, index) in navlinks" :key="link.to?.name || index">
|
|
62
|
+
<span v-if="index > 0" class="navlinks-separator">|</span>
|
|
63
|
+
<RouterLink
|
|
64
|
+
:to="link.to"
|
|
65
|
+
class="navlink"
|
|
66
|
+
:class="{ 'navlink--active': link.active }"
|
|
67
|
+
>
|
|
68
|
+
{{ link.label }}
|
|
69
|
+
</RouterLink>
|
|
70
|
+
</template>
|
|
71
|
+
</div>
|
|
72
|
+
</div>
|
|
73
|
+
</template>
|
|
74
|
+
|
|
75
|
+
<style scoped>
|
|
76
|
+
/*
|
|
77
|
+
* Only keep styles here that REQUIRE scoping (:deep, dynamic binding, component-specific overrides).
|
|
78
|
+
* Generic/reusable styles belong in src/styles/ partials (see _forms.scss, _cards.scss, etc.).
|
|
79
|
+
*/
|
|
80
|
+
.default-breadcrumb {
|
|
81
|
+
display: flex;
|
|
82
|
+
justify-content: space-between;
|
|
83
|
+
align-items: center;
|
|
84
|
+
padding: 1.5rem;
|
|
85
|
+
padding-bottom: 0;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
.breadcrumb-navlinks {
|
|
89
|
+
display: flex;
|
|
90
|
+
align-items: center;
|
|
91
|
+
gap: 0.5rem;
|
|
92
|
+
font-size: 0.875rem;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
.navlinks-separator {
|
|
96
|
+
color: var(--p-surface-400, #94a3b8);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
.navlink {
|
|
100
|
+
color: var(--p-primary-500, #3b82f6);
|
|
101
|
+
text-decoration: none;
|
|
102
|
+
transition: color 0.15s;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
.navlink:hover {
|
|
106
|
+
color: var(--p-primary-700, #1d4ed8);
|
|
107
|
+
text-decoration: underline;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
.navlink--active {
|
|
111
|
+
color: var(--p-surface-700, #334155);
|
|
112
|
+
font-weight: 500;
|
|
113
|
+
pointer-events: none;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/* Override PrimeVue Breadcrumb styles for flat look */
|
|
117
|
+
.default-breadcrumb :deep(.p-breadcrumb) {
|
|
118
|
+
background: transparent;
|
|
119
|
+
border: none;
|
|
120
|
+
padding: 0;
|
|
121
|
+
border-radius: 0;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
.default-breadcrumb :deep(.p-breadcrumb-list) {
|
|
125
|
+
gap: 0.5rem;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
.default-breadcrumb :deep(.p-breadcrumb-item-link),
|
|
129
|
+
.breadcrumb-link {
|
|
130
|
+
color: var(--p-primary-500, #3b82f6);
|
|
131
|
+
text-decoration: none;
|
|
132
|
+
font-size: 0.875rem;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
.default-breadcrumb :deep(.p-breadcrumb-item-link:hover),
|
|
136
|
+
.breadcrumb-link:hover {
|
|
137
|
+
color: var(--p-primary-700, #1d4ed8);
|
|
138
|
+
text-decoration: underline;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
.breadcrumb-current {
|
|
142
|
+
color: var(--p-surface-600, #475569);
|
|
143
|
+
font-size: 0.875rem;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
.default-breadcrumb :deep(.p-breadcrumb-separator) {
|
|
147
|
+
color: var(--p-surface-400, #94a3b8);
|
|
148
|
+
}
|
|
149
|
+
</style>
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* DefaultFooter - Default footer component for BaseLayout
|
|
4
|
+
*
|
|
5
|
+
* Displays "powered by qdadm" branding with version.
|
|
6
|
+
* Extracted from AppLayout for zone-based architecture.
|
|
7
|
+
*
|
|
8
|
+
* This is the default component rendered in the "footer" zone
|
|
9
|
+
* when no blocks are registered.
|
|
10
|
+
*/
|
|
11
|
+
import { inject } from 'vue'
|
|
12
|
+
import Zone from '../Zone.vue'
|
|
13
|
+
import qdadmLogo from '../../../assets/logo.svg'
|
|
14
|
+
import { version as qdadmVersion } from '../../../../package.json'
|
|
15
|
+
|
|
16
|
+
interface Features {
|
|
17
|
+
poweredBy?: boolean
|
|
18
|
+
[key: string]: boolean | undefined
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const features = inject<Features>('qdadmFeatures', { poweredBy: true })
|
|
22
|
+
</script>
|
|
23
|
+
|
|
24
|
+
<template>
|
|
25
|
+
<div class="default-footer-wrapper">
|
|
26
|
+
<a
|
|
27
|
+
v-if="features.poweredBy"
|
|
28
|
+
href="https://github.com/quazardous/qdadm"
|
|
29
|
+
target="_blank"
|
|
30
|
+
rel="noopener noreferrer"
|
|
31
|
+
class="default-footer"
|
|
32
|
+
>
|
|
33
|
+
<div class="footer-logo-wrapper">
|
|
34
|
+
<img :src="qdadmLogo" alt="qdadm" class="footer-logo" />
|
|
35
|
+
<!-- Notification badge overlay on logo -->
|
|
36
|
+
<Zone name="_app:notification-badge" />
|
|
37
|
+
</div>
|
|
38
|
+
<span class="footer-text">
|
|
39
|
+
powered by <strong>qdadm</strong> v{{ qdadmVersion }}
|
|
40
|
+
</span>
|
|
41
|
+
</a>
|
|
42
|
+
</div>
|
|
43
|
+
</template>
|
|
44
|
+
|
|
45
|
+
<style scoped>
|
|
46
|
+
/*
|
|
47
|
+
* Only keep styles here that REQUIRE scoping (:deep, dynamic binding, component-specific overrides).
|
|
48
|
+
* Generic/reusable styles belong in src/styles/ partials (see _forms.scss, _cards.scss, etc.).
|
|
49
|
+
*/
|
|
50
|
+
.default-footer {
|
|
51
|
+
padding: 0.75rem 1rem;
|
|
52
|
+
border-top: 1px solid var(--p-surface-700, #334155);
|
|
53
|
+
display: flex;
|
|
54
|
+
align-items: center;
|
|
55
|
+
gap: 0.5rem;
|
|
56
|
+
opacity: 0.6;
|
|
57
|
+
transition: opacity 0.15s;
|
|
58
|
+
text-decoration: none;
|
|
59
|
+
cursor: pointer;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
.default-footer:hover {
|
|
63
|
+
opacity: 1;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
.default-footer-wrapper {
|
|
67
|
+
position: relative;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
.footer-logo-wrapper {
|
|
71
|
+
position: relative;
|
|
72
|
+
flex-shrink: 0;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
.footer-logo {
|
|
76
|
+
width: 1.25rem;
|
|
77
|
+
height: 1.25rem;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
.footer-text {
|
|
81
|
+
font-size: 0.625rem;
|
|
82
|
+
color: var(--p-surface-400, #94a3b8);
|
|
83
|
+
letter-spacing: 0.02em;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
.footer-text strong {
|
|
87
|
+
color: var(--p-surface-300, #cbd5e1);
|
|
88
|
+
}
|
|
89
|
+
</style>
|