From ebaab8f9748ccd575244f32ba29862b06447ef92 Mon Sep 17 00:00:00 2001 From: Strudelgame <183094843+Strudelcode@users.noreply.github.com> Date: Fri, 2 Oct 2026 08:11:23 +0000 Subject: [PATCH] Update 13 files\n\nCo-authored-by: Freebuff Agent --- CHANGELOG.md | 24 + package.json | 2 +- src/App.tsx | 16 +- src/components/AccountScopedWorkspace.tsx | 17 +- src/components/AuthGate.tsx | 599 +++++++++++++++++++--- src/components/DocumentationApp.tsx | 11 + src/components/SettingsModule.tsx | 317 +++++++----- src/lib/auth.ts | 2 +- src/lib/i18n.ts | 276 +++++++++- src/lib/version.ts | 43 +- todo/completed_todo.md | 30 ++ versions/V24.md | 23 + versions/releases/v24-release.md | 33 ++ 13 files changed, 1162 insertions(+), 231 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9a7b3ac..a37d2da 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,30 @@ Format guidelines: - Avoid exhaustive UI/visual layout breakdowns. --> +### v24.6.2 +- 🔄 **Geändert / Improved**: + - **Übersichtlichere Einstellungen / Clearer Settings**: Das schlecht lesbare native Menü wurde durch eine kontrastreiche, gruppierte Auswahl ersetzt. Die Startseite blendet Platzhalter-Firmendaten aus und bietet klarere Schnellzugriffe. + - **Mehrsprachige Navigation / Localized Navigation**: Tastaturbedienung und Einstellungen-Hilfe sind in Deutsch, Englisch, Französisch und Spanisch verfügbar. + +### v24.6.1 +- 🔄 **Geändert / Improved**: + - **Systemmodus als fester Standard / System Theme as the Default**: Die Ersteinrichtung wählt immer „System“ voraus, auch wenn zuvor ein anderer Firmenmodus gespeichert war. + - **Beschriebene App-Kacheln / Descriptive App Cards**: Apps werden mit Symbol, Kurzbeschreibung und klarer Auswahl dargestellt; alle Texte sind in vier Sprachen verfügbar. + - **Mehrsprachige Einrichtungs-Hilfe / Localized Onboarding Guide**: Die neue Ersteinrichtung ist im integrierten Handbuch erklärt. + - **Verifiziert / Verified**: Lint und Produktions-Build erfolgreich; die Vorschau wurde im folgenden Einstellungen-Update geprüft. + +### v24.6.0 +- 🔄 **Geändert / Improved**: + - **Einstellungen auf schmalen Fenstern / Settings on Narrow Windows**: Die abgeschnittene Kategorie-Leiste wurde durch eine kompakte, nach Bereichen gruppierte Auswahl ersetzt; Suchfeld, Übersichtskarten und Schnelloptionen passen sich an schmale Fenster an. + +### v24.5.0 +- 🚀 **Neu / What's New**: + - **Optionaler Einrichtungsassistent für neue Konten / Optional First-Account Setup**: Bei der ersten Kontoerstellung können Akzentfarbe, Hell-/Dunkel-/Systemmodus, ein lokales Hintergrundbild und passende App-Vorlagen gewählt werden. + - **App-Vorlagen / App Templates**: Privat, Geschäftlich, Praxis und Einzelhandel bieten passende Module wie Rechnungen, Buchhaltung, Lager, Einkauf und Kasse; Apps lassen sich zusätzlich einzeln auswählen. +- 🔄 **Geändert / Improved**: + - **Überspringen und später anpassen / Skip and personalize later**: Die Einrichtung ist vollständig optional; Live-Vorschauen werden beim Überspringen zurückgesetzt und gewählte Einstellungen dem neuen lokalen Konto zugeordnet. + - **Mehrsprachige Einrichtung / Quad-Language Setup**: Alle neuen Texte sind auf Deutsch, Englisch, Französisch und Spanisch verfügbar. + ### v24.4.0 - 🚀 **Neu / What's New**: - **Word (.docx) Tabellen-Engine für mehrseitige Dokumente / Word (.docx) Multi-Page Table Engine**: Vollständige XML-AST-Analyse für komplexe Word-Tabellen mit vertikaler (`w:vMerge`) und horizontaler (`w:gridSpan`) Zellverschmelzung, Unterdrückung von Seitenumbrüchen innerhalb von Tabellenzeilen (`w:cantSplit`) und verschachtelten Tabellen. diff --git a/package.json b/package.json index 6b465cd..6c16fb8 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "socdof", "private": true, - "version": "24.4.0", + "version": "24.6.2", "description": "Strudel's Organization, Commerce & Documentation Offline Flow - Offline-first Windows ERP Desktop Suite & Web Workspace", "author": "Yuri / Strudel", "publisher": "Yuri / Strudel", diff --git a/src/App.tsx b/src/App.tsx index 18e9fc6..4aa2eb9 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,4 +1,4 @@ -import React, { useState, useEffect, useCallback } from 'react'; +import React, { useState, useEffect, useCallback, useRef } from 'react'; import { Contact, Product, @@ -121,11 +121,8 @@ export default function App() { if (session && !session.locked) { const user = getUserById(session.userId); if (user) { - const media = window.matchMedia('(prefers-color-scheme: dark)'); - const enableDark = nextMode === 'dark' || (nextMode !== 'light' && media.matches); - const userTheme = enableDark ? 'dark' : 'light'; - if (user.preferences.theme !== userTheme) { - updateUserPreferences(user.id, { ...user.preferences, theme: userTheme }); + if (user.preferences.theme !== nextMode) { + updateUserPreferences(user.id, { ...user.preferences, theme: nextMode }); } } } @@ -397,11 +394,8 @@ export default function App() { if (session && !session.locked) { const user = getUserById(session.userId); if (user) { - const media = window.matchMedia('(prefers-color-scheme: dark)'); - const enableDark = updated.theme_mode === 'dark' || (updated.theme_mode !== 'light' && media.matches); - const userTheme = enableDark ? 'dark' : 'light'; - if (user.preferences.theme !== userTheme) { - updateUserPreferences(user.id, { ...user.preferences, theme: userTheme }); + if (user.preferences.theme !== updated.theme_mode) { + updateUserPreferences(user.id, { ...user.preferences, theme: updated.theme_mode }); } } } diff --git a/src/components/AccountScopedWorkspace.tsx b/src/components/AccountScopedWorkspace.tsx index b6f9c30..f300c13 100644 --- a/src/components/AccountScopedWorkspace.tsx +++ b/src/components/AccountScopedWorkspace.tsx @@ -129,6 +129,15 @@ function applyUserAppearance(user: UserAccount | null): void { } catch { // ignore } + } else if (user.preferences.theme === 'system') { + const isDark = window.matchMedia('(prefers-color-scheme: dark)').matches; + root.classList.toggle('dark', isDark); + root.style.colorScheme = isDark ? 'dark' : 'light'; + try { + localStorage.removeItem('odoo_theme_dark'); + } catch { + // ignore + } } // ACCENT COLOR: If the user account explicitly has an accent color preference, apply it. // Otherwise, respect the saved system / company / localStorage accent color! DO NOT force 'indigo'! @@ -162,20 +171,18 @@ export const AccountScopedWorkspace: React.FC = ({ chil const account = session && !session.locked ? getUserById(session.userId) : null; setActiveAccount(account); }; - const onAccentOrCompanyChanged = () => { + const onCompanyChanged = () => { const session = getSession(); const account = session && !session.locked ? getUserById(session.userId) : null; applyUserAppearance(account); }; window.addEventListener(AUTH_CHANGE_EVENT_NAME, refreshAccount); window.addEventListener('storage', refreshAccount); - window.addEventListener('socdof-company-updated', onAccentOrCompanyChanged); - window.addEventListener('socdof-accent-changed', onAccentOrCompanyChanged); + window.addEventListener('socdof-company-updated', onCompanyChanged); return () => { window.removeEventListener(AUTH_CHANGE_EVENT_NAME, refreshAccount); window.removeEventListener('storage', refreshAccount); - window.removeEventListener('socdof-company-updated', onAccentOrCompanyChanged); - window.removeEventListener('socdof-accent-changed', onAccentOrCompanyChanged); + window.removeEventListener('socdof-company-updated', onCompanyChanged); }; }, []); diff --git a/src/components/AuthGate.tsx b/src/components/AuthGate.tsx index 3a68b6b..928a7c8 100644 --- a/src/components/AuthGate.tsx +++ b/src/components/AuthGate.tsx @@ -3,6 +3,25 @@ import { ArrowRight, Building2, Check, + BriefcaseBusiness, + ShoppingCart, + Calculator, + CalendarDays, + ChartNoAxesCombined, + ContactRound, + Download, + FileText, + CreditCard, + HeartPulse, + Layers, + LayoutGrid, + LifeBuoy, + MessageSquare, + Image as ImageIcon, + Moon, + Monitor, + Palette, + Sun, CheckCircle2, Eye, EyeOff, @@ -49,7 +68,8 @@ import { type UserRole, RECOVERY_QUESTIONS } from '../lib/auth'; -import { applyAccentColor } from '../lib/accent'; +import { ACCENT_LIST, applyAccentColor } from '../lib/accent'; +import type { ActiveModule } from '../types'; import { SUPPORTED_LANGUAGES, formatSystemDate, @@ -145,6 +165,80 @@ const getAuthCopy = (lang: LanguageCode) => ({ type AuthText = ReturnType; const fieldClass = 'w-full rounded-xl border border-slate-200/80 dark:border-white/10 bg-white/80 dark:bg-slate-900/70 text-slate-900 dark:text-white scheme-light dark:scheme-dark px-4 py-3 sm:py-2.5 text-base sm:text-sm outline-none focus:ring-2 focus:ring-indigo-500/40 min-h-[44px] transition'; const avatars = ['●', '◆', '▲', '■', '✦', '✚', '◉', '⬢']; +const SETUP_CORE_MODULES: ActiveModule[] = ['dashboard', 'appstore', 'docs', 'settings']; +const SETUP_MODULE_TEMPLATES: Record = { + personal: ['contacts', 'calendar', 'calculator', 'widgets'], + business: ['invoices', 'accounting', 'contacts', 'products', 'stock', 'purchases', 'calendar', 'calculator'], + practice: ['therapy_practice', 'contacts', 'calendar', 'invoices', 'accounting'], + retail: ['pos', 'products', 'stock', 'purchases', 'accounting', 'contacts', 'invoices'], +}; +const SETUP_TEMPLATE_KEYS = ['personal', 'business', 'practice', 'retail'] as const; +const SETUP_BUSINESS_ONLY_MODULES = new Set([ + 'invoices', 'accounting', 'purchases', 'pos', 'support_services' +]); +const SETUP_MODULE_OPTIONS: ActiveModule[] = [ + 'invoices', 'accounting', 'contacts', 'products', 'stock', 'purchases', 'calendar', 'calculator', + 'therapy_practice', 'support_services', 'pos', 'widgets', 'feedback', +]; +const SETUP_MODULE_ICONS: Partial>> = { + invoices: FileText, + accounting: ChartNoAxesCombined, + contacts: ContactRound, + products: BriefcaseBusiness, + stock: Layers, + purchases: Download, + calendar: CalendarDays, + calculator: Calculator, + therapy_practice: HeartPulse, + support_services: LifeBuoy, + pos: CreditCard, + widgets: LayoutGrid, + feedback: MessageSquare, +}; +const SETUP_MODULE_DESCRIPTIONS: Partial> = { + invoices: 'auth.setupModuleDesc.invoices', + accounting: 'auth.setupModuleDesc.accounting', + contacts: 'auth.setupModuleDesc.contacts', + products: 'auth.setupModuleDesc.products', + stock: 'auth.setupModuleDesc.stock', + purchases: 'auth.setupModuleDesc.purchases', + calendar: 'auth.setupModuleDesc.calendar', + calculator: 'auth.setupModuleDesc.calculator', + therapy_practice: 'auth.setupModuleDesc.therapy_practice', + support_services: 'auth.setupModuleDesc.support_services', + pos: 'auth.setupModuleDesc.pos', + widgets: 'auth.setupModuleDesc.widgets', + feedback: 'auth.setupModuleDesc.feedback', +}; + +function getSetupModulesForAccount(modules: ActiveModule[], accountType: AccountType): ActiveModule[] { + return accountType === 'personal' + ? modules.filter((module) => !SETUP_BUSINESS_ONLY_MODULES.has(module)) + : modules; +} + +function resizeOnboardingWallpaper(file: File): Promise { + return new Promise((resolve, reject) => { + const reader = new FileReader(); + reader.onerror = () => reject(new Error('image_read_failed')); + reader.onload = () => { + const image = new Image(); + image.onerror = () => reject(new Error('image_decode_failed')); + image.onload = () => { + const ratio = Math.min(1, 1920 / image.width, 1080 / image.height); + const canvas = document.createElement('canvas'); + canvas.width = Math.round(image.width * ratio); + canvas.height = Math.round(image.height * ratio); + const context = canvas.getContext('2d'); + if (!context) return reject(new Error('image_canvas_failed')); + context.drawImage(image, 0, 0, canvas.width, canvas.height); + resolve(canvas.toDataURL('image/webp', 0.82)); + }; + image.src = String(reader.result); + }; + reader.readAsDataURL(file); + }); +} function computePasswordStrength(password: string): { score: number; @@ -294,7 +388,7 @@ export function AuthGate({ children, company }: { children: React.ReactNode; com } if (currentUser.mustChangePassword) return ; if (locked) return ; - return
{children}
; + return
{children}
; } function LanguageSelectionScreen({ @@ -415,12 +509,12 @@ function LanguageSelectionScreen({ }; return ( -
-
+
+
{/* Header with icon and step badge */} -
-
-
+
+
+
{isFirstRunOnboarding && ( @@ -440,7 +534,7 @@ function LanguageSelectionScreen({ {/* Search bar when over 10 languages are present */} {availableLanguages.length > 10 && ( -
+
+
{filteredLanguages.map((language) => { const isSelected = selectedId === language.id; return ( @@ -477,7 +571,7 @@ function LanguageSelectionScreen({ : 'border-slate-200 dark:border-white/10 hover:bg-slate-50 dark:hover:bg-white/5 hover:border-slate-300 dark:hover:border-white/20' }`} > -
+
{language.flagImage ? ( ) : language.emoji && language.emoji.length > 0 && language.emoji !== '🏳️' ? ( @@ -503,7 +597,7 @@ function LanguageSelectionScreen({ ); })} {filteredLanguages.length === 0 && ( -
+

{t('lang_modal.no_search_results', lang, 'Keine passende Sprache gefunden')} @@ -543,6 +637,19 @@ function FirstAccount({ }) { const lang = useLanguage(); const fileInputRef = useRef(null); + const wallpaperInputRef = useRef(null); + const [showInitialSetup, setShowInitialSetup] = useState(true); + const [setupAccent, setSetupAccent] = useState(() => initialCompany?.accent_color || (typeof localStorage !== 'undefined' ? localStorage.getItem('socdof_accent_color') : null) || 'indigo'); + const [setupTheme, setSetupTheme] = useState<'light' | 'dark' | 'system'>('system'); + const [setupWallpaper, setSetupWallpaper] = useState(() => initialCompany?.desktop_wallpaper_url || ''); + const [setupApps, setSetupApps] = useState(() => [...SETUP_MODULE_TEMPLATES.personal]); + const previewBaselineRef = useRef({ + isDark: typeof document !== 'undefined' && document.documentElement.classList.contains('dark'), + colorScheme: typeof document !== 'undefined' ? document.documentElement.style.colorScheme : '', + storedAccent: typeof localStorage !== 'undefined' ? localStorage.getItem('socdof_accent_color') : null, + accent: initialCompany?.accent_color || (typeof localStorage !== 'undefined' ? localStorage.getItem('socdof_accent_color') : null) || 'indigo', + }); + const keepSetupPreviewRef = useRef(false); const [form, setForm] = useState({ username: '', displayName: '', @@ -573,6 +680,86 @@ function FirstAccount({ const strength = computePasswordStrength(form.password); + const restoreSetupPreview = () => { + const root = document.documentElement; + const baseline = previewBaselineRef.current; + applyAccentColor(baseline.accent); + if (baseline.storedAccent === null) { + try { + localStorage.removeItem('socdof_accent_color'); + } catch { + // Ignore storage failures while restoring the preview. + } + } + root.classList.toggle('dark', baseline.isDark); + root.style.colorScheme = baseline.colorScheme; + }; + + useEffect(() => { + return () => { + if (!keepSetupPreviewRef.current) restoreSetupPreview(); + }; + }, []); + + useEffect(() => { + if (!showInitialSetup || setupTheme !== 'system') return; + const media = window.matchMedia('(prefers-color-scheme: dark)'); + const applySystemPreview = () => { + const isDark = media.matches; + document.documentElement.classList.toggle('dark', isDark); + document.documentElement.style.colorScheme = isDark ? 'dark' : 'light'; + }; + applySystemPreview(); + media.addEventListener?.('change', applySystemPreview); + return () => media.removeEventListener?.('change', applySystemPreview); + }, [showInitialSetup, setupTheme]); + + const handleThemePreview = (mode: 'light' | 'dark' | 'system') => { + setSetupTheme(mode); + if (!showInitialSetup) return; + const isDark = mode === 'dark' || (mode === 'system' && window.matchMedia('(prefers-color-scheme: dark)').matches); + document.documentElement.classList.toggle('dark', isDark); + document.documentElement.style.colorScheme = isDark ? 'dark' : 'light'; + }; + + const handleAccentPreview = (accent: string) => { + setSetupAccent(accent); + if (showInitialSetup) applyAccentColor(accent); + }; + + const toggleInitialSetup = () => { + const nextEnabled = !showInitialSetup; + setShowInitialSetup(nextEnabled); + if (nextEnabled) { + applyAccentColor(setupAccent); + const isDark = setupTheme === 'dark' || (setupTheme === 'system' && window.matchMedia('(prefers-color-scheme: dark)').matches); + document.documentElement.classList.toggle('dark', isDark); + document.documentElement.style.colorScheme = isDark ? 'dark' : 'light'; + } else { + restoreSetupPreview(); + } + }; + + const handleSetupWallpaperFile = async (file?: File) => { + if (!file?.type.startsWith('image/')) return; + try { + setSetupWallpaper(await resizeOnboardingWallpaper(file)); + setError(''); + } catch { + setError(t('auth.setupWallpaperError', lang)); + } + }; + + const selectAppTemplate = (template: keyof typeof SETUP_MODULE_TEMPLATES) => { + setSetupApps(getSetupModulesForAccount(SETUP_MODULE_TEMPLATES[template], form.accountType)); + }; + + const toggleSetupApp = (module: ActiveModule) => { + setSetupApps((current) => current.includes(module) + ? current.filter((item) => item !== module) + : [...current, module]); + }; + const handleAvatarFile = (file: File) => { if (!file.type.startsWith('image/')) return; const reader = new FileReader(); @@ -646,24 +833,36 @@ function FirstAccount({ try { setLanguage(lang); - await createUser({ + const initialPreferences = showInitialSetup ? { + language: lang, + accentColor: setupAccent, + theme: setupTheme, + wallpaper: setupWallpaper || undefined, + } : { language: lang }; + const createdUser = await createUser({ username: form.username.trim(), displayName: form.displayName.trim(), password: form.password, accountType: form.accountType, avatar: form.avatar ? form.avatar : undefined, - preferences: { language: lang }, + preferences: initialPreferences, recoveryQuestion, recoveryAnswer, autoLogin: false, }); + let updatedCompany: CompanyProfile | undefined; try { const settingRecord = await db.settings.get('company_profile'); const currentCompany: CompanyProfile = (settingRecord?.value as CompanyProfile) || initialCompany || ({} as CompanyProfile); - const updatedCompany: CompanyProfile = { + updatedCompany = { ...currentCompany, language: lang, + ...(showInitialSetup ? { + accent_color: setupAccent, + theme_mode: setupTheme, + desktop_wallpaper_url: setupWallpaper || undefined, + } : {}), ...(form.accountType === 'business' ? { name: businessDetails.companyName.trim() || currentCompany.name || '', ...(!isBusinessSetupSkipped ? { @@ -678,11 +877,44 @@ function FirstAccount({ } : {}) }; await db.settings.put({ key: 'company_profile', value: updatedCompany }); - window.dispatchEvent(new CustomEvent('socdof-company-updated', { detail: updatedCompany })); } catch (compErr) { console.error('Failed to update company profile from onboarding:', compErr); } + if (showInitialSetup) { + const selectedModules = Array.from(new Set([ + ...SETUP_CORE_MODULES, + ...getSetupModulesForAccount(setupApps, form.accountType), + ])); + const selectedDesktopPins = selectedModules; + const selectedTaskbarPins = ['dashboard', 'invoices', 'accounting', 'contacts', 'calendar', 'settings'] + .filter((module): module is ActiveModule => selectedModules.includes(module as ActiveModule)); + const moduleValues: Record = { + odoo_installed_modules: selectedModules, + odoo_pinned_desktop: selectedDesktopPins, + odoo_pinned_taskbar: selectedTaskbarPins, + }; + const scopedPrefix = `socdof.user.${createdUser.id}.`; + try { + Object.entries(moduleValues).forEach(([key, value]) => { + const serialized = JSON.stringify(value); + localStorage.setItem(key, serialized); + localStorage.setItem(scopedPrefix + key, serialized); + localStorage.setItem(scopedPrefix + 'all.' + key, serialized); + }); + } catch (storageErr) { + console.warn('Could not persist all onboarding module preferences:', storageErr); + } + applyAccentColor(setupAccent); + keepSetupPreviewRef.current = true; + const isDark = setupTheme === 'dark' || (setupTheme === 'system' && window.matchMedia('(prefers-color-scheme: dark)').matches); + document.documentElement.classList.toggle('dark', isDark); + document.documentElement.style.colorScheme = isDark ? 'dark' : 'light'; + } + if (updatedCompany) { + window.dispatchEvent(new CustomEvent('socdof-company-updated', { detail: updatedCompany })); + } + clearSession(); onCreated(form.username.trim()); } catch (err) { @@ -701,16 +933,16 @@ function FirstAccount({ }; return ( -

-
+
+
{/* Header with step indicator and language selector */} -
-
+
+
{t('auth.stepAccount', lang)}
-
+
setBusinessDetails({ ...businessDetails, currency: e.target.value })} /> -
+
{['€', '$', 'CHF', '£'].map((curr) => (
-
+
+ {/* Optional first-run appearance and app setup */} +
+
+
+ + + +
+

{t('auth.setupTitle', lang)}

+

{t('auth.setupDescription', lang)}

+
+
+ +
+ + {showInitialSetup ? ( +
+
+
+ {t('auth.setupAccent', lang)} +
+
+ {ACCENT_LIST.map((accent) => ( + + ))} +
+
+ +
+
+ {t('auth.setupTheme', lang)} +
+
+ {([ + { mode: 'light', label: t('auth.setupLight', lang), Icon: Sun }, + { mode: 'dark', label: t('auth.setupDark', lang), Icon: Moon }, + { mode: 'system', label: t('auth.setupSystem', lang), Icon: Monitor }, + ] as const).map(({ mode, label, Icon }) => ( + + ))} +
+
+ +
+
+ {t('auth.setupWallpaper', lang)} +
+ { + void handleSetupWallpaperFile(event.target.files?.[0]); + event.target.value = ''; + }} + /> +
+ {setupWallpaper ? ( + {t('auth.setupWallpaper', + ) : ( +
+ +
+ )} +
+

{t('auth.setupWallpaperHint', lang)}

+
+ + {setupWallpaper && ( + + )} +
+
+
+
+ +
+
+ {t('auth.setupTemplate', lang)} +
+
+ {SETUP_TEMPLATE_KEYS.filter((template) => form.accountType === 'business' || template === 'personal' || template === 'practice').map((template) => { + const templateApps = getSetupModulesForAccount(SETUP_MODULE_TEMPLATES[template], form.accountType); + const isSelected = templateApps.length > 0 && templateApps.every((module) => setupApps.includes(module)) && setupApps.length === templateApps.length; + const TemplateIcon = template === 'personal' ? UserRound : template === 'business' ? BriefcaseBusiness : template === 'practice' ? HeartPulse : ShoppingCart; + return ( + + ); + })} +
+
+

{t('auth.setupApps', lang)}

+ {t('auth.setupAppsHint', lang)} +
+
+ {SETUP_MODULE_OPTIONS.filter((module) => form.accountType === 'business' || !SETUP_BUSINESS_ONLY_MODULES.has(module)).map((module) => { + const isSelected = setupApps.includes(module); + const ModuleIcon = SETUP_MODULE_ICONS[module] || BriefcaseBusiness; + return ( + + ); + })} +
+
+
+ ) : ( +

{t('auth.setupSkippedNotice', lang)}

+ )} +
+ {/* Profile Picture (Optional, gray silhouette default, upload/remove) */}
-
-
+
+
{form.avatar ? ( Avatar ) : ( )}
-
+
-
+
{users.map((user) => )}
{canManage && <>
{avatars.map((avatar) => )}
updateUser(selected.id, { displayName: e.target.value })} />
}
setNewPassword(e.target.value)} />

{text.forcePassword}

-{canManage &&

{text.security}

} -{message &&

{message}

}
{canManage &&
setNewUser({ ...newUser, displayName: e.target.value })} /> setNewUser({ ...newUser, username: e.target.value })} /> setNewUser({ ...newUser, password: e.target.value })} /> setNewUser({ ...newUser, recoveryAnswer: e.target.value })} />
}
; + return

{text.users}

{text.current}: {currentUser.displayName}

{users.map((user) => )}
{canManage && <>
{avatars.map((avatar) => )}
updateUser(selected.id, { displayName: e.target.value })} />
}
setNewPassword(e.target.value)} />

{text.forcePassword}

+{canManage &&

{text.security}

} +{message &&

{message}

}
{canManage &&
setNewUser({ ...newUser, displayName: e.target.value })} /> setNewUser({ ...newUser, username: e.target.value })} /> setNewUser({ ...newUser, password: e.target.value })} /> setNewUser({ ...newUser, recoveryAnswer: e.target.value })} />
}
; } diff --git a/src/components/DocumentationApp.tsx b/src/components/DocumentationApp.tsx index 1f2bf48..26d0e53 100644 --- a/src/components/DocumentationApp.tsx +++ b/src/components/DocumentationApp.tsx @@ -163,6 +163,15 @@ export const DocumentationApp: React.FC = () => { es: 'Accede a todos los módulos comerciales, búsqueda y configuración desde el menú de inicio inferior.' })} +
  • + {getLoc({ de: 'Einstellungen finden: ', en: 'Find a setting: ', fr: 'Trouver un réglage : ', es: 'Buscar un ajuste: ' })} + {getLoc({ + de: 'In Einstellungen führen die Schnellzugriffe zu häufig genutzten Bereichen. Am Desktop wählen Sie Kategorien in der Seitenleiste; in schmalen Fenstern öffnen Sie das gruppierte Bereiche-Menü. Über die Suche finden Sie Einstellungen direkt. Firmendaten ergänzen Sie unter Allgemein & Stammdaten.', + en: 'Settings shortcuts open commonly used sections. On desktop, choose categories in the sidebar; in narrow windows, open the grouped Sections menu. Search finds settings directly. Add company details under General & Master Data.', + fr: 'Les raccourcis des paramètres ouvrent les sections courantes. Sur ordinateur, choisissez une catégorie dans la barre latérale ; dans une fenêtre étroite, ouvrez le menu Sections groupé. La recherche permet de trouver directement un réglage. Ajoutez les informations de l’entreprise dans Général et données principales.', + es: 'Los accesos de Configuración abren las secciones habituales. En escritorio, elige una categoría en la barra lateral; en ventanas estrechas, abre el menú Secciones agrupado. La búsqueda encuentra ajustes directamente. Añade los datos de empresa en General y datos principales.' + })} +
  • @@ -224,6 +233,8 @@ export const DocumentationApp: React.FC = () => {
    • {getLoc({ de: 'Beim ersten Start wird ein lokales Administratorkonto erstellt. Weitere Konten werden durch einen aktiven Administrator angelegt.', en: 'The first setup creates a local administrator. Additional accounts are created by an active administrator.', fr: 'La première configuration crée un administrateur local. Les comptes supplémentaires sont créés par un administrateur actif.', es: 'La primera configuración crea un administrador local. Las cuentas adicionales las crea un administrador activo.' })}
    • +
    • {getLoc({ de: 'Während der ersten Kontoerstellung kann die optionale Arbeitsbereich-Einrichtung übersprungen werden: Akzentfarbe, Hell-/Dunkel-/Systemmodus, lokales Hintergrundbild und App-Vorlagen mit individueller App-Auswahl. Systemmodus ist vorausgewählt; die Auswahl lässt sich später unter Einstellungen anpassen.', en: 'During first-account creation, users can skip the optional workspace setup: accent color, light/dark/system theme, local wallpaper, and app templates with individual app selection. System mode is selected by default; these choices can be changed later in Settings.', fr: 'Lors de la création du premier compte, la configuration facultative de l’espace de travail peut être ignorée : couleur d’accentuation, thème clair/sombre/système, fond d’écran local et modèles d’applications avec sélection individuelle. Le mode système est sélectionné par défaut ; ces choix restent modifiables dans les Paramètres.', es: 'Al crear la primera cuenta se puede omitir la configuración opcional del espacio de trabajo: color de acento, tema claro/oscuro/sistema, fondo local y plantillas de aplicaciones con selección individual. El modo sistema está seleccionado de forma predeterminada y estas opciones se pueden cambiar después en Configuración.' })}
    • +
    • {getLoc({ de: 'Persönlich, Geschäftlich, Praxis und Einzelhandel stellen passende App-Gruppen bereit. Jede App-Kachel zeigt Name, Kurzbeschreibung, Symbol und Auswahlstatus.', en: 'Personal, Business, Practice, and Retail templates provide relevant app groups. Each app card shows its name, short description, icon, and selection state.', fr: 'Les modèles Personnel, Entreprise, Cabinet et Commerce proposent des groupes d’applications adaptés. Chaque carte indique le nom, une courte description, une icône et l’état de sélection.', es: 'Las plantillas Personal, Empresa, Consulta y Comercio ofrecen grupos de aplicaciones adecuados. Cada tarjeta muestra el nombre, una breve descripción, un icono y el estado de selección.' })}
    • {getLoc({ de: 'Persönliche und geschäftliche Kontotypen, Rollen, Aktivierungsstatus, Avatare und persönliche Einstellungen werden pro Benutzer gespeichert.', en: 'Personal and business account types, roles, activation state, avatars, and personal preferences are stored per user.', fr: 'Les types de compte personnel/professionnel, rôles, état d’activation, avatars et préférences sont stockés par utilisateur.', es: 'Los tipos de cuenta personal/empresa, roles, estado, avatares y preferencias se guardan por usuario.' })}
    • {getLoc({ de: 'Die neue Kategorie „Benutzer & Konten“ befindet sich in Einstellungen und ist ausschließlich für aktive Administratoren verfügbar. Dort werden lokale Konten, Rollen, Kontotypen, Avatare, Aktivierung, automatische Sperre und Anmeldesicherheit verwaltet.', en: 'The new “Users & Accounts” category is available in Settings for active administrators only. It manages local accounts, roles, account types, avatars, activation, auto-lock, and sign-in security.', fr: 'La nouvelle catégorie « Utilisateurs et comptes » est disponible dans les paramètres uniquement pour les administrateurs actifs. Elle gère les comptes locaux, rôles, types de compte, avatars, activation, verrouillage automatique et sécurité de connexion.', es: 'La nueva categoría “Usuarios y cuentas” está disponible en Configuración solo para administradores activos. Permite gestionar cuentas locales, roles, tipos, avatares, activación, bloqueo automático y seguridad de inicio de sesión.' })}
    • {getLoc({ de: 'Das SOCDOF-Startmenü enthält über das Ein/Aus-Symbol jetzt Sperren, Benutzer wechseln, Abmelden, Neustart des Arbeitsbereichs und Schließen der App. Die Verwaltung lokaler Benutzer erfolgt zentral in Einstellungen unter „Benutzer & Konten“ und ist nur für aktive Administratoren verfügbar.', en: 'The SOCDOF Start menu power control now provides Lock, Switch user, Sign out, Restart workspace, and Close SOCDOF. Local user administration is centralized in Settings under Users & Accounts and is available only to active administrators.', fr: 'Le contrôle d’alimentation du menu Démarrer de SOCDOF propose désormais Verrouiller, Changer d’utilisateur, Se déconnecter, Redémarrer l’espace de travail et Fermer SOCDOF. La gestion des utilisateurs locaux est centralisée dans Paramètres, sous Utilisateurs et comptes, et réservée aux administrateurs actifs.', es: 'El control de energía del menú Inicio de SOCDOF ofrece ahora Bloquear, Cambiar de usuario, Cerrar sesión, Reiniciar el espacio de trabajo y Cerrar SOCDOF. La administración de usuarios locales está centralizada en Configuración, dentro de Usuarios y cuentas, y solo está disponible para administradores activos.' })}
    • diff --git a/src/components/SettingsModule.tsx b/src/components/SettingsModule.tsx index 5867762..dfb634a 100644 --- a/src/components/SettingsModule.tsx +++ b/src/components/SettingsModule.tsx @@ -211,6 +211,7 @@ export const SettingsModule: React.FC = ({ const [activeSection, setActiveSection] = useState(initialSection || 'home'); const canManageUsers = getCurrentUser()?.role === 'admin' && getCurrentUser()?.active === true; const [searchQuery, setSearchQuery] = useState(''); + const [isMobileSectionMenuOpen, setIsMobileSectionMenuOpen] = useState(false); const rootRef = useRef(null); // Subcategory Navigation in Personalization (Windows 11 Style) @@ -760,14 +761,20 @@ export const SettingsModule: React.FC = ({ // Recent Searches / Quick Links list const recentSearches = useMemo(() => [ - { id: 'personalization', title: t('settings.recent_darkmode_title', activeLang, 'Dark Mode & Design'), category: t('settings.recent_darkmode_cat', activeLang, 'Personalization'), icon: Palette, color: 'text-purple-500 bg-purple-50 dark:bg-purple-950/60' }, - { id: 'shortcuts', title: t('settings.shortcuts', activeLang, 'Tastenkombinationen & Kürzel'), category: t('settings.shortcuts_desc', activeLang, 'Hotkeys & Schnellzugriff'), icon: Keyboard, color: 'text-amber-500 bg-amber-50 dark:bg-amber-950/60' }, - { id: 'language', title: t('settings.recent_language_title', activeLang, 'Language & Region'), category: t('settings.recent_language_cat', activeLang, 'Language & Time'), icon: Globe, color: 'text-sky-500 bg-sky-50 dark:bg-sky-950/60' }, - { id: 'connections', title: t('settings.recent_calendar_title', activeLang, 'Google Calendar & iCal'), category: t('settings.recent_calendar_cat', activeLang, 'Connections'), icon: CalendarIcon, color: 'text-blue-500 bg-blue-50 dark:bg-blue-950/60' }, - { id: 'letterhead', title: t('settings.recent_letterhead_title', activeLang, 'Letterhead & DIN 5008'), category: t('settings.recent_letterhead_cat', activeLang, 'Documents'), icon: FileText, color: 'text-amber-500 bg-amber-50 dark:bg-amber-950/60' }, - { id: 'general', title: t('settings.recent_bank_title', activeLang, 'Bank Details & IBAN'), category: t('settings.recent_bank_cat', activeLang, 'Company'), icon: CreditCard, color: 'text-emerald-500 bg-emerald-50 dark:bg-emerald-950/60' }, - { id: 'storage', title: t('settings.recent_backup_title', activeLang, 'JSON Data Backup'), category: t('settings.recent_backup_cat', activeLang, 'Storage'), icon: HardDrive, color: 'text-indigo-500 bg-indigo-50 dark:bg-indigo-950/60' }, - ], [activeLang, canManageUsers]); + { id: 'personalization', title: t('settings.recent_darkmode_title', activeLang, 'Dark Mode & Design'), category: t('settings.recent_darkmode_cat', activeLang, 'Personalization'), icon: Palette }, + { id: 'shortcuts', title: t('settings.shortcuts', activeLang, 'Tastenkombinationen & Kürzel'), category: t('settings.shortcuts_desc', activeLang, 'Hotkeys & Schnellzugriff'), icon: Keyboard }, + { id: 'language', title: t('settings.recent_language_title', activeLang, 'Language & Region'), category: t('settings.recent_language_cat', activeLang, 'Language & Time'), icon: Globe }, + { id: 'connections', title: t('settings.recent_calendar_title', activeLang, 'Google Calendar & iCal'), category: t('settings.recent_calendar_cat', activeLang, 'Connections'), icon: CalendarIcon }, + { id: 'letterhead', title: t('settings.recent_letterhead_title', activeLang, 'Letterhead & DIN 5008'), category: t('settings.recent_letterhead_cat', activeLang, 'Documents'), icon: FileText }, + { id: 'general', title: t('settings.recent_bank_title', activeLang, 'Bank Details & IBAN'), category: t('settings.recent_bank_cat', activeLang, 'Company'), icon: CreditCard }, + { id: 'storage', title: t('settings.recent_backup_title', activeLang, 'JSON Data Backup'), category: t('settings.recent_backup_cat', activeLang, 'Storage'), icon: HardDrive }, + ], [activeLang]); + + const companyName = profile.name?.trim() || ''; + const companyNamePlaceholder = ['ihr firmenname', 'firmenname', 'unternehmensname', 'your company name', 'company name', 'your company', 'socdof']; + const companyDisplayName = companyName && !companyNamePlaceholder.includes(companyName.toLowerCase()) + ? companyName + : ''; useEffect(() => { loadStorageInfo(); @@ -775,6 +782,19 @@ export const SettingsModule: React.FC = ({ setCustomCalendarEvents(getStoredCalendarEvents()); }, [company]); + useEffect(() => { + if (!isMobileSectionMenuOpen) return; + + const handleOutsidePointerDown = (event: PointerEvent) => { + if (!rootRef.current?.contains(event.target as Node)) { + setIsMobileSectionMenuOpen(false); + } + }; + + document.addEventListener('pointerdown', handleOutsidePointerDown); + return () => document.removeEventListener('pointerdown', handleOutsidePointerDown); + }, [isMobileSectionMenuOpen]); + const loadStorageInfo = async () => { try { const stats = await getDatabaseStorageStats(); @@ -1218,49 +1238,49 @@ export const SettingsModule: React.FC = ({ id: 'overview', label: t('settings.category_overview', activeLang, 'Übersicht'), items: [ - { id: 'home' as SettingsSection, label: t('settings.home', activeLang, 'Startseite'), icon: Settings, desc: 'Schnellzugriff & Personalisierung' } + { id: 'home' as SettingsSection, label: t('settings.home', activeLang, 'Startseite'), icon: Settings, desc: t('settings.section_desc.home', activeLang, 'Quick access to workspace settings.') } ] }, { id: 'system_display', label: t('settings.category_display', activeLang, 'Anzeige & Bildschirme'), items: [ - { id: 'display' as SettingsSection, label: t('settings.display', activeLang, 'Bildschirme & Skalierung'), icon: Monitor, desc: 'Multi-Monitor, Bildschirmlayout, Skalierung & Nachtmodus' } + { id: 'display' as SettingsSection, label: t('settings.display', activeLang, 'Bildschirme & Skalierung'), icon: Monitor, desc: t('settings.section_desc.display', activeLang, 'Manage displays, scaling, and screen layout.') } ] }, { id: 'business', label: t('settings.category_business', activeLang, 'Unternehmen & Workflow'), items: [ - { id: 'general' as SettingsSection, label: t('settings.general', activeLang, 'Allgemein & Stammdaten'), icon: Building2, desc: 'Firmenanschrift, Steuern & Basisdaten' }, - { id: 'payments' as SettingsSection, label: t('settings.payments', activeLang, 'Zahlungsmethoden & Terminals'), icon: CreditCard, desc: t('settings.payments_desc', activeLang, 'Bankverbindung, IBAN/BIC, EC- & Kreditkarten-Terminals') }, - { id: 'letterhead' as SettingsSection, label: t('settings.letterhead', activeLang, 'Briefkopf & DIN 5008'), icon: FileText, desc: 'Logo, Faltmarken & PDF-Layout' }, - { id: 'connections' as SettingsSection, label: t('settings.connections', activeLang, 'Verbindungen & Kalender'), icon: Link2, desc: 'Google Kalender 2-Way Sync & iCal' } + { id: 'general' as SettingsSection, label: t('settings.general', activeLang, 'Allgemein & Stammdaten'), icon: Building2, desc: t('settings.section_desc.general', activeLang, 'Company address, tax details, and master data.') }, + { id: 'payments' as SettingsSection, label: t('settings.payments', activeLang, 'Zahlungsmethoden & Terminals'), icon: CreditCard, desc: t('settings.section_desc.payments', activeLang, 'Bank details, payment methods, and terminals.') }, + { id: 'letterhead' as SettingsSection, label: t('settings.letterhead', activeLang, 'Briefkopf & DIN 5008'), icon: FileText, desc: t('settings.section_desc.letterhead', activeLang, 'Logo, folding marks, and invoice layout.') }, + { id: 'connections' as SettingsSection, label: t('settings.connections', activeLang, 'Verbindungen & Kalender'), icon: Link2, desc: t('settings.section_desc.connections', activeLang, 'Google Calendar sync and iCal tools.') } ] }, { id: 'interface', label: t('settings.category_interface', activeLang, 'System & Personalisierung'), items: [ - { id: 'personalization' as SettingsSection, label: t('settings.personalization', activeLang, 'Personalisierung & Farben'), icon: Palette, desc: 'Darkmode, Akzentfarben & Desktop-Design' }, - { id: 'language' as SettingsSection, label: t('settings.language', activeLang, 'Sprache & Sprachpakete'), icon: Globe, desc: 'Systemsprache, Währung & .JSON Pakete' }, - { id: 'shortcuts' as SettingsSection, label: t('settings.shortcuts', activeLang, 'Tastenkombinationen & Kürzel'), icon: Keyboard, desc: t('settings.shortcuts_desc', activeLang, 'Tastaturkürzel, Hotkeys & Schnellzugriff') }, - { id: 'audio' as SettingsSection, label: t('settings.audio', activeLang, 'Sound & Lautstärke'), icon: Volume2, desc: 'Kassentöne, Effekte & Lautstärkeregler' } + { id: 'personalization' as SettingsSection, label: t('settings.personalization', activeLang, 'Personalisierung & Farben'), icon: Palette, desc: t('settings.section_desc.personalization', activeLang, 'Theme, accent colors, and desktop design.') }, + { id: 'language' as SettingsSection, label: t('settings.language', activeLang, 'Sprache & Sprachpakete'), icon: Globe, desc: t('settings.section_desc.language', activeLang, 'App language, regional formats, and time.') }, + { id: 'shortcuts' as SettingsSection, label: t('settings.shortcuts', activeLang, 'Tastenkombinationen & Kürzel'), icon: Keyboard, desc: t('settings.section_desc.shortcuts', activeLang, 'Keyboard shortcuts, hotkeys, and quick access.') }, + { id: 'audio' as SettingsSection, label: t('settings.audio', activeLang, 'Sound & Lautstärke'), icon: Volume2, desc: t('settings.section_desc.audio', activeLang, 'Sound effects and volume controls.') } ] }, { id: 'accounts', label: t('settings.category_accounts', activeLang, 'Konten & Profile'), items: [ - { id: 'users' as SettingsSection, label: t('settings.user_accounts', activeLang, 'Benutzerkonto & Profilbild'), icon: UserCircle, desc: t('settings.user_accounts_desc', activeLang, 'Profilbild, Kontoinformationen, Rollen & Sicherheit'), badge: canManageUsers ? t('users.admin_badge', activeLang, 'Admin') : undefined } + { id: 'users' as SettingsSection, label: t('settings.user_accounts', activeLang, 'Benutzerkonto & Profilbild'), icon: UserCircle, desc: t('settings.section_desc.users', activeLang, 'Profile, account details, roles, and security.'), badge: canManageUsers ? t('users.admin_badge', activeLang, 'Admin') : undefined } ] }, { id: 'admin', label: t('settings.category_admin', activeLang, 'Wartung & Datensicherheit'), items: [ - { id: 'windows' as SettingsSection, label: t('settings.windows', activeLang, 'Windows Desktop-App'), icon: Monitor, desc: 'Offline-Betrieb, Autostart & EXE' }, - { id: 'storage' as SettingsSection, label: t('settings.storage', activeLang, 'Speicher & Backup'), icon: HardDrive, desc: 'Snapshots, JSON Export & Backup-Ordner' }, + { id: 'windows' as SettingsSection, label: t('settings.windows', activeLang, 'Windows Desktop-App'), icon: Monitor, desc: t('settings.section_desc.windows', activeLang, 'Offline app, startup, and desktop options.') }, + { id: 'storage' as SettingsSection, label: t('settings.storage', activeLang, 'Speicher & Backup'), icon: HardDrive, desc: t('settings.section_desc.storage', activeLang, 'Snapshots, JSON export, and backup location.') }, { id: 'danger' as SettingsSection, label: t('settings.reset_system_title', activeLang, 'System zurücksetzen'), icon: ShieldAlert, danger: true, desc: t('settings.reset_system_desc', activeLang, 'Setzen Sie Ihr gesamtes System zurück und installieren Sie alles neu.') } ] } @@ -1272,16 +1292,22 @@ export const SettingsModule: React.FC = ({ const CurrentIcon = currentNav?.icon || Settings; return ( -
      +
      { + if (event.key === 'Escape') setIsMobileSectionMenuOpen(false); + }} + className="relative h-full w-full flex flex-col overflow-hidden animate-fade-in text-slate-900 dark:text-slate-100" + > {/* 1. Sleek, Space-Efficient Top Toolbar */} -
      -
      -
      +
      +
      +
      {/* Left: Current View Indicator */}
      -
      +
      @@ -1299,15 +1325,15 @@ export const SettingsModule: React.FC = ({
      {/* Right: Fixed Position Search Box & Quick Save Action */} -
      -
      +
      +
      setSearchQuery(e.target.value)} - className="w-full pl-8 pr-7 py-1.5 text-xs bg-slate-50 dark:bg-slate-800/80 border border-slate-200 dark:border-slate-700 rounded-xl text-slate-900 dark:text-white placeholder:text-slate-400 focus:outline-none focus:ring-2 focus:ring-indigo-500 shadow-xs transition" + className="w-full pl-8 pr-7 py-1.5 text-xs bg-slate-50 dark:bg-slate-800/80 border border-slate-200 dark:border-slate-700 rounded-xl text-slate-900 dark:text-white placeholder:text-slate-400 focus:outline-none focus:ring-2 focus:ring-[var(--accent-ring)] shadow-xs transition" /> {searchQuery && ( ))}
      @@ -1384,47 +1410,80 @@ export const SettingsModule: React.FC = ({ )}
      - {/* Mobile / Small Screen Compact Horizontal Category Pills (prevents huge vertical stack) */} -
      - {navItems.map((item) => { - const Icon = item.icon; - const isActive = activeSection === item.id; - return ( - - ); - })} + {/* The themed section menu avoids the low-contrast native select popup on compact windows. */} +
      +
      + + +
      + {categoryGroups.map((group) => ( +
      +
      + {group.label} +
      +
      + {group.items.map((item) => { + const Icon = item.icon; + const isActive = activeSection === item.id; + return ( + + ); + })} +
      +
      + ))} +
      +
      {/* 2. Main Responsive Two-Column Layout (Windows Settings Style Sidebar + Content) */} -
      -
      +
      +
      {/* Left Sidebar Navigation (Desktop / Tablet view: side by side, independently scrollable) */}
      @@ -1574,21 +1633,32 @@ export const SettingsModule: React.FC = ({ {activeSection === 'home' && (
      - {/* Windows 11 Profile Banner */} -
      -
      -
      -
      - {profile.name ? profile.name.slice(0, 2).toUpperCase() : 'SO'} + {/* Workspace overview */} +
      +
      +
      +
      + {companyDisplayName ? companyDisplayName.slice(0, 2).toUpperCase() : }
      -
      -

      {profile.name || t('settings.your_company', activeLang, 'Your Company')}

      -

      {profile.email || t('settings.no_email_hint', activeLang, 'No email configured')} • {profile.city || t('settings.default_country', activeLang, 'Germany')}

      -
      - +
      +

      + {t('settings.category_overview', activeLang, 'Übersicht & Schnelleinstieg')} +

      +

      + {companyDisplayName || t('settings.workspace_title', activeLang, 'Your workspace')} +

      +

      + {[profile.email, profile.city].filter(Boolean).join(' · ') || t('settings.workspace_local_ready', activeLang, 'Local workspace · Add your company details anytime.')} +

      +
      + + {t('settings.offline_free_badge', activeLang, '100% Offline & Free')} - + {t('settings.din5008_ready', activeLang, 'DIN 5008 Ready')}
      @@ -1596,28 +1666,32 @@ export const SettingsModule: React.FC = ({
      - {/* Zuletzt gesucht & Empfohlene Einstellungen (Windows Inspired) */} -
      + {/* Quick access to recommended settings */} +

      - - {t('settings.recent_quick_start', activeLang, 'Recent Searches & Quick Access')} + + {t('settings.recent_quick_start', activeLang, 'Quick access')}

      - {t('settings.recent_quick_start_desc', activeLang, 'Frequently used settings and personalization options at a glance:')} + {t('settings.recent_quick_start_desc', activeLang, 'Jump directly to commonly used settings.')}

      -
      - {recentSearches.map((tile) => { +
      + {recentSearches.map((tile, recentIndex) => { const Icon = tile.icon; return ( ); })} @@ -1647,15 +1726,16 @@ export const SettingsModule: React.FC = ({
      {/* Quick Preferences Toggles */} -
      +
      {/* 1. Dark Mode Quick Switch */}
      { sounds.playClick(); onToggleTheme(); }} - className="p-3.5 bg-white dark:bg-slate-900 border border-slate-200/80 dark:border-slate-800 hover:border-indigo-300 dark:hover:border-indigo-800 rounded-2xl flex items-center justify-between cursor-pointer transition shadow-xs group select-none" - role="button" + className="p-3.5 bg-white dark:bg-slate-900 border border-slate-200/80 dark:border-slate-800 hover:border-[var(--accent-border-subtle)] rounded-2xl flex items-center justify-between cursor-pointer transition shadow-xs group select-none focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--accent-ring)]" + role="switch" + aria-checked={isDark} tabIndex={0} onKeyDown={(e) => { if (e.key === 'Enter' || e.key === ' ') { @@ -1666,9 +1746,7 @@ export const SettingsModule: React.FC = ({ }} >
      -
      +
      {isDark ? : }
      @@ -1684,8 +1762,9 @@ export const SettingsModule: React.FC = ({ {/* Elegant Animated Toggle Switch */}
      = ({ sounds.playClick(); onToggleSound(); }} - className="p-3.5 bg-white dark:bg-slate-900 border border-slate-200/80 dark:border-slate-800 hover:border-emerald-300 dark:hover:border-emerald-800 rounded-2xl flex items-center justify-between cursor-pointer transition shadow-xs group select-none" - role="button" + className="p-3.5 bg-white dark:bg-slate-900 border border-slate-200/80 dark:border-slate-800 hover:border-[var(--accent-companion-border)] rounded-2xl flex items-center justify-between cursor-pointer transition shadow-xs group select-none focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--accent-ring)]" + role="switch" + aria-checked={!isMuted} tabIndex={0} onKeyDown={(e) => { if (e.key === 'Enter' || e.key === ' ') { @@ -1713,9 +1793,7 @@ export const SettingsModule: React.FC = ({ }} >
      -
      +
      {!isMuted ? : }
      @@ -1731,8 +1809,9 @@ export const SettingsModule: React.FC = ({ {/* Elegant Animated Toggle Switch */}
      = ({ sounds.playClick(); handleSelectSection('connections'); }} - className="p-3.5 bg-white dark:bg-slate-900 border border-slate-200/80 dark:border-slate-800 hover:border-blue-300 dark:hover:border-blue-800 rounded-2xl flex items-center justify-between cursor-pointer transition shadow-xs group select-none" + className="p-3.5 bg-white dark:bg-slate-900 border border-slate-200/80 dark:border-slate-800 hover:border-[var(--accent-border-subtle)] rounded-2xl flex items-center justify-between cursor-pointer transition shadow-xs group select-none focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--accent-ring)]" role="button" tabIndex={0} onKeyDown={(e) => { @@ -1760,7 +1839,7 @@ export const SettingsModule: React.FC = ({ }} >
      -
      +
      @@ -1772,7 +1851,7 @@ export const SettingsModule: React.FC = ({
      -
      +
      {t('settings.quick_open', activeLang, 'Öffnen')}
      @@ -1784,7 +1863,7 @@ export const SettingsModule: React.FC = ({

      - + {t('settings.category_overview', activeLang, 'Einstellungsbereiche & Module')}

      @@ -1793,7 +1872,7 @@ export const SettingsModule: React.FC = ({

      -
      +
      {categoryGroups.filter(g => g.id !== 'overview').map((group) => (
      = ({ >
      - + {group.label}
      @@ -1818,14 +1897,14 @@ export const SettingsModule: React.FC = ({ className={`w-full p-2.5 rounded-2xl text-left transition flex items-start justify-between group ${ item.danger ? 'hover:bg-rose-50/80 dark:hover:bg-rose-950/30' - : 'hover:bg-indigo-50/80 dark:hover:bg-indigo-950/30' + : 'hover:bg-[var(--accent-light)]' }`} >
      @@ -1833,7 +1912,7 @@ export const SettingsModule: React.FC = ({
      {item.label}
      @@ -1842,7 +1921,7 @@ export const SettingsModule: React.FC = ({
      - + ); })} @@ -5770,7 +5849,7 @@ export const SettingsModule: React.FC = ({ value={shortcutSearch} onChange={(e) => setShortcutSearch(e.target.value)} placeholder={t('settings.shortcuts_search_placeholder', activeLang, 'Tastenkürzel suchen...')} - className="w-full pl-8 pr-7 py-1.5 text-xs bg-slate-50 dark:bg-slate-800/80 border border-slate-200 dark:border-slate-700 rounded-xl text-slate-900 dark:text-white placeholder:text-slate-400 focus:outline-none focus:ring-2 focus:ring-indigo-500 shadow-xs transition" + className="w-full pl-8 pr-7 py-1.5 text-xs bg-slate-50 dark:bg-slate-800/80 border border-slate-200 dark:border-slate-700 rounded-xl text-slate-900 dark:text-white placeholder:text-slate-400 focus:outline-none focus:ring-2 focus:ring-[var(--accent-ring)] shadow-xs transition" /> {shortcutSearch && (