managarten/packages/shared-ui/src/onboarding/create-app-onboarding.svelte.ts
Till JS 3e81a6ebef fix: dev startup — Redis eviction policy, mana-media port crash, Svelte warnings
- Redis: allkeys-lru → noeviction to prevent silent data loss when memory full
- mana-media: --watch → --hot to fix EADDRINUSE crash on Bun HMR reload
- Svelte: build initial values before $state() to avoid state_referenced_locally warnings
  in create-app-onboarding.svelte.ts and shared-llm/store.svelte.ts

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 18:33:41 +02:00

222 lines
4.8 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 '@mana/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;
// Build initial preferences from step defaults before creating state
const initialPreferences: AppOnboardingPreferences = {};
for (const step of steps) {
if ((step.type === 'select' || step.type === 'toggle') && step.defaultValue !== undefined) {
initialPreferences[step.id] = step.defaultValue;
}
}
// State
let currentStep = $state(0);
let preferences = $state<AppOnboardingPreferences>(initialPreferences);
let saving = $state(false);
let completed = $state(false);
// 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;
preferences = { ...initialPreferences };
} 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,
};
}