feat(contacts): add onboarding wizard — sort preference, import option, tips

3-step onboarding using shared-app-onboarding package (same as calendar):
1. Sort order: first name vs last name
2. Import: Google, vCard/CSV, or skip — navigates to import page on completion
3. Tips: self-contact card, quick input, focus mode, tags

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Till JS 2026-03-21 11:31:38 +01:00
parent 78526f1d92
commit 16fe3aa61e
4 changed files with 572 additions and 446 deletions

View file

@ -36,6 +36,7 @@
},
"dependencies": {
"@manacore/shared-api-client": "workspace:*",
"@manacore/shared-app-onboarding": "workspace:*",
"@manacore/shared-auth": "workspace:*",
"@manacore/shared-auth-ui": "workspace:*",
"@manacore/shared-branding": "workspace:*",

View file

@ -0,0 +1,112 @@
import { createAppOnboardingStore, type AppOnboardingStep } from '@manacore/shared-app-onboarding';
import { userSettings } from './user-settings.svelte';
import { contactsFilterStore } from './filter.svelte';
/**
* Contacts-specific onboarding steps
*/
const contactsOnboardingSteps: AppOnboardingStep[] = [
{
id: 'sortOrder',
type: 'select',
question: 'Wie sortierst du Kontakte?',
description: 'Bestimmt die Reihenfolge deiner Kontaktliste.',
emoji: '🔤',
gradient: { from: 'blue-500', to: 'blue-700' },
options: [
{
id: 'firstName',
label: 'Vorname',
description: 'Anna, Max, Till ...',
emoji: '👤',
},
{
id: 'lastName',
label: 'Nachname',
description: 'Müller, Schmidt, Weber ...',
emoji: '📋',
},
],
defaultValue: 'firstName',
},
{
id: 'importSource',
type: 'select',
question: 'Kontakte importieren?',
description: 'Du kannst jederzeit später über Daten importieren.',
emoji: '📥',
gradient: { from: 'indigo-500', to: 'indigo-700' },
options: [
{
id: 'google',
label: 'Google Kontakte',
description: 'Über dein Google-Konto',
emoji: '🔗',
},
{
id: 'file',
label: 'Datei (vCard/CSV)',
description: 'Aus einer exportierten Datei',
emoji: '📄',
},
{
id: 'later',
label: 'Später',
description: 'Erstmal ohne Import starten',
emoji: '⏭️',
},
],
defaultValue: 'later',
},
{
id: 'welcome',
type: 'info',
question: 'Deine Kontakte sind bereit!',
description: 'Hier sind einige Tipps für den Start:',
emoji: '🎉',
gradient: { from: 'primary', to: 'primary/70' },
bullets: [
'Deine eigene Kontaktkarte ist schon angelegt — klick sie an und füll sie aus',
'Nutze die Schnelleingabe unten, um Kontakte per Text zu erstellen',
'Drücke "F" für den Fokus-Modus ohne Ablenkungen',
'Tagge Kontakte für bessere Organisation',
],
},
];
/**
* Contacts app onboarding store
*
* Usage in components:
* ```svelte
* <script>
* import { contactsOnboarding } from '$lib/stores/app-onboarding.svelte';
* import { MiniOnboardingModal } from '@manacore/shared-app-onboarding';
* </script>
*
* {#if contactsOnboarding.shouldShow}
* <MiniOnboardingModal
* store={contactsOnboarding}
* appName="Kontakte"
* appEmoji="👥"
* />
* {/if}
* ```
*/
export const contactsOnboarding = createAppOnboardingStore({
appId: 'contacts',
steps: contactsOnboardingSteps,
userSettings,
onComplete: async (preferences) => {
// Apply sort order preference
const sortOrder = preferences.sortOrder as string;
if (sortOrder === 'firstName' || sortOrder === 'lastName') {
contactsFilterStore.setSortField(sortOrder);
}
// Import navigation is handled by the layout after onboarding completes
},
onSkip: async () => {
// Defaults are sensible, nothing to do
},
});

View file

@ -42,6 +42,8 @@
formatParsedContactPreview,
} from '$lib/utils/contact-parser';
import ContactsToolbar from '$lib/components/ContactsToolbar.svelte';
import { contactsOnboarding } from '$lib/stores/app-onboarding.svelte';
import { MiniOnboardingModal } from '@manacore/shared-app-onboarding';
// Tags state for Quick-Create
let availableTags = $state<{ id: string; name: string }[]>([]);
@ -243,6 +245,22 @@
});
}
// Navigate to import page after onboarding if user chose to import
let previousOnboardingShow = true;
$effect(() => {
const showing = contactsOnboarding.shouldShow;
if (previousOnboardingShow && !showing) {
// Onboarding just closed
const importSource = contactsOnboarding.preferences.importSource as string;
if (importSource === 'google') {
goto('/data?tab=import&source=google');
} else if (importSource === 'file') {
goto('/data?tab=import&source=file');
}
}
previousOnboardingShow = showing;
});
onMount(async () => {
// Initialize auth and redirect if not authenticated
await authStore.initialize();
@ -367,6 +385,11 @@
{#if newContactModalStore.isOpen}
<NewContactModal onClose={() => newContactModalStore.close()} />
{/if}
<!-- Onboarding Modal -->
{#if contactsOnboarding.shouldShow}
<MiniOnboardingModal store={contactsOnboarding} appName="Kontakte" appEmoji="👥" />
{/if}
</div>
</SplitPaneContainer>

882
pnpm-lock.yaml generated

File diff suppressed because it is too large Load diff