@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,35 @@
1
+ <script setup lang="ts">
2
+ /**
3
+ * NotificationBadge - Clickable overlay for the sidebar footer logo
4
+ *
5
+ * Always rendered as a transparent click zone over the logo.
6
+ * When there are alerts, the parent layout applies an opacity blink
7
+ * on the logo wrapper via :has(.notification-badge-zone--alert).
8
+ *
9
+ * Click toggles the notification panel open/close.
10
+ */
11
+ import { useNotifications } from './NotificationStore'
12
+
13
+ const store = useNotifications()
14
+ </script>
15
+
16
+ <template>
17
+ <div
18
+ class="notification-badge-zone"
19
+ :class="{ 'notification-badge-zone--alert': store.hasAlert.value || store.unreadCount.value > 0 }"
20
+ @click.stop.prevent="store.toggle()"
21
+ />
22
+ </template>
23
+
24
+ <style scoped>
25
+ /*
26
+ * Only keep styles here that REQUIRE scoping (:deep, dynamic binding, component-specific overrides).
27
+ * Generic/reusable styles belong in src/styles/ partials (see _forms.scss, _cards.scss, etc.).
28
+ */
29
+ .notification-badge-zone {
30
+ position: absolute;
31
+ inset: 0;
32
+ cursor: pointer;
33
+ z-index: 1;
34
+ }
35
+ </style>
@@ -0,0 +1,71 @@
1
+ <script setup lang="ts">
2
+ /**
3
+ * NotificationListener - Captures toast signals to notification store
4
+ *
5
+ * Replaces ToastListener when NotificationModule is active.
6
+ * Intercepts toast:* signals and:
7
+ * - Always captures to NotificationStore
8
+ * - If data.forceToast === true, also shows a classic PrimeVue toast
9
+ */
10
+ import { onMounted, onUnmounted, inject } from 'vue'
11
+ import { useToast } from 'primevue/usetoast'
12
+ import type { SignalBus } from '../kernel/SignalBus'
13
+ import { useNotifications } from './NotificationStore'
14
+ import type { NotificationSeverity } from './NotificationStore'
15
+
16
+ interface ToastEventData {
17
+ summary?: string
18
+ detail?: string
19
+ life?: number
20
+ emitter?: string
21
+ forceToast?: boolean
22
+ }
23
+
24
+ const toast = useToast()
25
+ const signals = inject<SignalBus | null>('qdadmSignals', null)
26
+ const store = useNotifications()
27
+
28
+ let unsubscribe: (() => void) | null = null
29
+
30
+ onMounted(() => {
31
+ if (!signals) {
32
+ console.warn('[NotificationListener] No signals bus injected')
33
+ return
34
+ }
35
+
36
+ unsubscribe = signals.on('toast:*', (event) => {
37
+ const data = event.data as ToastEventData | undefined
38
+ const severity = event.name.split(':')[1] as NotificationSeverity
39
+
40
+ // Always capture to notification store
41
+ store.addNotification({
42
+ severity,
43
+ summary: data?.summary || '',
44
+ detail: data?.detail,
45
+ emitter: data?.emitter,
46
+ })
47
+
48
+ // If forceToast is set, also show classic PrimeVue toast
49
+ if (data?.forceToast) {
50
+ toast.add({
51
+ severity,
52
+ summary: data?.summary,
53
+ detail: data?.detail,
54
+ life: data?.life ?? 3000,
55
+ })
56
+ }
57
+ })
58
+ })
59
+
60
+ onUnmounted(() => {
61
+ if (unsubscribe) {
62
+ unsubscribe()
63
+ unsubscribe = null
64
+ }
65
+ })
66
+ </script>
67
+
68
+ <template>
69
+ <!-- Invisible listener component -->
70
+ <span style="display: none" />
71
+ </template>
@@ -0,0 +1,76 @@
1
+ /**
2
+ * NotificationModule - Optional notification panel system
3
+ *
4
+ * When loaded, this module:
5
+ * - Replaces the default ToastListener with NotificationListener
6
+ * (captures toasts to notification store instead of ephemeral PrimeVue toasts)
7
+ * - Registers NotificationBadge for the sidebar footer logo overlay
8
+ * - Registers NotificationPanel zone for the panel component
9
+ *
10
+ * Without this module, classic toast behavior is unchanged (ToastBridgeModule).
11
+ * With this module, toasts are captured. Use `forceToast: true` in signal data
12
+ * to also show a classic PrimeVue toast.
13
+ *
14
+ * @example
15
+ * // In kernel config
16
+ * const kernel = new Kernel({
17
+ * notifications: { enabled: true, maxNotifications: 100 }
18
+ * })
19
+ */
20
+
21
+ import { Module } from '../kernel/Module'
22
+ import type { KernelContext } from '../kernel/KernelContext'
23
+ import { TOAST_ZONE } from '../toast/ToastBridgeModule'
24
+ import NotificationListener from './NotificationListener.vue'
25
+ import NotificationBadge from './NotificationBadge.vue'
26
+ import NotificationPanel from './NotificationPanel.vue'
27
+
28
+ /**
29
+ * Zone names for notification components
30
+ */
31
+ export const NOTIFICATION_ZONE = '_app:notifications'
32
+ export const NOTIFICATION_BADGE_ZONE = '_app:notification-badge'
33
+ export const NOTIFICATION_STATUS_ZONE = '_app:notification-status'
34
+
35
+ export class NotificationModule extends Module {
36
+ static override moduleName = 'notifications'
37
+ static override requires: string[] = []
38
+ static override priority = 5 // Before toast-bridge (10) to intercept first
39
+
40
+ static override styles = () => import('./styles.scss')
41
+
42
+ async connect(ctx: KernelContext): Promise<void> {
43
+ // Define notification zones
44
+ ctx.zone(NOTIFICATION_ZONE)
45
+ ctx.zone(NOTIFICATION_BADGE_ZONE)
46
+ ctx.zone(NOTIFICATION_STATUS_ZONE)
47
+
48
+ // Replace the toast-listener block in the toast zone with our NotificationListener.
49
+ // This intercepts toast signals and captures them to the notification store.
50
+ // The toast zone is defined by ToastBridgeModule - we replace its listener block.
51
+ ctx.zone(TOAST_ZONE) // Ensure the zone exists
52
+ ctx.block(TOAST_ZONE, {
53
+ id: 'toast-listener',
54
+ component: NotificationListener,
55
+ weight: 0,
56
+ operation: 'replace',
57
+ replaces: 'toast-listener',
58
+ })
59
+
60
+ // Register badge component for sidebar footer overlay
61
+ ctx.block(NOTIFICATION_BADGE_ZONE, {
62
+ id: 'notification-badge',
63
+ component: NotificationBadge,
64
+ weight: 0,
65
+ })
66
+
67
+ // Register panel component
68
+ ctx.block(NOTIFICATION_ZONE, {
69
+ id: 'notification-panel',
70
+ component: NotificationPanel,
71
+ weight: 0,
72
+ })
73
+ }
74
+ }
75
+
76
+ export default NotificationModule
@@ -0,0 +1,478 @@
1
+ <script setup lang="ts">
2
+ /**
3
+ * NotificationPanel - Notification panel anchored to the sidebar
4
+ *
5
+ * Position:
6
+ * - Desktop: Fixed, left edge aligned to sidebar right edge, bottom of screen
7
+ * - Collapsed sidebar: Follows collapsed width
8
+ * - Mobile: Full width overlay at bottom of screen
9
+ *
10
+ * Features:
11
+ * - Inline action bar (mark read, clear, close) at top right
12
+ * - Status items section (custom module items)
13
+ * - Notification list (most recent first)
14
+ * - Empty state
15
+ */
16
+ import { RouterLink } from 'vue-router'
17
+ import { useNotifications } from './NotificationStore'
18
+ import type { NotificationSeverity } from './NotificationStore'
19
+
20
+ const store = useNotifications()
21
+
22
+ const severityIcons: Record<NotificationSeverity, string> = {
23
+ success: 'pi pi-check-circle',
24
+ info: 'pi pi-info-circle',
25
+ warn: 'pi pi-exclamation-triangle',
26
+ error: 'pi pi-times-circle',
27
+ }
28
+
29
+ const statusIcons: Record<string, string> = {
30
+ nominal: 'pi pi-check',
31
+ warn: 'pi pi-exclamation-triangle',
32
+ error: 'pi pi-times-circle',
33
+ }
34
+
35
+ function formatTime(timestamp: number): string {
36
+ const date = new Date(timestamp)
37
+ const now = new Date()
38
+ const diffMs = now.getTime() - date.getTime()
39
+ const diffMins = Math.floor(diffMs / 60000)
40
+
41
+ if (diffMins < 1) return 'just now'
42
+ if (diffMins < 60) return `${diffMins}m ago`
43
+
44
+ const diffHours = Math.floor(diffMins / 60)
45
+ if (diffHours < 24) return `${diffHours}h ago`
46
+
47
+ return date.toLocaleDateString()
48
+ }
49
+ </script>
50
+
51
+ <template>
52
+ <Transition name="notification-panel">
53
+ <div
54
+ v-if="store.isOpen.value"
55
+ class="notification-panel"
56
+ >
57
+ <!-- Inline toolbar -->
58
+ <div class="notification-toolbar">
59
+ <button
60
+ v-if="store.unreadCount.value > 0"
61
+ class="notification-toolbar-btn"
62
+ title="Mark all read"
63
+ @click="store.markAllRead()"
64
+ >
65
+ <i class="pi pi-check-circle" />
66
+ </button>
67
+ <button
68
+ v-if="store.notifications.value.length > 0"
69
+ class="notification-toolbar-btn"
70
+ title="Clear all"
71
+ @click="store.clearNotifications()"
72
+ >
73
+ <i class="pi pi-trash" />
74
+ </button>
75
+ <button
76
+ class="notification-toolbar-btn notification-toolbar-close"
77
+ title="Close"
78
+ @click="store.close()"
79
+ >
80
+ <i class="pi pi-times" />
81
+ </button>
82
+ </div>
83
+
84
+ <!-- Status items -->
85
+ <div
86
+ v-if="store.statusItems.value.length > 0"
87
+ class="notification-panel-status"
88
+ >
89
+ <component
90
+ v-for="item in store.statusItems.value"
91
+ :key="item.id"
92
+ :is="item.to ? RouterLink : 'div'"
93
+ :to="item.to || undefined"
94
+ class="notification-status-item"
95
+ :class="[
96
+ `notification-status-item--${item.severity}`,
97
+ { 'notification-status-item--link': !!item.to }
98
+ ]"
99
+ @click="item.to ? store.close() : undefined"
100
+ >
101
+ <i :class="item.icon || statusIcons[item.severity]" />
102
+ <span class="notification-status-label">{{ item.label }}</span>
103
+ <i v-if="item.to" class="pi pi-chevron-right notification-status-arrow" />
104
+ <span v-else-if="item.count != null" class="notification-status-count">{{ item.count }}</span>
105
+ </component>
106
+ </div>
107
+
108
+ <!-- Notification list -->
109
+ <div class="notification-panel-list">
110
+ <div
111
+ v-for="notif in store.notifications.value"
112
+ :key="notif.id"
113
+ class="notification-item"
114
+ :class="{
115
+ 'notification-item--unread': !notif.read,
116
+ [`notification-item--${notif.severity}`]: true,
117
+ }"
118
+ @click="store.markRead(notif.id)"
119
+ >
120
+ <div class="notification-item-icon">
121
+ <i :class="severityIcons[notif.severity]" />
122
+ </div>
123
+ <div class="notification-item-content">
124
+ <div class="notification-item-summary">{{ notif.summary }}</div>
125
+ <div v-if="notif.detail" class="notification-item-detail">{{ notif.detail }}</div>
126
+ <div class="notification-item-meta">
127
+ <span class="notification-item-time">{{ formatTime(notif.timestamp) }}</span>
128
+ <span v-if="notif.emitter" class="notification-item-emitter">{{ notif.emitter }}</span>
129
+ </div>
130
+ </div>
131
+ <button
132
+ class="notification-item-dismiss"
133
+ title="Dismiss"
134
+ @click.stop="store.removeNotification(notif.id)"
135
+ >
136
+ <i class="pi pi-times" />
137
+ </button>
138
+ </div>
139
+
140
+ <!-- Empty state -->
141
+ <div
142
+ v-if="store.notifications.value.length === 0 && store.statusItems.value.length === 0"
143
+ class="notification-panel-empty"
144
+ >
145
+ <i class="pi pi-bell" />
146
+ <span>No notifications</span>
147
+ </div>
148
+ </div>
149
+ </div>
150
+ </Transition>
151
+ </template>
152
+
153
+ <style scoped>
154
+ /*
155
+ * Only keep styles here that REQUIRE scoping (:deep, dynamic binding, component-specific overrides).
156
+ * Generic/reusable styles belong in src/styles/ partials (see _forms.scss, _cards.scss, etc.).
157
+ */
158
+ .notification-panel {
159
+ position: fixed;
160
+ left: calc(var(--fad-sidebar-width, 15rem) + 0.375rem);
161
+ bottom: 0.5rem;
162
+ width: 22rem;
163
+ min-height: 6.6rem;
164
+ max-height: 50vh;
165
+ background: var(--p-surface-0, #ffffff);
166
+ border: 1px solid var(--p-surface-200, #e2e8f0);
167
+ border-radius: 2px;
168
+ box-shadow: 0 1px 4px rgba(0, 0, 0, 0.1);
169
+ display: flex;
170
+ flex-direction: column;
171
+ z-index: 200;
172
+ overflow: hidden;
173
+ }
174
+
175
+ /* Collapsed sidebar */
176
+ :root:has(.sidebar--collapsed) .notification-panel {
177
+ left: calc(var(--fad-sidebar-width-collapsed, 2.5rem) + 0.375rem);
178
+ }
179
+
180
+ /* Inline toolbar - floats top-right inside the panel */
181
+ .notification-toolbar {
182
+ position: absolute;
183
+ top: 0;
184
+ right: 0;
185
+ display: flex;
186
+ align-items: center;
187
+ gap: 1px;
188
+ padding: 0.25rem;
189
+ z-index: 1;
190
+ }
191
+
192
+ .notification-toolbar-btn {
193
+ display: inline-flex;
194
+ align-items: center;
195
+ justify-content: center;
196
+ width: 1.375rem;
197
+ height: 1.375rem;
198
+ border: none;
199
+ background: none;
200
+ border-radius: 2px;
201
+ color: var(--p-surface-400, #94a3b8);
202
+ cursor: pointer;
203
+ font-size: 0.6875rem;
204
+ transition: color 0.1s, background 0.1s;
205
+ }
206
+
207
+ .notification-toolbar-btn:hover {
208
+ background: var(--p-surface-100, #f1f5f9);
209
+ color: var(--p-surface-600, #475569);
210
+ }
211
+
212
+ .notification-toolbar-close:hover {
213
+ color: var(--p-surface-700, #334155);
214
+ }
215
+
216
+ /* Status items */
217
+ .notification-panel-status {
218
+ padding: 0.5rem 0.625rem;
219
+ padding-right: 2.5rem;
220
+ border-bottom: 1px solid var(--p-surface-100, #f1f5f9);
221
+ flex-shrink: 0;
222
+ }
223
+
224
+ .notification-status-item {
225
+ display: flex;
226
+ align-items: center;
227
+ gap: 0.375rem;
228
+ padding: 0.1875rem 0;
229
+ font-size: 0.75rem;
230
+ color: var(--p-surface-600, #475569);
231
+ }
232
+
233
+ .notification-status-item i {
234
+ font-size: 0.6875rem;
235
+ }
236
+
237
+ .notification-status-item--nominal i {
238
+ color: var(--p-green-500, #22c55e);
239
+ }
240
+
241
+ .notification-status-item--warn i {
242
+ color: var(--p-orange-500, #f97316);
243
+ }
244
+
245
+ .notification-status-item--error i {
246
+ color: var(--p-red-500, #ef4444);
247
+ }
248
+
249
+ .notification-status-label {
250
+ flex: 1;
251
+ }
252
+
253
+ .notification-status-count {
254
+ font-weight: 600;
255
+ font-size: 0.6875rem;
256
+ }
257
+
258
+ .notification-status-item--link {
259
+ text-decoration: none;
260
+ cursor: pointer;
261
+ border-radius: 2px;
262
+ padding: 0.1875rem 0.375rem;
263
+ margin: 0 -0.375rem;
264
+ transition: background 0.1s;
265
+ }
266
+
267
+ .notification-status-item--link:hover {
268
+ background: var(--p-surface-50, #f8fafc);
269
+ }
270
+
271
+ .notification-status-arrow {
272
+ font-size: 0.5625rem;
273
+ opacity: 0;
274
+ transition: opacity 0.1s;
275
+ color: var(--p-surface-400, #94a3b8);
276
+ }
277
+
278
+ .notification-status-item--link:hover .notification-status-arrow {
279
+ opacity: 1;
280
+ }
281
+
282
+ /* Notification list */
283
+ .notification-panel-list {
284
+ flex: 1;
285
+ overflow-y: auto;
286
+ max-height: calc(50vh - 3rem);
287
+ }
288
+
289
+ /* Individual notification */
290
+ .notification-item {
291
+ display: flex;
292
+ align-items: flex-start;
293
+ gap: 0.5rem;
294
+ padding: 0.5rem 0.625rem;
295
+ border-bottom: 1px solid var(--p-surface-50, #f8fafc);
296
+ cursor: pointer;
297
+ transition: background 0.1s;
298
+ }
299
+
300
+ .notification-item:first-child {
301
+ padding-right: 2.5rem;
302
+ }
303
+
304
+ .notification-item:hover {
305
+ background: var(--p-surface-50, #f8fafc);
306
+ }
307
+
308
+ .notification-item:last-child {
309
+ border-bottom: none;
310
+ }
311
+
312
+ .notification-item--unread {
313
+ background: var(--p-blue-50, #eff6ff);
314
+ }
315
+
316
+ .notification-item--unread:hover {
317
+ background: var(--p-blue-100, #dbeafe);
318
+ }
319
+
320
+ .notification-item-icon {
321
+ flex-shrink: 0;
322
+ width: 1.125rem;
323
+ height: 1.125rem;
324
+ display: flex;
325
+ align-items: center;
326
+ justify-content: center;
327
+ margin-top: 0.0625rem;
328
+ }
329
+
330
+ .notification-item-icon i {
331
+ font-size: 0.75rem;
332
+ }
333
+
334
+ .notification-item--success .notification-item-icon i {
335
+ color: var(--p-green-500, #22c55e);
336
+ }
337
+
338
+ .notification-item--info .notification-item-icon i {
339
+ color: var(--p-blue-500, #3b82f6);
340
+ }
341
+
342
+ .notification-item--warn .notification-item-icon i {
343
+ color: var(--p-orange-500, #f97316);
344
+ }
345
+
346
+ .notification-item--error .notification-item-icon i {
347
+ color: var(--p-red-500, #ef4444);
348
+ }
349
+
350
+ .notification-item-content {
351
+ flex: 1;
352
+ min-width: 0;
353
+ }
354
+
355
+ .notification-item-summary {
356
+ font-size: 0.75rem;
357
+ font-weight: 500;
358
+ color: var(--p-surface-700, #334155);
359
+ line-height: 1.3;
360
+ }
361
+
362
+ .notification-item-detail {
363
+ font-size: 0.6875rem;
364
+ color: var(--p-surface-500, #64748b);
365
+ margin-top: 0.0625rem;
366
+ line-height: 1.3;
367
+ }
368
+
369
+ .notification-item-meta {
370
+ display: flex;
371
+ align-items: center;
372
+ gap: 0.375rem;
373
+ margin-top: 0.125rem;
374
+ font-size: 0.625rem;
375
+ color: var(--p-surface-400, #94a3b8);
376
+ }
377
+
378
+ .notification-item-emitter::before {
379
+ content: '\00b7';
380
+ margin-right: 0.375rem;
381
+ }
382
+
383
+ .notification-item-dismiss {
384
+ flex-shrink: 0;
385
+ display: inline-flex;
386
+ align-items: center;
387
+ justify-content: center;
388
+ width: 1.125rem;
389
+ height: 1.125rem;
390
+ border: none;
391
+ background: none;
392
+ border-radius: 2px;
393
+ color: var(--p-surface-400, #94a3b8);
394
+ cursor: pointer;
395
+ font-size: 0.5625rem;
396
+ opacity: 0;
397
+ transition: opacity 0.1s;
398
+ }
399
+
400
+ .notification-item:hover .notification-item-dismiss {
401
+ opacity: 1;
402
+ }
403
+
404
+ .notification-item-dismiss:hover {
405
+ background: var(--p-surface-200, #e2e8f0);
406
+ color: var(--p-surface-600, #475569);
407
+ }
408
+
409
+ /* Empty state */
410
+ .notification-panel-empty {
411
+ display: flex;
412
+ flex-direction: column;
413
+ align-items: center;
414
+ justify-content: center;
415
+ padding: 1.5rem 1rem;
416
+ color: var(--p-surface-300, #cbd5e1);
417
+ gap: 0.375rem;
418
+ }
419
+
420
+ .notification-panel-empty i {
421
+ font-size: 1.25rem;
422
+ }
423
+
424
+ .notification-panel-empty span {
425
+ font-size: 0.75rem;
426
+ }
427
+
428
+ /* Transition */
429
+ .notification-panel-enter-active {
430
+ transition: opacity 0.15s ease, transform 0.15s ease;
431
+ }
432
+
433
+ .notification-panel-leave-active {
434
+ transition: opacity 0.1s ease, transform 0.1s ease;
435
+ }
436
+
437
+ .notification-panel-enter-from,
438
+ .notification-panel-leave-to {
439
+ opacity: 0;
440
+ transform: translateY(0.5rem);
441
+ }
442
+
443
+ /* Mobile: full width bottom overlay */
444
+ @media (max-width: 767px) {
445
+ .notification-panel {
446
+ left: 0 !important;
447
+ right: 0;
448
+ bottom: 0;
449
+ width: auto;
450
+ max-height: 60vh;
451
+ border-radius: 2px 2px 0 0;
452
+ border-bottom: none;
453
+ }
454
+ }
455
+
456
+ /* Dark mode */
457
+ .dark-mode .notification-panel {
458
+ background: var(--p-surface-800, #1e293b);
459
+ border-color: var(--p-surface-700, #334155);
460
+ }
461
+
462
+ .dark-mode .notification-toolbar-btn:hover {
463
+ background: var(--p-surface-700, #334155);
464
+ color: var(--p-surface-300, #cbd5e1);
465
+ }
466
+
467
+ .dark-mode .notification-item--unread {
468
+ background: rgba(59, 130, 246, 0.1);
469
+ }
470
+
471
+ .dark-mode .notification-item:hover {
472
+ background: var(--p-surface-700, #334155);
473
+ }
474
+
475
+ .dark-mode .notification-item--unread:hover {
476
+ background: rgba(59, 130, 246, 0.15);
477
+ }
478
+ </style>