refactor(mana): remove post-signup onboarding wizard

Drop the 8-step wizard (Welcome, Profile, Context, Apps, AI-Tier, Sync,
Credits, Complete) in favor of contextual, per-module intros — todo and
news already own their first-run flows, and the workbench empty state
handles the initial surface for new users.

Removes components/onboarding/, stores/onboarding.svelte.ts, the
ONBOARDING storage key, and all trigger/render wiring in (app)/+layout.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Till JS 2026-04-20 15:46:21 +02:00
parent 8d00ee0697
commit a44e3df1d0
13 changed files with 1 additions and 1329 deletions

View file

@ -1,196 +0,0 @@
<script lang="ts">
import { onboardingStore } from '$lib/stores/onboarding.svelte';
import { ManaEvents } from '@mana/shared-utils/analytics';
import { profileService } from '$lib/api/profile';
import WelcomeStep from './steps/WelcomeStep.svelte';
import ProfileStep from './steps/ProfileStep.svelte';
import AppsStep from './steps/AppsStep.svelte';
import AiTierStep from './steps/AiTierStep.svelte';
import SyncStep from './steps/SyncStep.svelte';
import CreditsStep from './steps/CreditsStep.svelte';
import ContextStep from './steps/ContextStep.svelte';
import CompleteStep from './steps/CompleteStep.svelte';
import { Check } from '@mana/shared-icons';
interface Props {
onComplete: () => void;
/**
* Optional callback fired when the user explicitly skips the wizard
* (vs. completing it normally). Layout consumers use it to track
* skip-rate analytics; the default skip path still calls onComplete
* so the modal closes either way.
*/
onSkip?: () => void;
}
let { onComplete, onSkip }: Props = $props();
// Reference to profile name for auto-save on step transition
let profileNameRef = $state('');
const STEPS = [
{ id: 'welcome', label: 'Willkommen', component: WelcomeStep },
{ id: 'profile', label: 'Profil', component: ProfileStep },
{ id: 'context', label: 'Über dich', component: ContextStep },
{ id: 'apps', label: 'Apps', component: AppsStep },
{ id: 'ai-tier', label: 'KI', component: AiTierStep },
{ id: 'sync', label: 'Sync', component: SyncStep },
{ id: 'credits', label: 'Credits', component: CreditsStep },
{ id: 'complete', label: 'Fertig', component: CompleteStep },
];
let currentStep = $derived(onboardingStore.currentStep);
let currentStepData = $derived(STEPS[currentStep] || STEPS[0]);
let isFirstStep = $derived(currentStep === 0);
let isLastStep = $derived(currentStep === STEPS.length - 1);
let progress = $derived(((currentStep + 1) / STEPS.length) * 100);
async function handleNext() {
// Auto-save profile name when leaving the profile step
if (currentStepData.id === 'profile' && profileNameRef.trim()) {
try {
await profileService.updateProfile({ name: profileNameRef.trim() });
} catch {
// Non-blocking: profile save failure shouldn't block onboarding
}
}
if (isLastStep) {
onboardingStore.complete();
onComplete();
} else {
ManaEvents.onboardingStepCompleted(currentStepData.id, currentStep + 1);
onboardingStore.completeStep(currentStepData.id);
onboardingStore.nextStep();
}
}
function handlePrev() {
onboardingStore.prevStep();
}
function handleSkip() {
onboardingStore.skip();
onSkip?.();
onComplete();
}
function handleStepClick(index: number) {
// Only allow going to completed steps or the next step
if (index <= currentStep) {
onboardingStore.goToStep(index);
}
}
</script>
<!-- Backdrop -->
<div class="fixed inset-0 z-50 bg-black/60 backdrop-blur-sm flex items-center justify-center p-4">
<!-- Modal Container - uses surface-elevated-2 for proper elevation hierarchy -->
<div
class="bg-surface-elevated-2 rounded-2xl shadow-2xl w-full max-w-lg max-h-[85vh] flex flex-col overflow-hidden animate-in fade-in zoom-in-95 duration-200 border border-border"
>
<!-- Header with progress -->
<header class="border-b px-5 py-4 flex-shrink-0">
<div class="flex items-center justify-between mb-3">
<div class="flex items-center gap-3">
<div
class="h-9 w-9 rounded-lg bg-gradient-to-br from-primary to-primary/70 flex items-center justify-center"
>
<span class="text-lg font-semibold text-primary-foreground">M</span>
</div>
<div>
<h1 class="font-semibold">Willkommen bei Mana</h1>
<p class="text-xs text-muted-foreground">
Schritt {currentStep + 1} von {STEPS.length}
</p>
</div>
</div>
<button
onclick={handleSkip}
class="text-xs text-muted-foreground hover:text-foreground transition-colors"
>
Überspringen
</button>
</div>
<!-- Progress bar -->
<div class="h-1 bg-muted rounded-full overflow-hidden">
<div
class="h-full bg-primary transition-all duration-300 ease-out rounded-full"
style="width: {progress}%"
></div>
</div>
<!-- Step indicators (compact) -->
<div class="flex justify-between mt-2">
{#each STEPS as step, index}
<button
onclick={() => handleStepClick(index)}
disabled={index > currentStep}
class="flex flex-col items-center gap-0.5 group"
>
<div
class="h-6 w-6 rounded-full flex items-center justify-center text-xs font-medium transition-all
{index < currentStep
? 'bg-primary text-primary-foreground'
: index === currentStep
? 'bg-primary/20 text-primary border-2 border-primary'
: 'bg-muted text-muted-foreground'}"
>
{#if index < currentStep}
<Check size={20} />
{:else}
{index + 1}
{/if}
</div>
<span
class="text-[10px] {index === currentStep
? 'text-foreground font-medium'
: 'text-muted-foreground'} hidden sm:block"
>
{step.label}
</span>
</button>
{/each}
</div>
</header>
<!-- Step content -->
<main class="flex-1 overflow-y-auto px-5 py-4">
{#if currentStepData.id === 'welcome'}
<WelcomeStep />
{:else if currentStepData.id === 'profile'}
<ProfileStep bind:nameValue={profileNameRef} />
{:else if currentStepData.id === 'apps'}
<AppsStep />
{:else if currentStepData.id === 'ai-tier'}
<AiTierStep />
{:else if currentStepData.id === 'sync'}
<SyncStep />
{:else if currentStepData.id === 'credits'}
<CreditsStep />
{:else if currentStepData.id === 'complete'}
<CompleteStep />
{/if}
</main>
<!-- Footer with navigation -->
<footer class="border-t px-5 py-3 flex-shrink-0">
<div class="flex justify-between">
<button
onclick={handlePrev}
disabled={isFirstStep}
class="px-4 py-2 text-sm border rounded-lg hover:bg-muted transition-colors disabled:opacity-0 disabled:pointer-events-none"
>
Zurück
</button>
<button
onclick={handleNext}
class="px-4 py-2 text-sm bg-primary text-primary-foreground rounded-lg hover:bg-primary/90 transition-colors font-medium"
>
{isLastStep ? "Los geht's!" : 'Weiter'}
</button>
</div>
</footer>
</div>
</div>

View file

@ -1,6 +0,0 @@
export { default as OnboardingWizard } from './OnboardingWizard.svelte';
export { default as WelcomeStep } from './steps/WelcomeStep.svelte';
export { default as ProfileStep } from './steps/ProfileStep.svelte';
export { default as AppsStep } from './steps/AppsStep.svelte';
export { default as CreditsStep } from './steps/CreditsStep.svelte';
export { default as CompleteStep } from './steps/CompleteStep.svelte';

View file

@ -1,145 +0,0 @@
<script lang="ts">
/**
* Onboarding step: introduces the four LLM tiers and lets the user
* pick which ones Mana is allowed to use. Same routing semantics as
* the AiSettings card on the main settings page, but compressed into
* a single onboarding screen with a "you can change this anytime"
* note at the bottom.
*
* Default: nothing selected → user explicitly opts in.
*/
import { llmSettingsState, updateLlmSettings, tierLabel, type LlmTier } from '@mana/shared-llm';
import { isLocalLlmSupported } from '@mana/local-llm';
import { Robot, Cpu, HardDrive, Cloud, CheckCircle, Info } from '@mana/shared-icons';
const settings = $derived(llmSettingsState.current);
const webgpuSupported = isLocalLlmSupported();
function toggleTier(tier: LlmTier) {
const current = settings.allowedTiers;
const next = current.includes(tier) ? current.filter((t) => t !== tier) : [...current, tier];
updateLlmSettings({ allowedTiers: next });
}
const cards = [
{
tier: 'browser' as LlmTier,
icon: Cpu,
title: 'Auf deinem Gerät',
tagline: 'Maximale Privatsphäre',
description: 'Gemma 4 läuft direkt im Browser. ~500 MB einmaliger Download.',
privacyDot: 'bg-emerald-500',
disabled: !webgpuSupported,
disabledHint: 'Braucht WebGPU (Chrome 113+, Safari 18+).',
},
{
tier: 'mana-server' as LlmTier,
icon: HardDrive,
title: 'Mana-Server',
tagline: 'Selbst-gehostet',
description: 'Anfragen laufen zu unserem Server. Schneller, immer noch privat.',
privacyDot: 'bg-blue-500',
disabled: false,
},
{
tier: 'cloud' as LlmTier,
icon: Cloud,
title: 'Google Gemini',
tagline: 'Stärkstes Modell',
description: 'Beste Qualität für komplexe Aufgaben — Daten gehen zu Google.',
privacyDot: 'bg-amber-500',
disabled: false,
},
];
</script>
<div class="mx-auto max-w-2xl">
<div class="mb-6 text-center">
<div class="mx-auto mb-3 flex h-14 w-14 items-center justify-center rounded-full bg-primary/10">
<Robot size={28} class="text-primary" />
</div>
<div class="mb-2 text-2xl font-bold">Wie soll Mana KI nutzen?</div>
<p class="text-muted-foreground">
Mana bietet KI-Funktionen auf vier Ebenen — von "gar keine" bis zu allem. Du entscheidest,
welche Schichten dein Vertrauen haben.
</p>
</div>
<!-- Always-on tier 0 -->
<div class="mb-3 rounded-xl border border-emerald-500/30 bg-emerald-500/5 p-4">
<div class="flex items-center gap-3">
<CheckCircle size={20} weight="fill" class="text-emerald-500" />
<div class="flex-1">
<div class="font-semibold">Lokal (ohne KI) — immer aktiv</div>
<div class="text-sm text-muted-foreground">
Datum-Erkennung, Suche und einfache Klassifikation laufen offline ohne KI. Brauchst du
nichts auswählen — das ist immer da.
</div>
</div>
</div>
</div>
<!-- Toggleable tiers -->
<div class="space-y-3">
{#each cards as card}
{@const enabled = settings.allowedTiers.includes(card.tier)}
<button
type="button"
onclick={() => !card.disabled && toggleTier(card.tier)}
disabled={card.disabled}
class="w-full rounded-xl border p-4 text-left transition-all {enabled
? 'border-primary bg-primary/5 ring-1 ring-primary/30'
: 'border-border bg-card hover:border-primary/40'} {card.disabled
? 'cursor-not-allowed opacity-50'
: 'cursor-pointer'}"
>
<div class="flex items-start gap-3">
<div
class="flex h-10 w-10 shrink-0 items-center justify-center rounded-lg {enabled
? 'bg-primary/20 text-primary'
: 'bg-muted text-muted-foreground'}"
>
<card.icon size={20} />
</div>
<div class="min-w-0 flex-1">
<div class="flex items-center gap-2">
<div class="text-base font-semibold">{card.title}</div>
<span class="h-1.5 w-1.5 rounded-full {card.privacyDot}"></span>
<span class="text-xs text-muted-foreground">{card.tagline}</span>
{#if enabled}
<span
class="ml-auto rounded-full bg-primary px-2 py-0.5 text-[10px] font-medium text-primary-foreground"
>
aktiv
</span>
{/if}
</div>
<p class="mt-1 text-sm text-muted-foreground">{card.description}</p>
{#if card.disabled && card.disabledHint}
<p class="mt-1 text-xs text-amber-600 dark:text-amber-400">
{card.disabledHint}
</p>
{/if}
</div>
</div>
</button>
{/each}
</div>
<!-- Live chain summary -->
{#if settings.allowedTiers.length > 0}
<div class="mt-4 rounded-lg bg-muted/30 px-3 py-2 text-xs text-muted-foreground">
<span class="font-medium text-foreground">Reihenfolge: </span>
{settings.allowedTiers.map((t) => tierLabel(t)).join(' → ')} → Lokal (Fallback)
</div>
{/if}
<div class="mt-6 flex items-start gap-3 rounded-xl bg-primary/5 p-4">
<Info size={20} class="mt-0.5 shrink-0 text-primary" />
<div class="text-sm text-muted-foreground">
Du kannst diese Auswahl jederzeit in den Einstellungen ändern. Es ist auch komplett okay, hier
nichts auszuwählen — KI-Funktionen sind in Mana optional und alle Kern-Features funktionieren
ohne sie.
</div>
</div>
</div>

View file

@ -1,119 +0,0 @@
<script lang="ts">
import { Info } from '@mana/shared-icons';
// Apps overview step - shows available Mana apps
const APPS = [
{
id: 'todo',
name: 'Todo',
description: 'Aufgaben verwalten mit Projekten und Prioritäten',
icon: '✓',
color: 'bg-blue-500',
category: 'Produktivität',
},
{
id: 'calendar',
name: 'Calendar',
description: 'Termine und Ereignisse planen',
icon: '📅',
color: 'bg-green-500',
category: 'Produktivität',
},
{
id: 'contacts',
name: 'Contacts',
description: 'Kontakte organisieren und verwalten',
icon: '👤',
color: 'bg-purple-500',
category: 'Produktivität',
},
{
id: 'chat',
name: 'Chat',
description: 'KI-gestützte Konversationen',
icon: '💬',
color: 'bg-pink-500',
category: 'AI',
},
{
id: 'picture',
name: 'Picture',
description: 'Bilder mit KI generieren',
icon: '🎨',
color: 'bg-orange-500',
category: 'AI',
},
{
id: 'quotes',
name: 'Quotes',
description: 'Tägliche Inspiration mit Zitaten',
icon: '💡',
color: 'bg-yellow-500',
category: 'Lifestyle',
},
{
id: 'clock',
name: 'Clock',
description: 'Timer, Stoppuhr und Weltuhr',
icon: '⏰',
color: 'bg-red-500',
category: 'Utility',
},
{
id: 'cards',
name: 'Cards',
description: 'Karteikarten für effektives Lernen',
icon: '📚',
color: 'bg-indigo-500',
category: 'Bildung',
},
];
const categories = [...new Set(APPS.map((app) => app.category))];
</script>
<div class="max-w-2xl mx-auto">
<div class="text-center mb-8">
<h2 class="text-2xl font-bold mb-2">Entdecke die Mana-Apps</h2>
<p class="text-muted-foreground">Alle Apps sind mit deinem Mana-Konto verbunden.</p>
</div>
{#each categories as category}
<div class="mb-6">
<h3 class="text-sm font-medium text-muted-foreground mb-3">{category}</h3>
<div class="grid grid-cols-1 sm:grid-cols-2 gap-3">
{#each APPS.filter((a) => a.category === category) as app}
<div
class="p-4 rounded-xl bg-card border hover:border-primary/50 hover:shadow-sm transition-all cursor-pointer group"
>
<div class="flex items-start gap-3">
<div
class="{app.color} h-10 w-10 rounded-lg flex items-center justify-center text-white text-lg"
>
{app.icon}
</div>
<div class="flex-1 min-w-0">
<h4 class="font-semibold group-hover:text-primary transition-colors">{app.name}</h4>
<p class="text-sm text-muted-foreground line-clamp-2">{app.description}</p>
</div>
</div>
</div>
{/each}
</div>
</div>
{/each}
<div class="mt-8 p-4 rounded-xl bg-primary/5 border border-primary/20">
<div class="flex gap-3">
<div class="h-10 w-10 rounded-lg bg-primary/10 flex items-center justify-center shrink-0">
<Info size={20} class="text-primary" />
</div>
<div>
<h4 class="font-medium">Ein Konto, alle Apps</h4>
<p class="text-sm text-muted-foreground">
Dein Mana-Login funktioniert in allen Apps. Deine Credits werden geteilt.
</p>
</div>
</div>
</div>
</div>

View file

@ -1,111 +0,0 @@
<script lang="ts">
import { Check, Gear, House, SquaresFour, CurrencyCircleDollar } from '@mana/shared-icons';
// Completion step with celebration
</script>
<div class="text-center max-w-md mx-auto">
<!-- Celebration animation -->
<div class="mb-8 relative">
<div
class="inline-flex h-24 w-24 rounded-full bg-gradient-to-br from-green-400 to-emerald-500 items-center justify-center shadow-lg shadow-green-500/25 animate-bounce"
>
<Check size={20} class="text-white" />
</div>
<!-- Confetti-like decorations -->
<div class="absolute top-0 left-1/2 -translate-x-1/2 -translate-y-2">
<span class="text-2xl animate-pulse">🎉</span>
</div>
<div class="absolute top-4 left-8">
<span class="text-xl animate-bounce" style="animation-delay: 0.1s;"></span>
</div>
<div class="absolute top-4 right-8">
<span class="text-xl animate-bounce" style="animation-delay: 0.2s;">🌟</span>
</div>
</div>
<h2 class="text-3xl font-bold mb-3">Alles bereit!</h2>
<p class="text-lg text-muted-foreground mb-8">
Du kannst jetzt loslegen und alle Mana-Apps nutzen.
</p>
<!-- Quick actions -->
<div class="grid grid-cols-1 sm:grid-cols-2 gap-3 text-left mb-8">
<a
href="/"
class="p-4 rounded-xl bg-card border hover:border-primary/50 hover:shadow-md transition-all group"
>
<div class="flex items-center gap-3">
<div
class="h-10 w-10 rounded-lg bg-primary/10 flex items-center justify-center group-hover:bg-primary/20 transition-colors"
>
<House size={20} class="text-primary" />
</div>
<div>
<div class="font-semibold group-hover:text-primary transition-colors">Dashboard</div>
<div class="text-xs text-muted-foreground">Dein Überblick</div>
</div>
</div>
</a>
<a
href="/"
class="p-4 rounded-xl bg-card border hover:border-primary/50 hover:shadow-md transition-all group"
>
<div class="flex items-center gap-3">
<div
class="h-10 w-10 rounded-lg bg-blue-100 dark:bg-blue-900/30 flex items-center justify-center group-hover:bg-blue-200 dark:group-hover:bg-blue-900/50 transition-colors"
>
<SquaresFour size={20} class="text-blue-600 dark:text-blue-400" />
</div>
<div>
<div class="font-semibold group-hover:text-blue-600 transition-colors">Workbench</div>
<div class="text-xs text-muted-foreground">Deine Apps entdecken</div>
</div>
</div>
</a>
<a
href="/?app=credits"
class="p-4 rounded-xl bg-card border hover:border-primary/50 hover:shadow-md transition-all group"
>
<div class="flex items-center gap-3">
<div
class="h-10 w-10 rounded-lg bg-yellow-100 dark:bg-yellow-900/30 flex items-center justify-center group-hover:bg-yellow-200 dark:group-hover:bg-yellow-900/50 transition-colors"
>
<CurrencyCircleDollar size={20} class="text-yellow-600 dark:text-yellow-400" />
</div>
<div>
<div class="font-semibold group-hover:text-yellow-600 transition-colors">Credits</div>
<div class="text-xs text-muted-foreground">150 Gratis-Credits</div>
</div>
</div>
</a>
<a
href="/?app=settings"
class="p-4 rounded-xl bg-card border hover:border-primary/50 hover:shadow-md transition-all group"
>
<div class="flex items-center gap-3">
<div
class="h-10 w-10 rounded-lg bg-gray-100 dark:bg-gray-800 flex items-center justify-center group-hover:bg-gray-200 dark:group-hover:bg-gray-700 transition-colors"
>
<Gear size={20} class="text-gray-600 dark:text-gray-400" />
</div>
<div>
<div
class="font-semibold group-hover:text-gray-600 dark:group-hover:text-gray-300 transition-colors"
>
Einstellungen
</div>
<div class="text-xs text-muted-foreground">Personalisieren</div>
</div>
</div>
</a>
</div>
<p class="text-sm text-muted-foreground">
Klicke auf "Los geht's!" um zum Dashboard zu gelangen.
</p>
</div>

View file

@ -1,55 +0,0 @@
<!--
Onboarding Context Step — 5 key questions to populate the user context.
Uses the same ContextInterview component in compact mode.
-->
<script lang="ts">
import ContextInterview from '$lib/modules/profile/ContextInterview.svelte';
</script>
<div class="context-step">
<div class="step-header">
<h2 class="step-title">Erzähl Mana über dich</h2>
<p class="step-subtitle">
Je besser Mana dich kennt, desto relevanter werden Vorschläge und Automatisierungen. Du kannst
alles jederzeit im Profil ändern oder ergänzen.
</p>
</div>
<div class="interview-area">
<ContextInterview
compact
limitCategories={['about', 'routine', 'leisure', 'nutrition', 'goals']}
/>
</div>
</div>
<style>
.context-step {
display: flex;
flex-direction: column;
gap: 1rem;
height: 100%;
}
.step-header {
text-align: center;
}
.step-title {
margin: 0;
font-size: 1.25rem;
font-weight: 600;
}
.step-subtitle {
margin: 0.5rem 0 0;
font-size: 0.875rem;
color: hsl(var(--color-muted-foreground));
max-width: 32rem;
margin-inline: auto;
}
.interview-area {
flex: 1;
min-height: 0;
overflow-y: auto;
}
</style>

View file

@ -1,109 +0,0 @@
<script lang="ts">
import { Check, CurrencyCircleDollar, Gift, Lightbulb } from '@mana/shared-icons';
// Credits explanation step
</script>
<div class="max-w-xl mx-auto">
<div class="text-center mb-8">
<div
class="inline-flex h-16 w-16 rounded-2xl bg-gradient-to-br from-yellow-400 to-orange-500 items-center justify-center mb-4 shadow-lg shadow-orange-500/25"
>
<CurrencyCircleDollar size={32} class="text-white" />
</div>
<h2 class="text-2xl font-bold mb-2">Das Credits-System</h2>
<p class="text-muted-foreground">Einfach und transparent für alle AI-Features.</p>
</div>
<div class="space-y-4">
<!-- How it works -->
<div class="p-5 rounded-xl bg-card border">
<h3 class="font-semibold mb-3 flex items-center gap-2">
<span
class="h-6 w-6 rounded-full bg-primary/10 flex items-center justify-center text-sm text-primary"
>1</span
>
So funktioniert's
</h3>
<ul class="space-y-2 text-sm text-muted-foreground">
<li class="flex items-start gap-2">
<Check size={20} class="text-green-500 mt-0.5 shrink-0" />
<span>Du hast ein Credit-Guthaben für alle Mana-Apps</span>
</li>
<li class="flex items-start gap-2">
<Check size={20} class="text-green-500 mt-0.5 shrink-0" />
<span>AI-Features wie Chat, Bildgenerierung etc. kosten Credits</span>
</li>
<li class="flex items-start gap-2">
<Check size={20} class="text-green-500 mt-0.5 shrink-0" />
<span>Einfache Preisregel: <strong class="text-foreground">1 Mana = 1 Cent</strong></span>
</li>
</ul>
</div>
<!-- Free credits -->
<div
class="p-5 rounded-xl bg-gradient-to-br from-green-50 to-emerald-50 dark:from-green-900/20 dark:to-emerald-900/20 border border-green-200 dark:border-green-800"
>
<h3 class="font-semibold mb-3 flex items-center gap-2">
<span
class="h-6 w-6 rounded-full bg-green-500 flex items-center justify-center text-sm text-white"
>
<Gift size={16} />
</span>
Gratis-Credits
</h3>
<div class="grid grid-cols-2 gap-4">
<div class="bg-white dark:bg-background rounded-lg p-3 text-center">
<div class="text-2xl font-bold text-green-600">150</div>
<div class="text-xs text-muted-foreground">Willkommensbonus</div>
</div>
<div class="bg-white dark:bg-background rounded-lg p-3 text-center">
<div class="text-2xl font-bold text-green-600">+5</div>
<div class="text-xs text-muted-foreground">Täglich gratis</div>
</div>
</div>
</div>
<!-- Credit packages -->
<div class="p-5 rounded-xl bg-card border">
<h3 class="font-semibold mb-3 flex items-center gap-2">
<span
class="h-6 w-6 rounded-full bg-primary/10 flex items-center justify-center text-sm text-primary"
>2</span
>
Credit-Pakete
</h3>
<div class="grid grid-cols-2 sm:grid-cols-4 gap-2">
<div class="p-3 rounded-lg bg-muted text-center">
<div class="font-bold">100</div>
<div class="text-xs text-muted-foreground">€1</div>
</div>
<div class="p-3 rounded-lg bg-muted text-center">
<div class="font-bold">500</div>
<div class="text-xs text-muted-foreground">€5</div>
</div>
<div class="p-3 rounded-lg bg-muted text-center">
<div class="font-bold">1.500</div>
<div class="text-xs text-muted-foreground">€15</div>
</div>
<div class="p-3 rounded-lg bg-muted text-center">
<div class="font-bold">5.000</div>
<div class="text-xs text-muted-foreground">€50</div>
</div>
</div>
</div>
<!-- Pro tip -->
<div
class="p-4 rounded-xl bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800"
>
<div class="flex gap-3">
<Lightbulb size={20} class="text-blue-500 shrink-0 mt-0.5" />
<p class="text-sm text-blue-800 dark:text-blue-200">
<strong>Tipp:</strong> Mit einer Mana Quelle M erhältst du monatlich 1.000 Mana ab 9,99€/Monat.
</p>
</div>
</div>
</div>
</div>

View file

@ -1,200 +0,0 @@
<script lang="ts">
import { _ } from 'svelte-i18n';
import { authStore } from '$lib/stores/auth.svelte';
import { profileService } from '$lib/api/profile';
import { Camera, Check } from '@mana/shared-icons';
let { nameValue = $bindable('') }: { nameValue?: string } = $props();
let name = $state(authStore.user?.name || '');
// Sync name to parent via bindable
$effect(() => {
nameValue = name;
});
let avatarPreview = $state<string | null>(authStore.user?.image || null);
let selectedFile = $state<File | null>(null);
let saving = $state(false);
let saved = $state(false);
let error = $state<string | null>(null);
let fileInput: HTMLInputElement;
function handleFileSelect(event: Event) {
const input = event.target as HTMLInputElement;
const file = input.files?.[0];
if (!file) return;
if (!file.type.startsWith('image/')) {
error = 'Bitte wähle ein Bild aus';
return;
}
if (file.size > 5 * 1024 * 1024) {
error = 'Max. 5MB erlaubt';
return;
}
selectedFile = file;
error = null;
const reader = new FileReader();
reader.onload = (e) => {
avatarPreview = e.target?.result as string;
};
reader.readAsDataURL(file);
}
function triggerFileInput() {
fileInput?.click();
}
async function saveProfile() {
if (!name.trim()) {
error = 'Bitte gib deinen Namen ein';
return;
}
saving = true;
error = null;
try {
// Upload avatar if selected
if (selectedFile) {
await profileService.uploadAvatar(selectedFile);
}
// Update name
await profileService.updateProfile({ name: name.trim() });
saved = true;
// Refresh auth store
// The profile update should update the user in authStore
} catch (e) {
error = e instanceof Error ? e.message : $_('common.error_saving');
} finally {
saving = false;
}
}
function getInitials(name: string): string {
return (
name
.split(' ')
.map((n) => n[0])
.join('')
.toUpperCase()
.slice(0, 2) || 'U'
);
}
</script>
<div class="max-w-md mx-auto">
<div class="text-center mb-8">
<h2 class="text-2xl font-bold mb-2">Vervollständige dein Profil</h2>
<p class="text-muted-foreground">Füge ein Profilbild und deinen Namen hinzu.</p>
</div>
<div class="space-y-6">
<!-- Avatar Upload -->
<div class="flex flex-col items-center">
<input
bind:this={fileInput}
type="file"
accept="image/*"
class="hidden"
onchange={handleFileSelect}
/>
<button onclick={triggerFileInput} class="relative group">
{#if avatarPreview}
<img
src={avatarPreview}
alt="Avatar"
class="h-28 w-28 rounded-full object-cover border-4 border-border group-hover:border-primary/50 transition-colors"
/>
{:else}
<div
class="h-28 w-28 rounded-full bg-primary/10 flex items-center justify-center text-primary text-3xl font-semibold border-4 border-border group-hover:border-primary/50 transition-colors"
>
{getInitials(name || 'U')}
</div>
{/if}
<!-- Camera overlay -->
<div
class="absolute inset-0 rounded-full bg-black/50 opacity-0 group-hover:opacity-100 transition-opacity flex items-center justify-center"
>
<Camera size={20} class="text-white" />
</div>
</button>
<p class="mt-2 text-sm text-muted-foreground">Klicke zum Ändern</p>
</div>
<!-- Name Input -->
<div>
<label for="onboarding-name" class="block text-sm font-medium mb-2">Dein Name</label>
<input
id="onboarding-name"
type="text"
bind:value={name}
placeholder="Max Mustermann"
class="w-full px-4 py-3 border rounded-xl bg-background focus:outline-none focus:ring-2 focus:ring-primary/50 text-lg"
/>
</div>
<!-- Email (readonly) -->
<div>
<p class="block text-sm font-medium mb-2 text-muted-foreground">E-Mail</p>
<div class="px-4 py-3 border rounded-xl bg-muted text-muted-foreground">
{authStore.user?.email || 'Nicht verfügbar'}
</div>
</div>
{#if error}
<div
class="p-3 bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-lg"
>
<p class="text-sm text-red-600 dark:text-red-400">{error}</p>
</div>
{/if}
{#if saved}
<div
class="p-3 bg-green-50 dark:bg-green-900/20 border border-green-200 dark:border-green-800 rounded-lg"
>
<p class="text-sm text-green-600 dark:text-green-400 flex items-center gap-2">
<Check size={20} />
Profil gespeichert!
</p>
</div>
{:else}
<button
onclick={saveProfile}
disabled={saving || !name.trim()}
class="w-full px-4 py-3 bg-card border rounded-xl hover:bg-muted transition-colors disabled:opacity-50 font-medium flex items-center justify-center gap-2"
>
{#if saving}
<svg class="animate-spin h-5 w-5" fill="none" viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"
></circle>
<path
class="opacity-75"
fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"
></path>
</svg>
Speichern...
{:else}
<Check size={20} />
Jetzt speichern
{/if}
</button>
{/if}
<p class="text-center text-sm text-muted-foreground">
Du kannst dein Profil jederzeit in den Einstellungen ändern.
</p>
</div>
</div>

View file

@ -1,158 +0,0 @@
<script lang="ts">
/**
* Onboarding step: introduces Cloud Sync and lets the user decide
* whether to activate it. Shows pricing and explains the local-first
* model — sync is optional, data always stays local.
*/
import { syncBilling } from '$lib/stores/sync-billing.svelte';
import { creditsService, type CreditBalance } from '$lib/api/credits';
import { Cloud, DeviceMobile, Info, CheckCircle, Warning } from '@mana/shared-icons';
import type { BillingInterval } from '$lib/api/sync';
import { onMount } from 'svelte';
const SYNC_PRICES: Record<BillingInterval, { credits: number; label: string }> = {
monthly: { credits: 30, label: 'Monatlich' },
quarterly: { credits: 90, label: 'Quartalsweise' },
yearly: { credits: 360, label: 'Jährlich' },
};
let balance = $state<CreditBalance | null>(null);
let selectedInterval = $state<BillingInterval>('monthly');
let activating = $state(false);
let error = $state<string | null>(null);
onMount(async () => {
try {
balance = await creditsService.getBalance();
} catch {
// Non-critical
}
});
async function handleActivate() {
activating = true;
error = null;
try {
await syncBilling.activate(selectedInterval);
} catch (e) {
error = e instanceof Error ? e.message : 'Aktivierung fehlgeschlagen';
} finally {
activating = false;
}
}
</script>
<div class="mx-auto max-w-2xl">
<div class="mb-6 text-center">
<div class="mx-auto mb-3 flex h-14 w-14 items-center justify-center rounded-full bg-primary/10">
<Cloud size={28} class="text-primary" />
</div>
<div class="mb-2 text-2xl font-bold">Cloud Sync</div>
<p class="text-muted-foreground">
Synchronisiere deine Daten verschlüsselt über alle Geräte — oder nutze Mana nur lokal.
</p>
</div>
<!-- Local-first info -->
<div class="mb-4 rounded-xl border border-emerald-500/30 bg-emerald-500/5 p-4">
<div class="flex items-center gap-3">
<DeviceMobile size={20} class="text-emerald-500" />
<div class="flex-1">
<div class="font-semibold">Lokal — immer verfügbar</div>
<div class="text-sm text-muted-foreground">
Alle deine Daten sind lokal auf deinem Gerät gespeichert. Mana funktioniert vollständig
offline — auch ohne Cloud Sync.
</div>
</div>
</div>
</div>
<!-- Sync option -->
{#if syncBilling.active}
<div class="rounded-xl border border-primary bg-primary/5 p-4">
<div class="flex items-center gap-3">
<CheckCircle size={20} weight="fill" class="text-primary" />
<div>
<div class="font-semibold">Cloud Sync ist aktiv</div>
<div class="text-sm text-muted-foreground">
Deine Daten werden über alle Geräte synchronisiert.
</div>
</div>
</div>
</div>
{:else}
<div class="rounded-xl border border-border bg-card p-4">
<div class="flex items-start gap-3 mb-4">
<div
class="flex h-10 w-10 shrink-0 items-center justify-center rounded-lg bg-primary/10 text-primary"
>
<Cloud size={20} />
</div>
<div>
<div class="text-base font-semibold">Cloud Sync aktivieren</div>
<p class="mt-1 text-sm text-muted-foreground">
Multi-Device-Sync, automatische Backups, Ende-zu-Ende-Verschlüsselung.
</p>
</div>
</div>
<!-- Interval selection -->
<div class="space-y-2 mb-4">
{#each ['monthly', 'quarterly', 'yearly'] as const as iv}
<label
class="flex items-center justify-between rounded-lg border p-3 cursor-pointer transition-colors {selectedInterval ===
iv
? 'border-primary bg-primary/5'
: 'border-border hover:border-primary/40'}"
>
<div class="flex items-center gap-2">
<input
type="radio"
name="sync-interval"
value={iv}
checked={selectedInterval === iv}
onchange={() => (selectedInterval = iv)}
class="h-3.5 w-3.5 text-primary"
/>
<span class="text-sm font-medium">{SYNC_PRICES[iv].label}</span>
</div>
<span class="text-sm font-bold text-primary">{SYNC_PRICES[iv].credits} Credits</span>
</label>
{/each}
</div>
{#if error}
<div class="mb-3 flex items-center gap-2 rounded-lg bg-red-50 dark:bg-red-900/20 p-3">
<Warning size={16} class="text-red-600 dark:text-red-400" />
<p class="text-sm text-red-800 dark:text-red-200">{error}</p>
</div>
{/if}
<button
onclick={handleActivate}
disabled={activating ||
(balance !== null && balance.balance < SYNC_PRICES[selectedInterval].credits)}
class="w-full rounded-lg bg-primary py-2.5 text-sm font-semibold text-primary-foreground hover:bg-primary/90 transition-colors disabled:opacity-50"
>
{activating
? 'Wird aktiviert...'
: `Sync aktivieren (${SYNC_PRICES[selectedInterval].credits} Credits)`}
</button>
{#if balance !== null && balance.balance < SYNC_PRICES[selectedInterval].credits}
<p class="mt-2 text-center text-xs text-amber-600 dark:text-amber-400">
Nicht genügend Credits ({balance.balance} verfügbar). Du kannst Sync jederzeit später in den
Einstellungen aktivieren.
</p>
{/if}
</div>
{/if}
<div class="mt-6 flex items-start gap-3 rounded-xl bg-primary/5 p-4">
<Info size={20} class="mt-0.5 shrink-0 text-primary" />
<div class="text-sm text-muted-foreground">
Sync ist optional — du kannst diesen Schritt überspringen und Mana nur lokal nutzen. Alle
Features funktionieren auch ohne Sync. Du kannst jederzeit in den Einstellungen aktivieren.
</div>
</div>
</div>

View file

@ -1,61 +0,0 @@
<script lang="ts">
import { Palette, SquaresFour, CurrencyCircleDollar } from '@mana/shared-icons';
// Welcome step - introduces Mana
</script>
<div class="text-center">
<!-- Hero Icon -->
<div class="mb-4">
<div
class="inline-flex h-16 w-16 rounded-xl bg-gradient-to-br from-primary via-primary/80 to-primary/60 items-center justify-center shadow-lg shadow-primary/25"
>
<span class="text-3xl text-primary-foreground font-bold">M</span>
</div>
</div>
<h2 class="text-xl font-bold mb-2">Willkommen bei Mana!</h2>
<p class="text-sm text-muted-foreground mb-4">
Dein zentrales Dashboard für alle Mana-Apps. Verwalte deine Aufgaben, Termine, Kontakte und mehr
- alles an einem Ort.
</p>
<!-- Feature highlights - using surface-elevated-1 (one level below the modal) -->
<div class="space-y-2 text-left">
<div class="p-3 rounded-lg bg-surface-elevated-1 border flex items-start gap-3">
<div
class="h-8 w-8 rounded-md bg-blue-100 dark:bg-blue-900/30 flex items-center justify-center flex-shrink-0"
>
<SquaresFour size={16} class="text-blue-600 dark:text-blue-400" />
</div>
<div>
<h3 class="font-medium text-sm">Alle Apps</h3>
<p class="text-xs text-muted-foreground">Todo, Kalender, Chat, Picture und mehr.</p>
</div>
</div>
<div class="p-3 rounded-lg bg-surface-elevated-1 border flex items-start gap-3">
<div
class="h-8 w-8 rounded-md bg-green-100 dark:bg-green-900/30 flex items-center justify-center flex-shrink-0"
>
<CurrencyCircleDollar size={16} class="text-green-600 dark:text-green-400" />
</div>
<div>
<h3 class="font-medium text-sm">Credits-System</h3>
<p class="text-xs text-muted-foreground">Ein Guthaben für alle AI-Features.</p>
</div>
</div>
<div class="p-3 rounded-lg bg-surface-elevated-1 border flex items-start gap-3">
<div
class="h-8 w-8 rounded-md bg-purple-100 dark:bg-purple-900/30 flex items-center justify-center flex-shrink-0"
>
<Palette size={20} class="text-purple-600 dark:text-purple-400" />
</div>
<div>
<h3 class="font-medium text-sm">Personalisierung</h3>
<p class="text-xs text-muted-foreground">Themes und Einstellungen nach deinem Geschmack.</p>
</div>
</div>
</div>
</div>

View file

@ -9,8 +9,6 @@ export const STORAGE_KEYS = {
NAV_COLLAPSED: 'mana-nav-collapsed',
/** Whether the welcome page has been seen */
HAS_SEEN_WELCOME: 'hasSeenWelcome',
/** Onboarding wizard state (JSON) */
ONBOARDING: 'mana-onboarding',
/** Dashboard widget layout (JSON) — defined in default-dashboard.ts */
// DASHBOARD: 'mana-dashboard-config',
} as const;

View file

@ -1,124 +0,0 @@
/**
* Onboarding Store
* Tracks user onboarding progress and completion status
*/
import { STORAGE_KEYS } from '$lib/config/storage-keys';
const STORAGE_KEY = STORAGE_KEYS.ONBOARDING;
interface OnboardingState {
completed: boolean;
currentStep: number;
completedSteps: string[];
skipped: boolean;
startedAt: string | null;
completedAt: string | null;
}
function createOnboardingStore() {
let state = $state<OnboardingState>({
completed: false,
currentStep: 0,
completedSteps: [],
skipped: false,
startedAt: null,
completedAt: null,
});
// Load from localStorage on init
function load() {
if (typeof localStorage === 'undefined') return;
const saved = localStorage.getItem(STORAGE_KEY);
if (saved) {
try {
const parsed = JSON.parse(saved);
state = { ...state, ...parsed };
} catch {
// Ignore parse errors
}
}
}
// Save to localStorage
function save() {
if (typeof localStorage === 'undefined') return;
localStorage.setItem(STORAGE_KEY, JSON.stringify(state));
}
return {
get completed() {
return state.completed;
},
get currentStep() {
return state.currentStep;
},
get completedSteps() {
return state.completedSteps;
},
get skipped() {
return state.skipped;
},
get shouldShow() {
return !state.completed && !state.skipped;
},
load,
start() {
state.startedAt = new Date().toISOString();
state.currentStep = 0;
save();
},
nextStep() {
state.currentStep++;
save();
},
prevStep() {
if (state.currentStep > 0) {
state.currentStep--;
save();
}
},
goToStep(step: number) {
state.currentStep = step;
save();
},
completeStep(stepId: string) {
if (!state.completedSteps.includes(stepId)) {
state.completedSteps = [...state.completedSteps, stepId];
save();
}
},
complete() {
state.completed = true;
state.completedAt = new Date().toISOString();
save();
},
skip() {
state.skipped = true;
save();
},
reset() {
state = {
completed: false,
currentStep: 0,
completedSteps: [],
skipped: false,
startedAt: null,
completedAt: null,
};
save();
},
};
}
export const onboardingStore = createOnboardingStore();

View file

@ -80,7 +80,6 @@
import { userSettings } from '$lib/stores/user-settings.svelte';
import { isNavCollapsed as collapsedStore } from '$lib/stores/navigation';
import { getPillAppItems } from '@mana/shared-branding';
import { onboardingStore } from '$lib/stores/onboarding.svelte';
import { STORAGE_KEYS } from '$lib/config/storage-keys';
import { SearchRegistry } from '$lib/search/registry';
import { registerAllProviders } from '$lib/search/providers';
@ -146,7 +145,6 @@
// ── UI State ────────────────────────────────────────────
let isCollapsed = $state(false);
let showShortcuts = $state(false);
let showOnboarding = $state(false);
// ── Theme ───────────────────────────────────────────────
let isDark = $derived(theme.isDark);
@ -459,7 +457,6 @@
// where {@const} narrows the component back to its concrete type.
type AnyComponent = Component<any>;
let KeyboardShortcutsModalC = $state<AnyComponent | null>(null);
let OnboardingWizardC = $state<AnyComponent | null>(null);
let GuestWelcomeModalC = $state<AnyComponent | null>(null);
let SessionWarningC = $state<AnyComponent | null>(null);
let EncryptionIntroBannerC = $state<AnyComponent | null>(null);
@ -474,13 +471,6 @@
});
}
});
$effect(() => {
if (showOnboarding && !OnboardingWizardC) {
import('$lib/components/onboarding').then((m) => {
OnboardingWizardC = m.OnboardingWizard;
});
}
});
$effect(() => {
if (guestMode?.showWelcome && !GuestWelcomeModalC) {
import('@mana/shared-auth-ui').then((m) => {
@ -507,19 +497,6 @@
});
});
// ── Onboarding ──────────────────────────────────────────
function handleOnboardingComplete() {
onboardingStore.complete();
ManaEvents.onboardingCompleted();
showOnboarding = false;
}
function handleOnboardingSkip() {
ManaEvents.onboardingSkipped(onboardingStore.currentStep);
onboardingStore.skip();
showOnboarding = false;
}
// ── Auth Ready (replaces monolith onMount) ──────────────
async function handleAuthReady() {
// Wire the unified guest-prompt singleton to SvelteKit's `goto`
@ -623,19 +600,10 @@
// value (0 on a fresh tab) until a sync actually runs.
refreshPendingCount();
// Phase B-idle: settings, onboarding gating and return-visit
// telemetry. None of this gates rendering — onboarding shows
// via showOnboarding after the store resolves, which is fine
// on a delay.
// Phase B-idle: settings + return-visit telemetry. Non-gating.
idle(async () => {
trackReturnVisit();
userSettings.load().catch(() => {});
await onboardingStore.load();
if (onboardingStore.shouldShow) {
onboardingStore.start();
ManaEvents.onboardingStarted();
showOnboarding = true;
}
});
}
// IMPORTANT: do NOT call notificationService.requestPermission() here.
@ -775,16 +743,6 @@
appName="Mana"
locale={($locale || 'de') === 'de' ? 'de' : 'en'}
>
<!-- Onboarding Wizard (auth only) — loaded on demand -->
{#if showOnboarding && authStore.isAuthenticated && OnboardingWizardC}
{@const OnboardingWizard = OnboardingWizardC}
<div
class="fixed inset-0 z-50 flex items-center justify-center bg-background/95 backdrop-blur-sm"
>
<OnboardingWizard onComplete={handleOnboardingComplete} onSkip={handleOnboardingSkip} />
</div>
{/if}
<div class="min-h-screen" class:bg-background={!wallpaperStore.hasWallpaper}>
<WallpaperLayer config={wallpaperStore.effective} />