@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,1032 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* AppLayout - Generic admin layout with sidebar navigation
|
|
4
|
+
*
|
|
5
|
+
* Navigation is auto-built from moduleRegistry.
|
|
6
|
+
* Branding comes from createQdadm({ app: {...} }) config.
|
|
7
|
+
* Auth is optional via authAdapter.
|
|
8
|
+
*
|
|
9
|
+
* Usage:
|
|
10
|
+
* <AppLayout>
|
|
11
|
+
* <RouterView />
|
|
12
|
+
* </AppLayout>
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { ref, watch, onMounted, onUnmounted, computed, inject, provide, useSlots } from 'vue'
|
|
16
|
+
import { SIDEBAR_COLLAPSED_KEY } from '../../composables/useSidebarState'
|
|
17
|
+
import { RouterLink, RouterView, useRouter, useRoute } from 'vue-router'
|
|
18
|
+
import { useNavigation, type NavSection } from '../../composables/useNavigation'
|
|
19
|
+
import { useApp } from '../../composables/useApp'
|
|
20
|
+
import { useAuth } from '../../composables/useAuth'
|
|
21
|
+
import { useGuardDialog } from '../../composables/useGuardStore'
|
|
22
|
+
import { useNavContext, type BreadcrumbItem, type NavLinkItem } from '../../composables/useNavContext'
|
|
23
|
+
import Button from 'primevue/button'
|
|
24
|
+
import Breadcrumb from 'primevue/breadcrumb'
|
|
25
|
+
import UnsavedChangesDialog from '../dialogs/UnsavedChangesDialog.vue'
|
|
26
|
+
import SidebarBox from './SidebarBox.vue'
|
|
27
|
+
import Zone from './Zone.vue'
|
|
28
|
+
import qdadmLogo from '../../assets/logo.svg'
|
|
29
|
+
import { version as qdadmVersion } from '../../../package.json'
|
|
30
|
+
|
|
31
|
+
interface FeaturesConfig {
|
|
32
|
+
poweredBy?: boolean
|
|
33
|
+
breadcrumb?: boolean
|
|
34
|
+
[key: string]: unknown
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
interface SignalBus {
|
|
38
|
+
emit: (event: string, payload: unknown) => void
|
|
39
|
+
[key: string]: unknown
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
interface UserData {
|
|
43
|
+
username?: string
|
|
44
|
+
name?: string
|
|
45
|
+
email?: string
|
|
46
|
+
role?: string
|
|
47
|
+
[key: string]: unknown
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const features = inject<FeaturesConfig>('qdadmFeatures', { poweredBy: true, breadcrumb: true })
|
|
51
|
+
const homeRoute = inject<string>('qdadmHomeRoute', 'home')
|
|
52
|
+
|
|
53
|
+
// Guard dialog from shared store (registered by useBareForm/useForm when a form is active)
|
|
54
|
+
const guardDialog = useGuardDialog()
|
|
55
|
+
|
|
56
|
+
const router = useRouter()
|
|
57
|
+
const route = useRoute()
|
|
58
|
+
const app = useApp()
|
|
59
|
+
const { navSections, isNavActive, sectionHasActiveItem, handleNavClick } = useNavigation()
|
|
60
|
+
const { user, logout, authEnabled } = useAuth()
|
|
61
|
+
const signals = inject<SignalBus | null>('qdadmSignals', null)
|
|
62
|
+
|
|
63
|
+
// LocalStorage key for collapsed sections state (namespaced by app)
|
|
64
|
+
const STORAGE_KEY = computed<string>(() => `${app.shortName.toLowerCase()}_nav_collapsed`)
|
|
65
|
+
|
|
66
|
+
// Collapsed sections state (section title -> boolean)
|
|
67
|
+
const collapsedSections = ref<Record<string, boolean>>({})
|
|
68
|
+
|
|
69
|
+
// Mobile sidebar state
|
|
70
|
+
const sidebarOpen = ref<boolean>(false)
|
|
71
|
+
const MOBILE_BREAKPOINT = 768
|
|
72
|
+
|
|
73
|
+
// Desktop sidebar collapsed state (provided for child components via useSidebarState)
|
|
74
|
+
const sidebarCollapsed = ref<boolean>(false)
|
|
75
|
+
provide(SIDEBAR_COLLAPSED_KEY, sidebarCollapsed)
|
|
76
|
+
const COLLAPSED_STORAGE_KEY = computed<string>(() => `${app.shortName.toLowerCase()}_sidebar_collapsed`)
|
|
77
|
+
|
|
78
|
+
function toggleSidebar(): void {
|
|
79
|
+
sidebarOpen.value = !sidebarOpen.value
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function closeSidebar(): void {
|
|
83
|
+
sidebarOpen.value = false
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function toggleSidebarCollapse(): void {
|
|
87
|
+
sidebarCollapsed.value = !sidebarCollapsed.value
|
|
88
|
+
saveSidebarCollapsed()
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function loadSidebarCollapsed(): void {
|
|
92
|
+
try {
|
|
93
|
+
const stored = localStorage.getItem(COLLAPSED_STORAGE_KEY.value)
|
|
94
|
+
if (stored === 'true') {
|
|
95
|
+
sidebarCollapsed.value = true
|
|
96
|
+
}
|
|
97
|
+
} catch {
|
|
98
|
+
// Ignore
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function saveSidebarCollapsed(): void {
|
|
103
|
+
try {
|
|
104
|
+
localStorage.setItem(COLLAPSED_STORAGE_KEY.value, String(sidebarCollapsed.value))
|
|
105
|
+
} catch {
|
|
106
|
+
// Ignore
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Check if we're on mobile
|
|
111
|
+
function isMobile(): boolean {
|
|
112
|
+
return window.innerWidth < MOBILE_BREAKPOINT
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Close sidebar on resize to desktop
|
|
116
|
+
function handleResize(): void {
|
|
117
|
+
if (!isMobile() && sidebarOpen.value) {
|
|
118
|
+
sidebarOpen.value = false
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Load collapsed state from localStorage
|
|
124
|
+
*/
|
|
125
|
+
function loadCollapsedState(): void {
|
|
126
|
+
try {
|
|
127
|
+
const stored = localStorage.getItem(STORAGE_KEY.value)
|
|
128
|
+
if (stored) {
|
|
129
|
+
collapsedSections.value = JSON.parse(stored) as Record<string, boolean>
|
|
130
|
+
}
|
|
131
|
+
} catch (e) {
|
|
132
|
+
console.warn('Failed to load nav state:', e)
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Save collapsed state to localStorage
|
|
138
|
+
*/
|
|
139
|
+
function saveCollapsedState(): void {
|
|
140
|
+
try {
|
|
141
|
+
localStorage.setItem(STORAGE_KEY.value, JSON.stringify(collapsedSections.value))
|
|
142
|
+
} catch (e) {
|
|
143
|
+
console.warn('Failed to save nav state:', e)
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Toggle section collapsed state
|
|
149
|
+
*/
|
|
150
|
+
function toggleSection(sectionTitle: string): void {
|
|
151
|
+
collapsedSections.value[sectionTitle] = !collapsedSections.value[sectionTitle]
|
|
152
|
+
saveCollapsedState()
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Check if section should be shown expanded
|
|
157
|
+
* - Never collapsed if it contains active item
|
|
158
|
+
* - Otherwise respect user preference
|
|
159
|
+
*/
|
|
160
|
+
function isSectionExpanded(section: NavSection): boolean {
|
|
161
|
+
if (sectionHasActiveItem(section)) {
|
|
162
|
+
return true
|
|
163
|
+
}
|
|
164
|
+
const title = section.title || section.label || ''
|
|
165
|
+
return !collapsedSections.value[title]
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Load state on mount + setup resize listener
|
|
169
|
+
onMounted(() => {
|
|
170
|
+
loadCollapsedState()
|
|
171
|
+
loadSidebarCollapsed()
|
|
172
|
+
window.addEventListener('resize', handleResize)
|
|
173
|
+
})
|
|
174
|
+
|
|
175
|
+
onUnmounted(() => {
|
|
176
|
+
window.removeEventListener('resize', handleResize)
|
|
177
|
+
})
|
|
178
|
+
|
|
179
|
+
// Auto-expand section when navigating to an item in it + close mobile sidebar
|
|
180
|
+
watch(() => route.path, () => {
|
|
181
|
+
// Close mobile sidebar on navigation
|
|
182
|
+
closeSidebar()
|
|
183
|
+
|
|
184
|
+
for (const section of navSections.value) {
|
|
185
|
+
const title = section.title || section.label || ''
|
|
186
|
+
if (sectionHasActiveItem(section) && collapsedSections.value[title]) {
|
|
187
|
+
// Auto-expand but don't save (user can collapse again if they want)
|
|
188
|
+
collapsedSections.value[title] = false
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
})
|
|
192
|
+
|
|
193
|
+
const userInitials = computed<string>(() => {
|
|
194
|
+
const userData = user.value as UserData | null
|
|
195
|
+
const username = userData?.username
|
|
196
|
+
if (!username) return '?'
|
|
197
|
+
return username.substring(0, 2).toUpperCase()
|
|
198
|
+
})
|
|
199
|
+
|
|
200
|
+
const userDisplayName = computed<string>(() => {
|
|
201
|
+
const userData = user.value as UserData | null
|
|
202
|
+
return userData?.username || userData?.name || 'User'
|
|
203
|
+
})
|
|
204
|
+
|
|
205
|
+
const userSubtitle = computed<string>(() => {
|
|
206
|
+
const userData = user.value as UserData | null
|
|
207
|
+
return userData?.email || userData?.role || ''
|
|
208
|
+
})
|
|
209
|
+
|
|
210
|
+
async function handleLogout(): Promise<void> {
|
|
211
|
+
logout()
|
|
212
|
+
await router.push({ name: 'login' })
|
|
213
|
+
signals?.emit('auth:logout', { reason: 'user' })
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Handle sidebar header click
|
|
218
|
+
* Mobile: close sidebar drawer
|
|
219
|
+
* Desktop: navigate to home
|
|
220
|
+
*/
|
|
221
|
+
function handleHeaderClick(): void {
|
|
222
|
+
if (isMobile()) {
|
|
223
|
+
closeSidebar()
|
|
224
|
+
} else {
|
|
225
|
+
router.push({ name: homeRoute })
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// Check if slot content is provided
|
|
230
|
+
const slots = useSlots()
|
|
231
|
+
const hasSlotContent = computed<boolean>(() => !!slots.default)
|
|
232
|
+
|
|
233
|
+
// Clear overrides on route change (before new page mounts)
|
|
234
|
+
// This ensures list pages get default breadcrumb, detail pages can override via PageNav
|
|
235
|
+
watch(() => route.fullPath, () => {
|
|
236
|
+
breadcrumbOverride.value = null
|
|
237
|
+
navlinksOverride.value = null
|
|
238
|
+
})
|
|
239
|
+
|
|
240
|
+
// Navigation context (breadcrumb + navlinks from route config)
|
|
241
|
+
// Entity data comes from activeStack (populated by useEntityItemPage/useForm)
|
|
242
|
+
const { breadcrumb: defaultBreadcrumb, navlinks: defaultNavlinks } = useNavContext()
|
|
243
|
+
|
|
244
|
+
// Allow child pages to override breadcrumb/navlinks via provide/inject
|
|
245
|
+
const breadcrumbOverride = ref<BreadcrumbItem[] | null>(null)
|
|
246
|
+
const navlinksOverride = ref<NavLinkItem[] | null>(null)
|
|
247
|
+
provide('qdadmBreadcrumbOverride', breadcrumbOverride)
|
|
248
|
+
provide('qdadmNavlinksOverride', navlinksOverride)
|
|
249
|
+
|
|
250
|
+
// Use override if provided, otherwise default from useNavContext
|
|
251
|
+
const breadcrumbItems = computed<BreadcrumbItem[]>(() => breadcrumbOverride.value || defaultBreadcrumb.value)
|
|
252
|
+
const navlinks = computed<NavLinkItem[]>(() => navlinksOverride.value || defaultNavlinks.value)
|
|
253
|
+
|
|
254
|
+
// Show breadcrumb if enabled, has items, and not on home page
|
|
255
|
+
const showBreadcrumb = computed<boolean>(() => {
|
|
256
|
+
if (!features.breadcrumb || breadcrumbItems.value.length === 0) return false
|
|
257
|
+
// Don't show on home page (just "Dashboard" with no parents)
|
|
258
|
+
if (route.name === 'dashboard') return false
|
|
259
|
+
return true
|
|
260
|
+
})
|
|
261
|
+
</script>
|
|
262
|
+
|
|
263
|
+
<template>
|
|
264
|
+
<div class="app-layout">
|
|
265
|
+
<!-- Mobile overlay -->
|
|
266
|
+
<div
|
|
267
|
+
class="sidebar-overlay"
|
|
268
|
+
:class="{ 'sidebar-overlay--visible': sidebarOpen }"
|
|
269
|
+
@click="closeSidebar"
|
|
270
|
+
></div>
|
|
271
|
+
|
|
272
|
+
<!-- Sidebar -->
|
|
273
|
+
<aside class="sidebar" :class="{ 'sidebar--open': sidebarOpen, 'sidebar--collapsed': sidebarCollapsed }">
|
|
274
|
+
<!-- Collapse toggle button (desktop only) - fixed position in sidebar -->
|
|
275
|
+
<button
|
|
276
|
+
class="sidebar-collapse-btn"
|
|
277
|
+
@click="toggleSidebarCollapse"
|
|
278
|
+
:title="sidebarCollapsed ? 'Expand menu' : 'Collapse menu'"
|
|
279
|
+
>
|
|
280
|
+
<i class="pi" :class="sidebarCollapsed ? 'pi-chevron-right' : 'pi-chevron-left'"></i>
|
|
281
|
+
</button>
|
|
282
|
+
<div class="sidebar-header" :data-short-name="app.shortName" @click="handleHeaderClick">
|
|
283
|
+
<div class="sidebar-header-top">
|
|
284
|
+
<img v-if="app.logo" :src="app.logo" :alt="app.name" class="sidebar-logo" />
|
|
285
|
+
<h1 v-else>{{ app.name }}</h1>
|
|
286
|
+
</div>
|
|
287
|
+
<span v-if="app.version" class="version">v{{ app.version }}</span>
|
|
288
|
+
</div>
|
|
289
|
+
|
|
290
|
+
<nav class="sidebar-nav">
|
|
291
|
+
<div v-for="section in navSections" :key="section.title || section.label" class="nav-section">
|
|
292
|
+
<div
|
|
293
|
+
class="nav-section-title"
|
|
294
|
+
:class="{ 'nav-section-active': sectionHasActiveItem(section) }"
|
|
295
|
+
@click="toggleSection(section.title || section.label || '')"
|
|
296
|
+
>
|
|
297
|
+
<span>{{ section.title }}</span>
|
|
298
|
+
<i
|
|
299
|
+
class="nav-section-chevron pi"
|
|
300
|
+
:class="isSectionExpanded(section) ? 'pi-chevron-down' : 'pi-chevron-right'"
|
|
301
|
+
></i>
|
|
302
|
+
</div>
|
|
303
|
+
<div class="nav-section-items" :class="{ 'nav-section-collapsed': !isSectionExpanded(section) }">
|
|
304
|
+
<RouterLink
|
|
305
|
+
v-for="item in section.items"
|
|
306
|
+
:key="item.route"
|
|
307
|
+
:to="{ name: item.route }"
|
|
308
|
+
class="nav-item"
|
|
309
|
+
:class="{ 'nav-item-active': isNavActive(item) }"
|
|
310
|
+
@click="handleNavClick($event, item)"
|
|
311
|
+
>
|
|
312
|
+
<i :class="item.icon"></i>
|
|
313
|
+
<span>{{ item.label }}</span>
|
|
314
|
+
</RouterLink>
|
|
315
|
+
</div>
|
|
316
|
+
</div>
|
|
317
|
+
</nav>
|
|
318
|
+
|
|
319
|
+
<!-- Slot for custom content after navigation (e.g., impersonation controls) -->
|
|
320
|
+
<div class="sidebar-slot">
|
|
321
|
+
<slot name="sidebar-after-nav"></slot>
|
|
322
|
+
</div>
|
|
323
|
+
|
|
324
|
+
<SidebarBox
|
|
325
|
+
v-if="authEnabled"
|
|
326
|
+
id="user-zone"
|
|
327
|
+
icon="pi-sign-out"
|
|
328
|
+
:title="userDisplayName"
|
|
329
|
+
:subtitle="userSubtitle"
|
|
330
|
+
@click="handleLogout"
|
|
331
|
+
v-tooltip.top="'Logout'"
|
|
332
|
+
>
|
|
333
|
+
<div class="user-avatar">{{ userInitials }}</div>
|
|
334
|
+
</SidebarBox>
|
|
335
|
+
|
|
336
|
+
<SidebarBox v-if="features.poweredBy" id="powered-by">
|
|
337
|
+
<template #icon>
|
|
338
|
+
<div class="footer-logo-wrapper">
|
|
339
|
+
<img :src="qdadmLogo" alt="qdadm" />
|
|
340
|
+
<!-- Notification badge overlay on logo -->
|
|
341
|
+
<Zone name="_app:notification-badge" />
|
|
342
|
+
</div>
|
|
343
|
+
</template>
|
|
344
|
+
<template #subtitle-content>
|
|
345
|
+
<span class="sidebar-box-subtitle">
|
|
346
|
+
powered by <a href="https://github.com/quazardous/qdadm" target="_blank" rel="noopener" class="powered-by-link">qdadm</a> v{{ qdadmVersion }}
|
|
347
|
+
</span>
|
|
348
|
+
</template>
|
|
349
|
+
</SidebarBox>
|
|
350
|
+
|
|
351
|
+
<!-- Always-visible notification status (hidden when collapsed) -->
|
|
352
|
+
<Zone name="_app:notification-status" class="sidebar-notification-status" />
|
|
353
|
+
</aside>
|
|
354
|
+
|
|
355
|
+
<!-- Main content -->
|
|
356
|
+
<main class="main-content" :class="{ 'main-content--sidebar-collapsed': sidebarCollapsed }">
|
|
357
|
+
<!-- Mobile header bar -->
|
|
358
|
+
<div class="mobile-header">
|
|
359
|
+
<Button
|
|
360
|
+
icon="pi pi-bars"
|
|
361
|
+
severity="secondary"
|
|
362
|
+
text
|
|
363
|
+
class="hamburger-btn"
|
|
364
|
+
@click="toggleSidebar"
|
|
365
|
+
aria-label="Toggle menu"
|
|
366
|
+
/>
|
|
367
|
+
<span class="mobile-header-title">{{ app.name }}</span>
|
|
368
|
+
</div>
|
|
369
|
+
|
|
370
|
+
<!-- Breadcrumb + Navlinks bar -->
|
|
371
|
+
<div v-if="showBreadcrumb" class="layout-nav-bar">
|
|
372
|
+
<Breadcrumb :model="breadcrumbItems" class="layout-breadcrumb">
|
|
373
|
+
<template #item="{ item }">
|
|
374
|
+
<RouterLink v-if="item.to" :to="item.to" class="breadcrumb-link">
|
|
375
|
+
<i v-if="item.icon" :class="item.icon"></i>
|
|
376
|
+
<span>{{ item.label }}</span>
|
|
377
|
+
</RouterLink>
|
|
378
|
+
<span v-else class="breadcrumb-current">
|
|
379
|
+
<i v-if="item.icon" :class="item.icon"></i>
|
|
380
|
+
<span>{{ item.label }}</span>
|
|
381
|
+
</span>
|
|
382
|
+
</template>
|
|
383
|
+
</Breadcrumb>
|
|
384
|
+
|
|
385
|
+
<!-- Navlinks (provided by PageNav for child routes) -->
|
|
386
|
+
<div v-if="navlinks.length > 0" class="layout-navlinks">
|
|
387
|
+
<template v-for="(link, index) in navlinks" :key="link.to?.name || index">
|
|
388
|
+
<span v-if="index > 0" class="layout-navlinks-separator">|</span>
|
|
389
|
+
<RouterLink
|
|
390
|
+
:to="link.to"
|
|
391
|
+
class="layout-navlink"
|
|
392
|
+
:class="{ 'layout-navlink--active': link.active }"
|
|
393
|
+
>
|
|
394
|
+
{{ link.label }}
|
|
395
|
+
</RouterLink>
|
|
396
|
+
</template>
|
|
397
|
+
</div>
|
|
398
|
+
</div>
|
|
399
|
+
|
|
400
|
+
<div class="page-content">
|
|
401
|
+
<!-- Use slot if provided, otherwise RouterView for nested routes -->
|
|
402
|
+
<slot v-if="hasSlotContent" />
|
|
403
|
+
<RouterView v-else />
|
|
404
|
+
</div>
|
|
405
|
+
</main>
|
|
406
|
+
|
|
407
|
+
<!-- Notification panel (rendered outside sidebar, follows sidebar position) -->
|
|
408
|
+
<Zone name="_app:notifications" />
|
|
409
|
+
|
|
410
|
+
<!-- Unsaved Changes Dialog (auto-rendered when a form registers guardDialog) -->
|
|
411
|
+
<UnsavedChangesDialog
|
|
412
|
+
v-if="guardDialog"
|
|
413
|
+
:visible="guardDialog.visible.value"
|
|
414
|
+
:saving="guardDialog.saving?.value ?? false"
|
|
415
|
+
:message="guardDialog.message"
|
|
416
|
+
:hasOnSave="guardDialog.hasOnSave"
|
|
417
|
+
@saveAndLeave="guardDialog.onSaveAndLeave"
|
|
418
|
+
@leave="guardDialog.onLeave"
|
|
419
|
+
@stay="guardDialog.onStay"
|
|
420
|
+
/>
|
|
421
|
+
<!-- Note: guardDialog is a shallowRef, Vue auto-unwraps it in templates -->
|
|
422
|
+
</div>
|
|
423
|
+
</template>
|
|
424
|
+
|
|
425
|
+
<style scoped>
|
|
426
|
+
/*
|
|
427
|
+
* Only keep styles here that REQUIRE scoping (:deep, dynamic binding, component-specific overrides).
|
|
428
|
+
* Generic/reusable styles belong in src/styles/ partials (see _forms.scss, _cards.scss, etc.).
|
|
429
|
+
*/
|
|
430
|
+
/* =============================================================================
|
|
431
|
+
MODERN ADMIN LAYOUT - VSCode/Material-inspired styling
|
|
432
|
+
|
|
433
|
+
Design principles:
|
|
434
|
+
- Compact spacing, no wasted space
|
|
435
|
+
- Subtle hover states (slight bg change, no bold colors)
|
|
436
|
+
- Light font weights
|
|
437
|
+
- Fast, snappy transitions
|
|
438
|
+
- Minimal borders, rely on bg contrast
|
|
439
|
+
============================================================================= */
|
|
440
|
+
|
|
441
|
+
.app-layout {
|
|
442
|
+
display: flex;
|
|
443
|
+
min-height: 100vh;
|
|
444
|
+
font-family: var(--fad-font-family);
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
/* -----------------------------------------------------------------------------
|
|
448
|
+
Sidebar
|
|
449
|
+
----------------------------------------------------------------------------- */
|
|
450
|
+
|
|
451
|
+
.sidebar {
|
|
452
|
+
width: var(--fad-sidebar-width);
|
|
453
|
+
background: var(--p-surface-800, #1e293b);
|
|
454
|
+
color: var(--p-surface-100, #f1f5f9);
|
|
455
|
+
display: flex;
|
|
456
|
+
flex-direction: column;
|
|
457
|
+
position: fixed;
|
|
458
|
+
top: 0;
|
|
459
|
+
left: 0;
|
|
460
|
+
bottom: 0;
|
|
461
|
+
z-index: 100;
|
|
462
|
+
transition: width var(--fad-transition-slow);
|
|
463
|
+
font-size: var(--fad-font-size-sm);
|
|
464
|
+
overflow: hidden;
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
/* Header - compact branding, clickable to home */
|
|
468
|
+
.sidebar-header {
|
|
469
|
+
position: relative;
|
|
470
|
+
padding: 0.75rem 1rem;
|
|
471
|
+
padding-top: 0.5rem;
|
|
472
|
+
border-bottom: 1px solid var(--p-surface-700, #334155);
|
|
473
|
+
display: flex;
|
|
474
|
+
flex-direction: column;
|
|
475
|
+
align-items: flex-start;
|
|
476
|
+
gap: 0.25rem;
|
|
477
|
+
text-decoration: none;
|
|
478
|
+
color: inherit;
|
|
479
|
+
cursor: pointer;
|
|
480
|
+
height: 3.75rem;
|
|
481
|
+
box-sizing: border-box;
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
.sidebar-header:hover {
|
|
485
|
+
background: var(--p-surface-700, #334155);
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
.sidebar-header::after {
|
|
489
|
+
content: '';
|
|
490
|
+
opacity: 0;
|
|
491
|
+
transition: opacity var(--fad-transition-fast);
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
.sidebar-header-top {
|
|
495
|
+
display: flex;
|
|
496
|
+
align-items: center;
|
|
497
|
+
gap: 0.5rem;
|
|
498
|
+
width: 100%;
|
|
499
|
+
padding-left: 1.5rem;
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
.sidebar-header h1 {
|
|
503
|
+
font-size: var(--fad-font-size-md);
|
|
504
|
+
font-weight: var(--fad-font-weight-medium);
|
|
505
|
+
margin: 0;
|
|
506
|
+
white-space: nowrap;
|
|
507
|
+
overflow: hidden;
|
|
508
|
+
transition: opacity var(--fad-transition-normal);
|
|
509
|
+
color: var(--p-surface-0, white);
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
.sidebar-logo {
|
|
513
|
+
max-height: 24px;
|
|
514
|
+
max-width: 120px;
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
.version {
|
|
518
|
+
font-size: var(--fad-font-size-2xs);
|
|
519
|
+
color: var(--p-surface-400, #94a3b8);
|
|
520
|
+
background: var(--p-surface-700, #334155);
|
|
521
|
+
padding: 0.125rem 0.375rem;
|
|
522
|
+
border-radius: 0.25rem;
|
|
523
|
+
margin-left: 1.5rem;
|
|
524
|
+
font-weight: var(--fad-font-weight-normal);
|
|
525
|
+
white-space: nowrap;
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
/* Navigation - compact, subtle */
|
|
529
|
+
.sidebar-nav {
|
|
530
|
+
flex: 1;
|
|
531
|
+
overflow-y: auto;
|
|
532
|
+
padding: 0.5rem 0;
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
.nav-section {
|
|
536
|
+
margin-bottom: var(--fad-nav-section-gap);
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
.nav-section-title {
|
|
540
|
+
padding: var(--fad-nav-item-padding-y) var(--fad-nav-item-padding-x);
|
|
541
|
+
padding-left: 1rem;
|
|
542
|
+
font-size: var(--fad-font-size-xs);
|
|
543
|
+
font-weight: var(--fad-font-weight-medium);
|
|
544
|
+
text-transform: uppercase;
|
|
545
|
+
letter-spacing: 0.06em;
|
|
546
|
+
color: var(--p-surface-500, #64748b);
|
|
547
|
+
display: flex;
|
|
548
|
+
height: 1.5rem;
|
|
549
|
+
transition: height var(--fad-transition-slow), padding var(--fad-transition-slow), opacity var(--fad-transition-fast);
|
|
550
|
+
overflow: hidden;
|
|
551
|
+
white-space: nowrap;
|
|
552
|
+
justify-content: space-between;
|
|
553
|
+
align-items: center;
|
|
554
|
+
cursor: pointer;
|
|
555
|
+
user-select: none;
|
|
556
|
+
transition: color var(--fad-transition-fast);
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
.nav-section-title:hover {
|
|
560
|
+
color: var(--p-surface-300, #cbd5e1);
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
.nav-section-active {
|
|
564
|
+
color: var(--p-primary-300, #93c5fd);
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
.nav-section-chevron {
|
|
568
|
+
font-size: 0.5rem;
|
|
569
|
+
transition: transform var(--fad-transition-fast);
|
|
570
|
+
opacity: 0.6;
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
.nav-section-items {
|
|
574
|
+
max-height: 500px;
|
|
575
|
+
overflow: hidden;
|
|
576
|
+
transition: max-height var(--fad-transition-normal), opacity var(--fad-transition-fast);
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
.nav-section-collapsed {
|
|
580
|
+
max-height: 0;
|
|
581
|
+
opacity: 0;
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
/* Sidebar slot */
|
|
585
|
+
.sidebar-slot {
|
|
586
|
+
position: relative;
|
|
587
|
+
z-index: 10;
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
/* Nav items are styled in _sidebar.scss (global styles)
|
|
591
|
+
See .sidebar-nav .nav-item for base styles */
|
|
592
|
+
|
|
593
|
+
/* -----------------------------------------------------------------------------
|
|
594
|
+
Main Content
|
|
595
|
+
----------------------------------------------------------------------------- */
|
|
596
|
+
|
|
597
|
+
.main-content {
|
|
598
|
+
flex: 1;
|
|
599
|
+
margin-left: var(--fad-sidebar-width);
|
|
600
|
+
background: var(--p-surface-50, #f8fafc);
|
|
601
|
+
min-height: 100vh;
|
|
602
|
+
display: flex;
|
|
603
|
+
flex-direction: column;
|
|
604
|
+
transition: margin-left var(--fad-transition-slow);
|
|
605
|
+
overflow-y: auto;
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
.page-content {
|
|
609
|
+
flex: 1;
|
|
610
|
+
padding: 1rem;
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
/* Dark mode support */
|
|
614
|
+
.dark-mode .sidebar {
|
|
615
|
+
background: var(--p-surface-900);
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
.dark-mode .main-content {
|
|
619
|
+
background: var(--p-surface-900);
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
/* User zone - overrides for sidebar-box */
|
|
623
|
+
#user-zone .user-avatar {
|
|
624
|
+
position: absolute;
|
|
625
|
+
right: 0.75rem;
|
|
626
|
+
top: 50%;
|
|
627
|
+
transform: translateY(-50%);
|
|
628
|
+
width: 28px;
|
|
629
|
+
height: 28px;
|
|
630
|
+
border-radius: 50%;
|
|
631
|
+
background: var(--p-surface-600, #475569);
|
|
632
|
+
display: flex;
|
|
633
|
+
align-items: center;
|
|
634
|
+
justify-content: center;
|
|
635
|
+
font-size: var(--fad-font-size-xs);
|
|
636
|
+
font-weight: var(--fad-font-weight-medium);
|
|
637
|
+
/* Fade in with delay when sidebar opens */
|
|
638
|
+
opacity: 1;
|
|
639
|
+
transition: opacity 0.1s ease 0.2s;
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
/* Avatar: disappear IMMEDIATELY when closing */
|
|
643
|
+
.sidebar--collapsed #user-zone .user-avatar {
|
|
644
|
+
opacity: 0;
|
|
645
|
+
visibility: hidden;
|
|
646
|
+
transition: opacity 0s, visibility 0s;
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
/* Powered by - overrides for sidebar-box */
|
|
650
|
+
#powered-by {
|
|
651
|
+
height: 2rem;
|
|
652
|
+
opacity: 0.5;
|
|
653
|
+
transition: opacity var(--fad-transition-fast), background var(--fad-transition-fast);
|
|
654
|
+
cursor: default;
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
#powered-by:hover {
|
|
658
|
+
opacity: 0.8;
|
|
659
|
+
background: transparent;
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
#powered-by .sidebar-box-icon {
|
|
663
|
+
width: 1rem;
|
|
664
|
+
height: 1rem;
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
#powered-by .sidebar-box-icon img {
|
|
668
|
+
width: 100%;
|
|
669
|
+
height: 100%;
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
.footer-logo-wrapper {
|
|
673
|
+
position: relative;
|
|
674
|
+
display: inline-flex;
|
|
675
|
+
width: 100%;
|
|
676
|
+
height: 100%;
|
|
677
|
+
align-items: center;
|
|
678
|
+
justify-content: center;
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
.footer-logo-wrapper > :deep(.qdadm-zone) {
|
|
682
|
+
display: contents;
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
.powered-by-link {
|
|
686
|
+
color: var(--p-surface-300, #cbd5e1);
|
|
687
|
+
text-decoration: none;
|
|
688
|
+
font-weight: var(--fad-font-weight-medium);
|
|
689
|
+
transition: color var(--fad-transition-fast);
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
.powered-by-link:hover {
|
|
693
|
+
color: var(--p-primary-300, #93c5fd);
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
/* Nav bar (breadcrumb + navlinks) - compact */
|
|
697
|
+
.layout-nav-bar {
|
|
698
|
+
display: flex;
|
|
699
|
+
justify-content: space-between;
|
|
700
|
+
align-items: center;
|
|
701
|
+
padding: 0.75rem 1rem;
|
|
702
|
+
padding-bottom: 0;
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
.layout-navlinks {
|
|
706
|
+
display: flex;
|
|
707
|
+
align-items: center;
|
|
708
|
+
gap: 0.375rem;
|
|
709
|
+
font-size: var(--fad-font-size-sm);
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
.layout-navlinks-separator {
|
|
713
|
+
color: var(--p-surface-400, #94a3b8);
|
|
714
|
+
opacity: 0.5;
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
.layout-navlink {
|
|
718
|
+
color: var(--p-primary-500, #3b82f6);
|
|
719
|
+
text-decoration: none;
|
|
720
|
+
transition: color var(--fad-transition-fast);
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
.layout-navlink:hover {
|
|
724
|
+
color: var(--p-primary-600, #2563eb);
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
.layout-navlink--active {
|
|
728
|
+
color: var(--p-surface-600, #475569);
|
|
729
|
+
font-weight: var(--fad-font-weight-medium);
|
|
730
|
+
pointer-events: none;
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
/* Override PrimeVue Breadcrumb styles for flat look */
|
|
734
|
+
.layout-nav-bar :deep(.p-breadcrumb) {
|
|
735
|
+
background: transparent;
|
|
736
|
+
border: none;
|
|
737
|
+
padding: 0;
|
|
738
|
+
border-radius: 0;
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
.layout-nav-bar :deep(.p-breadcrumb-list) {
|
|
742
|
+
gap: 0.375rem;
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
.layout-nav-bar :deep(.p-breadcrumb-item-link),
|
|
746
|
+
.layout-nav-bar .breadcrumb-link {
|
|
747
|
+
color: var(--p-primary-500, #3b82f6);
|
|
748
|
+
text-decoration: none;
|
|
749
|
+
font-size: var(--fad-font-size-sm);
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
.layout-nav-bar :deep(.p-breadcrumb-item-link:hover),
|
|
753
|
+
.layout-nav-bar .breadcrumb-link:hover {
|
|
754
|
+
color: var(--p-primary-600, #2563eb);
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
.layout-nav-bar .breadcrumb-current {
|
|
758
|
+
color: var(--p-surface-600, #475569);
|
|
759
|
+
font-size: var(--fad-font-size-sm);
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
.layout-nav-bar :deep(.p-breadcrumb-separator) {
|
|
763
|
+
color: var(--p-surface-400, #94a3b8);
|
|
764
|
+
opacity: 0.5;
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
/* -----------------------------------------------------------------------------
|
|
768
|
+
Sidebar Collapse Button (Desktop) - minimal toggle
|
|
769
|
+
----------------------------------------------------------------------------- */
|
|
770
|
+
|
|
771
|
+
.sidebar-collapse-btn {
|
|
772
|
+
position: absolute;
|
|
773
|
+
left: 0.5rem;
|
|
774
|
+
top: 0.5rem;
|
|
775
|
+
padding: 0.25rem;
|
|
776
|
+
background: transparent;
|
|
777
|
+
border: none;
|
|
778
|
+
border-radius: var(--fad-radius-sm);
|
|
779
|
+
color: var(--p-surface-500, #64748b);
|
|
780
|
+
cursor: pointer;
|
|
781
|
+
display: flex;
|
|
782
|
+
align-items: center;
|
|
783
|
+
justify-content: center;
|
|
784
|
+
transition: background var(--fad-transition-fast), color var(--fad-transition-fast);
|
|
785
|
+
z-index: 10;
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
.sidebar-collapse-btn:hover {
|
|
789
|
+
background: var(--p-surface-700, #334155);
|
|
790
|
+
color: var(--p-surface-200, #e2e8f0);
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
.sidebar-collapse-btn i {
|
|
794
|
+
font-size: 0.75rem;
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
/* -----------------------------------------------------------------------------
|
|
798
|
+
Sidebar Collapsed State (Desktop) - Icon-only mini sidebar
|
|
799
|
+
----------------------------------------------------------------------------- */
|
|
800
|
+
|
|
801
|
+
.sidebar--collapsed {
|
|
802
|
+
width: var(--fad-sidebar-width-collapsed) !important;
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
.sidebar--collapsed .sidebar-header {
|
|
806
|
+
padding: 0.25rem;
|
|
807
|
+
padding-top: 1.75rem;
|
|
808
|
+
align-items: center;
|
|
809
|
+
justify-content: flex-start;
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
.sidebar--collapsed .sidebar-header h1,
|
|
813
|
+
.sidebar--collapsed .sidebar-header .version,
|
|
814
|
+
.sidebar--collapsed .sidebar-logo {
|
|
815
|
+
display: none;
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
/* Show a small icon/initial in collapsed header - positioned absolutely to stay in place during animation */
|
|
819
|
+
.sidebar-header::after {
|
|
820
|
+
content: attr(data-short-name);
|
|
821
|
+
position: absolute;
|
|
822
|
+
left: calc(var(--fad-sidebar-width-collapsed) / 2);
|
|
823
|
+
top: 2rem;
|
|
824
|
+
transform: translateX(-50%);
|
|
825
|
+
font-size: var(--fad-font-size-xs);
|
|
826
|
+
font-weight: var(--fad-font-weight-medium);
|
|
827
|
+
color: var(--p-surface-400);
|
|
828
|
+
opacity: 0;
|
|
829
|
+
transition: opacity var(--fad-transition-fast);
|
|
830
|
+
pointer-events: none;
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
.sidebar--collapsed .sidebar-header::after {
|
|
834
|
+
opacity: 1;
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
.sidebar--collapsed .sidebar-nav {
|
|
838
|
+
padding: 0.25rem 0;
|
|
839
|
+
overflow-y: hidden;
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
.sidebar--collapsed .nav-section-title {
|
|
843
|
+
height: 0;
|
|
844
|
+
padding-top: 0;
|
|
845
|
+
padding-bottom: 0;
|
|
846
|
+
opacity: 0;
|
|
847
|
+
pointer-events: none;
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
/* In collapsed mode, slot stays visible but content is hidden via child styles */
|
|
851
|
+
|
|
852
|
+
.sidebar--collapsed .nav-section-items {
|
|
853
|
+
max-height: none !important;
|
|
854
|
+
opacity: 1 !important;
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
/* Nav item collapsed styles are in _sidebar.scss (global styles)
|
|
858
|
+
See .sidebar--collapsed .sidebar-nav .nav-item */
|
|
859
|
+
|
|
860
|
+
/* Powered-by collapsed: smaller height */
|
|
861
|
+
.sidebar--collapsed #powered-by {
|
|
862
|
+
height: 2rem;
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
.main-content--sidebar-collapsed {
|
|
866
|
+
margin-left: var(--fad-sidebar-width-collapsed) !important;
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
/* -----------------------------------------------------------------------------
|
|
870
|
+
Mobile / Responsive Styles
|
|
871
|
+
----------------------------------------------------------------------------- */
|
|
872
|
+
|
|
873
|
+
.mobile-header {
|
|
874
|
+
display: none;
|
|
875
|
+
align-items: center;
|
|
876
|
+
gap: 0.5rem;
|
|
877
|
+
padding: 0.5rem 0.75rem;
|
|
878
|
+
background: var(--p-surface-0, white);
|
|
879
|
+
border-bottom: 1px solid var(--p-surface-200, #e2e8f0);
|
|
880
|
+
position: sticky;
|
|
881
|
+
top: 0;
|
|
882
|
+
z-index: 50;
|
|
883
|
+
height: var(--fad-header-height);
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
.mobile-header-title {
|
|
887
|
+
font-weight: var(--fad-font-weight-medium);
|
|
888
|
+
font-size: var(--fad-font-size-md);
|
|
889
|
+
color: var(--p-surface-800, #1e293b);
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
.hamburger-btn {
|
|
893
|
+
display: none !important;
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
/* Sidebar overlay for mobile */
|
|
897
|
+
.sidebar-overlay {
|
|
898
|
+
display: none;
|
|
899
|
+
position: fixed;
|
|
900
|
+
inset: 0;
|
|
901
|
+
background: rgba(0, 0, 0, 0.4);
|
|
902
|
+
z-index: 99;
|
|
903
|
+
opacity: 0;
|
|
904
|
+
transition: opacity var(--fad-transition-normal);
|
|
905
|
+
pointer-events: none;
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
/* Tablet breakpoint (< 768px) */
|
|
909
|
+
@media (max-width: 767px) {
|
|
910
|
+
.mobile-header {
|
|
911
|
+
display: flex;
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
.hamburger-btn {
|
|
915
|
+
display: flex !important;
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
/* Hide collapse button on mobile */
|
|
919
|
+
.sidebar-collapse-btn {
|
|
920
|
+
display: none !important;
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
.sidebar {
|
|
924
|
+
transform: translateX(-100%);
|
|
925
|
+
transition: transform var(--fad-transition-normal);
|
|
926
|
+
box-shadow: none;
|
|
927
|
+
/* Mobile: fullscreen */
|
|
928
|
+
width: 100vw !important;
|
|
929
|
+
height: 100dvh;
|
|
930
|
+
max-height: 100dvh;
|
|
931
|
+
overflow-y: auto;
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
.sidebar.sidebar--open {
|
|
935
|
+
transform: translateX(0);
|
|
936
|
+
box-shadow: none;
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
/* Ignore collapsed state on mobile - restore all elements */
|
|
940
|
+
.sidebar.sidebar--collapsed {
|
|
941
|
+
width: 100vw !important;
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
.sidebar.sidebar--collapsed .sidebar-header {
|
|
945
|
+
padding: 0.75rem 1rem;
|
|
946
|
+
align-items: flex-start;
|
|
947
|
+
}
|
|
948
|
+
|
|
949
|
+
.sidebar.sidebar--collapsed .sidebar-header::after {
|
|
950
|
+
display: none;
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
.sidebar.sidebar--collapsed .sidebar-header h1,
|
|
954
|
+
.sidebar.sidebar--collapsed .sidebar-header .version,
|
|
955
|
+
.sidebar.sidebar--collapsed .sidebar-logo {
|
|
956
|
+
display: block;
|
|
957
|
+
opacity: 1;
|
|
958
|
+
width: auto;
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
.sidebar.sidebar--collapsed .nav-section-title {
|
|
962
|
+
opacity: 1;
|
|
963
|
+
height: auto;
|
|
964
|
+
padding: var(--fad-nav-item-padding-y) var(--fad-nav-item-padding-x);
|
|
965
|
+
padding-left: 1rem;
|
|
966
|
+
pointer-events: auto;
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
/* Nav item mobile collapsed styles are in _sidebar.scss (global styles) */
|
|
970
|
+
|
|
971
|
+
.sidebar.sidebar--collapsed #powered-by,
|
|
972
|
+
.sidebar.sidebar--collapsed #user-zone {
|
|
973
|
+
display: flex;
|
|
974
|
+
}
|
|
975
|
+
|
|
976
|
+
.sidebar.sidebar--collapsed #user-zone .user-avatar,
|
|
977
|
+
.sidebar.sidebar--collapsed .sidebar-box-content {
|
|
978
|
+
opacity: 1;
|
|
979
|
+
width: auto;
|
|
980
|
+
}
|
|
981
|
+
|
|
982
|
+
.sidebar-overlay {
|
|
983
|
+
display: block;
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
.sidebar-overlay.sidebar-overlay--visible {
|
|
987
|
+
opacity: 1;
|
|
988
|
+
pointer-events: auto;
|
|
989
|
+
}
|
|
990
|
+
|
|
991
|
+
.main-content,
|
|
992
|
+
.main-content--sidebar-collapsed {
|
|
993
|
+
margin-left: 0 !important;
|
|
994
|
+
}
|
|
995
|
+
|
|
996
|
+
.page-content {
|
|
997
|
+
padding: 0.75rem;
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
.layout-nav-bar {
|
|
1001
|
+
padding: 0.75rem;
|
|
1002
|
+
padding-bottom: 0;
|
|
1003
|
+
}
|
|
1004
|
+
}
|
|
1005
|
+
|
|
1006
|
+
/* Large tablet breakpoint (768px - 1023px) */
|
|
1007
|
+
@media (min-width: 768px) and (max-width: 1023px) {
|
|
1008
|
+
.sidebar {
|
|
1009
|
+
width: var(--fad-sidebar-width-tablet);
|
|
1010
|
+
}
|
|
1011
|
+
|
|
1012
|
+
.main-content {
|
|
1013
|
+
margin-left: var(--fad-sidebar-width-tablet);
|
|
1014
|
+
}
|
|
1015
|
+
|
|
1016
|
+
.main-content--sidebar-collapsed {
|
|
1017
|
+
margin-left: var(--fad-sidebar-width-collapsed) !important;
|
|
1018
|
+
}
|
|
1019
|
+
}
|
|
1020
|
+
</style>
|
|
1021
|
+
|
|
1022
|
+
<style>
|
|
1023
|
+
/* Non-scoped: logo alpha blink triggered by notification badge alert state */
|
|
1024
|
+
.footer-logo-wrapper:has(.notification-badge-zone--alert) {
|
|
1025
|
+
animation: logo-alpha-blink 1s ease-in-out infinite;
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1028
|
+
@keyframes logo-alpha-blink {
|
|
1029
|
+
0%, 100% { opacity: 1; filter: none; }
|
|
1030
|
+
50% { opacity: 0.4; filter: brightness(1.2) sepia(1) hue-rotate(-30deg) saturate(3); }
|
|
1031
|
+
}
|
|
1032
|
+
</style>
|