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:
@@ -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,
|
||||
}
|
||||
})
|
||||
Reference in New Issue
Block a user