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>
|
||||||
+27
-2
@@ -952,7 +952,32 @@
|
|||||||
"backupRestoredToast": "تمت استعادة قاعدة البيانات. جارٍ إعادة التحميل...",
|
"backupRestoredToast": "تمت استعادة قاعدة البيانات. جارٍ إعادة التحميل...",
|
||||||
"backupCliTitle": "استعادة CLI / Docker Compose",
|
"backupCliTitle": "استعادة CLI / Docker Compose",
|
||||||
"backupCliHint": "For operational restores, stop the app, mount the backup in a temporary container and replace the database file.",
|
"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": {
|
"login": {
|
||||||
"tagline": "تخطيط عائلي. آمن. يحترم الخصوصية. مفتوح المصدر.",
|
"tagline": "تخطيط عائلي. آمن. يحترم الخصوصية. مفتوح المصدر.",
|
||||||
@@ -1191,4 +1216,4 @@
|
|||||||
"new": "إنشاء إدخال جديد",
|
"new": "إنشاء إدخال جديد",
|
||||||
"search": "فتح البحث"
|
"search": "فتح البحث"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+46
-2
@@ -758,14 +758,26 @@
|
|||||||
},
|
},
|
||||||
"settings": {
|
"settings": {
|
||||||
"title": "Einstellungen",
|
"title": "Einstellungen",
|
||||||
|
"navigationLabel": "Einstellungsnavigation",
|
||||||
|
"breadcrumbLabel": "Pfad",
|
||||||
|
"sectionPersonal": "Persönlich",
|
||||||
|
"sectionModulesNav": "Module",
|
||||||
|
"sectionSync": "Synchronisation",
|
||||||
|
"sectionAdmin": "Administration",
|
||||||
"tabGeneral": "Allgemein",
|
"tabGeneral": "Allgemein",
|
||||||
"tabMeals": "Mahlzeiten",
|
"tabMeals": "Mahlzeiten",
|
||||||
"tabBudget": "Budget",
|
"tabBudget": "Budget",
|
||||||
"tabShopping": "Einkauf",
|
"tabShopping": "Einkauf",
|
||||||
"tabCalendar": "Kalender",
|
"tabCalendar": "Kalender",
|
||||||
"tabFamily": "Familienverwaltung",
|
"tabSync": "Synchronisation",
|
||||||
|
"tabSyncCalendar": "Kalender",
|
||||||
|
"tabSyncContacts": "Kontakte",
|
||||||
|
"sectionContactSync": "Kontakt-Synchronisation",
|
||||||
|
"cardavTitle": "CardDAV Kontakte",
|
||||||
|
"tabFamily": "Familie",
|
||||||
"tabApiTokens": "API-Tokens",
|
"tabApiTokens": "API-Tokens",
|
||||||
"tabAccount": "Konto",
|
"tabAccount": "Konto",
|
||||||
|
"tabBackup": "Backup",
|
||||||
"tabsAriaLabel": "Einstellungsbereiche",
|
"tabsAriaLabel": "Einstellungsbereiche",
|
||||||
"sectionDesign": "Design",
|
"sectionDesign": "Design",
|
||||||
"sectionAppName": "Anwendungsname",
|
"sectionAppName": "Anwendungsname",
|
||||||
@@ -1015,7 +1027,39 @@
|
|||||||
"calendarDisabled": "Kalender deaktiviert",
|
"calendarDisabled": "Kalender deaktiviert",
|
||||||
"calendarsRefreshed": "Kalender aktualisiert",
|
"calendarsRefreshed": "Kalender aktualisiert",
|
||||||
"deleteAccountConfirm": "CalDAV-Konto wirklich löschen? Alle synchronisierten Kalender werden entfernt.",
|
"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": {
|
"login": {
|
||||||
"tagline": "Familienplanung. Sicher. Datenschutzfreundlich. Open Source.",
|
"tagline": "Familienplanung. Sicher. Datenschutzfreundlich. Open Source.",
|
||||||
|
|||||||
+27
-2
@@ -952,7 +952,32 @@
|
|||||||
"backupRestoredToast": "Η βάση επαναφέρθηκε. Επαναφόρτωση...",
|
"backupRestoredToast": "Η βάση επαναφέρθηκε. Επαναφόρτωση...",
|
||||||
"backupCliTitle": "Επαναφορά CLI / Docker Compose",
|
"backupCliTitle": "Επαναφορά CLI / Docker Compose",
|
||||||
"backupCliHint": "For operational restores, stop the app, mount the backup in a temporary container and replace the database file.",
|
"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": {
|
"login": {
|
||||||
"tagline": "Οικογενειακός προγραμματισμός. Ασφαλής. Φιλικός προς την ιδιωτικότητα. Ανοιχτός κώδικας.",
|
"tagline": "Οικογενειακός προγραμματισμός. Ασφαλής. Φιλικός προς την ιδιωτικότητα. Ανοιχτός κώδικας.",
|
||||||
@@ -1191,4 +1216,4 @@
|
|||||||
"new": "Δημιουργία νέας εγγραφής",
|
"new": "Δημιουργία νέας εγγραφής",
|
||||||
"search": "Άνοιγμα αναζήτησης"
|
"search": "Άνοιγμα αναζήτησης"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+26
-1
@@ -760,7 +760,9 @@
|
|||||||
"tabFamily": "Family Management",
|
"tabFamily": "Family Management",
|
||||||
"tabApiTokens": "API Tokens",
|
"tabApiTokens": "API Tokens",
|
||||||
"tabAccount": "Account",
|
"tabAccount": "Account",
|
||||||
|
"tabSync": "Synchronization",
|
||||||
"tabsAriaLabel": "Settings sections",
|
"tabsAriaLabel": "Settings sections",
|
||||||
|
"sectionContactSync": "Contact Synchronization",
|
||||||
"sectionDesign": "Appearance",
|
"sectionDesign": "Appearance",
|
||||||
"sectionAppName": "Application name",
|
"sectionAppName": "Application name",
|
||||||
"sectionModules": "Modules",
|
"sectionModules": "Modules",
|
||||||
@@ -1009,7 +1011,30 @@
|
|||||||
"calendarDisabled": "Calendar disabled",
|
"calendarDisabled": "Calendar disabled",
|
||||||
"calendarsRefreshed": "Calendars refreshed",
|
"calendarsRefreshed": "Calendars refreshed",
|
||||||
"deleteAccountConfirm": "Really delete CalDAV account? All synced calendars will be removed.",
|
"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": {
|
"login": {
|
||||||
"tagline": "Family planning. Secure. Privacy-friendly. Open source.",
|
"tagline": "Family planning. Secure. Privacy-friendly. Open source.",
|
||||||
|
|||||||
+27
-2
@@ -952,7 +952,32 @@
|
|||||||
"backupRestoredToast": "Base de datos restaurada. Recargando...",
|
"backupRestoredToast": "Base de datos restaurada. Recargando...",
|
||||||
"backupCliTitle": "Restauración por CLI / Docker Compose",
|
"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.",
|
"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": {
|
"login": {
|
||||||
"tagline": "Planificación familiar. Segura. Privada. Código abierto.",
|
"tagline": "Planificación familiar. Segura. Privada. Código abierto.",
|
||||||
@@ -1191,4 +1216,4 @@
|
|||||||
"new": "Crear nueva entrada",
|
"new": "Crear nueva entrada",
|
||||||
"search": "Abrir búsqueda"
|
"search": "Abrir búsqueda"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+27
-2
@@ -952,7 +952,32 @@
|
|||||||
"backupRestoredToast": "Base restaurée. Rechargement...",
|
"backupRestoredToast": "Base restaurée. Rechargement...",
|
||||||
"backupCliTitle": "Restauration CLI / Docker Compose",
|
"backupCliTitle": "Restauration CLI / Docker Compose",
|
||||||
"backupCliHint": "For operational restores, stop the app, mount the backup in a temporary container and replace the database file.",
|
"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": {
|
"login": {
|
||||||
"tagline": "Planification familiale. Sécurisée. Respectueuse de la vie privée. Open source.",
|
"tagline": "Planification familiale. Sécurisée. Respectueuse de la vie privée. Open source.",
|
||||||
@@ -1191,4 +1216,4 @@
|
|||||||
"new": "Créer une nouvelle entrée",
|
"new": "Créer une nouvelle entrée",
|
||||||
"search": "Ouvrir la recherche"
|
"search": "Ouvrir la recherche"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+27
-2
@@ -952,7 +952,32 @@
|
|||||||
"backupRestoredToast": "डेटाबेस पुनर्स्थापित हुआ। फिर से लोड हो रहा है...",
|
"backupRestoredToast": "डेटाबेस पुनर्स्थापित हुआ। फिर से लोड हो रहा है...",
|
||||||
"backupCliTitle": "CLI / Docker Compose पुनर्स्थापना",
|
"backupCliTitle": "CLI / Docker Compose पुनर्स्थापना",
|
||||||
"backupCliHint": "For operational restores, stop the app, mount the backup in a temporary container and replace the database file.",
|
"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": {
|
"login": {
|
||||||
"tagline": "पारिवारिक योजना। सुरक्षित। गोपनीयता-अनुकूल। ओपन सोर्स।",
|
"tagline": "पारिवारिक योजना। सुरक्षित। गोपनीयता-अनुकूल। ओपन सोर्स।",
|
||||||
@@ -1191,4 +1216,4 @@
|
|||||||
"new": "नई प्रविष्टि बनाएं",
|
"new": "नई प्रविष्टि बनाएं",
|
||||||
"search": "खोज खोलें"
|
"search": "खोज खोलें"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+27
-2
@@ -952,7 +952,32 @@
|
|||||||
"backupRestoredToast": "Database ripristinato. Ricaricamento...",
|
"backupRestoredToast": "Database ripristinato. Ricaricamento...",
|
||||||
"backupCliTitle": "Ripristino CLI / Docker Compose",
|
"backupCliTitle": "Ripristino CLI / Docker Compose",
|
||||||
"backupCliHint": "For operational restores, stop the app, mount the backup in a temporary container and replace the database file.",
|
"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": {
|
"login": {
|
||||||
"tagline": "Pianificazione familiare. Sicura. Rispettosa della privacy. Open source.",
|
"tagline": "Pianificazione familiare. Sicura. Rispettosa della privacy. Open source.",
|
||||||
@@ -1191,4 +1216,4 @@
|
|||||||
"new": "Crea nuova voce",
|
"new": "Crea nuova voce",
|
||||||
"search": "Apri ricerca"
|
"search": "Apri ricerca"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+27
-2
@@ -952,7 +952,32 @@
|
|||||||
"backupRestoredToast": "データベースを復元しました。再読み込み中...",
|
"backupRestoredToast": "データベースを復元しました。再読み込み中...",
|
||||||
"backupCliTitle": "CLI / Docker Compose 復元",
|
"backupCliTitle": "CLI / Docker Compose 復元",
|
||||||
"backupCliHint": "For operational restores, stop the app, mount the backup in a temporary container and replace the database file.",
|
"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": {
|
"login": {
|
||||||
"tagline": "家族計画。安全。プライバシー重視。オープンソース。",
|
"tagline": "家族計画。安全。プライバシー重視。オープンソース。",
|
||||||
@@ -1191,4 +1216,4 @@
|
|||||||
"new": "新規エントリを作成",
|
"new": "新規エントリを作成",
|
||||||
"search": "検索を開く"
|
"search": "検索を開く"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+27
-2
@@ -952,7 +952,32 @@
|
|||||||
"backupRestoredToast": "Banco de dados restaurado. Recarregando...",
|
"backupRestoredToast": "Banco de dados restaurado. Recarregando...",
|
||||||
"backupCliTitle": "Restauração via CLI / Docker Compose",
|
"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.",
|
"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": {
|
"login": {
|
||||||
"tagline": "Planejamento familiar. Seguro. Privado. Código aberto.",
|
"tagline": "Planejamento familiar. Seguro. Privado. Código aberto.",
|
||||||
@@ -1191,4 +1216,4 @@
|
|||||||
"new": "Criar nova entrada",
|
"new": "Criar nova entrada",
|
||||||
"search": "Abrir pesquisa"
|
"search": "Abrir pesquisa"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+27
-2
@@ -952,7 +952,32 @@
|
|||||||
"backupRestoredToast": "База данных восстановлена. Перезагрузка...",
|
"backupRestoredToast": "База данных восстановлена. Перезагрузка...",
|
||||||
"backupCliTitle": "Восстановление CLI / Docker Compose",
|
"backupCliTitle": "Восстановление CLI / Docker Compose",
|
||||||
"backupCliHint": "For operational restores, stop the app, mount the backup in a temporary container and replace the database file.",
|
"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": {
|
"login": {
|
||||||
"tagline": "Семейное планирование. Безопасно. С уважением к приватности. Открытый исходный код.",
|
"tagline": "Семейное планирование. Безопасно. С уважением к приватности. Открытый исходный код.",
|
||||||
@@ -1191,4 +1216,4 @@
|
|||||||
"new": "Создать новую запись",
|
"new": "Создать новую запись",
|
||||||
"search": "Открыть поиск"
|
"search": "Открыть поиск"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+27
-2
@@ -952,7 +952,32 @@
|
|||||||
"backupRestoredToast": "Databasen återställd. Laddar om...",
|
"backupRestoredToast": "Databasen återställd. Laddar om...",
|
||||||
"backupCliTitle": "CLI / Docker Compose-återställning",
|
"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.",
|
"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": {
|
"login": {
|
||||||
"tagline": "Familjeplanering. Säker. Sekretessvänlig. Öppen källkod.",
|
"tagline": "Familjeplanering. Säker. Sekretessvänlig. Öppen källkod.",
|
||||||
@@ -1191,4 +1216,4 @@
|
|||||||
"goShop": "Inköpslista",
|
"goShop": "Inköpslista",
|
||||||
"goNotes": "Anteckningar"
|
"goNotes": "Anteckningar"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+27
-2
@@ -952,7 +952,32 @@
|
|||||||
"backupRestoredToast": "Veritabanı geri yüklendi. Yeniden yükleniyor...",
|
"backupRestoredToast": "Veritabanı geri yüklendi. Yeniden yükleniyor...",
|
||||||
"backupCliTitle": "CLI / Docker Compose geri yükleme",
|
"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.",
|
"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": {
|
"login": {
|
||||||
"tagline": "Aile planlaması. Güvenli. Gizlilik dostu. Açık kaynak.",
|
"tagline": "Aile planlaması. Güvenli. Gizlilik dostu. Açık kaynak.",
|
||||||
@@ -1191,4 +1216,4 @@
|
|||||||
"new": "Yeni giriş oluştur",
|
"new": "Yeni giriş oluştur",
|
||||||
"search": "Aramayı aç"
|
"search": "Aramayı aç"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+27
-2
@@ -952,7 +952,32 @@
|
|||||||
"backupRestoredToast": "Базу даних відновлено. Перезавантаження...",
|
"backupRestoredToast": "Базу даних відновлено. Перезавантаження...",
|
||||||
"backupCliTitle": "Відновлення CLI / Docker Compose",
|
"backupCliTitle": "Відновлення CLI / Docker Compose",
|
||||||
"backupCliHint": "For operational restores, stop the app, mount the backup in a temporary container and replace the database file.",
|
"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": {
|
"login": {
|
||||||
"tagline": "Планування для родини. Безпечно. Конфіденційно. Відкритий код.",
|
"tagline": "Планування для родини. Безпечно. Конфіденційно. Відкритий код.",
|
||||||
@@ -1191,4 +1216,4 @@
|
|||||||
"new": "Створити новий запис",
|
"new": "Створити новий запис",
|
||||||
"search": "Відкрити пошук"
|
"search": "Відкрити пошук"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+27
-2
@@ -952,7 +952,32 @@
|
|||||||
"backupRestoredToast": "数据库已恢复。正在重新加载...",
|
"backupRestoredToast": "数据库已恢复。正在重新加载...",
|
||||||
"backupCliTitle": "CLI / Docker Compose 恢复",
|
"backupCliTitle": "CLI / Docker Compose 恢复",
|
||||||
"backupCliHint": "For operational restores, stop the app, mount the backup in a temporary container and replace the database file.",
|
"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": {
|
"login": {
|
||||||
"tagline": "家庭规划。安全。注重隐私。开源。",
|
"tagline": "家庭规划。安全。注重隐私。开源。",
|
||||||
@@ -1191,4 +1216,4 @@
|
|||||||
"new": "创建新条目",
|
"new": "创建新条目",
|
||||||
"search": "打开搜索"
|
"search": "打开搜索"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+230
-14
@@ -1,13 +1,14 @@
|
|||||||
/**
|
/**
|
||||||
* Modul: Einstellungen (Settings)
|
* Modul: Einstellungen (Settings)
|
||||||
* Zweck: Benutzerkonto, Passwort, Kalender-Sync, Familienmitglieder
|
* Zweck: Benutzerkonto, Passwort, Kalender-Sync, Kontakte-Sync, Familienmitglieder
|
||||||
* Abhängigkeiten: /api.js
|
* Abhängigkeiten: /api.js, /utils/settings-nav.js
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { api, auth } from '/api.js';
|
import { api, auth } from '/api.js';
|
||||||
import { openModal, closeModal, confirmModal } from '/components/modal.js';
|
import { openModal, closeModal, confirmModal } from '/components/modal.js';
|
||||||
import { t, formatDate, formatTime, dateInputPlaceholder, formatDateInput, parseDateInput, isDateInputValid, getDateFormat } from '/i18n.js';
|
import { t, formatDate, formatTime, dateInputPlaceholder, formatDateInput, parseDateInput, isDateInputValid, getDateFormat } from '/i18n.js';
|
||||||
import { esc } from '/utils/html.js';
|
import { esc } from '/utils/html.js';
|
||||||
|
import { renderSettingsSidebar, renderBreadcrumb, getLastActivePage, setActivePage, findSectionAndPage } from '/utils/settings-nav.js';
|
||||||
import '/components/oikos-locale-picker.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'];
|
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');
|
: t('settings.notConnected');
|
||||||
|
|
||||||
const allowedTabs = [
|
const allowedTabs = [
|
||||||
'general', 'meals', 'budget', 'shopping', 'calendar',
|
'general', 'meals', 'budget', 'shopping', 'sync',
|
||||||
...(user?.role === 'admin' ? ['family', 'api-tokens'] : []),
|
...(user?.role === 'admin' ? ['family', 'api-tokens'] : []),
|
||||||
'account',
|
'account',
|
||||||
...(user?.role === 'admin' ? ['backup'] : []),
|
...(user?.role === 'admin' ? ['backup'] : []),
|
||||||
];
|
];
|
||||||
const storedTab = sessionStorage.getItem(SETTINGS_TAB_KEY) ?? 'general';
|
const storedTab = sessionStorage.getItem(SETTINGS_TAB_KEY) ?? 'general';
|
||||||
const activeTab = (syncOk || syncErr)
|
const activeTab = (syncOk || syncErr)
|
||||||
? 'calendar'
|
? 'sync'
|
||||||
: (allowedTabs.includes(storedTab) ? storedTab : 'general');
|
: (allowedTabs.includes(storedTab) ? storedTab : 'general');
|
||||||
|
|
||||||
const panelHidden = (id) => id === activeTab ? '' : ' hidden';
|
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('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('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('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>
|
<button class="${btnClass('sync')}" role="tab" data-tab="sync" aria-selected="${btnAria('sync')}" data-group="sync">${t('settings.tabSync')}</button>
|
||||||
${user?.role === 'admin' ? `<button class="${btnClass('family')}" role="tab" data-tab="family" aria-selected="${btnAria('family')}">${t('settings.tabFamily')}</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('api-tokens')}" role="tab" data-tab="api-tokens" aria-selected="${btnAria('api-tokens')}">${t('settings.tabApiTokens')}</button>` : ''}
|
${user?.role === 'admin' ? `<button class="${btnClass('family')}" role="tab" data-tab="family" aria-selected="${btnAria('family')}" data-group="admin">${t('settings.tabFamily')}</button>` : ''}
|
||||||
<button class="${btnClass('account')}" role="tab" data-tab="account" aria-selected="${btnAria('account')}">${t('settings.tabAccount')}</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')}">${t('settings.tabBackup')}</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>
|
</nav>
|
||||||
|
|
||||||
<!-- Panel: Allgemein (Design + Sprache) -->
|
<!-- Panel: Allgemein (Design + Sprache) -->
|
||||||
@@ -453,8 +454,9 @@ export async function render(container, { user }) {
|
|||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Panel: Kalender -->
|
<!-- Panel: Synchronisation -->
|
||||||
<div class="settings-tab-panel" data-panel="calendar" role="tabpanel"${panelHidden('calendar')}>
|
<div class="settings-tab-panel" data-panel="sync" role="tabpanel"${panelHidden('sync')}>
|
||||||
|
<!-- Sektion: Kalender-Synchronisation -->
|
||||||
<section class="settings-section">
|
<section class="settings-section">
|
||||||
<h2 class="settings-section__title">${t('settings.sectionCalendarSync')}</h2>
|
<h2 class="settings-section__title">${t('settings.sectionCalendarSync')}</h2>
|
||||||
|
|
||||||
@@ -586,6 +588,27 @@ export async function render(container, { user }) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</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>
|
</div>
|
||||||
|
|
||||||
${user?.role === 'admin' ? `
|
${user?.role === 'admin' ? `
|
||||||
@@ -1328,9 +1351,130 @@ function bindEvents(container, user, users, categories, icsSubscriptions, apiTok
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load CalDAV accounts on page load
|
// CardDAV-Konten laden
|
||||||
if (user?.role === 'admin') {
|
async function loadCardDAVAccounts(container) {
|
||||||
loadCalDAVAccounts(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
|
// 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)
|
// Mitglied hinzufügen (Admin)
|
||||||
const addMemberBtn = container.querySelector('#add-member-btn');
|
const addMemberBtn = container.querySelector('#add-member-btn');
|
||||||
if (addMemberBtn) {
|
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) {
|
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);
|
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
|
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
|
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