diff --git a/voice-assistant-apps/shared/index.html b/voice-assistant-apps/shared/index.html index a9bfde4..b66ea20 100644 --- a/voice-assistant-apps/shared/index.html +++ b/voice-assistant-apps/shared/index.html @@ -1,797 +1,133 @@ + - - - - - - - - - - Voice Assistant - - + + + + + - - + KI-Sprachassistent - - -
- - -
+ + + -
+
-
-
- - - -
-
-
-
-
-
- - function applySettings() { - applyAnimationSpeed(); - applyNebelColors(); - applyGlassOpacity(); - applyReducedMotion(); - } - - function applyAnimationSpeed() { - const speed = settings.animationSpeed; - document.documentElement.style.setProperty('--animation-speed', `${4/speed}s`); - document.documentElement.style.setProperty('--avatar-speed', `${4/speed}s`); - document.documentElement.style.setProperty('--nebel-speed', `${3/speed}s`); - } - - function applyNebelColors() { - const colors = settings.nebelColors; - document.documentElement.style.setProperty('--nebel-primary', colors.primary); - document.documentElement.style.setProperty('--nebel-secondary', colors.secondary); - document.documentElement.style.setProperty('--nebel-accent', colors.accent); - } - - function applyGlassOpacity() { - document.documentElement.style.setProperty('--glass-bg', `rgba(255, 255, 255, ${settings.glassOpacity})`); - } - - function applyReducedMotion() { - if (settings.reducedMotion) { - document.body.classList.add('reduced-motion'); - } else { - document.body.classList.remove('reduced-motion'); - } - } - - // Notification System (mit Settings-Check) - function showNotification(type, title, message, duration = 5000) { - if (!settings.notifications) return; - - const container = document.getElementById('notificationContainer'); - const notification = document.createElement('div'); - - notification.className = `notification ${type}`; - notification.innerHTML = ` -
- ${type === 'success' ? '✅' : type === 'error' ? '❌' : '⚠️'} +
+
+ +
+
+
+
+
+
+
+
+
+
+
-
-
${title}
-
${message}
+
+
+
+
+
+
+ + + +
+ + 0:00 +
- - `; - - container.appendChild(notification); - - // Animate in - setTimeout(() => notification.classList.add('show'), 100); - - // Auto remove - setTimeout(() => { - closeNotification(notification.querySelector('.notification-close')); - }, duration); - } - - // Aufnahme starten (mit neuen Settings) - function startRecording() { - const audioSettings = { - echoCancellation: settings.echoCancellation, - noiseSuppression: settings.noiseSuppression, - sampleRate: 44100 - }; - - navigator.mediaDevices.getUserMedia({ audio: audioSettings }) - .then(stream => { - mediaRecorder = new MediaRecorder(stream, { - mimeType: 'audio/webm;codecs=opus' - }); - - const audioChunks = []; - - mediaRecorder.ondataavailable = event => { - audioChunks.push(event.data); - }; - - mediaRecorder.onstop = () => { - const audioBlob = new Blob(audioChunks, { type: 'audio/webm' }); - const reader = new FileReader(); - - reader.onloadend = () => { - if (ws && ws.readyState === WebSocket.OPEN) { - showNebelAnimation(); - displayResponse("Verarbeite Spracheingabe..."); - - ws.send(JSON.stringify({ - type: "audio", - content: reader.result, - timestamp: Date.now() - })); - } else { - showNotification('error', 'Keine Verbindung', 'Spracheingabe konnte nicht übertragen werden'); - } - }; - - reader.readAsDataURL(audioBlob); - stream.getTracks().forEach(track => track.stop()); - }; - - mediaRecorder.start(); - isRecording = true; - updateRecordingUI(true); - - recordingStartTime = Date.now(); - recordingTimer = setInterval(updateRecordingTime, 100); - - showNotification('success', 'Aufnahme gestartet', 'Sprechen Sie jetzt...'); - - // Auto-stop basierend auf Settings - setTimeout(() => { - if (isRecording) stopRecording(); - }, settings.autoStopTime); - - }) - .catch(err => { - showNotification('error', 'Mikrofonzugriff verweigert', 'Bitte erlauben Sie den Zugriff auf das Mikrofon'); - if (settings.debugMode) { - console.error('Microphone Error:', err); - } - }); - } - - // WebSocket mit Settings - function initWebSocket() { - const url = window.BACKEND_URL || 'ws://127.0.0.1:48232/ws'; - try { - ws = new WebSocket(url); - window.__WS = ws; - - ws.onopen = () => { - const id = (crypto && crypto.randomUUID) ? crypto.randomUUID() : String(Date.now()); - ws.send(JSON.stringify({ op: 'hello', stream_id: id })); - }; - - ws.onmessage = (event) => { - try { - const data = JSON.parse(event.data); - handleWebSocketMessage(data); - } catch {} - }; - - ws.onclose = () => { - showNotification('error', 'Verbindung getrennt', 'Versuche automatisch zu reconnecten...'); - if (settings.autoReconnect) { - setTimeout(initWebSocket, settings.connectionTimeout); - } - }; - - ws.onerror = () => { - showNotification('error', 'Server nicht erreichbar', 'Bitte überprüfen Sie die Serververbindung'); - }; - } catch (e) { - showNotification('error', 'Server nicht erreichbar', 'Bitte überprüfen Sie die Konfiguration'); - } - } - - function closeNotification(button) { - const notification = button.closest('.notification'); - notification.classList.remove('show'); - setTimeout(() => notification.remove(), 300); - } - - // Settings Menu Functions - function toggleSettingsMenu() { - const menu = document.getElementById('settingsMenu'); - menu.classList.toggle('active'); - } - - function openSettingsTab(tabName) { - document.getElementById('settingsMenu').classList.remove('active'); - openSettingsModal(); - - // Switch to specific tab after modal opens - setTimeout(() => { - switchTab(tabName); - }, 100); - } - - function openLanguageSettings() { - document.getElementById('settingsMenu').classList.remove('active'); - - // Create language selection popup - const languages = [ - { code: 'de', name: '🇩🇪 Deutsch', selected: true }, - { code: 'en', name: '🇬🇧 English', selected: false }, - { code: 'fr', name: '🇫🇷 Français', selected: false }, - { code: 'es', name: '🇪🇸 Español', selected: false }, - { code: 'it', name: '🇮🇹 Italiano', selected: false } - ]; - - const currentLang = '🇩🇪 Deutsch'; - showNotification('success', 'Sprache', `Aktuelle Sprache: ${currentLang}`, 3000); - } - - function showSystemInfo() { - document.getElementById('settingsMenu').classList.remove('active'); - - const info = { - version: '2.1.0', - platform: 'Raspberry Pi', - connection: ws && ws.readyState === WebSocket.OPEN ? 'Verbunden' : 'Getrennt', - browser: navigator.userAgent.includes('Chrome') ? 'Chrome' : 'Anderer Browser', - features: 'WebRTC, WebSocket, MediaRecorder' - }; - - const infoHtml = ` - - - System Info - - - -

