feat(ux): Navigation Engine und Command Registry ergänzen

This commit is contained in:
Masterluke77
2026-07-13 14:20:06 +02:00
parent 79513ce5af
commit 0af3330a01
+315
View File
@@ -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();
})();