managarten/packages/shared-app-onboarding/src/create-app-onboarding.svelte.ts
Till-JS b92b9bd2b5 feat(onboarding): add app-specific mini-onboarding system
- Create @manacore/shared-app-onboarding package with:
  - createAppOnboardingStore factory function (Svelte 5 runes)
  - MiniOnboardingModal component for select/toggle/info steps
  - TypeScript types for flexible step configuration
- Integrate into Calendar app with questions for:
  - Week start (Monday/Sunday)
  - Default view (Day/Week/Month)
  - Timezone preference (Auto/Manual)
  - Welcome tips

The mini-onboarding stores completion state in deviceSettings,
allowing per-device, per-app onboarding experiences.
2026-02-16 12:50:04 +01:00

232 lines
5.1 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import type {
AppOnboardingConfig,
AppOnboardingStore,
AppOnboardingPreferences,
AppOnboardingStep,
} from './types';
const ONBOARDING_COMPLETED_KEY = 'onboarding_completed';
const ONBOARDING_PREFERENCES_KEY = 'onboarding_preferences';
/**
* Create an app-specific onboarding store
*
* This factory creates a store that:
* - Checks if onboarding was already completed (via deviceSettings)
* - Manages step navigation and preference collection
* - Saves completion state and preferences to deviceSettings
*
* @example
* ```typescript
* import { createAppOnboardingStore } from '@manacore/shared-app-onboarding';
*
* const calendarOnboarding = createAppOnboardingStore({
* appId: 'calendar',
* steps: [
* {
* id: 'weekStart',
* type: 'select',
* question: 'Wann beginnt deine Woche?',
* emoji: '📅',
* options: [
* { id: 'monday', label: 'Montag', emoji: '1⃣' },
* { id: 'sunday', label: 'Sonntag', emoji: '7⃣' },
* ],
* defaultValue: 'monday',
* },
* ],
* userSettings,
* });
* ```
*/
export function createAppOnboardingStore(config: AppOnboardingConfig): AppOnboardingStore {
const { appId, steps, userSettings, onComplete, onSkip } = config;
// State
let currentStep = $state(0);
let preferences = $state<AppOnboardingPreferences>({});
let saving = $state(false);
let completed = $state(false);
// Initialize preferences with default values
for (const step of steps) {
if (step.type === 'select' && step.defaultValue !== undefined) {
preferences[step.id] = step.defaultValue;
} else if (step.type === 'toggle' && step.defaultValue !== undefined) {
preferences[step.id] = step.defaultValue;
}
}
// Derived values
const totalSteps = $derived(steps.length);
const isFirstStep = $derived(currentStep === 0);
const isLastStep = $derived(currentStep === steps.length - 1);
const progress = $derived(((currentStep + 1) / steps.length) * 100);
const currentStepConfig = $derived<AppOnboardingStep | undefined>(steps[currentStep]);
/**
* Check if onboarding was already completed
*/
function checkCompleted(): boolean {
const deviceAppSettings = userSettings.getDeviceAppSettings();
return deviceAppSettings[ONBOARDING_COMPLETED_KEY] === true;
}
// Derived: should show modal
const shouldShow = $derived(!completed && !checkCompleted() && userSettings.loaded);
/**
* Go to next step
*/
function next(): void {
if (currentStep < steps.length - 1) {
currentStep++;
}
}
/**
* Go to previous step
*/
function prev(): void {
if (currentStep > 0) {
currentStep--;
}
}
/**
* Go to specific step
*/
function goToStep(index: number): void {
if (index >= 0 && index < steps.length) {
currentStep = index;
}
}
/**
* Set a preference value
*/
function setPreference(key: string, value: unknown): void {
preferences = { ...preferences, [key]: value };
}
/**
* Complete the onboarding and save preferences
*/
async function complete(): Promise<void> {
saving = true;
try {
// Save to deviceSettings
await userSettings.updateDeviceAppSettings({
[ONBOARDING_COMPLETED_KEY]: true,
[ONBOARDING_PREFERENCES_KEY]: preferences,
...preferences, // Also spread preferences at top level for easy access
});
completed = true;
// Call completion callback
if (onComplete) {
await onComplete(preferences);
}
} finally {
saving = false;
}
}
/**
* Skip the onboarding entirely
*/
async function skip(): Promise<void> {
saving = true;
try {
// Mark as completed but don't save preferences
await userSettings.updateDeviceAppSettings({
[ONBOARDING_COMPLETED_KEY]: true,
onboarding_skipped: true,
});
completed = true;
// Call skip callback
if (onSkip) {
await onSkip();
}
} finally {
saving = false;
}
}
/**
* Reset onboarding (for testing/debugging)
*/
async function reset(): Promise<void> {
saving = true;
try {
// Remove onboarding flags from deviceSettings
await userSettings.updateDeviceAppSettings({
[ONBOARDING_COMPLETED_KEY]: false,
onboarding_skipped: false,
});
completed = false;
currentStep = 0;
// Reset preferences to defaults
preferences = {};
for (const step of steps) {
if (step.type === 'select' && step.defaultValue !== undefined) {
preferences[step.id] = step.defaultValue;
} else if (step.type === 'toggle' && step.defaultValue !== undefined) {
preferences[step.id] = step.defaultValue;
}
}
} finally {
saving = false;
}
}
return {
get shouldShow() {
return shouldShow;
},
get currentStep() {
return currentStep;
},
get totalSteps() {
return totalSteps;
},
get isFirstStep() {
return isFirstStep;
},
get isLastStep() {
return isLastStep;
},
get progress() {
return progress;
},
get currentStepConfig() {
return currentStepConfig;
},
get preferences() {
return preferences;
},
get saving() {
return saving;
},
get appId() {
return appId;
},
get steps() {
return steps;
},
next,
prev,
goToStep,
setPreference,
complete,
skip,
reset,
checkCompleted,
};
}