0bd4e5498e
- portal: new admin/ and partner/ surfaces with full component library (AppLauncher, Avatar, Badge, Card, Modal, Tabs, etc.), composables, layouts, partner-routing middleware, and supporting server APIs - pricing: Price schema/module with operator CRUD, pricing.vue catalog UI, Subscription extended with cycle/currency/perSeatAmount/seats snapshots for stable MRR aggregation - partner staff: User.partnerId, invite-partner-user DTO and flow, /partners/:slug/users endpoints, InvitePartnerUserModal, shared dezky-partner-staff Authentik group - /me: partner-aware endpoint returning user + partner context so portal can route between end-user and partner-admin surfaces - tenant: seats field for portfolio displays and future MRR calculations - operator: pricing page, signed-out page, useMe/useToast composables, ToastStack
38 lines
1.0 KiB
TypeScript
38 lines
1.0 KiB
TypeScript
// Lightweight toast stack. Used by buttons/actions that want to confirm
|
|
// they fired. Rendered by components/ToastStack.vue in the default layout.
|
|
|
|
export type ToastTone = 'info' | 'ok' | 'warn' | 'bad'
|
|
|
|
export interface Toast {
|
|
id: number
|
|
tone: ToastTone
|
|
message: string
|
|
hint?: string
|
|
}
|
|
|
|
const toasts = ref<Toast[]>([])
|
|
let counter = 0
|
|
|
|
export const useToast = () => {
|
|
function push(tone: ToastTone, message: string, hint?: string) {
|
|
const id = ++counter
|
|
toasts.value = [...toasts.value, { id, tone, message, hint }]
|
|
const ttl = tone === 'bad' ? 7000 : 4000
|
|
setTimeout(() => {
|
|
toasts.value = toasts.value.filter((t) => t.id !== id)
|
|
}, ttl)
|
|
}
|
|
function dismiss(id: number) {
|
|
toasts.value = toasts.value.filter((t) => t.id !== id)
|
|
}
|
|
return {
|
|
toasts,
|
|
push,
|
|
info: (m: string, h?: string) => push('info', m, h),
|
|
ok: (m: string, h?: string) => push('ok', m, h),
|
|
warn: (m: string, h?: string) => push('warn', m, h),
|
|
bad: (m: string, h?: string) => push('bad', m, h),
|
|
dismiss,
|
|
}
|
|
}
|