System-Info

-
Version: ${info.version}\nPlattform: ${info.platform}\nVerbindung: ${info.connection}\nBrowser: ${info.browser}\nFeatures: ${info.features}
- -
-
Staged TTS Plan
-
- - - - - - -
-
- - - -
-
Antwort
-
-
- - - - `; - - const win = window.open('', 'systemInfo', 'width=400,height=300'); - if (win) { - win.document.write(infoHtml); - win.document.close(); - } else { - const infoText = `Version: ${info.version}\nPlattform: ${info.platform}\nVerbindung: ${info.connection}\nBrowser: ${info.browser}\nFeatures: ${info.features}`; - showNotification('success', '📊 System-Information', infoText, 8000); - } - } - - function showAbout() { - document.getElementById('settingsMenu').classList.remove('active'); - - const aboutText = `KI-Sprachassistent v2.1.0 - -Entwickelt für Raspberry Pi 4/400 -Unterstützt Sprach- und Texteingabe -Modulares Design mit WebSocket-Verbindung - -© 2025 - Open Source Projekt`; - - showNotification('success', 'ℹ️ Über den Assistenten', aboutText, 10000); - } - - // Close settings menu when clicking outside - document.addEventListener('click', function(event) { - const settingsBtn = document.getElementById('settingsBtn'); - const settingsMenu = document.getElementById('settingsMenu'); - - if (!settingsBtn.contains(event.target)) { - settingsMenu.classList.remove('active'); - } - }); - - // Avatar Steuerung - function activateAvatar() { - document.getElementById('avatar').classList.add('active'); - } - - function deactivateAvatar() { - document.getElementById('avatar').classList.remove('active'); - } - - // Nebel Animation zeigen/verstecken - function showNebelAnimation() { - activateAvatar(); - document.getElementById('nebelAnimation').classList.add('active'); - document.getElementById('response').style.opacity = '0.3'; - } - - function hideNebelAnimation() { - deactivateAvatar(); - document.getElementById('nebelAnimation').classList.remove('active'); - document.getElementById('response').style.opacity = '1'; - } - - // Antwort anzeigen - function displayResponse(content) { - const responseElement = document.getElementById('response'); - responseElement.innerHTML = '
'; - const container = responseElement.firstElementChild; - matrixRain(container, content); - - // Smooth scroll to response - responseElement.scrollIntoView({ behavior: 'smooth', block: 'nearest' }); - } - - function matrixRain(element, text) { - const letters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'; - let iteration = 0; - const interval = setInterval(() => { - element.textContent = text - .split('') - .map((char, index) => { - if (index < iteration) { - return char; - } - return letters[Math.floor(Math.random() * letters.length)]; - }) - .join(''); - if (iteration >= text.length) { - clearInterval(interval); - element.textContent = text; - } - iteration += 1; - }, 50); - } - - // Text senden - function sendText() { - const input = document.getElementById('textInput'); - const msg = input.value.trim(); - if (!msg || !ws || ws.readyState !== WebSocket.OPEN) return; - ws.send(JSON.stringify({ type: 'text', content: msg })); - input.value = ''; - } - - // Aufnahme umschalten - function toggleRecording() { - showNotification('info', 'Mikrofon', 'Aufnahmefunktion folgt'); - } - - function toggleSidebar() { - const el = document.getElementById('sidebar'); - if (el) { el.classList.toggle('active'); } - } - - function handleWebSocketMessage(data) { - if (!data) return; - if (data.type === 'response' || data.response) { - const resp = document.getElementById('response'); - if (resp) { - const div = document.createElement('div'); - div.textContent = data.response || data.content || ''; - resp.appendChild(div); - } - } - if (data.tts) { - try { - const audio = new Audio(`data:audio/wav;base64,${data.tts}`); - audio.play().catch(()=>{}); - } catch (e) { console.error(e); } - } - if (data.error) { - showNotification('error', 'Fehler', data.error); - } - if (data.op === 'ready' || data.type === 'ready') { - showNotification('success', 'Bereit', 'Verbindung hergestellt'); - } - } - - // Aufnahme starten (mit neuen Settings) - function startRecording() { - const audioSettings = { - echoCancellation: settings.echoCancellation, - noiseSuppression: settings.noiseSuppression, - sampleRate: 44100 - }; - - navigator.mediaDevices.getUserMedia({ audio: audioSettings }) - .then(stream => { - mediaRecorder = new MediaRecorder(stream, { - mimeType: 'audio/webm;codecs=opus' - }); - - const audioChunks = []; - - mediaRecorder.ondataavailable = event => { - audioChunks.push(event.data); - }; - - mediaRecorder.onstop = () => { - const audioBlob = new Blob(audioChunks, { type: 'audio/webm' }); - const reader = new FileReader(); - - reader.onloadend = () => { - if (ws && ws.readyState === WebSocket.OPEN) { - showNebelAnimation(); - displayResponse("Verarbeite Spracheingabe..."); - - ws.send(JSON.stringify({ - type: "audio", - content: reader.result, - timestamp: Date.now() - })); - } else { - showNotification('error', 'Keine Verbindung', 'Spracheingabe konnte nicht übertragen werden'); - } - }; - - reader.readAsDataURL(audioBlob); - stream.getTracks().forEach(track => track.stop()); - }; - - mediaRecorder.start(); - isRecording = true; - updateRecordingUI(true); - - recordingStartTime = Date.now(); - recordingTimer = setInterval(updateRecordingTime, 100); - - showNotification('success', 'Aufnahme gestartet', 'Sprechen Sie jetzt...'); - - // Auto-stop basierend auf Settings - setTimeout(() => { - if (isRecording) stopRecording(); - }, settings.autoStopTime); - - }) - .catch(err => { - showNotification('error', 'Mikrofonzugriff verweigert', 'Bitte erlauben Sie den Zugriff auf das Mikrofon'); - if (settings.debugMode) { - console.error('Microphone Error:', err); - } - }); - } - - // Aufnahme stoppen - function stopRecording() { - if (mediaRecorder && isRecording) { - mediaRecorder.stop(); - isRecording = false; - updateRecordingUI(false); - clearInterval(recordingTimer); - showNotification('success', 'Aufnahme beendet', 'Wird verarbeitet...'); - } - } - - // Recording UI aktualisieren - function updateRecordingUI(recording) { - const voiceBtn = document.getElementById('voiceBtn'); - const voiceIcon = document.getElementById('voiceIcon'); - const indicator = document.getElementById('recordingIndicator'); - - if (recording) { - voiceBtn.classList.add('recording'); - voiceIcon.textContent = '⏹️'; - indicator.classList.add('active'); - } else { - voiceBtn.classList.remove('recording'); - voiceIcon.textContent = '🎙️'; - indicator.classList.remove('active'); - } - } - - // Aufnahmezeit aktualisieren - function updateRecordingTime() { - if (recordingStartTime) { - const elapsed = Math.floor((Date.now() - recordingStartTime) / 1000); - document.getElementById('recordingTime').textContent = `${elapsed}s`; - } - } - - // App initialisieren - document.addEventListener('DOMContentLoaded', function() { - // Settings anwenden - applySettings(); - - // WebSocket initialisieren - initWebSocket(); - - const sendBtn = document.getElementById('sendBtn'); - const textInput = document.getElementById('textInput'); - const voiceBtn = document.getElementById('voiceBtn'); - const sidebarBtn = document.getElementById('toggleSidebarBtn'); - if (sendBtn) sendBtn.addEventListener('click', sendText); - if (textInput) { - textInput.addEventListener('keydown', e => { if (e.key === 'Enter') sendText(); }); - textInput.focus(); - } - if (voiceBtn) voiceBtn.addEventListener('click', toggleRecording); - if (sidebarBtn) sidebarBtn.addEventListener('click', toggleSidebar); - }); - - // Event Listeners - document.addEventListener('keydown', function(event) { - // Strg/Cmd + Enter für Spracheingabe - if ((event.ctrlKey || event.metaKey) && event.key === 'Enter') { - event.preventDefault(); - toggleRecording(); - } - - // Strg/Cmd + , für Settings - if ((event.ctrlKey || event.metaKey) && event.key === ',') { - event.preventDefault(); - openSettingsModal(); - } - - // Shortcuts für Dropdown-Menü (wenn geöffnet) - const settingsMenu = document.getElementById('settingsMenu'); - if (settingsMenu.classList.contains('active')) { - switch(event.key.toLowerCase()) { - case 'a': - event.preventDefault(); - openSettingsTab('audio'); - break; - case 'v': - event.preventDefault(); - openSettingsTab('connection'); - break; - case 's': - event.preventDefault(); - openLanguageSettings(); - break; - } - } - - // ESC für Aufnahme stoppen oder Menüs schließen - if (event.key === 'Escape') { - if (isRecording) { - stopRecording(); - } else { - // Schließe alle offenen Menüs/Modals - settingsMenu.classList.remove('active'); - closeSettingsModal(); - } - } - }); - - -
-
-
-
-
- - -
-
-
Ihre Antwort erscheint hier...
-
-
+
+
- -
- - - -
-
- Aufnahme läuft... 0s -
-
- - - -
- - - - - - - - - - - - - - diff --git a/voice-assistant-apps/shared/styles.css b/voice-assistant-apps/shared/styles.css index d090f81..f611dbd 100644 --- a/voice-assistant-apps/shared/styles.css +++ b/voice-assistant-apps/shared/styles.css @@ -1,3 +1,4 @@ +/* styles.css – extracted from monolith GUI; CSP-friendly */ :root { /* Color System */ --primary-color: #6366f1; diff --git a/voice-assistant-apps/shared/ui/sidebar-events.js b/voice-assistant-apps/shared/ui/sidebar-events.js index 3f5ae45..2dd1168 100644 --- a/voice-assistant-apps/shared/ui/sidebar-events.js +++ b/voice-assistant-apps/shared/ui/sidebar-events.js @@ -1,42 +1,31 @@ -// sidebar-events.js – bindet Header- & Sidebar-Events CSP-konform +// Sidebar header and panel events import { DOMHelpers } from '../core/dom-helpers.js'; -import { NotificationManager } from '../events.js'; +import { SidebarTabs } from './sidebar-tabs.js'; import * as Theme from './sidebar-theme.js'; -import { sidebarTabs } from './sidebar-tabs.js'; +import { NotificationManager } from '../events.js'; -export const sidebarEvents = { +export const SidebarEvents = { bind() { - // Sidebar open/close const sidebar = DOMHelpers.get('#sidebar'); const toggle = DOMHelpers.get('#sidebarToggle'); const close = DOMHelpers.get('#sidebarClose'); + if (toggle && sidebar) toggle.addEventListener('click', () => sidebar.classList.toggle('open')); + if (close && sidebar) close.addEventListener('click', () => sidebar.classList.remove('open')); - if (toggle && sidebar) { - toggle.addEventListener('click', () => { - sidebar.classList.toggle('open'); - }); - } - if (close && sidebar) { - close.addEventListener('click', () => sidebar.classList.remove('open')); - } - - // Theme toggle const themeToggle = DOMHelpers.get('#themeToggle'); if (themeToggle) { themeToggle.addEventListener('click', () => { if (Theme && typeof Theme.toggle === 'function') { - Theme?.toggle?.(); + Theme.toggle(); } else { - // Fallback: body[data-theme] toggeln const el = document.documentElement; - const next = (el.getAttribute('data-theme') === 'light') ? 'dark' : 'light'; + const next = el.getAttribute('data-theme') === 'light' ? 'dark' : 'light'; el.setAttribute('data-theme', next); } NotificationManager.show('Theme gewechselt', 'info', 1200); }); } - // Info const infoToggle = DOMHelpers.get('#infoToggle'); if (infoToggle) { infoToggle.addEventListener('click', () => { @@ -44,12 +33,24 @@ export const sidebarEvents = { }); } - // Tabs initialisieren (falls noch nicht) - sidebarTabs.init(); + const content = DOMHelpers.get('.sidebar-content'); + if (content) { + content.addEventListener('change', (ev) => { + const target = ev.target; + if (target.id) { + const evt = new CustomEvent('sidebar:change', { detail: { id: target.id, value: target.value } }); + document.dispatchEvent(evt); + } + }); + content.addEventListener('click', (ev) => { + const btn = ev.target.closest('button'); + if (btn && btn.id) { + const evt = new CustomEvent('sidebar:click', { detail: { id: btn.id } }); + document.dispatchEvent(evt); + } + }); + } + + SidebarTabs.initFromHash(); } }; - -export default sidebarEvents; - -/* alias for compatibility with sidebar.js */ -export const SidebarEvents = sidebarEvents; diff --git a/voice-assistant-apps/shared/ui/sidebar-tabs.js b/voice-assistant-apps/shared/ui/sidebar-tabs.js index 34090f0..c77a5fd 100644 --- a/voice-assistant-apps/shared/ui/sidebar-tabs.js +++ b/voice-assistant-apps/shared/ui/sidebar-tabs.js @@ -1,10 +1,10 @@ -// sidebar-tabs.js – Tab-Switching + Hash-Unterstützung +// Sidebar tab switching without aliases import { DOMHelpers } from '../core/dom-helpers.js'; -export const sidebarTabs = { +export const SidebarTabs = { init() { const nav = DOMHelpers.get('.sidebar-nav'); - const panels = DOMHelpers.all('.sidebar-panel, [data-tab]'); // tolerant + const panels = DOMHelpers.all('.sidebar-panel'); if (!nav || !panels.length) return; nav.addEventListener('click', (ev) => { @@ -13,41 +13,24 @@ export const sidebarTabs = { const tab = btn.getAttribute('data-tab'); if (!tab) return; - // Active-Status in der Navigation DOMHelpers.all('.sidebar-nav-item').forEach(b => b.classList.toggle('active', b === btn)); - - // Panels umschalten – Panels tragen data-tab="..." panels.forEach(p => { const match = p.getAttribute('data-tab') === tab; p.classList.toggle('active', match); - // 'hidden' Flag konsistent halten (falls CSS es nutzt) - if (match) p.removeAttribute('hidden'); else p.setAttribute('hidden',''); + if (match) p.removeAttribute('hidden'); else p.setAttribute('hidden', ''); }); }); }, - // Wird von sidebar.js beim Boot aufgerufen initFromHash() { - // Basis-Init (Listener setzen) this.init(); - - // Hash auswerten: #tab=audio ODER #audio const hash = window.location.hash || ''; - let tab = null; let m = hash.match(/tab=([a-z0-9_-]+)/i); if (!m) m = hash.match(/^#([a-z0-9_-]+)$/i); - if (m) tab = m[1]; - + const tab = m ? m[1] : null; if (tab) { const btn = DOMHelpers.get(`.sidebar-nav-item[data-tab="${tab}"]`); - if (btn) { - // Navigation sauber triggern - btn.click(); - } + if (btn) btn.click(); } } }; - -// Kompatible Aliase für bestehenden Code -export const SidebarTabs = sidebarTabs; -export default sidebarTabs; diff --git a/voice-assistant-apps/shared/ui/sidebar-theme.js b/voice-assistant-apps/shared/ui/sidebar-theme.js index 4791140..3c35e1c 100644 --- a/voice-assistant-apps/shared/ui/sidebar-theme.js +++ b/voice-assistant-apps/shared/ui/sidebar-theme.js @@ -1,35 +1,28 @@ -// ui/sidebar-theme.js +// Theme helpers with named exports only import { DOMHelpers } from '../core/dom-helpers.js'; -export const SidebarTheme = { - init() { - // Theme aus localStorage laden - const saved = this._get(); - if (saved) this.apply(saved); +const STORAGE_KEY = 'va_theme'; - // Theme-Dropdown/Buttons binden (falls vorhanden) - const selector = DOMHelpers.get('#themeSelector'); - if (selector) { - selector.addEventListener('change', (e) => this.apply(e.target.value)); - } - }, +export function init() { + const saved = get(); + if (saved) apply(saved); + DOMHelpers.all('.theme-card').forEach(card => { + card.addEventListener('click', () => apply(card.dataset.theme)); + }); +} - apply(theme) { - document.body.setAttribute('data-theme', theme); - this._set(theme); +export function apply(theme) { + document.documentElement.setAttribute('data-theme', theme); + try { localStorage.setItem(STORAGE_KEY, theme); } catch {} +} - // Optional: Sidebar-spezifische Elemente aktualisieren - const icon = DOMHelpers.get('#themeToggle'); - if (icon) { - const icons = { dark: '☀️', light: '🌙', 'sci-fi': '🚀', nature: '🌿', 'high-contrast': '🔲' }; - icon.textContent = icons[theme] || '☀️'; - } - }, +export function toggle() { + const current = get() || document.documentElement.getAttribute('data-theme') || 'dark'; + const next = current === 'light' ? 'dark' : 'light'; + apply(next); + return next; +} - _get() { - try { return localStorage.getItem('va.settings.theme'); } catch { return null; } - }, - _set(theme) { - try { localStorage.setItem('va.settings.theme', theme); } catch {} - } -}; +export function get() { + try { return localStorage.getItem(STORAGE_KEY); } catch { return null; } +} diff --git a/voice-assistant-apps/shared/ui/sidebar.js b/voice-assistant-apps/shared/ui/sidebar.js index 9f0478a..1ff27c7 100644 --- a/voice-assistant-apps/shared/ui/sidebar.js +++ b/voice-assistant-apps/shared/ui/sidebar.js @@ -1,9 +1,8 @@ -// ui/sidebar.js -import { SidebarCore } from './sidebar-core.js'; -import { SidebarTabs } from './sidebar-tabs.js'; +// ui/sidebar.js – unified imports without aliases +import { SidebarCore } from './sidebar-core.js'; +import { SidebarTabs } from './sidebar-tabs.js'; import { SidebarEvents } from './sidebar-events.js'; -import { SidebarTheme } from './sidebar-theme.js'; - +import * as SidebarTheme from './sidebar-theme.js'; export const sidebarManager = { async initialize() { @@ -11,13 +10,13 @@ export const sidebarManager = { SidebarCore.queryDom(); SidebarCore.ensureDom(); SidebarEvents.bind(); - SidebarTabs.initFromHash(); SidebarTheme.init(); SidebarCore.state.initialized = true; console.log('[Sidebar] initialized'); }, switchTab(name) { - SidebarTabs.switchTab(name); + const btn = document.querySelector(`.sidebar-nav-item[data-tab="${name}"]`); + if (btn) btn.click(); }, isOpen() { return !!SidebarCore.state.open; @@ -32,5 +31,4 @@ export const sidebarManager = { } }; -// Optional global hook (falls index.js darauf zugreift) window.sidebarManager = window.sidebarManager || sidebarManager;