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
This commit is contained in:
Ronni Baslund
2026-05-28 20:00:33 +02:00
parent be430179d9
commit 0bd4e5498e
144 changed files with 22110 additions and 209 deletions
+59 -26
View File
@@ -1,9 +1,22 @@
<script setup lang="ts">
// Minimal Lucide-style line icons. Add more as needed from project/platform-tokens.jsx.
// Portal icon set. Lucide-style, single stroke, currentColor by default.
// Mirrors project/platform-tokens.jsx ICONS set so component code can use the
// same names as the design source.
const props = withDefaults(
export type IconName =
| 'home' | 'users' | 'globe' | 'building' | 'briefcase' | 'help'
| 'card' | 'database' | 'plug' | 'shield' | 'file' | 'mail'
| 'calendar' | 'folder' | 'video' | 'chat' | 'key'
| 'check' | 'x' | 'plus' | 'more'
| 'search' | 'bell' | 'logout' | 'brush' | 'device' | 'sun' | 'moon'
| 'chevDown' | 'chevRight' | 'chevLeft' | 'chevUpDown'
| 'arrowUp' | 'arrowDown' | 'arrowRight'
| 'external' | 'refresh' | 'copy' | 'upload' | 'download' | 'filter' | 'trash'
| 'waffle'
withDefaults(
defineProps<{
name: 'mail' | 'shield' | 'key' | 'check' | 'external' | 'arrowRight'
name: IconName
size?: number
stroke?: string
strokeWidth?: number
@@ -29,28 +42,48 @@ const props = withDefaults(
aria-hidden="true"
style="flex-shrink: 0"
>
<template v-if="name === 'mail'">
<rect x="2.5" y="5" width="19" height="14" rx="2" />
<path d="M3 7l9 6 9-6" />
</template>
<template v-else-if="name === 'shield'">
<path d="M12 3l8 3v6c0 5-3.5 8-8 9-4.5-1-8-4-8-9V6z" />
</template>
<template v-else-if="name === 'key'">
<circle cx="9" cy="14" r="4" />
<path d="M12.5 11L20 3.5l-2-2-2 2 2 2-2 2 2 2" />
</template>
<template v-else-if="name === 'check'">
<path d="M5 12l5 5L20 7" />
</template>
<template v-else-if="name === 'external'">
<path d="M14 4h6v6" />
<path d="M20 4l-9 9" />
<path d="M19 14v5a1 1 0 0 1-1 1H5a1 1 0 0 1-1-1V6a1 1 0 0 1 1-1h5" />
</template>
<template v-else-if="name === 'arrowRight'">
<path d="M5 12h14" />
<path d="M13 5l7 7-7 7" />
</template>
<template v-if="name === 'home'"><path d="M3 11l9-8 9 8" /><path d="M5 10v10h14V10" /></template>
<template v-else-if="name === 'users'"><circle cx="9" cy="8" r="3.5" /><path d="M2.5 20c0-3.6 2.9-6 6.5-6s6.5 2.4 6.5 6" /><circle cx="17" cy="9" r="2.5" /><path d="M21.5 19c0-2.6-2-4.5-4.5-4.5" /></template>
<template v-else-if="name === 'globe'"><circle cx="12" cy="12" r="9" /><path d="M3 12h18" /><path d="M12 3a14 14 0 0 1 0 18" /><path d="M12 3a14 14 0 0 0 0 18" /></template>
<template v-else-if="name === 'building'"><rect x="4" y="3" width="16" height="18" rx="1" /><path d="M8 7h2M14 7h2M8 11h2M14 11h2M8 15h2M14 15h2" /><path d="M10 21v-4h4v4" /></template>
<template v-else-if="name === 'briefcase'"><rect x="3" y="7" width="18" height="13" rx="2" /><path d="M8 7V5a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2" /><path d="M3 13h18" /></template>
<template v-else-if="name === 'help'"><circle cx="12" cy="12" r="9" /><path d="M9.5 9a2.5 2.5 0 1 1 3.5 2.3c-1 .4-1 1.2-1 1.7" /><circle cx="12" cy="16.5" r="0.5" fill="currentColor" /></template>
<template v-else-if="name === 'card'"><rect x="2.5" y="5.5" width="19" height="13" rx="2" /><path d="M2.5 10h19" /></template>
<template v-else-if="name === 'database'"><ellipse cx="12" cy="5" rx="8" ry="3" /><path d="M4 5v6c0 1.7 3.6 3 8 3s8-1.3 8-3V5" /><path d="M4 11v6c0 1.7 3.6 3 8 3s8-1.3 8-3v-6" /></template>
<template v-else-if="name === 'plug'"><path d="M9 2v6" /><path d="M15 2v6" /><rect x="6" y="8" width="12" height="6" rx="2" /><path d="M12 14v3" /><path d="M9 21h6" /></template>
<template v-else-if="name === 'shield'"><path d="M12 3l8 3v6c0 5-3.5 8-8 9-4.5-1-8-4-8-9V6z" /></template>
<template v-else-if="name === 'file'"><path d="M14 3H6a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V9z" /><path d="M14 3v6h6" /></template>
<template v-else-if="name === 'mail'"><rect x="2.5" y="5" width="19" height="14" rx="2" /><path d="M3 7l9 6 9-6" /></template>
<template v-else-if="name === 'calendar'"><rect x="3" y="5" width="18" height="16" rx="2" /><path d="M3 10h18" /><path d="M8 3v4" /><path d="M16 3v4" /></template>
<template v-else-if="name === 'folder'"><path d="M3 7a2 2 0 0 1 2-2h4l2 2h8a2 2 0 0 1 2 2v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z" /></template>
<template v-else-if="name === 'video'"><rect x="2.5" y="6" width="13" height="12" rx="2" /><path d="M15.5 10l5-2.5v9l-5-2.5" /></template>
<template v-else-if="name === 'chat'"><path d="M21 12a8 8 0 1 1-3.2-6.4L21 4l-1 4.2A8 8 0 0 1 21 12z" /></template>
<template v-else-if="name === 'key'"><circle cx="9" cy="14" r="4" /><path d="M12.5 11L20 3.5l-2-2-2 2 2 2-2 2 2 2" /></template>
<template v-else-if="name === 'check'"><path d="M5 12l5 5L20 7" /></template>
<template v-else-if="name === 'x'"><path d="M6 6l12 12" /><path d="M18 6L6 18" /></template>
<template v-else-if="name === 'plus'"><path d="M12 5v14" /><path d="M5 12h14" /></template>
<template v-else-if="name === 'more'"><circle cx="5" cy="12" r="1.5" fill="currentColor" /><circle cx="12" cy="12" r="1.5" fill="currentColor" /><circle cx="19" cy="12" r="1.5" fill="currentColor" /></template>
<template v-else-if="name === 'search'"><circle cx="11" cy="11" r="7" /><path d="M20 20l-3.5-3.5" /></template>
<template v-else-if="name === 'bell'"><path d="M6 8a6 6 0 0 1 12 0c0 4 1.5 6 2 7H4c.5-1 2-3 2-7z" /><path d="M10 19a2 2 0 0 0 4 0" /></template>
<template v-else-if="name === 'logout'"><path d="M14 4h5a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2h-5" /><path d="M9 12h11" /><path d="M14 8l4 4-4 4" /></template>
<template v-else-if="name === 'brush'"><path d="M9 16l-3 3-3-3c2-1 3-2 3-3s1-2 3-2 3 1 3 3-2 4-3 5z" /><path d="M14 8l6-6 2 2-6 6" /><path d="M9 16l5-5" /></template>
<template v-else-if="name === 'device'"><rect x="6" y="3" width="12" height="18" rx="2" /><path d="M11 18h2" /></template>
<template v-else-if="name === 'sun'"><circle cx="12" cy="12" r="4" /><path d="M12 3v2M12 19v2M3 12h2M19 12h2M5.6 5.6l1.4 1.4M17 17l1.4 1.4M5.6 18.4l1.4-1.4M17 7l1.4-1.4" /></template>
<template v-else-if="name === 'moon'"><path d="M21 13A9 9 0 0 1 11 3a7 7 0 1 0 10 10z" /></template>
<template v-else-if="name === 'chevDown'"><path d="M6 9l6 6 6-6" /></template>
<template v-else-if="name === 'chevRight'"><path d="M9 6l6 6-6 6" /></template>
<template v-else-if="name === 'chevLeft'"><path d="M15 6l-6 6 6 6" /></template>
<template v-else-if="name === 'chevUpDown'"><path d="M8 10l4-4 4 4" /><path d="M8 14l4 4 4-4" /></template>
<template v-else-if="name === 'arrowUp'"><path d="M12 19V5" /><path d="M5 12l7-7 7 7" /></template>
<template v-else-if="name === 'arrowDown'"><path d="M12 5v14" /><path d="M19 12l-7 7-7-7" /></template>
<template v-else-if="name === 'arrowRight'"><path d="M5 12h14" /><path d="M13 5l7 7-7 7" /></template>
<template v-else-if="name === 'external'"><path d="M14 4h6v6" /><path d="M20 4l-9 9" /><path d="M19 14v5a1 1 0 0 1-1 1H5a1 1 0 0 1-1-1V6a1 1 0 0 1 1-1h5" /></template>
<template v-else-if="name === 'refresh'"><path d="M3 12a9 9 0 0 1 15-6.7L21 8" /><path d="M21 3v5h-5" /><path d="M21 12a9 9 0 0 1-15 6.7L3 16" /><path d="M3 21v-5h5" /></template>
<template v-else-if="name === 'copy'"><rect x="8" y="8" width="12" height="12" rx="2" /><path d="M16 8V5a1 1 0 0 0-1-1H5a1 1 0 0 0-1 1v10a1 1 0 0 0 1 1h3" /></template>
<template v-else-if="name === 'upload'"><path d="M12 16V4" /><path d="M7 9l5-5 5 5" /><path d="M4 17v2a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-2" /></template>
<template v-else-if="name === 'download'"><path d="M12 4v12" /><path d="M7 11l5 5 5-5" /><path d="M4 17v2a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-2" /></template>
<template v-else-if="name === 'filter'"><path d="M3 5h18l-7 9v6l-4-2v-4z" /></template>
<template v-else-if="name === 'trash'"><path d="M4 7h16" /><path d="M9 7V4h6v3" /><path d="M6 7l1 13h10l1-13" /></template>
<template v-else-if="name === 'waffle'"><circle cx="6" cy="6" r="1.5" fill="currentColor" /><circle cx="12" cy="6" r="1.5" fill="currentColor" /><circle cx="18" cy="6" r="1.5" fill="currentColor" /><circle cx="6" cy="12" r="1.5" fill="currentColor" /><circle cx="12" cy="12" r="1.5" fill="currentColor" /><circle cx="18" cy="12" r="1.5" fill="currentColor" /><circle cx="6" cy="18" r="1.5" fill="currentColor" /><circle cx="12" cy="18" r="1.5" fill="currentColor" /><circle cx="18" cy="18" r="1.5" fill="currentColor" /></template>
</svg>
</template>