@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.
Files changed (291) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +111 -0
  3. package/package.json +101 -0
  4. package/src/assets/logo.svg +6 -0
  5. package/src/auth/SessionAuthAdapter.ts +459 -0
  6. package/src/auth/index.ts +15 -0
  7. package/src/chain/ActiveStack.ts +154 -0
  8. package/src/chain/StackHydrator.ts +292 -0
  9. package/src/chain/index.ts +25 -0
  10. package/src/chain/useActiveStack.ts +120 -0
  11. package/src/chain/useStackHydrator.ts +155 -0
  12. package/src/components/BoolCell.vue +34 -0
  13. package/src/components/InfoBanner.vue +73 -0
  14. package/src/components/QdadmRoot.vue +52 -0
  15. package/src/components/SeverityTag.vue +86 -0
  16. package/src/components/dialogs/BulkStatusDialog.vue +60 -0
  17. package/src/components/dialogs/MultiStepDialog.vue +331 -0
  18. package/src/components/dialogs/SimpleDialog.vue +122 -0
  19. package/src/components/dialogs/UnsavedChangesDialog.vue +80 -0
  20. package/src/components/display/BannerZone.vue +65 -0
  21. package/src/components/display/CardsGrid.vue +72 -0
  22. package/src/components/display/CopyableId.vue +84 -0
  23. package/src/components/display/EmptyState.vue +62 -0
  24. package/src/components/display/IntensityBar.vue +190 -0
  25. package/src/components/display/RichCardsGrid.vue +101 -0
  26. package/src/components/edit/FormActions.vue +78 -0
  27. package/src/components/edit/FormField.vue +82 -0
  28. package/src/components/edit/FormInput.vue +158 -0
  29. package/src/components/edit/FormPage.vue +260 -0
  30. package/src/components/edit/FormTab.vue +217 -0
  31. package/src/components/edit/FormTabs.vue +120 -0
  32. package/src/components/edit/LookupField.vue +406 -0
  33. package/src/components/edit/LookupPickerDialog.vue +230 -0
  34. package/src/components/editors/JsonEditorFoldable.vue +470 -0
  35. package/src/components/editors/JsonStructuredField.vue +264 -0
  36. package/src/components/editors/JsonViewer.vue +51 -0
  37. package/src/components/editors/KeyValueEditor.vue +314 -0
  38. package/src/components/editors/LanguageEditor.vue +259 -0
  39. package/src/components/editors/PermissionEditor.vue +558 -0
  40. package/src/components/editors/ScopeEditor.vue +372 -0
  41. package/src/components/editors/VanillaJsonEditor.vue +183 -0
  42. package/src/components/index.ts +83 -0
  43. package/src/components/item/FieldGroups.vue +392 -0
  44. package/src/components/layout/AppLayout.vue +1032 -0
  45. package/src/components/layout/BaseLayout.vue +190 -0
  46. package/src/components/layout/Breadcrumb.vue +55 -0
  47. package/src/components/layout/DashboardLayout.vue +104 -0
  48. package/src/components/layout/FormLayout.vue +256 -0
  49. package/src/components/layout/ListLayout.vue +278 -0
  50. package/src/components/layout/PageHeader.vue +156 -0
  51. package/src/components/layout/PageLayout.vue +126 -0
  52. package/src/components/layout/PageNav.vue +192 -0
  53. package/src/components/layout/SidebarBox.vue +87 -0
  54. package/src/components/layout/Zone.vue +182 -0
  55. package/src/components/layout/defaults/DefaultBreadcrumb.vue +149 -0
  56. package/src/components/layout/defaults/DefaultFooter.vue +89 -0
  57. package/src/components/layout/defaults/DefaultFormActions.vue +67 -0
  58. package/src/components/layout/defaults/DefaultHeader.vue +78 -0
  59. package/src/components/layout/defaults/DefaultMenu.vue +188 -0
  60. package/src/components/layout/defaults/DefaultPagination.vue +75 -0
  61. package/src/components/layout/defaults/DefaultTable.vue +137 -0
  62. package/src/components/layout/defaults/DefaultUserInfo.vue +111 -0
  63. package/src/components/layout/defaults/index.ts +16 -0
  64. package/src/components/lists/ActionButtons.vue +49 -0
  65. package/src/components/lists/ActionColumn.vue +40 -0
  66. package/src/components/lists/FilterBar.vue +54 -0
  67. package/src/components/lists/ListPage.vue +467 -0
  68. package/src/components/pages/LoginPage.vue +334 -0
  69. package/src/components/pages/NotFoundPage.vue +98 -0
  70. package/src/components/show/ShowDisplay.vue +343 -0
  71. package/src/components/show/ShowField.vue +67 -0
  72. package/src/components/show/ShowPage.vue +431 -0
  73. package/src/composables/index.ts +155 -0
  74. package/src/composables/useApp.ts +67 -0
  75. package/src/composables/useAuth.ts +98 -0
  76. package/src/composables/useBareForm.ts +319 -0
  77. package/src/composables/useBreadcrumb.ts +252 -0
  78. package/src/composables/useChildPage.ts +113 -0
  79. package/src/composables/useCurrentEntity.ts +60 -0
  80. package/src/composables/useDeferred.ts +94 -0
  81. package/src/composables/useDirtyState.ts +138 -0
  82. package/src/composables/useEntityItemFormPage.ts +942 -0
  83. package/src/composables/useEntityItemFormPage.types.ts +451 -0
  84. package/src/composables/useEntityItemPage.ts +438 -0
  85. package/src/composables/useEntityItemShowPage.ts +722 -0
  86. package/src/composables/useFieldManager.ts +692 -0
  87. package/src/composables/useForm.ts +544 -0
  88. package/src/composables/useGuardStore.ts +53 -0
  89. package/src/composables/useHooks.ts +56 -0
  90. package/src/composables/useInfoBanner.ts +217 -0
  91. package/src/composables/useJsonSyntax.ts +122 -0
  92. package/src/composables/useLayoutResolver.ts +329 -0
  93. package/src/composables/useListPage.ts +1535 -0
  94. package/src/composables/useListPage.types.ts +437 -0
  95. package/src/composables/useListPage.utils.ts +101 -0
  96. package/src/composables/useNavContext.ts +439 -0
  97. package/src/composables/useNavigation.ts +216 -0
  98. package/src/composables/useOptionsLookup.ts +323 -0
  99. package/src/composables/usePageTitle.ts +84 -0
  100. package/src/composables/useSSEBridge.ts +142 -0
  101. package/src/composables/useSemanticBreadcrumb.ts +222 -0
  102. package/src/composables/useSidebarState.ts +24 -0
  103. package/src/composables/useSignals.ts +52 -0
  104. package/src/composables/useUnsavedChangesGuard.ts +155 -0
  105. package/src/composables/useUserImpersonator.ts +317 -0
  106. package/src/composables/useZoneRegistry.ts +252 -0
  107. package/src/core/bundles.ts +558 -0
  108. package/src/core/decorator.ts +420 -0
  109. package/src/core/extension.ts +470 -0
  110. package/src/core/index.ts +59 -0
  111. package/src/deferred/DeferredRegistry.ts +353 -0
  112. package/src/deferred/index.ts +15 -0
  113. package/src/editors/index.ts +12 -0
  114. package/src/entity/EntityManager.cache.ts +530 -0
  115. package/src/entity/EntityManager.crud.ts +615 -0
  116. package/src/entity/EntityManager.interface.ts +78 -0
  117. package/src/entity/EntityManager.query.ts +221 -0
  118. package/src/entity/EntityManager.relations.ts +151 -0
  119. package/src/entity/EntityManager.ts +944 -0
  120. package/src/entity/EntityManager.types.ts +288 -0
  121. package/src/entity/auth/CompositeAuthAdapter.ts +194 -0
  122. package/src/entity/auth/EntityAuthAdapter.ts +119 -0
  123. package/src/entity/auth/PermissiveAdapter.ts +43 -0
  124. package/src/entity/auth/RoleHierarchy.ts +128 -0
  125. package/src/entity/auth/SecurityChecker.ts +202 -0
  126. package/src/entity/auth/factory.ts +203 -0
  127. package/src/entity/auth/index.ts +47 -0
  128. package/src/entity/factory.test.js +189 -0
  129. package/src/entity/factory.ts +146 -0
  130. package/src/entity/index.ts +49 -0
  131. package/src/entity/storage/ApiStorage.ts +228 -0
  132. package/src/entity/storage/IStorage.ts +100 -0
  133. package/src/entity/storage/LocalStorage.ts +200 -0
  134. package/src/entity/storage/MemoryStorage.ts +184 -0
  135. package/src/entity/storage/MockApiStorage.ts +318 -0
  136. package/src/entity/storage/SdkStorage.ts +377 -0
  137. package/src/entity/storage/factory.test.js +159 -0
  138. package/src/entity/storage/factory.ts +209 -0
  139. package/src/entity/storage/index.ts +75 -0
  140. package/src/gen/FieldMapper.ts +122 -0
  141. package/src/gen/StorageProfileFactory.ts +93 -0
  142. package/src/gen/connectors/BaseConnector.ts +150 -0
  143. package/src/gen/connectors/ManualConnector.test.js +499 -0
  144. package/src/gen/connectors/ManualConnector.ts +414 -0
  145. package/src/gen/connectors/OpenAPIConnector.test.js +836 -0
  146. package/src/gen/connectors/OpenAPIConnector.ts +693 -0
  147. package/src/gen/connectors/__fixtures__/sample-openapi.json +311 -0
  148. package/src/gen/connectors/index.ts +27 -0
  149. package/src/gen/createManagers.ts +245 -0
  150. package/src/gen/decorators.ts +134 -0
  151. package/src/gen/generateManagers.test.js +540 -0
  152. package/src/gen/generateManagers.ts +398 -0
  153. package/src/gen/index.ts +66 -0
  154. package/src/gen/schema.ts +248 -0
  155. package/src/gen/vite-plugin.ts +114 -0
  156. package/src/generated/managers/testManager.js +45 -0
  157. package/src/generated/managers/testManager.ts +46 -0
  158. package/src/hooks/HookRegistry.ts +16 -0
  159. package/src/hooks/index.ts +15 -0
  160. package/src/i18n/I18n.ts +344 -0
  161. package/src/i18n/IncrementalDomainProvider.ts +153 -0
  162. package/src/i18n/InlineTranslationProvider.ts +4 -0
  163. package/src/i18n/LazyTranslationProvider.ts +102 -0
  164. package/src/i18n/MessagesRegistry.ts +4 -0
  165. package/src/i18n/Resolver.ts +4 -0
  166. package/src/i18n/__tests__/I18n.test.ts +169 -0
  167. package/src/i18n/__tests__/IncrementalDomainProvider.test.ts +146 -0
  168. package/src/i18n/__tests__/LazyTranslationProvider.test.ts +100 -0
  169. package/src/i18n/__tests__/Resolver.test.ts +271 -0
  170. package/src/i18n/defaults/DefaultCoreProvider.ts +28 -0
  171. package/src/i18n/defaults/core.en.yml +55 -0
  172. package/src/i18n/defaults/core.fr.yml +55 -0
  173. package/src/i18n/index.ts +55 -0
  174. package/src/i18n/loaders/raw-modules.d.ts +15 -0
  175. package/src/i18n/loaders/yaml.ts +35 -0
  176. package/src/i18n/strategies.ts +4 -0
  177. package/src/i18n/types.ts +34 -0
  178. package/src/i18n/useI18n.ts +34 -0
  179. package/src/index.ts +453 -0
  180. package/src/kernel/EventRouter.ts +23 -0
  181. package/src/kernel/Kernel.api.ts +79 -0
  182. package/src/kernel/Kernel.auth.ts +120 -0
  183. package/src/kernel/Kernel.i18n.ts +29 -0
  184. package/src/kernel/Kernel.modules.ts +170 -0
  185. package/src/kernel/Kernel.registries.ts +237 -0
  186. package/src/kernel/Kernel.routing.ts +334 -0
  187. package/src/kernel/Kernel.ts +460 -0
  188. package/src/kernel/Kernel.types.ts +263 -0
  189. package/src/kernel/Kernel.vue.ts +304 -0
  190. package/src/kernel/KernelContext.entities.ts +80 -0
  191. package/src/kernel/KernelContext.events.ts +57 -0
  192. package/src/kernel/KernelContext.i18n.ts +37 -0
  193. package/src/kernel/KernelContext.permissions.ts +38 -0
  194. package/src/kernel/KernelContext.routing.ts +280 -0
  195. package/src/kernel/KernelContext.ts +252 -0
  196. package/src/kernel/KernelContext.types.ts +173 -0
  197. package/src/kernel/KernelContext.zones.ts +54 -0
  198. package/src/kernel/Module.ts +169 -0
  199. package/src/kernel/ModuleLoader.ts +645 -0
  200. package/src/kernel/SSEBridge.ts +11 -0
  201. package/src/kernel/SignalBus.ts +48 -0
  202. package/src/kernel/index.ts +56 -0
  203. package/src/module/index.ts +33 -0
  204. package/src/module/moduleRegistry.ts +492 -0
  205. package/src/modules/debug/AuthCollector.ts +501 -0
  206. package/src/modules/debug/Collector.ts +20 -0
  207. package/src/modules/debug/DebugBridge.ts +14 -0
  208. package/src/modules/debug/DebugModule.ts +220 -0
  209. package/src/modules/debug/EntitiesCollector.ts +839 -0
  210. package/src/modules/debug/ErrorCollector.ts +4 -0
  211. package/src/modules/debug/I18nCollector.ts +9 -0
  212. package/src/modules/debug/LocalStorageAdapter.ts +7 -0
  213. package/src/modules/debug/RouterCollector.ts +449 -0
  214. package/src/modules/debug/SignalCollector.ts +4 -0
  215. package/src/modules/debug/ToastCollector.ts +4 -0
  216. package/src/modules/debug/ZonesCollector.ts +447 -0
  217. package/src/modules/debug/components/DebugBar.vue +31 -0
  218. package/src/modules/debug/components/adminPanelsConfig.ts +42 -0
  219. package/src/modules/debug/components/index.ts +9 -0
  220. package/src/modules/debug/components/panels/AuthPanel.vue +267 -0
  221. package/src/modules/debug/components/panels/EntitiesPanel.vue +572 -0
  222. package/src/modules/debug/components/panels/I18nPanel.vue +738 -0
  223. package/src/modules/debug/components/panels/RouterPanel.vue +440 -0
  224. package/src/modules/debug/components/panels/ZonesPanel.vue +133 -0
  225. package/src/modules/debug/components/panels/index.ts +16 -0
  226. package/src/modules/debug/index.ts +58 -0
  227. package/src/modules/debug/styles.scss +2671 -0
  228. package/src/notifications/NotificationBadge.vue +35 -0
  229. package/src/notifications/NotificationListener.vue +71 -0
  230. package/src/notifications/NotificationModule.ts +76 -0
  231. package/src/notifications/NotificationPanel.vue +478 -0
  232. package/src/notifications/NotificationStore.ts +288 -0
  233. package/src/notifications/index.ts +42 -0
  234. package/src/notifications/styles.scss +32 -0
  235. package/src/orchestrator/Orchestrator.ts +427 -0
  236. package/src/orchestrator/index.ts +8 -0
  237. package/src/orchestrator/useOrchestrator.ts +80 -0
  238. package/src/plugin.ts +251 -0
  239. package/src/query/FilterQuery.ts +306 -0
  240. package/src/query/QueryExecutor.ts +341 -0
  241. package/src/query/index.ts +16 -0
  242. package/src/security/EntityRolesProvider.ts +385 -0
  243. package/src/security/PermissionMatcher.ts +152 -0
  244. package/src/security/PermissionRegistry.ts +273 -0
  245. package/src/security/PersistableRolesProvider.ts +584 -0
  246. package/src/security/RolesManager.ts +136 -0
  247. package/src/security/RolesProvider.ts +167 -0
  248. package/src/security/RolesProviderStorage.ts +211 -0
  249. package/src/security/SecurityModule.ts +94 -0
  250. package/src/security/StaticRolesProvider.ts +117 -0
  251. package/src/security/UsersManager.ts +163 -0
  252. package/src/security/index.ts +77 -0
  253. package/src/security/pages/RoleForm.vue +276 -0
  254. package/src/security/pages/RoleList.vue +123 -0
  255. package/src/styles/_alerts.scss +95 -0
  256. package/src/styles/_buttons.scss +391 -0
  257. package/src/styles/_cards.scss +241 -0
  258. package/src/styles/_code.scss +78 -0
  259. package/src/styles/_dialogs.scss +55 -0
  260. package/src/styles/_filter-bar.scss +70 -0
  261. package/src/styles/_forms.scss +434 -0
  262. package/src/styles/_helpers.scss +153 -0
  263. package/src/styles/_lists.scss +225 -0
  264. package/src/styles/_main.scss +356 -0
  265. package/src/styles/_markdown.scss +88 -0
  266. package/src/styles/_responsive.scss +255 -0
  267. package/src/styles/_show-pages.scss +186 -0
  268. package/src/styles/_sidebar.scss +380 -0
  269. package/src/styles/_states.scss +115 -0
  270. package/src/styles/_stats.scss +47 -0
  271. package/src/styles/_variables.scss +224 -0
  272. package/src/styles/index.scss +38 -0
  273. package/src/styles/theme/_components.scss +331 -0
  274. package/src/styles/theme/_index.scss +10 -0
  275. package/src/styles/theme/_tokens.scss +147 -0
  276. package/src/styles/theme/_utilities.scss +175 -0
  277. package/src/toast/ToastBridgeModule.ts +68 -0
  278. package/src/toast/ToastListener.vue +55 -0
  279. package/src/toast/index.ts +26 -0
  280. package/src/toast/useSignalToast.ts +154 -0
  281. package/src/types/index.ts +285 -0
  282. package/src/types/pluralize.d.ts +19 -0
  283. package/src/types/quarkernel.d.ts +50 -0
  284. package/src/utils/formatters.ts +174 -0
  285. package/src/utils/index.ts +35 -0
  286. package/src/utils/transformers.ts +131 -0
  287. package/src/vite/qdadmDebugPlugin.ts +401 -0
  288. package/src/vite-env.d.ts +50 -0
  289. package/src/zones/ZoneRegistry.ts +934 -0
  290. package/src/zones/index.ts +29 -0
  291. 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>