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