diff --git a/public/global-navigation-ux.js b/public/global-navigation-ux.js new file mode 100644 index 0000000..f1d1ab7 --- /dev/null +++ b/public/global-navigation-ux.js @@ -0,0 +1,272 @@ +(() => { + 'use strict'; + + if (document.documentElement.dataset.uxGlobalNavigationReady === '1') return; + document.documentElement.dataset.uxGlobalNavigationReady = '1'; + + const VIEW_LABELS = { + dashboard: ['Übersicht', 'Dashboard', 'Start'], + today: ['Aufgaben', 'Heute', 'Tagesarbeitsplatz'], + generator: ['Neuer Artikel', 'Artikel erstellen', 'Generator'], + history: ['Artikel', 'Listings', 'Artikelübersicht'], + 'quality-center': ['Qualität', 'Qualitätscenter', 'Prüfen'], + publish: ['Veröffentlichen', 'Publish', 'Marktplätze'], + inventory: ['Lager', 'Bestand', 'Lagerorte'], + templates: ['Vorlagen', 'Templates'], + 'media-library': ['Medien', 'Bilder', 'Galerie'], + 'image-factory': ['Bildwerkstatt', 'Bildproduktion'], + 'flux-studio': ['FLUX Studio', 'ComfyUI', 'AI Bilder'], + extensions: ['Erweiterungen', 'Extensions', 'Browser Erweiterung'], + settings: ['Einstellungen', 'Verbindungen', 'APIs'], + admin: ['Verwaltung', 'Benutzer', 'Admin'], + fees: ['Gebühren', 'Kosten'], + trash: ['Papierkorb', 'Gelöscht'], + }; + + const state = { + activeView: null, + restoring: false, + query: '', + selectedIndex: 0, + }; + + const el = (tag, className, text) => { + const node = document.createElement(tag); + if (className) node.className = className; + if (text !== undefined) node.textContent = text; + return node; + }; + + function getActiveView() { + return document.querySelector('.sidebar-nav .nav-item.active[data-tab]')?.dataset.tab + || document.querySelector('.tab-content.active')?.id + || 'dashboard'; + } + + function getNavItem(view) { + return document.querySelector(`.sidebar-nav .nav-item[data-tab="${CSS.escape(view)}"]`); + } + + function activateView(view, { replace = false } = {}) { + const item = getNavItem(view); + if (!item || item.classList.contains('hidden')) return false; + state.restoring = true; + item.closest('details.ux-nav-section')?.setAttribute('open', ''); + item.click(); + requestAnimationFrame(() => { + state.activeView = getActiveView(); + const url = new URL(window.location.href); + url.hash = `view=${encodeURIComponent(state.activeView)}`; + const method = replace ? 'replaceState' : 'pushState'; + history[method]({ vendooView: state.activeView }, '', url); + state.restoring = false; + updateHistoryButtons(); + }); + return true; + } + + function ensureInitialHistoryState() { + const hashView = new URLSearchParams(window.location.hash.replace(/^#/, '')).get('view'); + const initial = hashView && getNavItem(hashView) ? hashView : getActiveView(); + const url = new URL(window.location.href); + url.hash = `view=${encodeURIComponent(initial)}`; + history.replaceState({ vendooView: initial }, '', url); + state.activeView = initial; + if (initial !== getActiveView()) activateView(initial, { replace: true }); + } + + function updateHistoryButtons() { + const back = document.getElementById('ux-nav-back'); + const forward = document.getElementById('ux-nav-forward'); + if (back) back.disabled = history.length <= 1; + if (forward) forward.disabled = false; + } + + function createHistoryControls() { + const context = document.querySelector('.topbar-context'); + if (!context || document.getElementById('ux-nav-history')) return; + const controls = el('div', 'ux-nav-history'); + controls.id = 'ux-nav-history'; + controls.setAttribute('aria-label', 'Navigationsverlauf'); + const back = el('button', 'ux-nav-history-button'); + back.id = 'ux-nav-back'; + back.type = 'button'; + back.title = 'Zurück (Alt + Pfeil links)'; + back.setAttribute('aria-label', 'Zurück'); + back.textContent = '←'; + const forward = el('button', 'ux-nav-history-button'); + forward.id = 'ux-nav-forward'; + forward.type = 'button'; + forward.title = 'Vorwärts (Alt + Pfeil rechts)'; + forward.setAttribute('aria-label', 'Vorwärts'); + forward.textContent = '→'; + back.addEventListener('click', () => history.back()); + forward.addEventListener('click', () => history.forward()); + controls.append(back, forward); + context.querySelector('.topbar-heading')?.before(controls); + updateHistoryButtons(); + } + + function collectCommands() { + return Object.entries(VIEW_LABELS).map(([view, labels]) => ({ + id: `view:${view}`, + view, + title: labels[0], + keywords: labels.join(' ').toLocaleLowerCase('de-DE'), + group: view === 'settings' || view === 'admin' || view === 'extensions' ? 'System' : 'Navigation', + })).filter(command => { + const item = getNavItem(command.view); + return item && !item.classList.contains('hidden'); + }); + } + + function createDialog() { + if (document.getElementById('ux-global-search')) return document.getElementById('ux-global-search'); + const dialog = el('div', 'ux-global-search hidden'); + dialog.id = 'ux-global-search'; + dialog.setAttribute('role', 'dialog'); + dialog.setAttribute('aria-modal', 'true'); + dialog.setAttribute('aria-labelledby', 'ux-global-search-title'); + const panel = el('section', 'ux-global-search-panel'); + const header = el('header', 'ux-global-search-header'); + const title = el('h2', '', 'Suchen und öffnen'); + title.id = 'ux-global-search-title'; + const close = el('button', 'ux-global-search-close', '×'); + close.type = 'button'; + close.setAttribute('aria-label', 'Suche schließen'); + const inputWrap = el('div', 'ux-global-search-input-wrap'); + const input = el('input', 'ux-global-search-input'); + input.id = 'ux-global-search-input'; + input.type = 'search'; + input.placeholder = 'Seite, Bereich oder Einstellung suchen …'; + input.autocomplete = 'off'; + input.setAttribute('aria-controls', 'ux-global-search-results'); + const shortcut = el('kbd', '', 'Esc'); + inputWrap.append(input, shortcut); + const results = el('div', 'ux-global-search-results'); + results.id = 'ux-global-search-results'; + results.setAttribute('role', 'listbox'); + const footer = el('footer', 'ux-global-search-footer'); + footer.textContent = '↑↓ auswählen · Enter öffnen · Esc schließen'; + header.append(title, close); + panel.append(header, inputWrap, results, footer); + dialog.append(panel); + document.body.append(dialog); + + const closeDialog = () => { + dialog.classList.add('hidden'); + document.body.classList.remove('ux-search-open'); + state.query = ''; + state.selectedIndex = 0; + input.value = ''; + }; + + const executeSelected = () => { + const selected = results.querySelector('[aria-selected="true"]'); + const view = selected?.dataset.view; + if (!view) return; + closeDialog(); + activateView(view); + }; + + const render = () => { + const query = input.value.trim().toLocaleLowerCase('de-DE'); + state.query = query; + const commands = collectCommands().filter(command => !query || `${command.title} ${command.keywords} ${command.group}`.toLocaleLowerCase('de-DE').includes(query)); + state.selectedIndex = Math.min(state.selectedIndex, Math.max(0, commands.length - 1)); + results.replaceChildren(); + if (!commands.length) { + const empty = el('div', 'ux-global-search-empty'); + empty.append(el('strong', '', 'Keine passende Seite gefunden'), el('span', '', 'Versuche einen allgemeineren Begriff. Produktive Aktionen werden hier bewusst nicht automatisch ausgeführt.')); + results.append(empty); + return; + } + commands.forEach((command, index) => { + const button = el('button', 'ux-global-search-result'); + button.type = 'button'; + button.dataset.view = command.view; + button.setAttribute('role', 'option'); + button.setAttribute('aria-selected', index === state.selectedIndex ? 'true' : 'false'); + const copy = el('span', 'ux-global-search-result-copy'); + copy.append(el('strong', '', command.title), el('small', '', command.group)); + button.append(copy, el('span', 'ux-global-search-result-action', 'Öffnen')); + button.addEventListener('mouseenter', () => { state.selectedIndex = index; render(); }); + button.addEventListener('click', executeSelected); + results.append(button); + }); + }; + + input.addEventListener('input', () => { state.selectedIndex = 0; render(); }); + input.addEventListener('keydown', event => { + const count = results.querySelectorAll('[data-view]').length; + if (event.key === 'ArrowDown' && count) { event.preventDefault(); state.selectedIndex = (state.selectedIndex + 1) % count; render(); } + if (event.key === 'ArrowUp' && count) { event.preventDefault(); state.selectedIndex = (state.selectedIndex - 1 + count) % count; render(); } + if (event.key === 'Enter') { event.preventDefault(); executeSelected(); } + if (event.key === 'Escape') closeDialog(); + }); + close.addEventListener('click', closeDialog); + dialog.addEventListener('mousedown', event => { if (event.target === dialog) closeDialog(); }); + dialog._open = () => { + dialog.classList.remove('hidden'); + document.body.classList.add('ux-search-open'); + state.selectedIndex = 0; + render(); + requestAnimationFrame(() => input.focus()); + }; + dialog._close = closeDialog; + return dialog; + } + + function bindSearch() { + const dialog = createDialog(); + document.getElementById('topbar-search-btn')?.addEventListener('click', () => dialog._open()); + document.addEventListener('keydown', event => { + const modifier = /Mac|iPhone|iPad/.test(navigator.platform) ? event.metaKey : event.ctrlKey; + if (modifier && event.key.toLocaleLowerCase('de-DE') === 'k') { + event.preventDefault(); + dialog.classList.contains('hidden') ? dialog._open() : dialog._close(); + } + if (event.altKey && event.key === 'ArrowLeft') { event.preventDefault(); history.back(); } + if (event.altKey && event.key === 'ArrowRight') { event.preventDefault(); history.forward(); } + if (event.key === 'Escape' && !dialog.classList.contains('hidden')) dialog._close(); + }); + } + + function observeNavigation() { + const nav = document.querySelector('.sidebar-nav'); + if (!nav) return; + const observer = new MutationObserver(() => { + const next = getActiveView(); + if (!next || next === state.activeView || state.restoring) return; + state.activeView = next; + const url = new URL(window.location.href); + url.hash = `view=${encodeURIComponent(next)}`; + history.pushState({ vendooView: next }, '', url); + updateHistoryButtons(); + }); + nav.querySelectorAll('.nav-item[data-tab]').forEach(item => observer.observe(item, { attributes: true, attributeFilter: ['class'] })); + window.addEventListener('beforeunload', () => observer.disconnect(), { once: true }); + } + + window.addEventListener('popstate', event => { + const view = event.state?.vendooView || new URLSearchParams(window.location.hash.replace(/^#/, '')).get('view'); + if (!view || !getNavItem(view)) return; + state.restoring = true; + getNavItem(view).click(); + requestAnimationFrame(() => { + state.activeView = getActiveView(); + state.restoring = false; + updateHistoryButtons(); + }); + }); + + function initialize() { + createHistoryControls(); + ensureInitialHistoryState(); + bindSearch(); + observeNavigation(); + } + + if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', initialize, { once: true }); + else initialize(); +})(); \ No newline at end of file