WIP: Vendoo 1.44.0 – UX Foundation & Guided Workspaces #29
@@ -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 });
|
||||
})();
|
||||
Reference in New Issue
Block a user