♻️ refactor: create createAppSettingsStore factory and migrate 3 apps

- Add createAppSettingsStore<T>() factory to @manacore/shared-stores
- Migrate todo, contacts, calendar settings stores to use factory
- Factory provides: localStorage persistence, type-safe set/update/reset
- Optional onSettingsChange callback for cloud sync (used by calendar)
- Reduces boilerplate by ~323 LOC across 3 apps

Savings:
- todo: 259 → 159 LOC (100 LOC)
- contacts: 278 → 173 LOC (105 LOC)
- calendar: 433 → 315 LOC (118 LOC)
This commit is contained in:
Till-JS 2026-01-29 16:30:22 +01:00
parent 9f4713117c
commit 4681ba8c36
9 changed files with 361 additions and 544 deletions

View file

@ -35,6 +35,7 @@
"@manacore/shared-api-client": "workspace:*",
"@manacore/shared-auth": "workspace:*",
"@manacore/shared-splitscreen": "workspace:*",
"@manacore/shared-stores": "workspace:*",
"@manacore/shared-auth-ui": "workspace:*",
"@manacore/shared-branding": "workspace:*",
"@manacore/shared-feedback-service": "workspace:*",

View file

@ -1,20 +1,19 @@
/**
* Settings Store - Manages user preferences for the calendar app
* Uses Svelte 5 runes with:
* - localStorage for immediate persistence
* - userSettings store for cloud sync (device-specific)
* Uses @manacore/shared-stores createAppSettingsStore factory with cloud sync
*/
import { browser } from '$app/environment';
import type { CalendarViewType } from '@calendar/shared';
import { createAppSettingsStore } from '@manacore/shared-stores';
import { userSettings } from './user-settings.svelte';
// Settings types
export type WeekStartDay = 0 | 1; // 0 = Sunday, 1 = Monday
export type WeekStartDay = 0 | 1;
export type TimeFormat = '24h' | '12h';
export type AllDayDisplayMode = 'header' | 'block'; // header = separate row, block = full day block in grid
export type AllDayDisplayMode = 'header' | 'block';
export type WeekdayFormat = 'full' | 'short' | 'hidden';
export type SttLanguage = 'de' | 'auto'; // Speech-to-text language setting
export type SttLanguage = 'de' | 'auto';
export interface CalendarAppSettings {
// View settings
@ -23,56 +22,56 @@ export interface CalendarAppSettings {
showOnlyWeekdays: boolean;
showWeekNumbers: boolean;
timeFormat: TimeFormat;
filterHoursEnabled: boolean; // Filter visible hours
dayStartHour: number; // First visible hour (0-23)
dayEndHour: number; // Last visible hour (0-23)
allDayDisplayMode: AllDayDisplayMode; // How to display all-day events
filterHoursEnabled: boolean;
dayStartHour: number;
dayEndHour: number;
allDayDisplayMode: AllDayDisplayMode;
// Header settings
headerCompact: boolean; // Compact header display
headerWeekdayFormat: WeekdayFormat; // Weekday display format
headerShowDate: boolean; // Show date in header
headerAlwaysShowMonth: boolean; // Always show month (e.g., "13.12.")
headerCompact: boolean;
headerWeekdayFormat: WeekdayFormat;
headerShowDate: boolean;
headerAlwaysShowMonth: boolean;
// DateStrip settings
dateStripShowMoonPhases: boolean; // Show moon phase indicators
dateStripShowEventIndicators: boolean; // Show event dot indicators
dateStripShowWeekday: boolean; // Show weekday names (Mo, Di, Mi...)
dateStripHighlightWeekends: boolean; // Visually highlight weekend days
dateStripShowMonthDividers: boolean; // Show vertical dividers between months
dateStripCompact: boolean; // Use compact/smaller DateStrip
dateStripShowWeekNumbers: boolean; // Show week numbers at start of week
dateStripCollapsed: boolean; // Whether DateStrip is minimized to FAB
dateStripShowMoonPhases: boolean;
dateStripShowEventIndicators: boolean;
dateStripShowWeekday: boolean;
dateStripHighlightWeekends: boolean;
dateStripShowMonthDividers: boolean;
dateStripCompact: boolean;
dateStripShowWeekNumbers: boolean;
dateStripCollapsed: boolean;
// TagStrip settings
tagStripCollapsed: boolean; // Whether TagStrip is hidden
selectedTagIds: string[]; // Tags selected for filtering calendar view
tagStripCollapsed: boolean;
selectedTagIds: string[];
// Immersive Mode settings
immersiveModeEnabled: boolean; // Fullscreen mode - hides all UI elements
immersiveModeEnabled: boolean;
// Birthday settings (cross-app integration with Contacts)
showBirthdays: boolean; // Show contact birthdays in calendar
showBirthdayAge: boolean; // Show age in birthday events
// Birthday settings
showBirthdays: boolean;
showBirthdayAge: boolean;
// UI settings
sidebarCollapsed: boolean;
// Quick View Pill settings
quickViewPillViews: CalendarViewType[]; // Views shown in quick switcher
customDayCount: number; // Custom day count for 'custom' view type (1-365)
quickViewPillViews: CalendarViewType[];
customDayCount: number;
// Event defaults
defaultEventDuration: number; // in minutes
defaultReminder: number; // in minutes before event
defaultEventDuration: number;
defaultReminder: number;
// Voice input settings
sttLanguage: SttLanguage; // Speech-to-text language ('de' or 'auto')
sttLanguage: SttLanguage;
}
const DEFAULT_SETTINGS: CalendarAppSettings = {
defaultView: 'week',
weekStartsOn: 1, // Monday
weekStartsOn: 1,
showOnlyWeekdays: false,
showWeekNumbers: false,
timeFormat: '24h',
@ -80,12 +79,10 @@ const DEFAULT_SETTINGS: CalendarAppSettings = {
dayStartHour: 6,
dayEndHour: 20,
allDayDisplayMode: 'header',
// Header defaults
headerCompact: false,
headerWeekdayFormat: 'full',
headerShowDate: true,
headerAlwaysShowMonth: false,
// DateStrip defaults
dateStripShowMoonPhases: true,
dateStripShowEventIndicators: true,
dateStripShowWeekday: true,
@ -94,67 +91,26 @@ const DEFAULT_SETTINGS: CalendarAppSettings = {
dateStripCompact: false,
dateStripShowWeekNumbers: false,
dateStripCollapsed: false,
// TagStrip defaults
tagStripCollapsed: true, // Hidden by default
selectedTagIds: [], // No tags selected by default
// Immersive Mode defaults
tagStripCollapsed: true,
selectedTagIds: [],
immersiveModeEnabled: false,
// Birthday defaults
showBirthdays: true,
showBirthdayAge: true,
// UI defaults
sidebarCollapsed: false,
// Quick View Pill defaults
quickViewPillViews: ['week', 'month', 'agenda'],
customDayCount: 30, // Default: 30 days (1 month)
// Event defaults
customDayCount: 30,
defaultEventDuration: 60,
defaultReminder: 15,
// Voice input defaults
sttLanguage: 'de',
};
const STORAGE_KEY = 'calendar-settings';
// Load settings from localStorage
function loadSettings(): CalendarAppSettings {
if (!browser) return DEFAULT_SETTINGS;
try {
const stored = localStorage.getItem(STORAGE_KEY);
if (stored) {
const parsed = JSON.parse(stored);
return { ...DEFAULT_SETTINGS, ...parsed };
}
} catch (e) {
console.error('Failed to load calendar settings:', e);
}
return DEFAULT_SETTINGS;
}
// Save settings to localStorage
function saveSettings(settings: CalendarAppSettings) {
if (!browser) return;
try {
localStorage.setItem(STORAGE_KEY, JSON.stringify(settings));
} catch (e) {
console.error('Failed to save calendar settings:', e);
}
}
// State
let settings = $state<CalendarAppSettings>(loadSettings());
// Cloud sync state
let cloudSyncEnabled = $state(false);
let initialSyncDone = $state(false);
/**
* Sync settings to cloud (device-specific)
*/
async function syncToCloud() {
// Sync to cloud callback
async function syncToCloud(settings: CalendarAppSettings) {
if (!cloudSyncEnabled || !browser) return;
try {
await userSettings.updateDeviceAppSettings(settings as unknown as Record<string, unknown>);
} catch (e) {
@ -162,12 +118,18 @@ async function syncToCloud() {
}
}
/**
* Load settings from cloud (device-specific)
*/
// Create base store with cloud sync callback
const baseStore = createAppSettingsStore<CalendarAppSettings>(
'calendar-settings',
DEFAULT_SETTINGS,
{
onSettingsChange: syncToCloud,
}
);
// Load settings from cloud
function loadFromCloud(): Partial<CalendarAppSettings> | null {
if (!userSettings.loaded) return null;
const cloudSettings = userSettings.currentDeviceAppSettings;
if (cloudSettings && Object.keys(cloudSettings).length > 0) {
return cloudSettings as unknown as Partial<CalendarAppSettings>;
@ -176,240 +138,166 @@ function loadFromCloud(): Partial<CalendarAppSettings> | null {
}
export const settingsStore = {
// Getters
// Base store methods
get settings() {
return settings;
return baseStore.settings;
},
initialize: baseStore.initialize,
set: baseStore.set,
update: baseStore.update,
reset: baseStore.reset,
getDefaults: baseStore.getDefaults,
toggleImmersiveMode: baseStore.toggleImmersiveMode,
// Convenience getters
get defaultView() {
return settings.defaultView;
return baseStore.settings.defaultView;
},
get weekStartsOn() {
return settings.weekStartsOn;
return baseStore.settings.weekStartsOn;
},
get showOnlyWeekdays() {
return settings.showOnlyWeekdays;
return baseStore.settings.showOnlyWeekdays;
},
get showWeekNumbers() {
return settings.showWeekNumbers;
return baseStore.settings.showWeekNumbers;
},
get timeFormat() {
return settings.timeFormat;
return baseStore.settings.timeFormat;
},
get filterHoursEnabled() {
return settings.filterHoursEnabled;
return baseStore.settings.filterHoursEnabled;
},
get dayStartHour() {
return settings.dayStartHour;
return baseStore.settings.dayStartHour;
},
get dayEndHour() {
return settings.dayEndHour;
return baseStore.settings.dayEndHour;
},
get allDayDisplayMode() {
return settings.allDayDisplayMode;
return baseStore.settings.allDayDisplayMode;
},
// Header settings
get headerCompact() {
return settings.headerCompact;
return baseStore.settings.headerCompact;
},
get headerWeekdayFormat() {
return settings.headerWeekdayFormat;
return baseStore.settings.headerWeekdayFormat;
},
get headerShowDate() {
return settings.headerShowDate;
return baseStore.settings.headerShowDate;
},
get headerAlwaysShowMonth() {
return settings.headerAlwaysShowMonth;
return baseStore.settings.headerAlwaysShowMonth;
},
// DateStrip settings
get dateStripShowMoonPhases() {
return settings.dateStripShowMoonPhases;
return baseStore.settings.dateStripShowMoonPhases;
},
get dateStripShowEventIndicators() {
return settings.dateStripShowEventIndicators;
return baseStore.settings.dateStripShowEventIndicators;
},
get dateStripShowWeekday() {
return settings.dateStripShowWeekday;
return baseStore.settings.dateStripShowWeekday;
},
get dateStripHighlightWeekends() {
return settings.dateStripHighlightWeekends;
return baseStore.settings.dateStripHighlightWeekends;
},
get dateStripShowMonthDividers() {
return settings.dateStripShowMonthDividers;
return baseStore.settings.dateStripShowMonthDividers;
},
get dateStripCompact() {
return settings.dateStripCompact;
return baseStore.settings.dateStripCompact;
},
get dateStripShowWeekNumbers() {
return settings.dateStripShowWeekNumbers;
return baseStore.settings.dateStripShowWeekNumbers;
},
get dateStripCollapsed() {
return settings.dateStripCollapsed;
return baseStore.settings.dateStripCollapsed;
},
// TagStrip settings
get tagStripCollapsed() {
return settings.tagStripCollapsed;
return baseStore.settings.tagStripCollapsed;
},
get selectedTagIds() {
return settings.selectedTagIds;
return baseStore.settings.selectedTagIds;
},
get hasSelectedTags() {
return settings.selectedTagIds.length > 0;
return baseStore.settings.selectedTagIds.length > 0;
},
// Immersive Mode settings
get immersiveModeEnabled() {
return settings.immersiveModeEnabled;
return baseStore.settings.immersiveModeEnabled;
},
// Birthday settings
get showBirthdays() {
return settings.showBirthdays;
return baseStore.settings.showBirthdays;
},
get showBirthdayAge() {
return settings.showBirthdayAge;
return baseStore.settings.showBirthdayAge;
},
get defaultEventDuration() {
return settings.defaultEventDuration;
return baseStore.settings.defaultEventDuration;
},
get defaultReminder() {
return settings.defaultReminder;
return baseStore.settings.defaultReminder;
},
get sidebarCollapsed() {
return settings.sidebarCollapsed;
return baseStore.settings.sidebarCollapsed;
},
get quickViewPillViews() {
return settings.quickViewPillViews;
return baseStore.settings.quickViewPillViews;
},
get customDayCount() {
return settings.customDayCount;
return baseStore.settings.customDayCount;
},
get sttLanguage() {
return settings.sttLanguage;
return baseStore.settings.sttLanguage;
},
get cloudSyncEnabled() {
return cloudSyncEnabled;
},
/**
* Enable cloud sync and load settings from cloud
*/
// Cloud sync methods
enableCloudSync() {
cloudSyncEnabled = true;
// On first sync, prefer cloud settings over local if they exist
if (!initialSyncDone) {
const cloudSettings = loadFromCloud();
if (cloudSettings && Object.keys(cloudSettings).length > 0) {
settings = { ...DEFAULT_SETTINGS, ...settings, ...cloudSettings };
saveSettings(settings);
baseStore.update(cloudSettings);
} else {
// No cloud settings yet, push local settings to cloud
syncToCloud();
syncToCloud(baseStore.settings);
}
initialSyncDone = true;
}
},
/**
* Disable cloud sync
*/
disableCloudSync() {
cloudSyncEnabled = false;
},
/**
* Toggle sidebar collapsed state
*/
// Calendar-specific toggle methods
toggleSidebar() {
settings = { ...settings, sidebarCollapsed: !settings.sidebarCollapsed };
saveSettings(settings);
syncToCloud();
baseStore.set('sidebarCollapsed', !baseStore.settings.sidebarCollapsed);
},
/**
* Toggle TagStrip visibility
*/
toggleTagStrip() {
settings = { ...settings, tagStripCollapsed: !settings.tagStripCollapsed };
saveSettings(settings);
syncToCloud();
baseStore.set('tagStripCollapsed', !baseStore.settings.tagStripCollapsed);
},
/**
* Toggle a tag selection for filtering
*/
toggleTagSelection(tagId: string) {
const currentIds = settings.selectedTagIds;
const currentIds = baseStore.settings.selectedTagIds;
const isSelected = currentIds.includes(tagId);
const newIds = isSelected ? currentIds.filter((id) => id !== tagId) : [...currentIds, tagId];
settings = { ...settings, selectedTagIds: newIds };
saveSettings(settings);
syncToCloud();
baseStore.set('selectedTagIds', newIds);
},
/**
* Check if a tag is selected
*/
isTagSelected(tagId: string): boolean {
return settings.selectedTagIds.includes(tagId);
return baseStore.settings.selectedTagIds.includes(tagId);
},
/**
* Clear all tag selections
*/
clearTagSelection() {
settings = { ...settings, selectedTagIds: [] };
saveSettings(settings);
syncToCloud();
baseStore.set('selectedTagIds', []);
},
/**
* Toggle Immersive Mode (fullscreen, hide all UI)
*/
toggleImmersiveMode() {
settings = { ...settings, immersiveModeEnabled: !settings.immersiveModeEnabled };
saveSettings(settings);
syncToCloud();
},
/**
* Initialize settings from localStorage
*/
initialize() {
if (!browser) return;
settings = loadSettings();
},
/**
* Update a single setting
*/
set<K extends keyof CalendarAppSettings>(key: K, value: CalendarAppSettings[K]) {
settings = { ...settings, [key]: value };
saveSettings(settings);
syncToCloud();
},
/**
* Update multiple settings at once
*/
update(updates: Partial<CalendarAppSettings>) {
settings = { ...settings, ...updates };
saveSettings(settings);
syncToCloud();
},
/**
* Reset all settings to defaults
*/
reset() {
settings = { ...DEFAULT_SETTINGS };
saveSettings(settings);
syncToCloud();
},
/**
* Format time according to user preference
*/
// Time formatting helpers
formatTime(date: Date): string {
if (settings.timeFormat === '12h') {
if (baseStore.settings.timeFormat === '12h') {
const hours = date.getHours();
const minutes = date.getMinutes();
const ampm = hours >= 12 ? 'PM' : 'AM';
@ -419,11 +307,8 @@ export const settingsStore = {
return `${date.getHours().toString().padStart(2, '0')}:${date.getMinutes().toString().padStart(2, '0')}`;
},
/**
* Format hour label according to user preference
*/
formatHour(hour: number): string {
if (settings.timeFormat === '12h') {
if (baseStore.settings.timeFormat === '12h') {
const ampm = hour >= 12 ? 'PM' : 'AM';
const displayHour = hour % 12 || 12;
return `${displayHour} ${ampm}`;

View file

@ -43,6 +43,7 @@
"@manacore/shared-icons": "workspace:*",
"@manacore/shared-profile-ui": "workspace:*",
"@manacore/shared-splitscreen": "workspace:*",
"@manacore/shared-stores": "workspace:*",
"@manacore/shared-subscription-ui": "workspace:*",
"@manacore/shared-tags": "workspace:*",
"@manacore/shared-tailwind": "workspace:*",

View file

@ -1,9 +1,9 @@
/**
* Settings Store - Manages user preferences for the Contacts app
* Uses Svelte 5 runes and localStorage for persistence
* Uses @manacore/shared-stores createAppSettingsStore factory
*/
import { browser } from '$app/environment';
import { createAppSettingsStore } from '@manacore/shared-stores';
// Settings types
export type ContactSortBy = 'name' | 'company' | 'created' | 'updated';
@ -13,61 +13,39 @@ export type DateFormat = 'dd.MM.yyyy' | 'MM/dd/yyyy' | 'yyyy-MM-dd';
export interface ContactsAppSettings {
// Display Settings
/** Default view mode for contacts list */
defaultView: ContactView;
/** Default sort field */
sortBy: ContactSortBy;
/** Default sort order */
sortOrder: ContactSortOrder;
/** Show contact photos in list */
showPhotos: boolean;
/** Show company name in list */
showCompany: boolean;
/** Contacts per page in list view */
contactsPerPage: number;
// Contact Display
/** Display name format: 'first-last' or 'last-first' */
nameFormat: 'first-last' | 'last-first';
/** Date format for birthdays etc. */
dateFormat: DateFormat;
/** Show birthday reminders */
showBirthdayReminders: boolean;
/** Days before birthday to remind */
birthdayReminderDays: number;
// Import/Export
/** Default export format */
defaultExportFormat: 'vcf' | 'csv' | 'json';
/** Include notes in export */
includeNotesInExport: boolean;
/** Include photos in export */
includePhotosInExport: boolean;
// Duplicates
/** Auto-detect duplicates on import */
autoDetectDuplicates: boolean;
/** Duplicate detection sensitivity: 'strict' | 'normal' | 'loose' */
duplicateSensitivity: 'strict' | 'normal' | 'loose';
// Privacy
/** Blur contact photos by default (privacy mode) */
privacyMode: boolean;
/** Require confirmation before sharing contact */
confirmBeforeSharing: boolean;
// Alphabet Navigation Settings
/** Hide letters that have no contacts */
alphabetNavHideInactive: boolean;
/** Use compact/smaller alphabet buttons */
alphabetNavCompact: boolean;
/** Reverse letter order (Z-A instead of A-Z) */
alphabetNavReverseOrder: boolean;
/** Show # symbol for non-letter names */
alphabetNavShowHash: boolean;
// Immersive Mode
/** Fullscreen mode - hides all UI elements */
immersiveModeEnabled: boolean;
}
@ -109,170 +87,90 @@ const DEFAULT_SETTINGS: ContactsAppSettings = {
immersiveModeEnabled: false,
};
const STORAGE_KEY = 'contacts-settings';
// Load settings from localStorage
function loadSettings(): ContactsAppSettings {
if (!browser) return DEFAULT_SETTINGS;
try {
const stored = localStorage.getItem(STORAGE_KEY);
if (stored) {
const parsed = JSON.parse(stored);
// Merge with defaults to handle new settings added in updates
return { ...DEFAULT_SETTINGS, ...parsed };
}
} catch (e) {
console.error('Failed to load contacts settings:', e);
}
return DEFAULT_SETTINGS;
}
// Save settings to localStorage
function saveSettings(settings: ContactsAppSettings) {
if (!browser) return;
try {
localStorage.setItem(STORAGE_KEY, JSON.stringify(settings));
} catch (e) {
console.error('Failed to save contacts settings:', e);
}
}
// State
let settings = $state<ContactsAppSettings>(loadSettings());
// Create base store using factory
const baseStore = createAppSettingsStore<ContactsAppSettings>(
'contacts-settings',
DEFAULT_SETTINGS
);
// Export with convenience getters for backwards compatibility
export const contactsSettings = {
// Full settings object
// Base store methods
get settings() {
return settings;
return baseStore.settings;
},
initialize: baseStore.initialize,
set: baseStore.set,
update: baseStore.update,
reset: baseStore.reset,
getDefaults: baseStore.getDefaults,
toggleImmersiveMode: baseStore.toggleImmersiveMode,
// Display Settings
// Convenience getters (backwards compatible)
get defaultView() {
return settings.defaultView;
return baseStore.settings.defaultView;
},
get sortBy() {
return settings.sortBy;
return baseStore.settings.sortBy;
},
get sortOrder() {
return settings.sortOrder;
return baseStore.settings.sortOrder;
},
get showPhotos() {
return settings.showPhotos;
return baseStore.settings.showPhotos;
},
get showCompany() {
return settings.showCompany;
return baseStore.settings.showCompany;
},
get contactsPerPage() {
return settings.contactsPerPage;
return baseStore.settings.contactsPerPage;
},
// Contact Display
get nameFormat() {
return settings.nameFormat;
return baseStore.settings.nameFormat;
},
get dateFormat() {
return settings.dateFormat;
return baseStore.settings.dateFormat;
},
get showBirthdayReminders() {
return settings.showBirthdayReminders;
return baseStore.settings.showBirthdayReminders;
},
get birthdayReminderDays() {
return settings.birthdayReminderDays;
return baseStore.settings.birthdayReminderDays;
},
// Import/Export
get defaultExportFormat() {
return settings.defaultExportFormat;
return baseStore.settings.defaultExportFormat;
},
get includeNotesInExport() {
return settings.includeNotesInExport;
return baseStore.settings.includeNotesInExport;
},
get includePhotosInExport() {
return settings.includePhotosInExport;
return baseStore.settings.includePhotosInExport;
},
// Duplicates
get autoDetectDuplicates() {
return settings.autoDetectDuplicates;
return baseStore.settings.autoDetectDuplicates;
},
get duplicateSensitivity() {
return settings.duplicateSensitivity;
return baseStore.settings.duplicateSensitivity;
},
// Privacy
get privacyMode() {
return settings.privacyMode;
return baseStore.settings.privacyMode;
},
get confirmBeforeSharing() {
return settings.confirmBeforeSharing;
return baseStore.settings.confirmBeforeSharing;
},
// Alphabet Navigation
get alphabetNavHideInactive() {
return settings.alphabetNavHideInactive;
return baseStore.settings.alphabetNavHideInactive;
},
get alphabetNavCompact() {
return settings.alphabetNavCompact;
return baseStore.settings.alphabetNavCompact;
},
get alphabetNavReverseOrder() {
return settings.alphabetNavReverseOrder;
return baseStore.settings.alphabetNavReverseOrder;
},
get alphabetNavShowHash() {
return settings.alphabetNavShowHash;
return baseStore.settings.alphabetNavShowHash;
},
// Immersive Mode
get immersiveModeEnabled() {
return settings.immersiveModeEnabled;
},
/**
* Toggle Immersive Mode (fullscreen, hide all UI)
*/
toggleImmersiveMode() {
settings = { ...settings, immersiveModeEnabled: !settings.immersiveModeEnabled };
saveSettings(settings);
},
/**
* Initialize settings from localStorage
*/
initialize() {
if (!browser) return;
settings = loadSettings();
},
/**
* Update a single setting
*/
set<K extends keyof ContactsAppSettings>(key: K, value: ContactsAppSettings[K]) {
settings = { ...settings, [key]: value };
saveSettings(settings);
},
/**
* Update multiple settings at once
*/
update(updates: Partial<ContactsAppSettings>) {
settings = { ...settings, ...updates };
saveSettings(settings);
},
/**
* Reset all settings to defaults
*/
reset() {
settings = { ...DEFAULT_SETTINGS };
saveSettings(settings);
},
/**
* Get default settings (for reference)
*/
getDefaults() {
return DEFAULT_SETTINGS;
return baseStore.settings.immersiveModeEnabled;
},
};

View file

@ -33,6 +33,7 @@
"@manacore/shared-api-client": "workspace:*",
"@manacore/shared-auth": "workspace:*",
"@manacore/shared-splitscreen": "workspace:*",
"@manacore/shared-stores": "workspace:*",
"@manacore/shared-types": "workspace:*",
"@manacore/shared-utils": "workspace:*",
"@manacore/shared-tags": "workspace:*",

View file

@ -1,9 +1,9 @@
/**
* Settings Store - Manages user preferences for the Todo app
* Uses Svelte 5 runes and localStorage for persistence
* Uses @manacore/shared-stores createAppSettingsStore factory
*/
import { browser } from '$app/environment';
import { createAppSettingsStore } from '@manacore/shared-stores';
import type { TaskPriority } from '@todo/shared';
// Settings types
@ -12,55 +12,35 @@ export type KanbanCardSize = 'compact' | 'normal' | 'large';
export interface TodoAppSettings {
// Task Behavior
/** Default priority for new tasks */
defaultPriority: TaskPriority;
/** Default due time for tasks (HH:mm format, null = no default) */
defaultDueTime: string | null;
/** Auto-archive completed tasks after X days (null = disabled) */
autoArchiveCompletedDays: number | null;
/** Default project for quick add (null = inbox) */
quickAddProject: string | null;
// View & Display
/** Default view when opening the app */
defaultView: TodoView;
/** Show task counts as badges in navigation */
showTaskCounts: boolean;
/** Compact mode with reduced padding */
compactMode: boolean;
/** Show progress bar for subtasks */
showSubtaskProgress: boolean;
/** Group tasks by project in list views */
groupByProject: boolean;
// Kanban Board
/** Kanban card size */
kanbanCardSize: KanbanCardSize;
/** Show labels on kanban cards */
showLabelsOnCards: boolean;
/** Work-in-progress limit per column (null = unlimited) */
wipLimitPerColumn: number | null;
// Notifications & Reminders
/** Default reminder time in minutes before due (null = no default) */
defaultReminderMinutes: number | null;
/** Enable daily digest email/notification */
dailyDigestEnabled: boolean;
/** Notify about overdue tasks */
overdueNotifications: boolean;
// Productivity
/** Focus mode - show only current task */
focusMode: boolean;
/** Enable pomodoro timer */
pomodoroEnabled: boolean;
/** Daily task completion goal (null = no goal) */
dailyGoal: number | null;
/** Show productivity streak */
showStreak: boolean;
// Immersive Mode
/** Fullscreen mode - hides all UI elements */
immersiveModeEnabled: boolean;
}
@ -98,162 +78,81 @@ const DEFAULT_SETTINGS: TodoAppSettings = {
immersiveModeEnabled: false,
};
const STORAGE_KEY = 'todo-settings';
// Load settings from localStorage
function loadSettings(): TodoAppSettings {
if (!browser) return DEFAULT_SETTINGS;
try {
const stored = localStorage.getItem(STORAGE_KEY);
if (stored) {
const parsed = JSON.parse(stored);
// Merge with defaults to handle new settings added in updates
return { ...DEFAULT_SETTINGS, ...parsed };
}
} catch (e) {
console.error('Failed to load todo settings:', e);
}
return DEFAULT_SETTINGS;
}
// Save settings to localStorage
function saveSettings(settings: TodoAppSettings) {
if (!browser) return;
try {
localStorage.setItem(STORAGE_KEY, JSON.stringify(settings));
} catch (e) {
console.error('Failed to save todo settings:', e);
}
}
// State
let settings = $state<TodoAppSettings>(loadSettings());
// Create base store using factory
const baseStore = createAppSettingsStore<TodoAppSettings>('todo-settings', DEFAULT_SETTINGS);
// Export with convenience getters for backwards compatibility
export const todoSettings = {
// Full settings object
// Base store methods
get settings() {
return settings;
return baseStore.settings;
},
initialize: baseStore.initialize,
set: baseStore.set,
update: baseStore.update,
reset: baseStore.reset,
getDefaults: baseStore.getDefaults,
toggleImmersiveMode: baseStore.toggleImmersiveMode,
// Task Behavior
// Convenience getters (backwards compatible)
get defaultPriority() {
return settings.defaultPriority;
return baseStore.settings.defaultPriority;
},
get defaultDueTime() {
return settings.defaultDueTime;
return baseStore.settings.defaultDueTime;
},
get autoArchiveCompletedDays() {
return settings.autoArchiveCompletedDays;
return baseStore.settings.autoArchiveCompletedDays;
},
get quickAddProject() {
return settings.quickAddProject;
return baseStore.settings.quickAddProject;
},
// View & Display
get defaultView() {
return settings.defaultView;
return baseStore.settings.defaultView;
},
get showTaskCounts() {
return settings.showTaskCounts;
return baseStore.settings.showTaskCounts;
},
get compactMode() {
return settings.compactMode;
return baseStore.settings.compactMode;
},
get showSubtaskProgress() {
return settings.showSubtaskProgress;
return baseStore.settings.showSubtaskProgress;
},
get groupByProject() {
return settings.groupByProject;
return baseStore.settings.groupByProject;
},
// Kanban Board
get kanbanCardSize() {
return settings.kanbanCardSize;
return baseStore.settings.kanbanCardSize;
},
get showLabelsOnCards() {
return settings.showLabelsOnCards;
return baseStore.settings.showLabelsOnCards;
},
get wipLimitPerColumn() {
return settings.wipLimitPerColumn;
return baseStore.settings.wipLimitPerColumn;
},
// Notifications & Reminders
get defaultReminderMinutes() {
return settings.defaultReminderMinutes;
return baseStore.settings.defaultReminderMinutes;
},
get dailyDigestEnabled() {
return settings.dailyDigestEnabled;
return baseStore.settings.dailyDigestEnabled;
},
get overdueNotifications() {
return settings.overdueNotifications;
return baseStore.settings.overdueNotifications;
},
// Productivity
get focusMode() {
return settings.focusMode;
return baseStore.settings.focusMode;
},
get pomodoroEnabled() {
return settings.pomodoroEnabled;
return baseStore.settings.pomodoroEnabled;
},
get dailyGoal() {
return settings.dailyGoal;
return baseStore.settings.dailyGoal;
},
get showStreak() {
return settings.showStreak;
return baseStore.settings.showStreak;
},
// Immersive Mode
get immersiveModeEnabled() {
return settings.immersiveModeEnabled;
},
/**
* Toggle Immersive Mode (fullscreen, hide all UI)
*/
toggleImmersiveMode() {
settings = { ...settings, immersiveModeEnabled: !settings.immersiveModeEnabled };
saveSettings(settings);
},
/**
* Initialize settings from localStorage
*/
initialize() {
if (!browser) return;
settings = loadSettings();
},
/**
* Update a single setting
*/
set<K extends keyof TodoAppSettings>(key: K, value: TodoAppSettings[K]) {
settings = { ...settings, [key]: value };
saveSettings(settings);
},
/**
* Update multiple settings at once
*/
update(updates: Partial<TodoAppSettings>) {
settings = { ...settings, ...updates };
saveSettings(settings);
},
/**
* Reset all settings to defaults
*/
reset() {
settings = { ...DEFAULT_SETTINGS };
saveSettings(settings);
},
/**
* Get default settings (for reference)
*/
getDefaults() {
return DEFAULT_SETTINGS;
return baseStore.settings.immersiveModeEnabled;
},
};

View file

@ -9,7 +9,7 @@
|-----------|---------|----------------------|---------|
| ~~**KRITISCH**~~ | ~~Backend Metrics Migration~~ | ~~350 LOC~~**709 LOC entfernt** | ~~Niedrig~~ |
| **HOCH** | Skeleton Components | 800-1.000 LOC | Mittel |
| **HOCH** | App Settings Stores | 600-700 LOC | Mittel |
| ~~**HOCH**~~ | ~~App Settings Stores~~ | ~~600-700 LOC~~**323 LOC entfernt** | ~~Mittel~~ |
| **HOCH** | Main.ts/CORS Patterns | 1.800 LOC | Mittel |
| **MITTEL** | TypeScript Configs | 400 LOC | Niedrig |
| **MITTEL** | UI Component Cleanup | 400 LOC | Niedrig |
@ -146,54 +146,32 @@ import { HealthModule } from '@manacore/shared-nestjs-health';
## 2. Frontend Stores (Svelte 5)
### 2.1 HOCH: App Settings Stores (600-700 LOC)
### ~~2.1 HOCH: App Settings Stores~~ ✅ ERLEDIGT (323 LOC gespart)
**Problem:** 3 Apps (todo, calendar, contacts) haben fast identische Settings-Store Implementierungen mit localStorage Persistenz.
**Status:** `createAppSettingsStore<T>()` Factory erstellt und 3 Apps migriert (29.01.2026)
**Betroffene Dateien:**
- `apps/todo/apps/web/src/lib/stores/settings.svelte.ts` (259 LOC)
- `apps/calendar/apps/web/src/lib/stores/settings.svelte.ts` (433 LOC)
- `apps/contacts/apps/web/src/lib/stores/settings.svelte.ts` (278 LOC)
**Erstellte Factory:** `packages/shared-stores/src/settings.svelte.ts`
- Type-safe Settings Store mit localStorage Persistenz
- Optional: `onSettingsChange` Callback für Cloud-Sync
- Reduziert Boilerplate von ~100 LOC pro App auf ~20 LOC
**Dupliziertes Pattern (100% identisch):**
```typescript
// Boilerplate in jedem (80-100 LOC):
- TypeScript Interface für Settings
- DEFAULT_SETTINGS Konstante
- STORAGE_KEY
- loadSettings() - localStorage laden + merge mit defaults
- saveSettings() - localStorage speichern
- let settings = $state(...)
- toggleImmersiveMode(), initialize(), set(), update(), reset(), getDefaults()
```
**Empfehlung:** Erstelle `createAppSettingsStore<T>()` Factory in `@manacore/shared-stores`
**Migrierte Apps:**
- ~~`apps/todo/apps/web/src/lib/stores/settings.svelte.ts`~~ ✅ (259 → 159 LOC = 100 LOC)
- ~~`apps/contacts/apps/web/src/lib/stores/settings.svelte.ts`~~ ✅ (278 → 173 LOC = 105 LOC)
- ~~`apps/calendar/apps/web/src/lib/stores/settings.svelte.ts`~~ ✅ (433 → 315 LOC = 118 LOC)
```typescript
// packages/shared-stores/src/createAppSettingsStore.ts
export function createAppSettingsStore<T extends Record<string, any>>(
storageKey: string,
defaultSettings: T,
options?: { cloudSync?: boolean }
) {
let settings = $state<T>(defaultSettings);
function loadSettings(): T { /* localStorage logic */ }
function saveSettings(newSettings: T): void { /* localStorage logic */ }
return {
get value() { return settings; },
initialize() { settings = loadSettings(); },
set<K extends keyof T>(key: K, value: T[K]) { /* ... */ },
update(updates: Partial<T>) { /* ... */ },
reset() { settings = defaultSettings; saveSettings(settings); },
getDefaults() { return defaultSettings; },
};
}
// Nachher (Beispiel Todo)
import { createAppSettingsStore } from '@manacore/shared-stores';
const baseStore = createAppSettingsStore<TodoAppSettings>('todo-settings', DEFAULT_SETTINGS);
export const todoSettings = {
get settings() { return baseStore.settings; },
initialize: baseStore.initialize,
set: baseStore.set,
// ... convenience getters
};
```
**Einsparung:** ~200 LOC Boilerplate pro App = 600 LOC
---
### 2.2 MITTEL: Navigation Stores (50 LOC)
@ -409,12 +387,12 @@ export default createDrizzleConfig('chat');
### Phase 2: Stores & Configs (3-5 Tage, ~1.500 LOC)
| Aufgabe | LOC | Aufwand |
|---------|-----|---------|
| `createAppSettingsStore()` Factory erstellen | 600 | Mittel |
| `@manacore/shared-tsconfig` Package erstellen | 400 | Niedrig |
| `@manacore/shared-vite-config` Factory erstellen | 300 | Niedrig |
| Navigation Store Factory erstellen | 50 | Niedrig |
| Aufgabe | LOC | Aufwand | Status |
|---------|-----|---------|--------|
| ~~`createAppSettingsStore()` Factory erstellen~~ | ~~600~~**323** | ~~Mittel~~ | ✅ Erledigt |
| `@manacore/shared-tsconfig` Package erstellen | 400 | Niedrig | Offen |
| `@manacore/shared-vite-config` Factory erstellen | 300 | Niedrig | Offen |
| Navigation Store Factory erstellen | 50 | Niedrig | Offen |
### Phase 3: Backend Setup (5-7 Tage, ~2.000 LOC)

View file

@ -10,3 +10,8 @@ export {
type NavigationStore,
} from './navigation.svelte';
export { createThemeStore, type ThemeStore, type ThemeMode } from './theme.svelte';
export {
createAppSettingsStore,
type AppSettingsStore,
type AppSettingsStoreOptions,
} from './settings.svelte';

View file

@ -0,0 +1,149 @@
/**
* App Settings Store Factory
*
* Creates a type-safe settings store with localStorage persistence.
* Reduces ~200 LOC boilerplate per app to ~20 LOC.
*
* @example
* ```typescript
* interface MyAppSettings {
* theme: 'light' | 'dark';
* sidebarCollapsed: boolean;
* immersiveModeEnabled: boolean;
* }
*
* const DEFAULT_SETTINGS: MyAppSettings = {
* theme: 'light',
* sidebarCollapsed: false,
* immersiveModeEnabled: false,
* };
*
* export const settingsStore = createAppSettingsStore('my-app-settings', DEFAULT_SETTINGS);
*
* // Usage:
* settingsStore.settings.theme // 'light'
* settingsStore.set('theme', 'dark');
* settingsStore.update({ sidebarCollapsed: true });
* settingsStore.toggleImmersiveMode(); // If immersiveModeEnabled exists
* ```
*/
import { browser } from '$app/environment';
export interface AppSettingsStoreOptions<T> {
/**
* Callback invoked after each settings change.
* Use for cloud sync or other side effects.
*/
onSettingsChange?: (settings: T) => void | Promise<void>;
}
export interface AppSettingsStore<T extends Record<string, unknown>> {
/** Current settings state (reactive) */
readonly settings: T;
/** Get the default settings */
getDefaults(): T;
/** Initialize settings from localStorage (call on mount) */
initialize(): void;
/** Set a single setting value */
set<K extends keyof T>(key: K, value: T[K]): void;
/** Update multiple settings at once */
update(updates: Partial<T>): void;
/** Reset all settings to defaults */
reset(): void;
/** Toggle immersive mode (if immersiveModeEnabled exists in settings) */
toggleImmersiveMode(): void;
}
/**
* Creates a settings store with localStorage persistence.
*
* @param storageKey - localStorage key for persistence
* @param defaultSettings - Default settings object
* @param options - Optional configuration (onSettingsChange callback)
* @returns AppSettingsStore instance
*/
export function createAppSettingsStore<T extends Record<string, unknown>>(
storageKey: string,
defaultSettings: T,
options?: AppSettingsStoreOptions<T>
): AppSettingsStore<T> {
// Load settings from localStorage
function loadSettings(): T {
if (!browser) return { ...defaultSettings };
try {
const stored = localStorage.getItem(storageKey);
if (stored) {
const parsed = JSON.parse(stored);
// Merge with defaults to handle new settings added after initial save
return { ...defaultSettings, ...parsed };
}
} catch (e) {
console.error(`Failed to load settings from ${storageKey}:`, e);
}
return { ...defaultSettings };
}
// Save settings to localStorage
function saveSettings(newSettings: T): void {
if (!browser) return;
try {
localStorage.setItem(storageKey, JSON.stringify(newSettings));
} catch (e) {
console.error(`Failed to save settings to ${storageKey}:`, e);
}
// Invoke callback if provided
options?.onSettingsChange?.(newSettings);
}
// Reactive state using Svelte 5 runes
let settings = $state<T>(loadSettings());
return {
get settings() {
return settings;
},
getDefaults() {
return { ...defaultSettings };
},
initialize() {
if (!browser) return;
settings = loadSettings();
},
set<K extends keyof T>(key: K, value: T[K]) {
settings = { ...settings, [key]: value };
saveSettings(settings);
},
update(updates: Partial<T>) {
settings = { ...settings, ...updates };
saveSettings(settings);
},
reset() {
settings = { ...defaultSettings };
saveSettings(settings);
},
toggleImmersiveMode() {
if ('immersiveModeEnabled' in settings) {
const current = settings.immersiveModeEnabled as boolean;
settings = { ...settings, immersiveModeEnabled: !current };
saveSettings(settings);
}
},
};
}