feat(settings): add dedicated Sync tab with CardDAV contacts integration
- Rename Calendar tab to Synchronization with two sections: * Calendar Sync (Google, Apple, CalDAV, ICS) * Contact Sync (CardDAV) - NEW - Add visual tab grouping with CSS separators between sections - Implement CardDAV account management UI: * Add/delete accounts * Enable/disable addressbooks * Manual sync trigger * Connection testing - Add UX improvements: * Status badges (success/error/syncing) * Empty states with onboarding * Inline help tooltips (prepared) * Breadcrumb navigation (prepared) - Update i18n keys in all 14 locales - All 109 tests passing Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,349 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Settings Sidebar Navigation - Demo</title>
|
||||
<style>
|
||||
:root {
|
||||
--color-bg: #ffffff;
|
||||
--color-surface: #fafafa;
|
||||
--color-surface-2: #f5f5f5;
|
||||
--color-border: #e5e5e5;
|
||||
--color-text-primary: #171717;
|
||||
--color-text-secondary: #737373;
|
||||
--color-text-tertiary: #a3a3a3;
|
||||
--color-accent: #4F46E5;
|
||||
--space-1: 4px;
|
||||
--space-2: 8px;
|
||||
--space-3: 12px;
|
||||
--space-4: 16px;
|
||||
--space-6: 24px;
|
||||
--space-8: 32px;
|
||||
--radius-sm: 4px;
|
||||
--radius-md: 8px;
|
||||
--radius-lg: 12px;
|
||||
--radius-full: 9999px;
|
||||
--text-xs: 11px;
|
||||
--text-sm: 13px;
|
||||
--text-base: 14px;
|
||||
--text-lg: 16px;
|
||||
--text-2xl: 24px;
|
||||
--font-weight-medium: 500;
|
||||
--font-weight-semibold: 600;
|
||||
--font-weight-bold: 700;
|
||||
--target-base: 36px;
|
||||
--transition-fast: 0.15s ease;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
--color-bg: #18181b;
|
||||
--color-surface: #27272a;
|
||||
--color-surface-2: #3f3f46;
|
||||
--color-border: #3f3f46;
|
||||
--color-text-primary: #fafafa;
|
||||
--color-text-secondary: #a1a1aa;
|
||||
--color-text-tertiary: #71717a;
|
||||
}
|
||||
}
|
||||
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
background: var(--color-bg);
|
||||
color: var(--color-text-primary);
|
||||
}
|
||||
|
||||
/* Layout */
|
||||
.settings-layout {
|
||||
display: grid;
|
||||
grid-template-columns: 240px 1fr;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
/* Sidebar */
|
||||
.settings-sidebar {
|
||||
background: var(--color-surface);
|
||||
border-right: 1px solid var(--color-border);
|
||||
padding: var(--space-6) 0;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.settings-sidebar-section {
|
||||
margin-bottom: var(--space-6);
|
||||
}
|
||||
|
||||
.settings-sidebar-section__header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space-2);
|
||||
padding: 0 var(--space-4);
|
||||
margin-bottom: var(--space-2);
|
||||
color: var(--color-text-tertiary);
|
||||
font-size: var(--text-xs);
|
||||
font-weight: var(--font-weight-semibold);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
}
|
||||
|
||||
.settings-sidebar-section--active .settings-sidebar-section__header {
|
||||
color: var(--color-accent);
|
||||
}
|
||||
|
||||
.settings-sidebar-pages {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--space-1);
|
||||
}
|
||||
|
||||
.settings-sidebar-page {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space-2);
|
||||
padding: var(--space-2) var(--space-4);
|
||||
margin: 0 var(--space-2);
|
||||
border: none;
|
||||
border-radius: var(--radius-md);
|
||||
background: transparent;
|
||||
color: var(--color-text-secondary);
|
||||
font-family: inherit;
|
||||
font-size: var(--text-sm);
|
||||
font-weight: var(--font-weight-medium);
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
transition: background-color var(--transition-fast), color var(--transition-fast);
|
||||
min-height: var(--target-base);
|
||||
}
|
||||
|
||||
.settings-sidebar-page:hover {
|
||||
background: var(--color-surface-2);
|
||||
color: var(--color-text-primary);
|
||||
}
|
||||
|
||||
.settings-sidebar-page--active {
|
||||
background: color-mix(in srgb, var(--color-accent) 12%, transparent);
|
||||
color: var(--color-accent);
|
||||
font-weight: var(--font-weight-semibold);
|
||||
}
|
||||
|
||||
/* Content */
|
||||
.settings-content {
|
||||
padding: var(--space-6);
|
||||
max-width: 900px;
|
||||
}
|
||||
|
||||
.settings-breadcrumb {
|
||||
margin-bottom: var(--space-6);
|
||||
padding-bottom: var(--space-4);
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
font-size: var(--text-sm);
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.settings-breadcrumb__current {
|
||||
color: var(--color-text-primary);
|
||||
font-weight: var(--font-weight-semibold);
|
||||
}
|
||||
|
||||
.settings-page-header__title {
|
||||
font-size: var(--text-2xl);
|
||||
font-weight: var(--font-weight-bold);
|
||||
margin-bottom: var(--space-2);
|
||||
}
|
||||
|
||||
.settings-page-header__description {
|
||||
font-size: var(--text-base);
|
||||
color: var(--color-text-secondary);
|
||||
line-height: 1.6;
|
||||
margin-bottom: var(--space-6);
|
||||
}
|
||||
|
||||
.settings-card {
|
||||
background: var(--color-surface);
|
||||
border-radius: var(--radius-lg);
|
||||
padding: var(--space-4);
|
||||
margin-bottom: var(--space-4);
|
||||
border: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
.settings-empty-state {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: var(--space-8);
|
||||
text-align: center;
|
||||
border: 2px dashed var(--color-border);
|
||||
border-radius: var(--radius-lg);
|
||||
background: var(--color-surface-2);
|
||||
}
|
||||
|
||||
.settings-empty-state__title {
|
||||
font-size: var(--text-lg);
|
||||
font-weight: var(--font-weight-semibold);
|
||||
margin: var(--space-4) 0 var(--space-2);
|
||||
}
|
||||
|
||||
.settings-empty-state__description {
|
||||
color: var(--color-text-secondary);
|
||||
margin-bottom: var(--space-4);
|
||||
}
|
||||
|
||||
.btn {
|
||||
padding: var(--space-2) var(--space-4);
|
||||
border-radius: var(--radius-md);
|
||||
border: none;
|
||||
background: var(--color-accent);
|
||||
color: white;
|
||||
font-weight: var(--font-weight-medium);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.btn:hover {
|
||||
opacity: 0.9;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="settings-layout">
|
||||
<!-- Sidebar -->
|
||||
<nav class="settings-sidebar">
|
||||
<!-- Persönlich -->
|
||||
<div class="settings-sidebar-section">
|
||||
<div class="settings-sidebar-section__header">
|
||||
<span>👤</span>
|
||||
<span>Persönlich</span>
|
||||
</div>
|
||||
<div class="settings-sidebar-pages">
|
||||
<button class="settings-sidebar-page">
|
||||
<span>🔐</span>
|
||||
<span>Konto</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Module -->
|
||||
<div class="settings-sidebar-section">
|
||||
<div class="settings-sidebar-section__header">
|
||||
<span>🧩</span>
|
||||
<span>Module</span>
|
||||
</div>
|
||||
<div class="settings-sidebar-pages">
|
||||
<button class="settings-sidebar-page settings-sidebar-page--active">
|
||||
<span>⚙️</span>
|
||||
<span>Allgemein</span>
|
||||
</button>
|
||||
<button class="settings-sidebar-page">
|
||||
<span>🍽️</span>
|
||||
<span>Mahlzeiten</span>
|
||||
</button>
|
||||
<button class="settings-sidebar-page">
|
||||
<span>💰</span>
|
||||
<span>Budget</span>
|
||||
</button>
|
||||
<button class="settings-sidebar-page">
|
||||
<span>🛒</span>
|
||||
<span>Einkauf</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Synchronisation -->
|
||||
<div class="settings-sidebar-section settings-sidebar-section--active">
|
||||
<div class="settings-sidebar-section__header">
|
||||
<span>🔄</span>
|
||||
<span>Synchronisation</span>
|
||||
</div>
|
||||
<div class="settings-sidebar-pages">
|
||||
<button class="settings-sidebar-page">
|
||||
<span>📅</span>
|
||||
<span>Kalender</span>
|
||||
</button>
|
||||
<button class="settings-sidebar-page">
|
||||
<span>👥</span>
|
||||
<span>Kontakte</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Administration -->
|
||||
<div class="settings-sidebar-section">
|
||||
<div class="settings-sidebar-section__header">
|
||||
<span>🛡️</span>
|
||||
<span>Administration</span>
|
||||
</div>
|
||||
<div class="settings-sidebar-pages">
|
||||
<button class="settings-sidebar-page">
|
||||
<span>👨👩👧👦</span>
|
||||
<span>Familie</span>
|
||||
</button>
|
||||
<button class="settings-sidebar-page">
|
||||
<span>🔑</span>
|
||||
<span>API-Tokens</span>
|
||||
</button>
|
||||
<button class="settings-sidebar-page">
|
||||
<span>💾</span>
|
||||
<span>Backup</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<!-- Content -->
|
||||
<main class="settings-content">
|
||||
<nav class="settings-breadcrumb">
|
||||
Einstellungen › Module › <span class="settings-breadcrumb__current">Allgemein</span>
|
||||
</nav>
|
||||
|
||||
<header class="settings-page-header">
|
||||
<h1 class="settings-page-header__title">Allgemein</h1>
|
||||
<p class="settings-page-header__description">
|
||||
Grundlegende Einstellungen für Design, Sprache und Module.
|
||||
</p>
|
||||
</header>
|
||||
|
||||
<div class="settings-card">
|
||||
<h3 style="margin-bottom: 12px;">Design</h3>
|
||||
<p style="color: var(--color-text-secondary); font-size: var(--text-sm);">
|
||||
Wähle zwischen hellem, dunklem oder System-Modus.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="settings-card">
|
||||
<h3 style="margin-bottom: 12px;">Sprache</h3>
|
||||
<p style="color: var(--color-text-secondary); font-size: var(--text-sm);">
|
||||
Ändere die Anzeigesprache der Anwendung.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Demo: Kontakte-Sync Page -->
|
||||
<div style="display: none;">
|
||||
<nav class="settings-breadcrumb">
|
||||
Einstellungen › Synchronisation › <span class="settings-breadcrumb__current">Kontakte</span>
|
||||
</nav>
|
||||
|
||||
<header class="settings-page-header">
|
||||
<h1 class="settings-page-header__title">Kontakte-Synchronisation</h1>
|
||||
<p class="settings-page-header__description">
|
||||
Verbinde mehrere CardDAV-Konten und synchronisiere deine Kontakte mit iCloud, Nextcloud und anderen Diensten.
|
||||
</p>
|
||||
</header>
|
||||
|
||||
<div class="settings-empty-state">
|
||||
<span style="font-size: 48px;">📇</span>
|
||||
<h3 class="settings-empty-state__title">Noch keine CardDAV-Konten</h3>
|
||||
<p class="settings-empty-state__description">
|
||||
Füge dein erstes CardDAV-Konto hinzu, um Kontakte zu synchronisieren.
|
||||
</p>
|
||||
<button class="btn">+ CardDAV-Konto hinzufügen</button>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
+26
-1
@@ -952,7 +952,32 @@
|
||||
"backupRestoredToast": "تمت استعادة قاعدة البيانات. جارٍ إعادة التحميل...",
|
||||
"backupCliTitle": "استعادة CLI / Docker Compose",
|
||||
"backupCliHint": "For operational restores, stop the app, mount the backup in a temporary container and replace the database file.",
|
||||
"backupCliBackupHint": "يمكنك أيضًا إنشاء نسخة مباشرة عبر Docker Compose:"
|
||||
"backupCliBackupHint": "يمكنك أيضًا إنشاء نسخة مباشرة عبر Docker Compose:",
|
||||
"tabSync": "Synchronization",
|
||||
"sectionContactSync": "Contact Synchronization",
|
||||
"cardavTitle": "CardDAV Contacts",
|
||||
"cardavDescription": "Connect multiple CardDAV accounts (iCloud, Nextcloud, Radicale, etc.) and sync your contacts.",
|
||||
"cardavAddAccount": "Add CardDAV Account",
|
||||
"cardavEmptyState": "No CardDAV accounts connected yet. Add your first account to sync contacts.",
|
||||
"cardavNameLabel": "Account name",
|
||||
"cardavNamePlaceholder": "e.g. iCloud, Nextcloud",
|
||||
"cardavUrlLabel": "CardDAV Server URL",
|
||||
"cardavUrlPlaceholder": "https://contacts.icloud.com",
|
||||
"cardavUrlHint": "The base URL of your CardDAV server",
|
||||
"cardavUsernameLabel": "Username",
|
||||
"cardavPasswordLabel": "Password",
|
||||
"cardavPasswordHint": "For iCloud: Use app-specific password from appleid.apple.com",
|
||||
"cardavAccountAdded": "CardDAV account added successfully",
|
||||
"cardavAccountDeleted": "CardDAV account removed",
|
||||
"cardavSyncSuccess": "CardDAV sync successful",
|
||||
"cardavSyncFailed": "CardDAV sync failed",
|
||||
"cardavConnectionFailed": "Connection to CardDAV server failed",
|
||||
"cardavAddressbooksToggle": "Show/hide addressbooks",
|
||||
"cardavRefreshAddressbooks": "Refresh addressbooks",
|
||||
"addressbookEnabled": "Addressbook enabled",
|
||||
"addressbookDisabled": "Addressbook disabled",
|
||||
"addressbooksRefreshed": "Addressbooks refreshed",
|
||||
"deleteCardDAVAccountConfirm": "Really delete CardDAV account? All synced contacts will remain but lose their CardDAV link."
|
||||
},
|
||||
"login": {
|
||||
"tagline": "تخطيط عائلي. آمن. يحترم الخصوصية. مفتوح المصدر.",
|
||||
|
||||
+46
-2
@@ -758,14 +758,26 @@
|
||||
},
|
||||
"settings": {
|
||||
"title": "Einstellungen",
|
||||
"navigationLabel": "Einstellungsnavigation",
|
||||
"breadcrumbLabel": "Pfad",
|
||||
"sectionPersonal": "Persönlich",
|
||||
"sectionModulesNav": "Module",
|
||||
"sectionSync": "Synchronisation",
|
||||
"sectionAdmin": "Administration",
|
||||
"tabGeneral": "Allgemein",
|
||||
"tabMeals": "Mahlzeiten",
|
||||
"tabBudget": "Budget",
|
||||
"tabShopping": "Einkauf",
|
||||
"tabCalendar": "Kalender",
|
||||
"tabFamily": "Familienverwaltung",
|
||||
"tabSync": "Synchronisation",
|
||||
"tabSyncCalendar": "Kalender",
|
||||
"tabSyncContacts": "Kontakte",
|
||||
"sectionContactSync": "Kontakt-Synchronisation",
|
||||
"cardavTitle": "CardDAV Kontakte",
|
||||
"tabFamily": "Familie",
|
||||
"tabApiTokens": "API-Tokens",
|
||||
"tabAccount": "Konto",
|
||||
"tabBackup": "Backup",
|
||||
"tabsAriaLabel": "Einstellungsbereiche",
|
||||
"sectionDesign": "Design",
|
||||
"sectionAppName": "Anwendungsname",
|
||||
@@ -1015,7 +1027,39 @@
|
||||
"calendarDisabled": "Kalender deaktiviert",
|
||||
"calendarsRefreshed": "Kalender aktualisiert",
|
||||
"deleteAccountConfirm": "CalDAV-Konto wirklich löschen? Alle synchronisierten Kalender werden entfernt.",
|
||||
"lastSync": "Zuletzt synchronisiert"
|
||||
"lastSync": "Zuletzt synchronisiert",
|
||||
"cardavTitle": "CardDAV Kontakte",
|
||||
"cardavDescription": "Verbinde mehrere CardDAV-Konten (iCloud, Nextcloud, Radicale, etc.) und synchronisiere deine Kontakte.",
|
||||
"cardavAddAccount": "CardDAV-Konto hinzufügen",
|
||||
"cardavEmptyState": "Noch keine CardDAV-Konten verbunden. Füge dein erstes Konto hinzu, um Kontakte zu synchronisieren.",
|
||||
"cardavNameLabel": "Kontoname",
|
||||
"cardavNamePlaceholder": "z.B. iCloud, Nextcloud",
|
||||
"cardavUrlLabel": "CardDAV Server-URL",
|
||||
"cardavUrlPlaceholder": "https://contacts.icloud.com",
|
||||
"cardavUrlHint": "Die Basis-URL deines CardDAV-Servers",
|
||||
"cardavUsernameLabel": "Benutzername",
|
||||
"cardavPasswordLabel": "Passwort",
|
||||
"cardavPasswordHint": "Für iCloud: App-spezifisches Passwort von appleid.apple.com verwenden",
|
||||
"cardavAccountAdded": "CardDAV-Konto erfolgreich hinzugefügt",
|
||||
"cardavAccountDeleted": "CardDAV-Konto entfernt",
|
||||
"cardavSyncSuccess": "CardDAV-Synchronisation erfolgreich",
|
||||
"cardavSyncFailed": "CardDAV-Synchronisation fehlgeschlagen",
|
||||
"cardavConnectionFailed": "Verbindung zum CardDAV-Server fehlgeschlagen",
|
||||
"cardavAddressbooksToggle": "Adressbücher anzeigen/ausblenden",
|
||||
"cardavRefreshAddressbooks": "Adressbücher aktualisieren",
|
||||
"addressbookEnabled": "Adressbuch aktiviert",
|
||||
"addressbookDisabled": "Adressbuch deaktiviert",
|
||||
"addressbooksRefreshed": "Adressbücher aktualisiert",
|
||||
"deleteCardDAVAccountConfirm": "CardDAV-Konto wirklich löschen? Alle synchronisierten Kontakte bleiben erhalten, verlieren aber die CardDAV-Verknüpfung.",
|
||||
"statusSynced": "Synchronisiert",
|
||||
"statusError": "Fehler",
|
||||
"statusSyncing": "Synchronisiert…",
|
||||
"statusNeverSynced": "Noch nie synchronisiert",
|
||||
"syncedAgo": "vor {{time}}",
|
||||
"helpTooltipCalDAV": "CalDAV ermöglicht die Synchronisation von Kalendern mit iCloud, Nextcloud und anderen CalDAV-Servern.",
|
||||
"helpTooltipCardDAV": "CardDAV ermöglicht die Synchronisation von Kontakten mit iCloud, Nextcloud und anderen CardDAV-Servern.",
|
||||
"emptyStateAddFirst": "Füge dein erstes Konto hinzu",
|
||||
"emptyStateNoAccounts": "Noch keine Konten verbunden"
|
||||
},
|
||||
"login": {
|
||||
"tagline": "Familienplanung. Sicher. Datenschutzfreundlich. Open Source.",
|
||||
|
||||
+26
-1
@@ -952,7 +952,32 @@
|
||||
"backupRestoredToast": "Η βάση επαναφέρθηκε. Επαναφόρτωση...",
|
||||
"backupCliTitle": "Επαναφορά CLI / Docker Compose",
|
||||
"backupCliHint": "For operational restores, stop the app, mount the backup in a temporary container and replace the database file.",
|
||||
"backupCliBackupHint": "Μπορείτε επίσης να δημιουργήσετε αντίγραφο απευθείας με Docker Compose:"
|
||||
"backupCliBackupHint": "Μπορείτε επίσης να δημιουργήσετε αντίγραφο απευθείας με Docker Compose:",
|
||||
"tabSync": "Synchronization",
|
||||
"sectionContactSync": "Contact Synchronization",
|
||||
"cardavTitle": "CardDAV Contacts",
|
||||
"cardavDescription": "Connect multiple CardDAV accounts (iCloud, Nextcloud, Radicale, etc.) and sync your contacts.",
|
||||
"cardavAddAccount": "Add CardDAV Account",
|
||||
"cardavEmptyState": "No CardDAV accounts connected yet. Add your first account to sync contacts.",
|
||||
"cardavNameLabel": "Account name",
|
||||
"cardavNamePlaceholder": "e.g. iCloud, Nextcloud",
|
||||
"cardavUrlLabel": "CardDAV Server URL",
|
||||
"cardavUrlPlaceholder": "https://contacts.icloud.com",
|
||||
"cardavUrlHint": "The base URL of your CardDAV server",
|
||||
"cardavUsernameLabel": "Username",
|
||||
"cardavPasswordLabel": "Password",
|
||||
"cardavPasswordHint": "For iCloud: Use app-specific password from appleid.apple.com",
|
||||
"cardavAccountAdded": "CardDAV account added successfully",
|
||||
"cardavAccountDeleted": "CardDAV account removed",
|
||||
"cardavSyncSuccess": "CardDAV sync successful",
|
||||
"cardavSyncFailed": "CardDAV sync failed",
|
||||
"cardavConnectionFailed": "Connection to CardDAV server failed",
|
||||
"cardavAddressbooksToggle": "Show/hide addressbooks",
|
||||
"cardavRefreshAddressbooks": "Refresh addressbooks",
|
||||
"addressbookEnabled": "Addressbook enabled",
|
||||
"addressbookDisabled": "Addressbook disabled",
|
||||
"addressbooksRefreshed": "Addressbooks refreshed",
|
||||
"deleteCardDAVAccountConfirm": "Really delete CardDAV account? All synced contacts will remain but lose their CardDAV link."
|
||||
},
|
||||
"login": {
|
||||
"tagline": "Οικογενειακός προγραμματισμός. Ασφαλής. Φιλικός προς την ιδιωτικότητα. Ανοιχτός κώδικας.",
|
||||
|
||||
+26
-1
@@ -760,7 +760,9 @@
|
||||
"tabFamily": "Family Management",
|
||||
"tabApiTokens": "API Tokens",
|
||||
"tabAccount": "Account",
|
||||
"tabSync": "Synchronization",
|
||||
"tabsAriaLabel": "Settings sections",
|
||||
"sectionContactSync": "Contact Synchronization",
|
||||
"sectionDesign": "Appearance",
|
||||
"sectionAppName": "Application name",
|
||||
"sectionModules": "Modules",
|
||||
@@ -1009,7 +1011,30 @@
|
||||
"calendarDisabled": "Calendar disabled",
|
||||
"calendarsRefreshed": "Calendars refreshed",
|
||||
"deleteAccountConfirm": "Really delete CalDAV account? All synced calendars will be removed.",
|
||||
"lastSync": "Last synced"
|
||||
"lastSync": "Last synced",
|
||||
"cardavTitle": "CardDAV Contacts",
|
||||
"cardavDescription": "Connect multiple CardDAV accounts (iCloud, Nextcloud, Radicale, etc.) and sync your contacts.",
|
||||
"cardavAddAccount": "Add CardDAV Account",
|
||||
"cardavEmptyState": "No CardDAV accounts connected yet. Add your first account to sync contacts.",
|
||||
"cardavNameLabel": "Account name",
|
||||
"cardavNamePlaceholder": "e.g. iCloud, Nextcloud",
|
||||
"cardavUrlLabel": "CardDAV Server URL",
|
||||
"cardavUrlPlaceholder": "https://contacts.icloud.com",
|
||||
"cardavUrlHint": "The base URL of your CardDAV server",
|
||||
"cardavUsernameLabel": "Username",
|
||||
"cardavPasswordLabel": "Password",
|
||||
"cardavPasswordHint": "For iCloud: Use app-specific password from appleid.apple.com",
|
||||
"cardavAccountAdded": "CardDAV account added successfully",
|
||||
"cardavAccountDeleted": "CardDAV account removed",
|
||||
"cardavSyncSuccess": "CardDAV sync successful",
|
||||
"cardavSyncFailed": "CardDAV sync failed",
|
||||
"cardavConnectionFailed": "Connection to CardDAV server failed",
|
||||
"cardavAddressbooksToggle": "Show/hide addressbooks",
|
||||
"cardavRefreshAddressbooks": "Refresh addressbooks",
|
||||
"addressbookEnabled": "Addressbook enabled",
|
||||
"addressbookDisabled": "Addressbook disabled",
|
||||
"addressbooksRefreshed": "Addressbooks refreshed",
|
||||
"deleteCardDAVAccountConfirm": "Really delete CardDAV account? All synced contacts will remain but lose their CardDAV link."
|
||||
},
|
||||
"login": {
|
||||
"tagline": "Family planning. Secure. Privacy-friendly. Open source.",
|
||||
|
||||
+26
-1
@@ -952,7 +952,32 @@
|
||||
"backupRestoredToast": "Base de datos restaurada. Recargando...",
|
||||
"backupCliTitle": "Restauración por CLI / Docker Compose",
|
||||
"backupCliHint": "For operational restores, stop the app, mount the backup in a temporary container and replace the database file.",
|
||||
"backupCliBackupHint": "También puedes crear una copia directamente con Docker Compose:"
|
||||
"backupCliBackupHint": "También puedes crear una copia directamente con Docker Compose:",
|
||||
"tabSync": "Synchronization",
|
||||
"sectionContactSync": "Contact Synchronization",
|
||||
"cardavTitle": "CardDAV Contacts",
|
||||
"cardavDescription": "Connect multiple CardDAV accounts (iCloud, Nextcloud, Radicale, etc.) and sync your contacts.",
|
||||
"cardavAddAccount": "Add CardDAV Account",
|
||||
"cardavEmptyState": "No CardDAV accounts connected yet. Add your first account to sync contacts.",
|
||||
"cardavNameLabel": "Account name",
|
||||
"cardavNamePlaceholder": "e.g. iCloud, Nextcloud",
|
||||
"cardavUrlLabel": "CardDAV Server URL",
|
||||
"cardavUrlPlaceholder": "https://contacts.icloud.com",
|
||||
"cardavUrlHint": "The base URL of your CardDAV server",
|
||||
"cardavUsernameLabel": "Username",
|
||||
"cardavPasswordLabel": "Password",
|
||||
"cardavPasswordHint": "For iCloud: Use app-specific password from appleid.apple.com",
|
||||
"cardavAccountAdded": "CardDAV account added successfully",
|
||||
"cardavAccountDeleted": "CardDAV account removed",
|
||||
"cardavSyncSuccess": "CardDAV sync successful",
|
||||
"cardavSyncFailed": "CardDAV sync failed",
|
||||
"cardavConnectionFailed": "Connection to CardDAV server failed",
|
||||
"cardavAddressbooksToggle": "Show/hide addressbooks",
|
||||
"cardavRefreshAddressbooks": "Refresh addressbooks",
|
||||
"addressbookEnabled": "Addressbook enabled",
|
||||
"addressbookDisabled": "Addressbook disabled",
|
||||
"addressbooksRefreshed": "Addressbooks refreshed",
|
||||
"deleteCardDAVAccountConfirm": "Really delete CardDAV account? All synced contacts will remain but lose their CardDAV link."
|
||||
},
|
||||
"login": {
|
||||
"tagline": "Planificación familiar. Segura. Privada. Código abierto.",
|
||||
|
||||
+26
-1
@@ -952,7 +952,32 @@
|
||||
"backupRestoredToast": "Base restaurée. Rechargement...",
|
||||
"backupCliTitle": "Restauration CLI / Docker Compose",
|
||||
"backupCliHint": "For operational restores, stop the app, mount the backup in a temporary container and replace the database file.",
|
||||
"backupCliBackupHint": "Tu peux aussi créer une sauvegarde directement avec Docker Compose :"
|
||||
"backupCliBackupHint": "Tu peux aussi créer une sauvegarde directement avec Docker Compose :",
|
||||
"tabSync": "Synchronization",
|
||||
"sectionContactSync": "Contact Synchronization",
|
||||
"cardavTitle": "CardDAV Contacts",
|
||||
"cardavDescription": "Connect multiple CardDAV accounts (iCloud, Nextcloud, Radicale, etc.) and sync your contacts.",
|
||||
"cardavAddAccount": "Add CardDAV Account",
|
||||
"cardavEmptyState": "No CardDAV accounts connected yet. Add your first account to sync contacts.",
|
||||
"cardavNameLabel": "Account name",
|
||||
"cardavNamePlaceholder": "e.g. iCloud, Nextcloud",
|
||||
"cardavUrlLabel": "CardDAV Server URL",
|
||||
"cardavUrlPlaceholder": "https://contacts.icloud.com",
|
||||
"cardavUrlHint": "The base URL of your CardDAV server",
|
||||
"cardavUsernameLabel": "Username",
|
||||
"cardavPasswordLabel": "Password",
|
||||
"cardavPasswordHint": "For iCloud: Use app-specific password from appleid.apple.com",
|
||||
"cardavAccountAdded": "CardDAV account added successfully",
|
||||
"cardavAccountDeleted": "CardDAV account removed",
|
||||
"cardavSyncSuccess": "CardDAV sync successful",
|
||||
"cardavSyncFailed": "CardDAV sync failed",
|
||||
"cardavConnectionFailed": "Connection to CardDAV server failed",
|
||||
"cardavAddressbooksToggle": "Show/hide addressbooks",
|
||||
"cardavRefreshAddressbooks": "Refresh addressbooks",
|
||||
"addressbookEnabled": "Addressbook enabled",
|
||||
"addressbookDisabled": "Addressbook disabled",
|
||||
"addressbooksRefreshed": "Addressbooks refreshed",
|
||||
"deleteCardDAVAccountConfirm": "Really delete CardDAV account? All synced contacts will remain but lose their CardDAV link."
|
||||
},
|
||||
"login": {
|
||||
"tagline": "Planification familiale. Sécurisée. Respectueuse de la vie privée. Open source.",
|
||||
|
||||
+26
-1
@@ -952,7 +952,32 @@
|
||||
"backupRestoredToast": "डेटाबेस पुनर्स्थापित हुआ। फिर से लोड हो रहा है...",
|
||||
"backupCliTitle": "CLI / Docker Compose पुनर्स्थापना",
|
||||
"backupCliHint": "For operational restores, stop the app, mount the backup in a temporary container and replace the database file.",
|
||||
"backupCliBackupHint": "आप Docker Compose से सीधे बैकअप भी बना सकते हैं:"
|
||||
"backupCliBackupHint": "आप Docker Compose से सीधे बैकअप भी बना सकते हैं:",
|
||||
"tabSync": "Synchronization",
|
||||
"sectionContactSync": "Contact Synchronization",
|
||||
"cardavTitle": "CardDAV Contacts",
|
||||
"cardavDescription": "Connect multiple CardDAV accounts (iCloud, Nextcloud, Radicale, etc.) and sync your contacts.",
|
||||
"cardavAddAccount": "Add CardDAV Account",
|
||||
"cardavEmptyState": "No CardDAV accounts connected yet. Add your first account to sync contacts.",
|
||||
"cardavNameLabel": "Account name",
|
||||
"cardavNamePlaceholder": "e.g. iCloud, Nextcloud",
|
||||
"cardavUrlLabel": "CardDAV Server URL",
|
||||
"cardavUrlPlaceholder": "https://contacts.icloud.com",
|
||||
"cardavUrlHint": "The base URL of your CardDAV server",
|
||||
"cardavUsernameLabel": "Username",
|
||||
"cardavPasswordLabel": "Password",
|
||||
"cardavPasswordHint": "For iCloud: Use app-specific password from appleid.apple.com",
|
||||
"cardavAccountAdded": "CardDAV account added successfully",
|
||||
"cardavAccountDeleted": "CardDAV account removed",
|
||||
"cardavSyncSuccess": "CardDAV sync successful",
|
||||
"cardavSyncFailed": "CardDAV sync failed",
|
||||
"cardavConnectionFailed": "Connection to CardDAV server failed",
|
||||
"cardavAddressbooksToggle": "Show/hide addressbooks",
|
||||
"cardavRefreshAddressbooks": "Refresh addressbooks",
|
||||
"addressbookEnabled": "Addressbook enabled",
|
||||
"addressbookDisabled": "Addressbook disabled",
|
||||
"addressbooksRefreshed": "Addressbooks refreshed",
|
||||
"deleteCardDAVAccountConfirm": "Really delete CardDAV account? All synced contacts will remain but lose their CardDAV link."
|
||||
},
|
||||
"login": {
|
||||
"tagline": "पारिवारिक योजना। सुरक्षित। गोपनीयता-अनुकूल। ओपन सोर्स।",
|
||||
|
||||
+26
-1
@@ -952,7 +952,32 @@
|
||||
"backupRestoredToast": "Database ripristinato. Ricaricamento...",
|
||||
"backupCliTitle": "Ripristino CLI / Docker Compose",
|
||||
"backupCliHint": "For operational restores, stop the app, mount the backup in a temporary container and replace the database file.",
|
||||
"backupCliBackupHint": "Puoi anche creare un backup direttamente con Docker Compose:"
|
||||
"backupCliBackupHint": "Puoi anche creare un backup direttamente con Docker Compose:",
|
||||
"tabSync": "Synchronization",
|
||||
"sectionContactSync": "Contact Synchronization",
|
||||
"cardavTitle": "CardDAV Contacts",
|
||||
"cardavDescription": "Connect multiple CardDAV accounts (iCloud, Nextcloud, Radicale, etc.) and sync your contacts.",
|
||||
"cardavAddAccount": "Add CardDAV Account",
|
||||
"cardavEmptyState": "No CardDAV accounts connected yet. Add your first account to sync contacts.",
|
||||
"cardavNameLabel": "Account name",
|
||||
"cardavNamePlaceholder": "e.g. iCloud, Nextcloud",
|
||||
"cardavUrlLabel": "CardDAV Server URL",
|
||||
"cardavUrlPlaceholder": "https://contacts.icloud.com",
|
||||
"cardavUrlHint": "The base URL of your CardDAV server",
|
||||
"cardavUsernameLabel": "Username",
|
||||
"cardavPasswordLabel": "Password",
|
||||
"cardavPasswordHint": "For iCloud: Use app-specific password from appleid.apple.com",
|
||||
"cardavAccountAdded": "CardDAV account added successfully",
|
||||
"cardavAccountDeleted": "CardDAV account removed",
|
||||
"cardavSyncSuccess": "CardDAV sync successful",
|
||||
"cardavSyncFailed": "CardDAV sync failed",
|
||||
"cardavConnectionFailed": "Connection to CardDAV server failed",
|
||||
"cardavAddressbooksToggle": "Show/hide addressbooks",
|
||||
"cardavRefreshAddressbooks": "Refresh addressbooks",
|
||||
"addressbookEnabled": "Addressbook enabled",
|
||||
"addressbookDisabled": "Addressbook disabled",
|
||||
"addressbooksRefreshed": "Addressbooks refreshed",
|
||||
"deleteCardDAVAccountConfirm": "Really delete CardDAV account? All synced contacts will remain but lose their CardDAV link."
|
||||
},
|
||||
"login": {
|
||||
"tagline": "Pianificazione familiare. Sicura. Rispettosa della privacy. Open source.",
|
||||
|
||||
+26
-1
@@ -952,7 +952,32 @@
|
||||
"backupRestoredToast": "データベースを復元しました。再読み込み中...",
|
||||
"backupCliTitle": "CLI / Docker Compose 復元",
|
||||
"backupCliHint": "For operational restores, stop the app, mount the backup in a temporary container and replace the database file.",
|
||||
"backupCliBackupHint": "Docker Compose から直接バックアップを作成することもできます:"
|
||||
"backupCliBackupHint": "Docker Compose から直接バックアップを作成することもできます:",
|
||||
"tabSync": "Synchronization",
|
||||
"sectionContactSync": "Contact Synchronization",
|
||||
"cardavTitle": "CardDAV Contacts",
|
||||
"cardavDescription": "Connect multiple CardDAV accounts (iCloud, Nextcloud, Radicale, etc.) and sync your contacts.",
|
||||
"cardavAddAccount": "Add CardDAV Account",
|
||||
"cardavEmptyState": "No CardDAV accounts connected yet. Add your first account to sync contacts.",
|
||||
"cardavNameLabel": "Account name",
|
||||
"cardavNamePlaceholder": "e.g. iCloud, Nextcloud",
|
||||
"cardavUrlLabel": "CardDAV Server URL",
|
||||
"cardavUrlPlaceholder": "https://contacts.icloud.com",
|
||||
"cardavUrlHint": "The base URL of your CardDAV server",
|
||||
"cardavUsernameLabel": "Username",
|
||||
"cardavPasswordLabel": "Password",
|
||||
"cardavPasswordHint": "For iCloud: Use app-specific password from appleid.apple.com",
|
||||
"cardavAccountAdded": "CardDAV account added successfully",
|
||||
"cardavAccountDeleted": "CardDAV account removed",
|
||||
"cardavSyncSuccess": "CardDAV sync successful",
|
||||
"cardavSyncFailed": "CardDAV sync failed",
|
||||
"cardavConnectionFailed": "Connection to CardDAV server failed",
|
||||
"cardavAddressbooksToggle": "Show/hide addressbooks",
|
||||
"cardavRefreshAddressbooks": "Refresh addressbooks",
|
||||
"addressbookEnabled": "Addressbook enabled",
|
||||
"addressbookDisabled": "Addressbook disabled",
|
||||
"addressbooksRefreshed": "Addressbooks refreshed",
|
||||
"deleteCardDAVAccountConfirm": "Really delete CardDAV account? All synced contacts will remain but lose their CardDAV link."
|
||||
},
|
||||
"login": {
|
||||
"tagline": "家族計画。安全。プライバシー重視。オープンソース。",
|
||||
|
||||
+26
-1
@@ -952,7 +952,32 @@
|
||||
"backupRestoredToast": "Banco de dados restaurado. Recarregando...",
|
||||
"backupCliTitle": "Restauração via CLI / Docker Compose",
|
||||
"backupCliHint": "Para restaurações operacionais, pare a aplicação, monte o backup em um container temporário e substitua o arquivo do banco de dados.",
|
||||
"backupCliBackupHint": "Você também pode criar um backup diretamente pelo Docker Compose:"
|
||||
"backupCliBackupHint": "Você também pode criar um backup diretamente pelo Docker Compose:",
|
||||
"tabSync": "Synchronization",
|
||||
"sectionContactSync": "Contact Synchronization",
|
||||
"cardavTitle": "CardDAV Contacts",
|
||||
"cardavDescription": "Connect multiple CardDAV accounts (iCloud, Nextcloud, Radicale, etc.) and sync your contacts.",
|
||||
"cardavAddAccount": "Add CardDAV Account",
|
||||
"cardavEmptyState": "No CardDAV accounts connected yet. Add your first account to sync contacts.",
|
||||
"cardavNameLabel": "Account name",
|
||||
"cardavNamePlaceholder": "e.g. iCloud, Nextcloud",
|
||||
"cardavUrlLabel": "CardDAV Server URL",
|
||||
"cardavUrlPlaceholder": "https://contacts.icloud.com",
|
||||
"cardavUrlHint": "The base URL of your CardDAV server",
|
||||
"cardavUsernameLabel": "Username",
|
||||
"cardavPasswordLabel": "Password",
|
||||
"cardavPasswordHint": "For iCloud: Use app-specific password from appleid.apple.com",
|
||||
"cardavAccountAdded": "CardDAV account added successfully",
|
||||
"cardavAccountDeleted": "CardDAV account removed",
|
||||
"cardavSyncSuccess": "CardDAV sync successful",
|
||||
"cardavSyncFailed": "CardDAV sync failed",
|
||||
"cardavConnectionFailed": "Connection to CardDAV server failed",
|
||||
"cardavAddressbooksToggle": "Show/hide addressbooks",
|
||||
"cardavRefreshAddressbooks": "Refresh addressbooks",
|
||||
"addressbookEnabled": "Addressbook enabled",
|
||||
"addressbookDisabled": "Addressbook disabled",
|
||||
"addressbooksRefreshed": "Addressbooks refreshed",
|
||||
"deleteCardDAVAccountConfirm": "Really delete CardDAV account? All synced contacts will remain but lose their CardDAV link."
|
||||
},
|
||||
"login": {
|
||||
"tagline": "Planejamento familiar. Seguro. Privado. Código aberto.",
|
||||
|
||||
+26
-1
@@ -952,7 +952,32 @@
|
||||
"backupRestoredToast": "База данных восстановлена. Перезагрузка...",
|
||||
"backupCliTitle": "Восстановление CLI / Docker Compose",
|
||||
"backupCliHint": "For operational restores, stop the app, mount the backup in a temporary container and replace the database file.",
|
||||
"backupCliBackupHint": "Также можно создать копию напрямую через Docker Compose:"
|
||||
"backupCliBackupHint": "Также можно создать копию напрямую через Docker Compose:",
|
||||
"tabSync": "Synchronization",
|
||||
"sectionContactSync": "Contact Synchronization",
|
||||
"cardavTitle": "CardDAV Contacts",
|
||||
"cardavDescription": "Connect multiple CardDAV accounts (iCloud, Nextcloud, Radicale, etc.) and sync your contacts.",
|
||||
"cardavAddAccount": "Add CardDAV Account",
|
||||
"cardavEmptyState": "No CardDAV accounts connected yet. Add your first account to sync contacts.",
|
||||
"cardavNameLabel": "Account name",
|
||||
"cardavNamePlaceholder": "e.g. iCloud, Nextcloud",
|
||||
"cardavUrlLabel": "CardDAV Server URL",
|
||||
"cardavUrlPlaceholder": "https://contacts.icloud.com",
|
||||
"cardavUrlHint": "The base URL of your CardDAV server",
|
||||
"cardavUsernameLabel": "Username",
|
||||
"cardavPasswordLabel": "Password",
|
||||
"cardavPasswordHint": "For iCloud: Use app-specific password from appleid.apple.com",
|
||||
"cardavAccountAdded": "CardDAV account added successfully",
|
||||
"cardavAccountDeleted": "CardDAV account removed",
|
||||
"cardavSyncSuccess": "CardDAV sync successful",
|
||||
"cardavSyncFailed": "CardDAV sync failed",
|
||||
"cardavConnectionFailed": "Connection to CardDAV server failed",
|
||||
"cardavAddressbooksToggle": "Show/hide addressbooks",
|
||||
"cardavRefreshAddressbooks": "Refresh addressbooks",
|
||||
"addressbookEnabled": "Addressbook enabled",
|
||||
"addressbookDisabled": "Addressbook disabled",
|
||||
"addressbooksRefreshed": "Addressbooks refreshed",
|
||||
"deleteCardDAVAccountConfirm": "Really delete CardDAV account? All synced contacts will remain but lose their CardDAV link."
|
||||
},
|
||||
"login": {
|
||||
"tagline": "Семейное планирование. Безопасно. С уважением к приватности. Открытый исходный код.",
|
||||
|
||||
+26
-1
@@ -952,7 +952,32 @@
|
||||
"backupRestoredToast": "Databasen återställd. Laddar om...",
|
||||
"backupCliTitle": "CLI / Docker Compose-återställning",
|
||||
"backupCliHint": "För driftåterställningar, stoppa appen, montera säkerhetskopian i en tillfällig container och byt ut databasfilen.",
|
||||
"backupCliBackupHint": "Du kan också skapa en backup direkt med Docker Compose:"
|
||||
"backupCliBackupHint": "Du kan också skapa en backup direkt med Docker Compose:",
|
||||
"tabSync": "Synchronization",
|
||||
"sectionContactSync": "Contact Synchronization",
|
||||
"cardavTitle": "CardDAV Contacts",
|
||||
"cardavDescription": "Connect multiple CardDAV accounts (iCloud, Nextcloud, Radicale, etc.) and sync your contacts.",
|
||||
"cardavAddAccount": "Add CardDAV Account",
|
||||
"cardavEmptyState": "No CardDAV accounts connected yet. Add your first account to sync contacts.",
|
||||
"cardavNameLabel": "Account name",
|
||||
"cardavNamePlaceholder": "e.g. iCloud, Nextcloud",
|
||||
"cardavUrlLabel": "CardDAV Server URL",
|
||||
"cardavUrlPlaceholder": "https://contacts.icloud.com",
|
||||
"cardavUrlHint": "The base URL of your CardDAV server",
|
||||
"cardavUsernameLabel": "Username",
|
||||
"cardavPasswordLabel": "Password",
|
||||
"cardavPasswordHint": "For iCloud: Use app-specific password from appleid.apple.com",
|
||||
"cardavAccountAdded": "CardDAV account added successfully",
|
||||
"cardavAccountDeleted": "CardDAV account removed",
|
||||
"cardavSyncSuccess": "CardDAV sync successful",
|
||||
"cardavSyncFailed": "CardDAV sync failed",
|
||||
"cardavConnectionFailed": "Connection to CardDAV server failed",
|
||||
"cardavAddressbooksToggle": "Show/hide addressbooks",
|
||||
"cardavRefreshAddressbooks": "Refresh addressbooks",
|
||||
"addressbookEnabled": "Addressbook enabled",
|
||||
"addressbookDisabled": "Addressbook disabled",
|
||||
"addressbooksRefreshed": "Addressbooks refreshed",
|
||||
"deleteCardDAVAccountConfirm": "Really delete CardDAV account? All synced contacts will remain but lose their CardDAV link."
|
||||
},
|
||||
"login": {
|
||||
"tagline": "Familjeplanering. Säker. Sekretessvänlig. Öppen källkod.",
|
||||
|
||||
+26
-1
@@ -952,7 +952,32 @@
|
||||
"backupRestoredToast": "Veritabanı geri yüklendi. Yeniden yükleniyor...",
|
||||
"backupCliTitle": "CLI / Docker Compose geri yükleme",
|
||||
"backupCliHint": "For operational restores, stop the app, mount the backup in a temporary container and replace the database file.",
|
||||
"backupCliBackupHint": "Docker Compose ile doğrudan yedek de oluşturabilirsiniz:"
|
||||
"backupCliBackupHint": "Docker Compose ile doğrudan yedek de oluşturabilirsiniz:",
|
||||
"tabSync": "Synchronization",
|
||||
"sectionContactSync": "Contact Synchronization",
|
||||
"cardavTitle": "CardDAV Contacts",
|
||||
"cardavDescription": "Connect multiple CardDAV accounts (iCloud, Nextcloud, Radicale, etc.) and sync your contacts.",
|
||||
"cardavAddAccount": "Add CardDAV Account",
|
||||
"cardavEmptyState": "No CardDAV accounts connected yet. Add your first account to sync contacts.",
|
||||
"cardavNameLabel": "Account name",
|
||||
"cardavNamePlaceholder": "e.g. iCloud, Nextcloud",
|
||||
"cardavUrlLabel": "CardDAV Server URL",
|
||||
"cardavUrlPlaceholder": "https://contacts.icloud.com",
|
||||
"cardavUrlHint": "The base URL of your CardDAV server",
|
||||
"cardavUsernameLabel": "Username",
|
||||
"cardavPasswordLabel": "Password",
|
||||
"cardavPasswordHint": "For iCloud: Use app-specific password from appleid.apple.com",
|
||||
"cardavAccountAdded": "CardDAV account added successfully",
|
||||
"cardavAccountDeleted": "CardDAV account removed",
|
||||
"cardavSyncSuccess": "CardDAV sync successful",
|
||||
"cardavSyncFailed": "CardDAV sync failed",
|
||||
"cardavConnectionFailed": "Connection to CardDAV server failed",
|
||||
"cardavAddressbooksToggle": "Show/hide addressbooks",
|
||||
"cardavRefreshAddressbooks": "Refresh addressbooks",
|
||||
"addressbookEnabled": "Addressbook enabled",
|
||||
"addressbookDisabled": "Addressbook disabled",
|
||||
"addressbooksRefreshed": "Addressbooks refreshed",
|
||||
"deleteCardDAVAccountConfirm": "Really delete CardDAV account? All synced contacts will remain but lose their CardDAV link."
|
||||
},
|
||||
"login": {
|
||||
"tagline": "Aile planlaması. Güvenli. Gizlilik dostu. Açık kaynak.",
|
||||
|
||||
+26
-1
@@ -952,7 +952,32 @@
|
||||
"backupRestoredToast": "Базу даних відновлено. Перезавантаження...",
|
||||
"backupCliTitle": "Відновлення CLI / Docker Compose",
|
||||
"backupCliHint": "For operational restores, stop the app, mount the backup in a temporary container and replace the database file.",
|
||||
"backupCliBackupHint": "Також можна створити копію безпосередньо через Docker Compose:"
|
||||
"backupCliBackupHint": "Також можна створити копію безпосередньо через Docker Compose:",
|
||||
"tabSync": "Synchronization",
|
||||
"sectionContactSync": "Contact Synchronization",
|
||||
"cardavTitle": "CardDAV Contacts",
|
||||
"cardavDescription": "Connect multiple CardDAV accounts (iCloud, Nextcloud, Radicale, etc.) and sync your contacts.",
|
||||
"cardavAddAccount": "Add CardDAV Account",
|
||||
"cardavEmptyState": "No CardDAV accounts connected yet. Add your first account to sync contacts.",
|
||||
"cardavNameLabel": "Account name",
|
||||
"cardavNamePlaceholder": "e.g. iCloud, Nextcloud",
|
||||
"cardavUrlLabel": "CardDAV Server URL",
|
||||
"cardavUrlPlaceholder": "https://contacts.icloud.com",
|
||||
"cardavUrlHint": "The base URL of your CardDAV server",
|
||||
"cardavUsernameLabel": "Username",
|
||||
"cardavPasswordLabel": "Password",
|
||||
"cardavPasswordHint": "For iCloud: Use app-specific password from appleid.apple.com",
|
||||
"cardavAccountAdded": "CardDAV account added successfully",
|
||||
"cardavAccountDeleted": "CardDAV account removed",
|
||||
"cardavSyncSuccess": "CardDAV sync successful",
|
||||
"cardavSyncFailed": "CardDAV sync failed",
|
||||
"cardavConnectionFailed": "Connection to CardDAV server failed",
|
||||
"cardavAddressbooksToggle": "Show/hide addressbooks",
|
||||
"cardavRefreshAddressbooks": "Refresh addressbooks",
|
||||
"addressbookEnabled": "Addressbook enabled",
|
||||
"addressbookDisabled": "Addressbook disabled",
|
||||
"addressbooksRefreshed": "Addressbooks refreshed",
|
||||
"deleteCardDAVAccountConfirm": "Really delete CardDAV account? All synced contacts will remain but lose their CardDAV link."
|
||||
},
|
||||
"login": {
|
||||
"tagline": "Планування для родини. Безпечно. Конфіденційно. Відкритий код.",
|
||||
|
||||
+26
-1
@@ -952,7 +952,32 @@
|
||||
"backupRestoredToast": "数据库已恢复。正在重新加载...",
|
||||
"backupCliTitle": "CLI / Docker Compose 恢复",
|
||||
"backupCliHint": "For operational restores, stop the app, mount the backup in a temporary container and replace the database file.",
|
||||
"backupCliBackupHint": "也可以直接通过 Docker Compose 创建备份:"
|
||||
"backupCliBackupHint": "也可以直接通过 Docker Compose 创建备份:",
|
||||
"tabSync": "Synchronization",
|
||||
"sectionContactSync": "Contact Synchronization",
|
||||
"cardavTitle": "CardDAV Contacts",
|
||||
"cardavDescription": "Connect multiple CardDAV accounts (iCloud, Nextcloud, Radicale, etc.) and sync your contacts.",
|
||||
"cardavAddAccount": "Add CardDAV Account",
|
||||
"cardavEmptyState": "No CardDAV accounts connected yet. Add your first account to sync contacts.",
|
||||
"cardavNameLabel": "Account name",
|
||||
"cardavNamePlaceholder": "e.g. iCloud, Nextcloud",
|
||||
"cardavUrlLabel": "CardDAV Server URL",
|
||||
"cardavUrlPlaceholder": "https://contacts.icloud.com",
|
||||
"cardavUrlHint": "The base URL of your CardDAV server",
|
||||
"cardavUsernameLabel": "Username",
|
||||
"cardavPasswordLabel": "Password",
|
||||
"cardavPasswordHint": "For iCloud: Use app-specific password from appleid.apple.com",
|
||||
"cardavAccountAdded": "CardDAV account added successfully",
|
||||
"cardavAccountDeleted": "CardDAV account removed",
|
||||
"cardavSyncSuccess": "CardDAV sync successful",
|
||||
"cardavSyncFailed": "CardDAV sync failed",
|
||||
"cardavConnectionFailed": "Connection to CardDAV server failed",
|
||||
"cardavAddressbooksToggle": "Show/hide addressbooks",
|
||||
"cardavRefreshAddressbooks": "Refresh addressbooks",
|
||||
"addressbookEnabled": "Addressbook enabled",
|
||||
"addressbookDisabled": "Addressbook disabled",
|
||||
"addressbooksRefreshed": "Addressbooks refreshed",
|
||||
"deleteCardDAVAccountConfirm": "Really delete CardDAV account? All synced contacts will remain but lose their CardDAV link."
|
||||
},
|
||||
"login": {
|
||||
"tagline": "家庭规划。安全。注重隐私。开源。",
|
||||
|
||||
+230
-14
@@ -1,13 +1,14 @@
|
||||
/**
|
||||
* Modul: Einstellungen (Settings)
|
||||
* Zweck: Benutzerkonto, Passwort, Kalender-Sync, Familienmitglieder
|
||||
* Abhängigkeiten: /api.js
|
||||
* Zweck: Benutzerkonto, Passwort, Kalender-Sync, Kontakte-Sync, Familienmitglieder
|
||||
* Abhängigkeiten: /api.js, /utils/settings-nav.js
|
||||
*/
|
||||
|
||||
import { api, auth } from '/api.js';
|
||||
import { openModal, closeModal, confirmModal } from '/components/modal.js';
|
||||
import { t, formatDate, formatTime, dateInputPlaceholder, formatDateInput, parseDateInput, isDateInputValid, getDateFormat } from '/i18n.js';
|
||||
import { esc } from '/utils/html.js';
|
||||
import { renderSettingsSidebar, renderBreadcrumb, getLastActivePage, setActivePage, findSectionAndPage } from '/utils/settings-nav.js';
|
||||
import '/components/oikos-locale-picker.js';
|
||||
|
||||
const SUPPORTED_CURRENCIES = ['AED', 'AUD', 'BRL', 'CAD', 'CHF', 'CNY', 'CZK', 'DKK', 'EUR', 'GBP', 'HUF', 'INR', 'JPY', 'NOK', 'PLN', 'RUB', 'SAR', 'SEK', 'TRY', 'UAH', 'USD'];
|
||||
@@ -244,14 +245,14 @@ export async function render(container, { user }) {
|
||||
: t('settings.notConnected');
|
||||
|
||||
const allowedTabs = [
|
||||
'general', 'meals', 'budget', 'shopping', 'calendar',
|
||||
'general', 'meals', 'budget', 'shopping', 'sync',
|
||||
...(user?.role === 'admin' ? ['family', 'api-tokens'] : []),
|
||||
'account',
|
||||
...(user?.role === 'admin' ? ['backup'] : []),
|
||||
];
|
||||
const storedTab = sessionStorage.getItem(SETTINGS_TAB_KEY) ?? 'general';
|
||||
const activeTab = (syncOk || syncErr)
|
||||
? 'calendar'
|
||||
? 'sync'
|
||||
: (allowedTabs.includes(storedTab) ? storedTab : 'general');
|
||||
|
||||
const panelHidden = (id) => id === activeTab ? '' : ' hidden';
|
||||
@@ -272,11 +273,11 @@ export async function render(container, { user }) {
|
||||
<button class="${btnClass('meals')}" role="tab" data-tab="meals" aria-selected="${btnAria('meals')}">${t('settings.tabMeals')}</button>
|
||||
<button class="${btnClass('budget')}" role="tab" data-tab="budget" aria-selected="${btnAria('budget')}">${t('settings.tabBudget')}</button>
|
||||
<button class="${btnClass('shopping')}" role="tab" data-tab="shopping" aria-selected="${btnAria('shopping')}">${t('settings.tabShopping')}</button>
|
||||
<button class="${btnClass('calendar')}" role="tab" data-tab="calendar" aria-selected="${btnAria('calendar')}">${t('settings.tabCalendar')}</button>
|
||||
${user?.role === 'admin' ? `<button class="${btnClass('family')}" role="tab" data-tab="family" aria-selected="${btnAria('family')}">${t('settings.tabFamily')}</button>` : ''}
|
||||
${user?.role === 'admin' ? `<button class="${btnClass('api-tokens')}" role="tab" data-tab="api-tokens" aria-selected="${btnAria('api-tokens')}">${t('settings.tabApiTokens')}</button>` : ''}
|
||||
<button class="${btnClass('account')}" role="tab" data-tab="account" aria-selected="${btnAria('account')}">${t('settings.tabAccount')}</button>
|
||||
${user?.role === 'admin' ? `<button class="${btnClass('backup')}" role="tab" data-tab="backup" aria-selected="${btnAria('backup')}">${t('settings.tabBackup')}</button>` : ''}
|
||||
<button class="${btnClass('sync')}" role="tab" data-tab="sync" aria-selected="${btnAria('sync')}" data-group="sync">${t('settings.tabSync')}</button>
|
||||
<button class="${btnClass('account')}" role="tab" data-tab="account" aria-selected="${btnAria('account')}" data-group="personal">${t('settings.tabAccount')}</button>
|
||||
${user?.role === 'admin' ? `<button class="${btnClass('family')}" role="tab" data-tab="family" aria-selected="${btnAria('family')}" data-group="admin">${t('settings.tabFamily')}</button>` : ''}
|
||||
${user?.role === 'admin' ? `<button class="${btnClass('api-tokens')}" role="tab" data-tab="api-tokens" aria-selected="${btnAria('api-tokens')}" data-group="admin">${t('settings.tabApiTokens')}</button>` : ''}
|
||||
${user?.role === 'admin' ? `<button class="${btnClass('backup')}" role="tab" data-tab="backup" aria-selected="${btnAria('backup')}" data-group="admin">${t('settings.tabBackup')}</button>` : ''}
|
||||
</nav>
|
||||
|
||||
<!-- Panel: Allgemein (Design + Sprache) -->
|
||||
@@ -453,8 +454,9 @@ export async function render(container, { user }) {
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<!-- Panel: Kalender -->
|
||||
<div class="settings-tab-panel" data-panel="calendar" role="tabpanel"${panelHidden('calendar')}>
|
||||
<!-- Panel: Synchronisation -->
|
||||
<div class="settings-tab-panel" data-panel="sync" role="tabpanel"${panelHidden('sync')}>
|
||||
<!-- Sektion: Kalender-Synchronisation -->
|
||||
<section class="settings-section">
|
||||
<h2 class="settings-section__title">${t('settings.sectionCalendarSync')}</h2>
|
||||
|
||||
@@ -586,6 +588,27 @@ export async function render(container, { user }) {
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Sektion: Kontakt-Synchronisation (CardDAV) -->
|
||||
<section class="settings-section">
|
||||
<h2 class="settings-section__title">${t('settings.sectionContactSync')}</h2>
|
||||
|
||||
<div class="settings-card">
|
||||
<h3 class="settings-card__title">${t('settings.cardavTitle')}</h3>
|
||||
<p class="settings-card-description">${t('settings.cardavDescription')}</p>
|
||||
|
||||
<div id="cardav-accounts-list"></div>
|
||||
<div id="cardav-empty-state" class="caldav-empty-state" style="display: none;">
|
||||
<p>${t('settings.cardavEmptyState')}</p>
|
||||
</div>
|
||||
|
||||
${user?.role === 'admin' ? `
|
||||
<button class="btn btn--primary" id="cardav-add-account-btn">
|
||||
${t('settings.cardavAddAccount')}
|
||||
</button>
|
||||
` : ''}
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
${user?.role === 'admin' ? `
|
||||
@@ -1328,9 +1351,130 @@ function bindEvents(container, user, users, categories, icsSubscriptions, apiTok
|
||||
}
|
||||
}
|
||||
|
||||
// Load CalDAV accounts on page load
|
||||
if (user?.role === 'admin') {
|
||||
loadCalDAVAccounts(container);
|
||||
// CardDAV-Konten laden
|
||||
async function loadCardDAVAccounts(container) {
|
||||
const listEl = container.querySelector('#cardav-accounts-list');
|
||||
const emptyEl = container.querySelector('#cardav-empty-state');
|
||||
if (!listEl || !emptyEl) return;
|
||||
|
||||
try {
|
||||
const accountsRes = await api.get('/contacts/cardav/accounts');
|
||||
const accounts = accountsRes.data || [];
|
||||
|
||||
if (accounts.length === 0) {
|
||||
listEl.replaceChildren();
|
||||
emptyEl.style.display = '';
|
||||
return;
|
||||
}
|
||||
|
||||
emptyEl.style.display = 'none';
|
||||
listEl.replaceChildren();
|
||||
|
||||
for (const account of accounts) {
|
||||
const addressbooksRes = await api.get(`/contacts/cardav/accounts/${account.id}/addressbooks`);
|
||||
const addressbooks = addressbooksRes.data || [];
|
||||
|
||||
const accountCard = document.createElement('div');
|
||||
accountCard.className = 'caldav-account-item';
|
||||
accountCard.insertAdjacentHTML('beforeend', `
|
||||
<div class="caldav-account-header">
|
||||
<h4>${esc(account.name)}</h4>
|
||||
<div class="caldav-account-meta">
|
||||
<span>${esc(account.cardav_url)}</span>
|
||||
${account.last_sync ? `<span>${t('settings.lastSync')}: ${formatDateTime(account.last_sync)}</span>` : ''}
|
||||
</div>
|
||||
</div>
|
||||
<details class="caldav-calendars-details">
|
||||
<summary class="caldav-calendars-summary">
|
||||
${t('settings.cardavAddressbooksToggle')} (${addressbooks.length})
|
||||
</summary>
|
||||
<div class="caldav-calendars-list">
|
||||
${addressbooks.map((ab) => `
|
||||
<label class="caldav-calendar-item">
|
||||
<input type="checkbox" class="caldav-calendar-checkbox cardav-addressbook-checkbox"
|
||||
data-account-id="${account.id}"
|
||||
data-addressbook-url="${esc(ab.url)}"
|
||||
${ab.enabled ? 'checked' : ''}>
|
||||
<span class="caldav-calendar-name">${esc(ab.display_name || ab.url)}</span>
|
||||
</label>
|
||||
`).join('')}
|
||||
</div>
|
||||
</details>
|
||||
<div class="caldav-account-actions">
|
||||
<button class="btn btn--secondary btn--sm" data-cardav-sync="${account.id}">${t('settings.syncNow')}</button>
|
||||
<button class="btn btn--secondary btn--sm" data-cardav-refresh="${account.id}">${t('settings.cardavRefreshAddressbooks')}</button>
|
||||
<button class="btn btn--danger-outline btn--sm" data-cardav-delete="${account.id}">${t('settings.disconnect')}</button>
|
||||
</div>
|
||||
`);
|
||||
|
||||
// Addressbook toggle
|
||||
accountCard.querySelectorAll('.cardav-addressbook-checkbox').forEach((checkbox) => {
|
||||
checkbox.addEventListener('change', async () => {
|
||||
const accountId = parseInt(checkbox.dataset.accountId, 10);
|
||||
const addressbookUrl = checkbox.dataset.addressbookUrl;
|
||||
const enabled = checkbox.checked;
|
||||
try {
|
||||
await api.post(`/contacts/cardav/accounts/${accountId}/addressbooks/toggle`, {
|
||||
addressbookUrl,
|
||||
enabled,
|
||||
});
|
||||
window.oikos?.showToast(enabled ? t('settings.addressbookEnabled') : t('settings.addressbookDisabled'), 'success');
|
||||
} catch (err) {
|
||||
window.oikos?.showToast(err.message, 'error');
|
||||
checkbox.checked = !enabled;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Sync button
|
||||
const syncBtn = accountCard.querySelector(`[data-cardav-sync="${account.id}"]`);
|
||||
if (syncBtn) {
|
||||
syncBtn.addEventListener('click', async () => {
|
||||
try {
|
||||
await api.post(`/contacts/cardav/accounts/${account.id}/sync`);
|
||||
window.oikos?.showToast(t('settings.cardavSyncSuccess'), 'success');
|
||||
await loadCardDAVAccounts(container);
|
||||
} catch (err) {
|
||||
window.oikos?.showToast(t('settings.cardavSyncFailed'), 'error');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Refresh button
|
||||
const refreshBtn = accountCard.querySelector(`[data-cardav-refresh="${account.id}"]`);
|
||||
if (refreshBtn) {
|
||||
refreshBtn.addEventListener('click', async () => {
|
||||
try {
|
||||
await api.post(`/contacts/cardav/accounts/${account.id}/addressbooks/refresh`);
|
||||
window.oikos?.showToast(t('settings.addressbooksRefreshed'), 'success');
|
||||
await loadCardDAVAccounts(container);
|
||||
} catch (err) {
|
||||
window.oikos?.showToast(err.message, 'error');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Delete button
|
||||
const deleteBtn = accountCard.querySelector(`[data-cardav-delete="${account.id}"]`);
|
||||
if (deleteBtn) {
|
||||
deleteBtn.addEventListener('click', async () => {
|
||||
const confirmed = await confirmModal(t('settings.deleteCardDAVAccountConfirm'));
|
||||
if (!confirmed) return;
|
||||
try {
|
||||
await api.delete(`/contacts/cardav/accounts/${account.id}`);
|
||||
window.oikos?.showToast(t('settings.cardavAccountDeleted'), 'success');
|
||||
await loadCardDAVAccounts(container);
|
||||
} catch (err) {
|
||||
window.oikos?.showToast(err.message, 'error');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
listEl.appendChild(accountCard);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to load CardDAV accounts:', err);
|
||||
}
|
||||
}
|
||||
|
||||
// CalDAV add account button
|
||||
@@ -1398,6 +1542,70 @@ function bindEvents(container, user, users, categories, icsSubscriptions, apiTok
|
||||
});
|
||||
}
|
||||
|
||||
// CardDAV add account button
|
||||
const cardavAddBtn = container.querySelector('#cardav-add-account-btn');
|
||||
if (cardavAddBtn) {
|
||||
cardavAddBtn.addEventListener('click', () => {
|
||||
openModal({
|
||||
title: t('settings.cardavAddAccount'),
|
||||
size: 'sm',
|
||||
content: `
|
||||
<form id="cardav-add-form" novalidate autocomplete="off">
|
||||
<div class="form-group">
|
||||
<label class="form-label" for="cardav-name">${t('settings.cardavNameLabel')}<span class="required-marker" aria-hidden="true"> *</span></label>
|
||||
<input class="form-input" type="text" id="cardav-name" required
|
||||
placeholder="${t('settings.cardavNamePlaceholder')}" maxlength="100" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label" for="cardav-url">${t('settings.cardavUrlLabel')}<span class="required-marker" aria-hidden="true"> *</span></label>
|
||||
<input class="form-input" type="url" id="cardav-url" required
|
||||
placeholder="${t('settings.cardavUrlPlaceholder')}" />
|
||||
<small class="form-hint">${t('settings.cardavUrlHint')}</small>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label" for="cardav-username">${t('settings.cardavUsernameLabel')}<span class="required-marker" aria-hidden="true"> *</span></label>
|
||||
<input class="form-input" type="text" id="cardav-username" required autocomplete="username" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label" for="cardav-password">${t('settings.cardavPasswordLabel')}<span class="required-marker" aria-hidden="true"> *</span></label>
|
||||
<input class="form-input" type="password" id="cardav-password" required autocomplete="current-password" />
|
||||
<small class="form-hint">${t('settings.cardavPasswordHint')}</small>
|
||||
</div>
|
||||
<div id="cardav-add-error" class="form-error" hidden></div>
|
||||
</form>
|
||||
`,
|
||||
onSave: async (panel) => {
|
||||
const errorEl = panel.querySelector('#cardav-add-error');
|
||||
errorEl.hidden = true;
|
||||
|
||||
const name = panel.querySelector('#cardav-name').value.trim();
|
||||
const cardavUrl = panel.querySelector('#cardav-url').value.trim();
|
||||
const username = panel.querySelector('#cardav-username').value.trim();
|
||||
const password = panel.querySelector('#cardav-password').value;
|
||||
|
||||
if (!name || !cardavUrl || !username || !password) {
|
||||
showError(errorEl, t('common.allFieldsRequired'));
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await api.post('/contacts/cardav/accounts', {
|
||||
name,
|
||||
cardavUrl,
|
||||
username,
|
||||
password,
|
||||
});
|
||||
closeModal({ force: true });
|
||||
window.oikos?.showToast(t('settings.cardavAccountAdded'), 'success');
|
||||
await loadCardDAVAccounts(container);
|
||||
} catch (err) {
|
||||
showError(errorEl, err.message);
|
||||
}
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Mitglied hinzufügen (Admin)
|
||||
const addMemberBtn = container.querySelector('#add-member-btn');
|
||||
if (addMemberBtn) {
|
||||
@@ -2356,6 +2564,14 @@ function bindIcsEvents(container, user, initialSubs) {
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Initial Load: CalDAV & CardDAV Accounts
|
||||
if (container.querySelector('#caldav-accounts-list')) {
|
||||
loadCalDAVAccounts(container);
|
||||
}
|
||||
if (container.querySelector('#cardav-accounts-list')) {
|
||||
loadCardDAVAccounts(container);
|
||||
}
|
||||
}
|
||||
|
||||
function initials(name) {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,452 @@
|
||||
/**
|
||||
* Modul: Settings Navigation
|
||||
* Zweck: Zweistufige Sidebar-Navigation für Settings
|
||||
* Abhängigkeiten: tokens.css, layout.css
|
||||
*/
|
||||
|
||||
/* --------------------------------------------------------
|
||||
Layout Grid
|
||||
-------------------------------------------------------- */
|
||||
|
||||
.settings-layout {
|
||||
display: grid;
|
||||
grid-template-columns: 240px 1fr;
|
||||
gap: 0;
|
||||
min-height: calc(100vh - var(--nav-bottom-height, 0px));
|
||||
background: var(--color-bg);
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.settings-layout {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.settings-sidebar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Mobile: Zeige horizontale Tabs statt Sidebar */
|
||||
.settings-layout--mobile-tabs {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
/* --------------------------------------------------------
|
||||
Sidebar
|
||||
-------------------------------------------------------- */
|
||||
|
||||
.settings-sidebar {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
height: 100vh;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
background: var(--color-surface);
|
||||
border-right: 1px solid var(--color-border);
|
||||
padding: var(--space-4) 0;
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: var(--color-border) transparent;
|
||||
}
|
||||
|
||||
.settings-sidebar::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
}
|
||||
|
||||
.settings-sidebar::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.settings-sidebar::-webkit-scrollbar-thumb {
|
||||
background: var(--color-border);
|
||||
border-radius: var(--radius-full);
|
||||
}
|
||||
|
||||
.settings-sidebar::-webkit-scrollbar-thumb:hover {
|
||||
background: var(--color-text-tertiary);
|
||||
}
|
||||
|
||||
/* --------------------------------------------------------
|
||||
Sidebar Section
|
||||
-------------------------------------------------------- */
|
||||
|
||||
.settings-sidebar-section {
|
||||
margin-bottom: var(--space-6);
|
||||
}
|
||||
|
||||
.settings-sidebar-section:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.settings-sidebar-section__header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space-2);
|
||||
padding: 0 var(--space-4);
|
||||
margin-bottom: var(--space-2);
|
||||
color: var(--color-text-tertiary);
|
||||
font-size: var(--text-xs);
|
||||
font-weight: var(--font-weight-semibold);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.settings-sidebar-section__icon {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
flex-shrink: 0;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.settings-sidebar-section__label {
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
/* Active Section */
|
||||
.settings-sidebar-section--active .settings-sidebar-section__header {
|
||||
color: var(--color-accent);
|
||||
}
|
||||
|
||||
.settings-sidebar-section--active .settings-sidebar-section__icon {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
/* --------------------------------------------------------
|
||||
Sidebar Pages
|
||||
-------------------------------------------------------- */
|
||||
|
||||
.settings-sidebar-pages {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--space-1);
|
||||
}
|
||||
|
||||
.settings-sidebar-page {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space-2);
|
||||
padding: var(--space-2) var(--space-4);
|
||||
margin: 0 var(--space-2);
|
||||
border: none;
|
||||
border-radius: var(--radius-md);
|
||||
background: transparent;
|
||||
color: var(--color-text-secondary);
|
||||
font-family: inherit;
|
||||
font-size: var(--text-sm);
|
||||
font-weight: var(--font-weight-medium);
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
transition: background-color var(--transition-fast), color var(--transition-fast);
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
min-height: var(--target-base);
|
||||
}
|
||||
|
||||
.settings-sidebar-page:hover {
|
||||
background: var(--color-surface-2);
|
||||
color: var(--color-text-primary);
|
||||
}
|
||||
|
||||
.settings-sidebar-page--active {
|
||||
background: color-mix(in srgb, var(--color-accent) 12%, transparent);
|
||||
color: var(--color-accent);
|
||||
font-weight: var(--font-weight-semibold);
|
||||
}
|
||||
|
||||
.settings-sidebar-page--active:hover {
|
||||
background: color-mix(in srgb, var(--color-accent) 18%, transparent);
|
||||
}
|
||||
|
||||
.settings-sidebar-page__icon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.settings-sidebar-page__label {
|
||||
line-height: 1.3;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
/* --------------------------------------------------------
|
||||
Content Area
|
||||
-------------------------------------------------------- */
|
||||
|
||||
.settings-content {
|
||||
padding: var(--space-6) var(--space-6) var(--space-8);
|
||||
max-width: 900px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.settings-content {
|
||||
padding: var(--space-4);
|
||||
}
|
||||
}
|
||||
|
||||
/* --------------------------------------------------------
|
||||
Breadcrumb
|
||||
-------------------------------------------------------- */
|
||||
|
||||
.settings-breadcrumb {
|
||||
margin-bottom: var(--space-6);
|
||||
padding-bottom: var(--space-4);
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
.settings-breadcrumb__list {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space-2);
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.settings-breadcrumb__item {
|
||||
font-size: var(--text-sm);
|
||||
color: var(--color-text-secondary);
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.settings-breadcrumb__item--current {
|
||||
color: var(--color-text-primary);
|
||||
font-weight: var(--font-weight-semibold);
|
||||
}
|
||||
|
||||
.settings-breadcrumb__separator {
|
||||
font-size: var(--text-sm);
|
||||
color: var(--color-text-tertiary);
|
||||
line-height: 1;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
/* --------------------------------------------------------
|
||||
Page Header (in Content Area)
|
||||
-------------------------------------------------------- */
|
||||
|
||||
.settings-page-header {
|
||||
margin-bottom: var(--space-6);
|
||||
}
|
||||
|
||||
.settings-page-header__title {
|
||||
font-size: var(--text-2xl);
|
||||
font-weight: var(--font-weight-bold);
|
||||
color: var(--color-text-primary);
|
||||
margin: 0 0 var(--space-2);
|
||||
letter-spacing: -0.02em;
|
||||
}
|
||||
|
||||
.settings-page-header__description {
|
||||
font-size: var(--text-base);
|
||||
color: var(--color-text-secondary);
|
||||
line-height: 1.6;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* --------------------------------------------------------
|
||||
Mobile Tabs (Fallback für kleine Screens)
|
||||
-------------------------------------------------------- */
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.settings-mobile-tabs {
|
||||
display: flex;
|
||||
gap: var(--space-1);
|
||||
padding: var(--space-2) var(--space-4);
|
||||
overflow-x: auto;
|
||||
scrollbar-width: none;
|
||||
-ms-overflow-style: none;
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: var(--z-sticky);
|
||||
background: var(--color-bg);
|
||||
}
|
||||
|
||||
.settings-mobile-tabs::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.settings-mobile-tab {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: var(--space-1);
|
||||
padding: 0 var(--space-3);
|
||||
height: var(--target-base);
|
||||
border-radius: var(--radius-full);
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: var(--color-text-secondary);
|
||||
font-family: inherit;
|
||||
font-size: var(--text-sm);
|
||||
font-weight: var(--font-weight-medium);
|
||||
cursor: pointer;
|
||||
white-space: nowrap;
|
||||
flex-shrink: 0;
|
||||
transition: background-color var(--transition-fast), color var(--transition-fast);
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
}
|
||||
|
||||
.settings-mobile-tab--active {
|
||||
background-color: color-mix(in srgb, var(--color-accent) 14%, transparent);
|
||||
color: var(--color-accent);
|
||||
}
|
||||
|
||||
.settings-mobile-tab__icon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
}
|
||||
|
||||
/* --------------------------------------------------------
|
||||
Help Tooltip
|
||||
-------------------------------------------------------- */
|
||||
|
||||
.settings-help-icon {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
margin-left: var(--space-1);
|
||||
color: var(--color-text-tertiary);
|
||||
cursor: help;
|
||||
position: relative;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.settings-help-icon:hover {
|
||||
color: var(--color-accent);
|
||||
}
|
||||
|
||||
.settings-help-icon svg,
|
||||
.settings-help-icon i {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
}
|
||||
|
||||
/* Tooltip */
|
||||
.settings-help-tooltip {
|
||||
position: absolute;
|
||||
bottom: calc(100% + var(--space-2));
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
padding: var(--space-2) var(--space-3);
|
||||
max-width: 240px;
|
||||
background: var(--color-text-primary);
|
||||
color: var(--color-bg);
|
||||
font-size: var(--text-xs);
|
||||
line-height: 1.4;
|
||||
border-radius: var(--radius-sm);
|
||||
white-space: normal;
|
||||
pointer-events: none;
|
||||
opacity: 0;
|
||||
transition: opacity var(--transition-fast);
|
||||
z-index: var(--z-tooltip);
|
||||
box-shadow: var(--shadow-lg);
|
||||
}
|
||||
|
||||
.settings-help-icon:hover .settings-help-tooltip {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
/* Tooltip Arrow */
|
||||
.settings-help-tooltip::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
border: 4px solid transparent;
|
||||
border-top-color: var(--color-text-primary);
|
||||
}
|
||||
|
||||
/* --------------------------------------------------------
|
||||
Status Badge (für Sync-Status)
|
||||
-------------------------------------------------------- */
|
||||
|
||||
.settings-status-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: var(--space-1);
|
||||
padding: var(--space-1) var(--space-2);
|
||||
border-radius: var(--radius-sm);
|
||||
font-size: var(--text-xs);
|
||||
font-weight: var(--font-weight-medium);
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.settings-status-badge--success {
|
||||
background: var(--color-success-light);
|
||||
color: var(--color-success);
|
||||
}
|
||||
|
||||
.settings-status-badge--error {
|
||||
background: var(--color-danger-light);
|
||||
color: var(--color-danger);
|
||||
}
|
||||
|
||||
.settings-status-badge--syncing {
|
||||
background: var(--color-accent-light);
|
||||
color: var(--color-accent);
|
||||
}
|
||||
|
||||
.settings-status-badge__icon {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
}
|
||||
|
||||
/* Spinning animation für Sync-Icon */
|
||||
.settings-status-badge--syncing .settings-status-badge__icon {
|
||||
animation: spin 1.5s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
from { transform: rotate(0deg); }
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
/* --------------------------------------------------------
|
||||
Empty State (für Onboarding)
|
||||
-------------------------------------------------------- */
|
||||
|
||||
.settings-empty-state {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: var(--space-8) var(--space-6);
|
||||
text-align: center;
|
||||
border-radius: var(--radius-lg);
|
||||
background: var(--color-surface-2);
|
||||
border: 2px dashed var(--color-border);
|
||||
}
|
||||
|
||||
.settings-empty-state__icon {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
margin-bottom: var(--space-4);
|
||||
color: var(--color-text-tertiary);
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.settings-empty-state__title {
|
||||
font-size: var(--text-lg);
|
||||
font-weight: var(--font-weight-semibold);
|
||||
color: var(--color-text-primary);
|
||||
margin: 0 0 var(--space-2);
|
||||
}
|
||||
|
||||
.settings-empty-state__description {
|
||||
font-size: var(--text-sm);
|
||||
color: var(--color-text-secondary);
|
||||
line-height: 1.5;
|
||||
margin: 0 0 var(--space-4);
|
||||
max-width: 400px;
|
||||
}
|
||||
|
||||
.settings-empty-state__action {
|
||||
/* Button wird schon durch .btn gestyled */
|
||||
}
|
||||
@@ -91,6 +91,43 @@
|
||||
border-bottom-color: var(--color-accent);
|
||||
}
|
||||
|
||||
/* --------------------------------------------------------
|
||||
Visuelle Tab-Gruppierung (Vorschlag B)
|
||||
-------------------------------------------------------- */
|
||||
|
||||
/* Trennlinie vor Sync-Gruppe */
|
||||
.settings-tab-btn[data-group="sync"]::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: -8px;
|
||||
top: 20%;
|
||||
height: 60%;
|
||||
width: 2px;
|
||||
background: var(--color-border);
|
||||
}
|
||||
|
||||
/* Trennlinie vor Personal-Gruppe */
|
||||
.settings-tab-btn[data-group="personal"]::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: -8px;
|
||||
top: 20%;
|
||||
height: 60%;
|
||||
width: 2px;
|
||||
background: var(--color-border);
|
||||
}
|
||||
|
||||
/* Trennlinie vor Admin-Gruppe */
|
||||
.settings-tab-btn[data-group="admin"]::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: -8px;
|
||||
top: 20%;
|
||||
height: 60%;
|
||||
width: 2px;
|
||||
background: var(--color-border);
|
||||
}
|
||||
|
||||
/* --------------------------------------------------------
|
||||
Sections
|
||||
-------------------------------------------------------- */
|
||||
@@ -568,6 +605,168 @@
|
||||
}
|
||||
}
|
||||
|
||||
/* --------------------------------------------------------
|
||||
Breadcrumb Navigation (UX-Verbesserung 1)
|
||||
-------------------------------------------------------- */
|
||||
|
||||
.settings-breadcrumb {
|
||||
margin-bottom: var(--space-4);
|
||||
padding-bottom: var(--space-3);
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
font-size: var(--text-sm);
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.settings-breadcrumb__current {
|
||||
color: var(--color-text-primary);
|
||||
font-weight: var(--font-weight-semibold);
|
||||
}
|
||||
|
||||
/* --------------------------------------------------------
|
||||
Inline Help Icons mit Tooltips (UX-Verbesserung 2)
|
||||
-------------------------------------------------------- */
|
||||
|
||||
.settings-help-icon {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
margin-left: var(--space-1);
|
||||
color: var(--color-text-tertiary);
|
||||
cursor: help;
|
||||
position: relative;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.settings-help-icon:hover {
|
||||
color: var(--color-accent);
|
||||
}
|
||||
|
||||
.settings-help-icon svg,
|
||||
.settings-help-icon i {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
}
|
||||
|
||||
.settings-help-tooltip {
|
||||
position: absolute;
|
||||
bottom: calc(100% + var(--space-2));
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
padding: var(--space-2) var(--space-3);
|
||||
max-width: 240px;
|
||||
background: var(--color-text-primary);
|
||||
color: var(--color-bg);
|
||||
font-size: var(--text-xs);
|
||||
line-height: 1.4;
|
||||
border-radius: var(--radius-sm);
|
||||
white-space: normal;
|
||||
pointer-events: none;
|
||||
opacity: 0;
|
||||
transition: opacity var(--transition-fast);
|
||||
z-index: var(--z-tooltip);
|
||||
box-shadow: var(--shadow-lg);
|
||||
}
|
||||
|
||||
.settings-help-icon:hover .settings-help-tooltip {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.settings-help-tooltip::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
border: 4px solid transparent;
|
||||
border-top-color: var(--color-text-primary);
|
||||
}
|
||||
|
||||
/* --------------------------------------------------------
|
||||
Status Badges (UX-Verbesserung 3)
|
||||
-------------------------------------------------------- */
|
||||
|
||||
.settings-status-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: var(--space-1);
|
||||
padding: var(--space-1) var(--space-2);
|
||||
border-radius: var(--radius-sm);
|
||||
font-size: var(--text-xs);
|
||||
font-weight: var(--font-weight-medium);
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.settings-status-badge--success {
|
||||
background: var(--color-success-light);
|
||||
color: var(--color-success);
|
||||
}
|
||||
|
||||
.settings-status-badge--error {
|
||||
background: var(--color-danger-light);
|
||||
color: var(--color-danger);
|
||||
}
|
||||
|
||||
.settings-status-badge--syncing {
|
||||
background: var(--color-accent-light);
|
||||
color: var(--color-accent);
|
||||
}
|
||||
|
||||
.settings-status-badge__icon {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
}
|
||||
|
||||
.settings-status-badge--syncing .settings-status-badge__icon {
|
||||
animation: spin 1.5s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
from { transform: rotate(0deg); }
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
/* --------------------------------------------------------
|
||||
Enhanced Empty State (UX-Verbesserung 4)
|
||||
-------------------------------------------------------- */
|
||||
|
||||
.settings-empty-state {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: var(--space-8) var(--space-6);
|
||||
text-align: center;
|
||||
border-radius: var(--radius-lg);
|
||||
background: var(--color-surface-2);
|
||||
border: 2px dashed var(--color-border);
|
||||
margin: var(--space-4) 0;
|
||||
}
|
||||
|
||||
.settings-empty-state__icon {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
margin-bottom: var(--space-4);
|
||||
color: var(--color-text-tertiary);
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.settings-empty-state__title {
|
||||
font-size: var(--text-lg);
|
||||
font-weight: var(--font-weight-semibold);
|
||||
color: var(--color-text-primary);
|
||||
margin: 0 0 var(--space-2);
|
||||
}
|
||||
|
||||
.settings-empty-state__description {
|
||||
font-size: var(--text-sm);
|
||||
color: var(--color-text-secondary);
|
||||
line-height: 1.5;
|
||||
margin: 0 0 var(--space-4);
|
||||
max-width: 400px;
|
||||
}
|
||||
|
||||
/* --------------------------------------------------------
|
||||
Theme-Toggle
|
||||
-------------------------------------------------------- */
|
||||
|
||||
@@ -0,0 +1,231 @@
|
||||
/**
|
||||
* Settings Navigation Utility
|
||||
* Zweck: Zweistufige Sidebar-Navigation für Settings
|
||||
* Pattern: Hauptkategorien (links) + Unterkategorien (Content-Bereich)
|
||||
*/
|
||||
|
||||
import { t } from '/i18n.js';
|
||||
|
||||
export const SETTINGS_STORAGE_KEY = 'oikos-settings-section';
|
||||
|
||||
/**
|
||||
* Hauptkategorien mit ihren Unterkategorien
|
||||
*/
|
||||
export const SETTINGS_SECTIONS = (user) => [
|
||||
{
|
||||
id: 'personal',
|
||||
labelKey: 'settings.sectionPersonal',
|
||||
icon: 'user',
|
||||
pages: [
|
||||
{ id: 'account', labelKey: 'settings.tabAccount', icon: 'user-circle' },
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'modules',
|
||||
labelKey: 'settings.sectionModules',
|
||||
icon: 'layout-grid',
|
||||
pages: [
|
||||
{ id: 'general', labelKey: 'settings.tabGeneral', icon: 'settings' },
|
||||
{ id: 'meals', labelKey: 'settings.tabMeals', icon: 'utensils' },
|
||||
{ id: 'budget', labelKey: 'settings.tabBudget', icon: 'wallet' },
|
||||
{ id: 'shopping', labelKey: 'settings.tabShopping', icon: 'shopping-cart' },
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'sync',
|
||||
labelKey: 'settings.sectionSync',
|
||||
icon: 'refresh-cw',
|
||||
pages: [
|
||||
{ id: 'sync-calendar', labelKey: 'settings.tabSyncCalendar', icon: 'calendar' },
|
||||
{ id: 'sync-contacts', labelKey: 'settings.tabSyncContacts', icon: 'users' },
|
||||
]
|
||||
},
|
||||
...(user?.role === 'admin' ? [{
|
||||
id: 'admin',
|
||||
labelKey: 'settings.sectionAdmin',
|
||||
icon: 'shield',
|
||||
pages: [
|
||||
{ id: 'family', labelKey: 'settings.tabFamily', icon: 'users' },
|
||||
{ id: 'api-tokens', labelKey: 'settings.tabApiTokens', icon: 'key' },
|
||||
{ id: 'backup', labelKey: 'settings.tabBackup', icon: 'database' },
|
||||
]
|
||||
}] : [])
|
||||
];
|
||||
|
||||
/**
|
||||
* Findet Sektion und Seite für gegebene Page-ID
|
||||
*/
|
||||
export function findSectionAndPage(pageId, user) {
|
||||
const sections = SETTINGS_SECTIONS(user);
|
||||
for (const section of sections) {
|
||||
const page = section.pages.find(p => p.id === pageId);
|
||||
if (page) return { section, page };
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gibt letzte aktive Seite zurück (aus sessionStorage)
|
||||
*/
|
||||
export function getLastActivePage(user) {
|
||||
try {
|
||||
const stored = sessionStorage.getItem(SETTINGS_STORAGE_KEY);
|
||||
if (stored) {
|
||||
const found = findSectionAndPage(stored, user);
|
||||
if (found) return stored;
|
||||
}
|
||||
} catch { /* ignore */ }
|
||||
return 'general';
|
||||
}
|
||||
|
||||
/**
|
||||
* Speichert aktive Seite
|
||||
*/
|
||||
export function setActivePage(pageId) {
|
||||
try {
|
||||
sessionStorage.setItem(SETTINGS_STORAGE_KEY, pageId);
|
||||
} catch { /* ignore */ }
|
||||
}
|
||||
|
||||
/**
|
||||
* Rendert die Sidebar-Navigation
|
||||
*/
|
||||
export function renderSettingsSidebar(container, activePage, user) {
|
||||
const sections = SETTINGS_SECTIONS(user);
|
||||
const activeInfo = findSectionAndPage(activePage, user);
|
||||
const activeSectionId = activeInfo?.section.id;
|
||||
|
||||
const sidebar = document.createElement('nav');
|
||||
sidebar.className = 'settings-sidebar';
|
||||
sidebar.setAttribute('role', 'navigation');
|
||||
sidebar.setAttribute('aria-label', t('settings.navigationLabel'));
|
||||
|
||||
sections.forEach(section => {
|
||||
const sectionEl = document.createElement('div');
|
||||
sectionEl.className = 'settings-sidebar-section';
|
||||
if (section.id === activeSectionId) {
|
||||
sectionEl.classList.add('settings-sidebar-section--active');
|
||||
}
|
||||
|
||||
// Section Header
|
||||
const header = document.createElement('div');
|
||||
header.className = 'settings-sidebar-section__header';
|
||||
|
||||
const headerIcon = document.createElement('i');
|
||||
headerIcon.dataset.lucide = section.icon;
|
||||
headerIcon.className = 'settings-sidebar-section__icon';
|
||||
headerIcon.setAttribute('aria-hidden', 'true');
|
||||
|
||||
const headerLabel = document.createElement('span');
|
||||
headerLabel.className = 'settings-sidebar-section__label';
|
||||
headerLabel.textContent = t(section.labelKey);
|
||||
|
||||
header.appendChild(headerIcon);
|
||||
header.appendChild(headerLabel);
|
||||
sectionEl.appendChild(header);
|
||||
|
||||
// Pages List
|
||||
const pagesList = document.createElement('div');
|
||||
pagesList.className = 'settings-sidebar-pages';
|
||||
|
||||
section.pages.forEach(page => {
|
||||
const pageBtn = document.createElement('button');
|
||||
pageBtn.className = 'settings-sidebar-page';
|
||||
pageBtn.type = 'button';
|
||||
pageBtn.dataset.pageId = page.id;
|
||||
if (page.id === activePage) {
|
||||
pageBtn.classList.add('settings-sidebar-page--active');
|
||||
pageBtn.setAttribute('aria-current', 'page');
|
||||
}
|
||||
|
||||
const pageIcon = document.createElement('i');
|
||||
pageIcon.dataset.lucide = page.icon;
|
||||
pageIcon.className = 'settings-sidebar-page__icon';
|
||||
pageIcon.setAttribute('aria-hidden', 'true');
|
||||
|
||||
const pageLabel = document.createElement('span');
|
||||
pageLabel.className = 'settings-sidebar-page__label';
|
||||
pageLabel.textContent = t(page.labelKey);
|
||||
|
||||
pageBtn.appendChild(pageIcon);
|
||||
pageBtn.appendChild(pageLabel);
|
||||
pagesList.appendChild(pageBtn);
|
||||
});
|
||||
|
||||
sectionEl.appendChild(pagesList);
|
||||
sidebar.appendChild(sectionEl);
|
||||
});
|
||||
|
||||
// Event Delegation
|
||||
sidebar.addEventListener('click', (e) => {
|
||||
const pageBtn = e.target.closest('[data-page-id]');
|
||||
if (!pageBtn) return;
|
||||
|
||||
const pageId = pageBtn.dataset.pageId;
|
||||
if (pageId === activePage) return;
|
||||
|
||||
setActivePage(pageId);
|
||||
|
||||
// Trigger custom event für Page-Switch
|
||||
container.dispatchEvent(new CustomEvent('settings-page-change', {
|
||||
detail: { pageId },
|
||||
bubbles: true
|
||||
}));
|
||||
});
|
||||
|
||||
container.appendChild(sidebar);
|
||||
|
||||
// Hydrate Lucide Icons
|
||||
if (window.lucide) window.lucide.createIcons({ el: sidebar });
|
||||
}
|
||||
|
||||
/**
|
||||
* Rendert Breadcrumb für aktive Seite
|
||||
*/
|
||||
export function renderBreadcrumb(container, activePage, user) {
|
||||
const info = findSectionAndPage(activePage, user);
|
||||
if (!info) return;
|
||||
|
||||
const breadcrumb = document.createElement('nav');
|
||||
breadcrumb.className = 'settings-breadcrumb';
|
||||
breadcrumb.setAttribute('aria-label', t('settings.breadcrumbLabel'));
|
||||
|
||||
const ol = document.createElement('ol');
|
||||
ol.className = 'settings-breadcrumb__list';
|
||||
|
||||
// Home
|
||||
const homeItem = document.createElement('li');
|
||||
homeItem.className = 'settings-breadcrumb__item';
|
||||
homeItem.textContent = t('settings.title');
|
||||
ol.appendChild(homeItem);
|
||||
|
||||
// Separator
|
||||
const sep1 = document.createElement('li');
|
||||
sep1.className = 'settings-breadcrumb__separator';
|
||||
sep1.setAttribute('aria-hidden', 'true');
|
||||
sep1.textContent = '›';
|
||||
ol.appendChild(sep1);
|
||||
|
||||
// Section
|
||||
const sectionItem = document.createElement('li');
|
||||
sectionItem.className = 'settings-breadcrumb__item';
|
||||
sectionItem.textContent = t(info.section.labelKey);
|
||||
ol.appendChild(sectionItem);
|
||||
|
||||
// Separator
|
||||
const sep2 = document.createElement('li');
|
||||
sep2.className = 'settings-breadcrumb__separator';
|
||||
sep2.setAttribute('aria-hidden', 'true');
|
||||
sep2.textContent = '›';
|
||||
ol.appendChild(sep2);
|
||||
|
||||
// Page
|
||||
const pageItem = document.createElement('li');
|
||||
pageItem.className = 'settings-breadcrumb__item settings-breadcrumb__item--current';
|
||||
pageItem.setAttribute('aria-current', 'page');
|
||||
pageItem.textContent = t(info.page.labelKey);
|
||||
ol.appendChild(pageItem);
|
||||
|
||||
breadcrumb.appendChild(ol);
|
||||
container.insertBefore(breadcrumb, container.firstChild);
|
||||
}
|
||||
Reference in New Issue
Block a user