Files
Ronni Baslund 0bd4e5498e feat: portal redesign, pricing catalog, partner-staff invites
- 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
2026-05-28 20:00:33 +02:00

75 lines
1.9 KiB
Vue

<script setup lang="ts">
// Toast stack mounted once in the default layout. Top-right.
const { toasts, dismiss } = useToast()
const TONE_COLOR: Record<string, string> = {
info: 'var(--info)',
ok: 'var(--ok)',
warn: 'var(--warn)',
bad: 'var(--bad)',
}
</script>
<template>
<div class="stack">
<TransitionGroup name="toast">
<div v-for="t in toasts" :key="t.id" class="toast" :data-tone="t.tone">
<span class="dot" :style="{ background: TONE_COLOR[t.tone] }" />
<div class="body">
<div class="msg">{{ t.message }}</div>
<div v-if="t.hint" class="hint">{{ t.hint }}</div>
</div>
<button class="x" @click="dismiss(t.id)">
<UiIcon name="x" :size="12" />
</button>
</div>
</TransitionGroup>
</div>
</template>
<style scoped>
.stack {
position: fixed;
top: 16px;
right: 16px;
display: flex;
flex-direction: column;
gap: 8px;
z-index: 200;
pointer-events: none;
}
.toast {
pointer-events: auto;
background: var(--elevated);
border: 1px solid var(--border);
border-radius: 8px;
padding: 10px 12px;
display: flex;
gap: 10px;
align-items: flex-start;
min-width: 280px;
max-width: 360px;
box-shadow: 0 16px 40px rgba(0, 0, 0, 0.28);
}
.dot { width: 8px; height: 8px; border-radius: 999px; margin-top: 6px; flex-shrink: 0; }
.body { flex: 1; min-width: 0; }
.msg { font-size: 13px; font-weight: 500; }
.hint { font-family: var(--font-mono); font-size: 11px; color: var(--text-mute); margin-top: 2px; }
.x {
background: transparent;
border: 0;
padding: 4px;
border-radius: 4px;
color: var(--text-mute);
cursor: pointer;
}
.x:hover { background: var(--surface); color: var(--text); }
.toast-enter-active, .toast-leave-active { transition: opacity 0.18s, transform 0.18s; }
.toast-enter-from, .toast-leave-to { opacity: 0; transform: translateX(20px); }
</style>