Add global app banners and integrate into layout

Introduce a global app banners store (GUI/src/stores/appBanners.ts) and switch authentication error/notification flows to use it. GUI/src/Layout.vue now consumes the banner store, replaces the single snackbar with a stacked, dismissible banner UI (styles, transitions and z-index included), and adds navigateToBrandTarget() to route the brand button based on auth state. GUI/src/routes/authentication/Login.vue was updated to push errors to the new banner store and remove the local alert. Minor route adjustments in GUI/src/plugins/routesLayout.ts: rename 'Dash' to 'Dashboard' and change Login visibility to Unauthenticated. codexInfo.md updated to document these UI/behavior changes.
This commit is contained in:
Jonas
2026-04-18 23:02:01 +02:00
parent 86ed227566
commit 6c2a149f96
5 changed files with 226 additions and 37 deletions
+82
View File
@@ -0,0 +1,82 @@
import { ref } from 'vue'
import { defineStore } from 'pinia'
export type AppBannerType = 'success' | 'info' | 'warning' | 'error'
export interface AppBannerMessage {
id: string
type: AppBannerType
title?: string
message: string
createdAt: number
}
interface PushBannerInput {
type?: AppBannerType
title?: string
message: string
}
const MAX_BANNERS = 8
function createBannerId() {
if (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function') {
return crypto.randomUUID()
}
return `banner-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`
}
export const useAppBannersStore = defineStore('app-banners', () => {
const banners = ref<AppBannerMessage[]>([])
function push(input: PushBannerInput) {
const now = Date.now()
const type = input.type ?? 'info'
const normalizedMessage = input.message.trim()
if (normalizedMessage.length === 0) {
return ''
}
const lastBanner = banners.value[banners.value.length - 1]
if (
lastBanner &&
lastBanner.type === type &&
lastBanner.message === normalizedMessage &&
now - lastBanner.createdAt < 1200
) {
return lastBanner.id
}
const banner: AppBannerMessage = {
id: createBannerId(),
type,
title: input.title,
message: normalizedMessage,
createdAt: now,
}
banners.value = [...banners.value, banner].slice(-MAX_BANNERS)
return banner.id
}
function pushError(message: string, title = 'Fehler') {
return push({ type: 'error', title, message })
}
function dismiss(id: string) {
banners.value = banners.value.filter((banner) => banner.id !== id)
}
function clear() {
banners.value = []
}
return {
banners,
push,
pushError,
dismiss,
clear,
}
})