From 0af3330a01f36b88b82bd83b8fbaa9abaced30dd Mon Sep 17 00:00:00 2001 From: Masterluke77 <163446896+Masterluke77@users.noreply.github.com> Date: Mon, 13 Jul 2026 14:20:06 +0200 Subject: [PATCH] =?UTF-8?q?feat(ux):=20Navigation=20Engine=20und=20Command?= =?UTF-8?q?=20Registry=20erg=C3=A4nzen?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- public/command-platform.js | 315 +++++++++++++++++++++++++++++++++++++ 1 file changed, 315 insertions(+) create mode 100644 public/command-platform.js diff --git a/public/command-platform.js b/public/command-platform.js new file mode 100644 index 0000000..9c8099d --- /dev/null +++ b/public/command-platform.js @@ -0,0 +1,315 @@ +(() => { + 'use strict'; + + if (document.documentElement.dataset.uxCommandPlatformReady === '1') return; + document.documentElement.dataset.uxCommandPlatformReady = '1'; + + const FAVORITES_KEY = 'vendoo_command_favorites_v1'; + const RECENTS_KEY = 'vendoo_navigation_recents_v1'; + const MAX_RECENTS = 12; + const registry = new Map(); + const listeners = new Set(); + + const VIEW_META = { + dashboard: { title: 'Übersicht', group: 'Start', icon: '⌂', keywords: ['dashboard', 'start'] }, + today: { title: 'Aufgaben', group: 'Start', icon: '✓', keywords: ['heute', 'tagesarbeitsplatz'] }, + generator: { title: 'Neuer Artikel', group: 'Erstellen', icon: '+', keywords: ['generator', 'listing erstellen'] }, + history: { title: 'Artikel', group: 'Verkaufen', icon: '□', keywords: ['listings', 'artikelübersicht'] }, + 'quality-center': { title: 'Qualität', group: 'Optimieren', icon: '◇', keywords: ['qualitätscenter', 'prüfen'] }, + publish: { title: 'Veröffentlichen', group: 'Verkaufen', icon: '↗', keywords: ['publish', 'marktplätze'] }, + inventory: { title: 'Lager', group: 'Bestand', icon: '▣', keywords: ['bestand', 'lagerorte'] }, + templates: { title: 'Vorlagen', group: 'Inhalte', icon: '▤', keywords: ['templates'] }, + 'media-library': { title: 'Medien', group: 'Inhalte', icon: '▧', keywords: ['bilder', 'galerie'] }, + 'image-factory': { title: 'Bildwerkstatt', group: 'AI & Bilder', icon: '✦', keywords: ['bildproduktion'] }, + 'flux-studio': { title: 'FLUX Studio', group: 'AI & Bilder', icon: '✧', keywords: ['comfyui', 'ai bilder'] }, + extensions: { title: 'Erweiterungen', group: 'System', icon: '⌘', keywords: ['extensions', 'browser'] }, + settings: { title: 'Einstellungen', group: 'System', icon: '⚙', keywords: ['verbindungen', 'apis'] }, + admin: { title: 'Verwaltung', group: 'System', icon: '◈', keywords: ['benutzer', 'admin'] }, + fees: { title: 'Gebühren', group: 'Verkaufen', icon: '€', keywords: ['kosten'] }, + trash: { title: 'Papierkorb', group: 'System', icon: '⌫', keywords: ['gelöscht'] }, + }; + + const readList = (key) => { + try { + const value = JSON.parse(localStorage.getItem(key) || '[]'); + return Array.isArray(value) ? value.filter(item => typeof item === 'string') : []; + } catch { return []; } + }; + + const writeList = (key, value) => { + try { localStorage.setItem(key, JSON.stringify(value)); } catch {} + }; + + let favorites = readList(FAVORITES_KEY); + let recents = readList(RECENTS_KEY); + + const emit = (type, detail = {}) => { + window.dispatchEvent(new CustomEvent(`vendoo:${type}`, { detail })); + listeners.forEach(listener => { + try { listener({ type, detail }); } catch {} + }); + }; + + function normalizeCommand(input) { + if (!input || typeof input !== 'object') throw new TypeError('Command muss ein Objekt sein.'); + const id = String(input.id || '').trim(); + const title = String(input.title || '').trim(); + if (!id || !title) throw new TypeError('Command benötigt id und title.'); + const keywords = Array.isArray(input.keywords) ? input.keywords.map(String) : []; + return Object.freeze({ + id, + title, + description: String(input.description || ''), + group: String(input.group || 'Allgemein'), + icon: String(input.icon || '•'), + keywords, + shortcut: String(input.shortcut || ''), + permission: input.permission || null, + kind: String(input.kind || 'navigation'), + view: input.view ? String(input.view) : null, + execute: typeof input.execute === 'function' ? input.execute : null, + }); + } + + function register(command) { + const normalized = normalizeCommand(command); + registry.set(normalized.id, normalized); + emit('commands-changed', { commandId: normalized.id }); + return () => unregister(normalized.id); + } + + function unregister(id) { + const removed = registry.delete(String(id)); + if (removed) emit('commands-changed', { commandId: String(id) }); + return removed; + } + + function list({ query = '', group = null } = {}) { + const needle = String(query).trim().toLocaleLowerCase('de-DE'); + return [...registry.values()].filter(command => { + if (group && command.group !== group) return false; + if (!needle) return true; + const haystack = [command.title, command.description, command.group, ...command.keywords].join(' ').toLocaleLowerCase('de-DE'); + let cursor = 0; + for (const character of needle) { + cursor = haystack.indexOf(character, cursor); + if (cursor < 0) return false; + cursor += 1; + } + return true; + }); + } + + function getActiveView() { + return document.querySelector('.sidebar-nav .nav-item.active[data-tab]')?.dataset.tab + || document.querySelector('.tab-content.active')?.id + || 'dashboard'; + } + + function canOpen(view) { + const item = document.querySelector(`.sidebar-nav .nav-item[data-tab="${CSS.escape(view)}"]`); + return Boolean(item && !item.classList.contains('hidden')); + } + + function openView(view) { + const item = document.querySelector(`.sidebar-nav .nav-item[data-tab="${CSS.escape(view)}"]`); + if (!item || item.classList.contains('hidden')) return false; + item.closest('details.ux-nav-section')?.setAttribute('open', ''); + item.click(); + return true; + } + + function recordRecent(view) { + if (!view || !VIEW_META[view]) return; + recents = [view, ...recents.filter(item => item !== view)].slice(0, MAX_RECENTS); + writeList(RECENTS_KEY, recents); + renderNavigationChrome(); + emit('navigation-changed', { view, recents: [...recents] }); + } + + function toggleFavorite(commandId) { + const id = String(commandId); + favorites = favorites.includes(id) ? favorites.filter(item => item !== id) : [id, ...favorites]; + writeList(FAVORITES_KEY, favorites); + renderNavigationChrome(); + emit('favorites-changed', { commandId: id, favorites: [...favorites] }); + return favorites.includes(id); + } + + function execute(id, context = {}) { + const command = registry.get(String(id)); + if (!command) return false; + if (command.view && !canOpen(command.view)) return false; + const result = command.execute ? command.execute(context) : (command.view ? openView(command.view) : false); + if (result !== false && command.view) recordRecent(command.view); + emit('command-executed', { commandId: command.id, view: command.view }); + return result !== false; + } + + function createButton(className, label, title) { + const button = document.createElement('button'); + button.type = 'button'; + button.className = className; + button.textContent = label; + button.title = title; + return button; + } + + function ensureChrome() { + const topbar = document.getElementById('topbar'); + if (!topbar || document.getElementById('ux-command-platform-chrome')) return; + const chrome = document.createElement('div'); + chrome.id = 'ux-command-platform-chrome'; + chrome.className = 'ux-command-platform-chrome'; + + const breadcrumb = document.createElement('nav'); + breadcrumb.id = 'ux-command-breadcrumb'; + breadcrumb.className = 'ux-command-breadcrumb'; + breadcrumb.setAttribute('aria-label', 'Brotkrümelnavigation'); + + const favoriteButton = createButton('ux-command-favorite-toggle', '☆', 'Aktuellen Bereich zu Favoriten hinzufügen'); + favoriteButton.id = 'ux-command-favorite-toggle'; + favoriteButton.setAttribute('aria-pressed', 'false'); + + const drawerButton = createButton('ux-command-drawer-toggle', 'Favoriten', 'Favoriten und zuletzt geöffnete Bereiche anzeigen'); + drawerButton.id = 'ux-command-drawer-toggle'; + drawerButton.setAttribute('aria-expanded', 'false'); + + const drawer = document.createElement('section'); + drawer.id = 'ux-command-drawer'; + drawer.className = 'ux-command-drawer hidden'; + drawer.setAttribute('aria-label', 'Favoriten und Verlauf'); + + favoriteButton.addEventListener('click', () => toggleFavorite(`view:${getActiveView()}`)); + drawerButton.addEventListener('click', () => { + const open = drawer.classList.toggle('hidden') === false; + drawerButton.setAttribute('aria-expanded', open ? 'true' : 'false'); + if (open) drawer.querySelector('button')?.focus({ preventScroll: true }); + }); + document.addEventListener('keydown', event => { + if (event.key === 'Escape' && !drawer.classList.contains('hidden')) { + drawer.classList.add('hidden'); + drawerButton.setAttribute('aria-expanded', 'false'); + drawerButton.focus({ preventScroll: true }); + } + }); + + chrome.append(breadcrumb, favoriteButton, drawerButton, drawer); + topbar.insertAdjacentElement('afterend', chrome); + } + + function renderNavigationChrome() { + ensureChrome(); + const view = getActiveView(); + const meta = VIEW_META[view] || { title: view, group: 'Vendoo' }; + const breadcrumb = document.getElementById('ux-command-breadcrumb'); + const favoriteButton = document.getElementById('ux-command-favorite-toggle'); + const drawer = document.getElementById('ux-command-drawer'); + if (!breadcrumb || !favoriteButton || !drawer) return; + + breadcrumb.replaceChildren(); + const home = createButton('ux-command-crumb', 'Vendoo', 'Zur Übersicht'); + home.addEventListener('click', () => execute('view:dashboard')); + const separator = document.createElement('span'); + separator.className = 'ux-command-crumb-separator'; + separator.textContent = '›'; + separator.setAttribute('aria-hidden', 'true'); + const current = document.createElement('span'); + current.className = 'ux-command-crumb-current'; + current.textContent = `${meta.group} · ${meta.title}`; + breadcrumb.append(home, separator, current); + + const commandId = `view:${view}`; + const favorite = favorites.includes(commandId); + favoriteButton.textContent = favorite ? '★' : '☆'; + favoriteButton.setAttribute('aria-pressed', favorite ? 'true' : 'false'); + favoriteButton.title = favorite ? 'Aus Favoriten entfernen' : 'Aktuellen Bereich zu Favoriten hinzufügen'; + + drawer.replaceChildren(); + const appendSection = (title, ids) => { + const section = document.createElement('div'); + section.className = 'ux-command-drawer-section'; + const heading = document.createElement('strong'); + heading.textContent = title; + section.append(heading); + const valid = ids.map(id => id.startsWith('view:') ? id : `view:${id}`).filter(id => registry.has(id)); + if (!valid.length) { + const empty = document.createElement('span'); + empty.className = 'ux-command-drawer-empty'; + empty.textContent = title === 'Favoriten' ? 'Noch keine Favoriten angeheftet.' : 'Noch keine Bereiche geöffnet.'; + section.append(empty); + } else { + valid.forEach(id => { + const command = registry.get(id); + const button = createButton('ux-command-drawer-item', `${command.icon} ${command.title}`, command.description || command.title); + button.addEventListener('click', () => { + execute(id); + drawer.classList.add('hidden'); + document.getElementById('ux-command-drawer-toggle')?.setAttribute('aria-expanded', 'false'); + }); + section.append(button); + }); + } + drawer.append(section); + }; + appendSection('Favoriten', favorites); + appendSection('Zuletzt geöffnet', recents); + } + + function registerCoreNavigation() { + Object.entries(VIEW_META).forEach(([view, meta]) => { + register({ + id: `view:${view}`, + title: meta.title, + description: `${meta.group} öffnen`, + group: meta.group, + icon: meta.icon, + keywords: meta.keywords, + kind: 'navigation', + view, + execute: () => openView(view), + }); + }); + } + + function observeNavigation() { + const nav = document.querySelector('.sidebar-nav'); + if (!nav) return; + let current = getActiveView(); + recordRecent(current); + const observer = new MutationObserver(() => { + const next = getActiveView(); + if (!next || next === current) return; + current = next; + recordRecent(next); + }); + nav.querySelectorAll('.nav-item[data-tab]').forEach(item => observer.observe(item, { attributes: true, attributeFilter: ['class'] })); + window.addEventListener('beforeunload', () => observer.disconnect(), { once: true }); + } + + window.VendooCommands = Object.freeze({ + register, + unregister, + list, + get: id => registry.get(String(id)) || null, + execute, + toggleFavorite, + isFavorite: id => favorites.includes(String(id)), + get favorites() { return [...favorites]; }, + subscribe(listener) { listeners.add(listener); return () => listeners.delete(listener); }, + }); + + window.VendooNavigation = Object.freeze({ + get currentView() { return getActiveView(); }, + get recents() { return [...recents]; }, + open: openView, + back: () => history.back(), + forward: () => history.forward(), + home: () => execute('view:dashboard'), + }); + + registerCoreNavigation(); + ensureChrome(); + renderNavigationChrome(); + observeNavigation(); +})(); \ No newline at end of file