diff --git a/public/settings-control-center.js b/public/settings-control-center.js new file mode 100644 index 0000000..cd792ea --- /dev/null +++ b/public/settings-control-center.js @@ -0,0 +1,201 @@ +(() => { + 'use strict'; + + const root = document.getElementById('settings'); + if (!root || root.dataset.uxSettingsReady === '1') return; + root.dataset.uxSettingsReady = '1'; + + const storageKey = 'vendoo_settings_control_center_v1'; + const panelButtons = [...root.querySelectorAll('[data-settings-panel]')]; + const panels = [...root.querySelectorAll('[data-settings-panel-content]')]; + const hero = root.querySelector('.settings-hero'); + const shell = root.querySelector('.settings-shell'); + if (!hero || !shell || !panelButtons.length || !panels.length) return; + + const state = { query: '', lastPanel: 'general' }; + try { Object.assign(state, JSON.parse(localStorage.getItem(storageKey) || '{}')); } catch {} + + const create = (tag, className, text) => { + const element = document.createElement(tag); + if (className) element.className = className; + if (text !== undefined) element.textContent = text; + return element; + }; + + const command = create('section', 'ux-settings-command'); + command.setAttribute('aria-label', 'Einstellungen durchsuchen und Systemstatus prüfen'); + + const searchWrap = create('label', 'ux-settings-search'); + searchWrap.setAttribute('for', 'ux-settings-search-input'); + const searchTitle = create('span', '', 'Wo möchtest du etwas ändern?'); + const search = create('input'); + search.id = 'ux-settings-search-input'; + search.type = 'search'; + search.placeholder = 'Zum Beispiel eBay, Theme, OpenRouter, FLUX oder Sprache'; + search.autocomplete = 'off'; + search.value = state.query || ''; + searchWrap.append(searchTitle, search); + + const result = create('div', 'ux-settings-search-result'); + result.id = 'ux-settings-search-result'; + result.setAttribute('role', 'status'); + result.setAttribute('aria-live', 'polite'); + + const health = create('div', 'ux-settings-health'); + health.setAttribute('aria-label', 'Systemstatus'); + const healthItems = [ + ['ai', 'AI-Dienste', '#settings-ai-status-text'], + ['marketplaces', 'Marktplätze', '#etsy-status-badge, #ebay-status-badge'], + ['flux', 'Lokales FLUX', '#settings-flux-status-text, #local-flux-engine-title'], + ['general', 'Netzwerk', '#public-base-url-status'], + ]; + + const healthCards = healthItems.map(([panel, label, selector]) => { + const button = create('button', 'ux-settings-health-card'); + button.type = 'button'; + button.dataset.uxSettingsHealth = panel; + const dot = create('span', 'ux-settings-health-dot'); + const copy = create('span'); + copy.append(create('small', '', label), create('strong', '', 'Wird geprüft …')); + button.append(dot, copy); + button.addEventListener('click', () => activatePanel(panel, { focus: true })); + health.append(button); + return { button, dot, value: copy.querySelector('strong'), selector }; + }); + + command.append(searchWrap, result, health); + hero.insertAdjacentElement('afterend', command); + + const categoryGrid = create('section', 'ux-settings-category-grid'); + categoryGrid.setAttribute('aria-label', 'Einstellungsbereiche'); + const categoryCopy = { + general: ['Allgemein', 'Standards, Sprache, Netzwerk und Verkäuferhinweise'], + appearance: ['Darstellung', 'Theme, Dichte, Animationen und Kontrast'], + ai: ['AI & APIs', 'Text- und Bildprovider, Modelle und Sicherheitsoptionen'], + marketplaces: ['Marktplätze', 'eBay- und Etsy-Verbindungen kontrollieren'], + flux: ['Lokales FLUX', 'ComfyUI, Leistung, Installation und Wartung'], + }; + + panelButtons.forEach((navButton, index) => { + const panel = navButton.dataset.settingsPanel; + const [title, copy] = categoryCopy[panel] || [panel, 'Einstellungen öffnen']; + const card = create('button', 'ux-settings-category-card'); + card.type = 'button'; + card.dataset.uxSettingsCategory = panel; + card.setAttribute('aria-pressed', index === 0 ? 'true' : 'false'); + card.append(create('span', 'ux-settings-category-index', String(index + 1).padStart(2, '0')), + create('strong', '', title), create('small', '', copy), create('b', '', 'Öffnen →')); + card.addEventListener('click', () => activatePanel(panel, { focus: true })); + categoryGrid.append(card); + }); + command.insertAdjacentElement('afterend', categoryGrid); + + const advancedSelectors = [ + '.theme-admin-only', '.reference-control-card', '.local-flux-status-panel', + '.settings-disclosure', '.local-flux-install-options', '.local-flux-actions' + ]; + panels.forEach(panel => { + const advanced = advancedSelectors.flatMap(selector => [...panel.querySelectorAll(selector)]); + if (!advanced.length || panel.querySelector(':scope > .ux-settings-advanced')) return; + const details = create('details', 'ux-settings-advanced'); + const summary = create('summary'); + summary.append(create('strong', '', 'Erweiterte Einstellungen'), create('small', '', 'Technische und selten benötigte Optionen')); + details.append(summary); + advanced.forEach(element => details.append(element)); + panel.append(details); + }); + + const normalize = value => String(value || '').toLocaleLowerCase('de-DE').normalize('NFD').replace(/[\u0300-\u036f]/g, ''); + const searchable = panels.map(panel => ({ + panel, + id: panel.dataset.settingsPanelContent, + text: normalize(panel.textContent), + targets: [...panel.querySelectorAll('label, .settings-card-head strong, .settings-panel-head h3')] + .map(element => ({ element, text: normalize(element.textContent) })) + })); + + function activatePanel(id, { focus = false } = {}) { + const nav = panelButtons.find(button => button.dataset.settingsPanel === id); + if (!nav) return; + nav.click(); + state.lastPanel = id; + try { localStorage.setItem(storageKey, JSON.stringify(state)); } catch {} + root.querySelectorAll('[data-ux-settings-category]').forEach(card => { + card.setAttribute('aria-pressed', card.dataset.uxSettingsCategory === id ? 'true' : 'false'); + }); + if (focus) { + const panel = panels.find(item => item.dataset.settingsPanelContent === id); + window.requestAnimationFrame(() => panel?.querySelector('input, select, textarea, button')?.focus({ preventScroll: true })); + } + } + + function clearHighlights() { + root.querySelectorAll('.ux-settings-search-hit').forEach(element => element.classList.remove('ux-settings-search-hit')); + } + + function applySearch() { + const query = normalize(search.value.trim()); + state.query = search.value.trim(); + try { localStorage.setItem(storageKey, JSON.stringify(state)); } catch {} + clearHighlights(); + if (query.length < 2) { + result.textContent = query ? 'Bitte mindestens zwei Zeichen eingeben.' : 'Suche über alle Einstellungsbereiche.'; + return; + } + const matches = searchable.filter(entry => entry.text.includes(query)); + if (!matches.length) { + result.textContent = `Keine Einstellung zu „${search.value.trim()}“ gefunden.`; + return; + } + const first = matches[0]; + activatePanel(first.id); + const target = first.targets.find(item => item.text.includes(query))?.element || first.panel.querySelector('.settings-panel-head'); + target?.classList.add('ux-settings-search-hit'); + target?.scrollIntoView({ behavior: document.documentElement.dataset.reducedMotion === 'true' ? 'auto' : 'smooth', block: 'center' }); + result.textContent = `${matches.length} Bereich${matches.length === 1 ? '' : 'e'} gefunden · ${categoryCopy[first.id]?.[0] || first.id} geöffnet.`; + } + + let searchTimer; + search.addEventListener('input', () => { + window.clearTimeout(searchTimer); + searchTimer = window.setTimeout(applySearch, 180); + }); + search.addEventListener('keydown', event => { + if (event.key === 'Escape') { + search.value = ''; + applySearch(); + search.focus(); + } + }); + + function statusTone(text) { + const value = normalize(text); + if (/fehler|nicht|ungultig|deaktiviert|fehlgeschlagen|nicht erreichbar/.test(value)) return 'danger'; + if (/wird|pruf|unbekannt|noch keine|gestoppt/.test(value)) return 'warning'; + if (/bereit|verbunden|aktiv|erreichbar|ok|gesetzt/.test(value)) return 'success'; + return 'neutral'; + } + + function refreshHealth() { + healthCards.forEach(item => { + const sources = [...root.querySelectorAll(item.selector)].filter(element => element.textContent.trim()); + const text = sources.map(element => element.textContent.trim()).join(' · ') || 'Noch nicht geprüft'; + const tone = statusTone(text); + item.value.textContent = text.slice(0, 72); + item.button.dataset.tone = tone; + }); + } + + const observed = [ + '#settings-ai-status-text', '#settings-flux-status-text', '#etsy-status-badge', '#ebay-status-badge', + '#local-flux-engine-title', '#public-base-url-status' + ].flatMap(selector => [...root.querySelectorAll(selector)]); + const observer = new MutationObserver(() => window.requestAnimationFrame(refreshHealth)); + observed.forEach(element => observer.observe(element, { childList: true, characterData: true, subtree: true, attributes: true, attributeFilter: ['class'] })); + + panelButtons.forEach(button => button.addEventListener('click', () => activatePanel(button.dataset.settingsPanel))); + activatePanel(state.lastPanel || 'general'); + refreshHealth(); + applySearch(); + window.addEventListener('beforeunload', () => observer.disconnect(), { once: true }); +})(); \ No newline at end of file