WIP: Vendoo 1.44.0 – UX Foundation & Guided Workspaces #29

Draft
Masterluke77 wants to merge 284 commits from release/1.44.0-ux-foundation-production-update-v1 into main
Showing only changes of commit f6299b6b04 - Show all commits
+193
View File
@@ -0,0 +1,193 @@
(() => {
'use strict';
const moduleRoot = document.getElementById('admin-modules-panel');
const extensionRoot = document.getElementById('extensions');
if ((!moduleRoot && !extensionRoot) || document.documentElement.dataset.uxModuleExtensionReady === '1') return;
document.documentElement.dataset.uxModuleExtensionReady = '1';
const storageKey = 'vendoo_module_extension_workspace_v1';
const state = { moduleFilter: 'all', extensionFilter: 'all' };
try { Object.assign(state, JSON.parse(localStorage.getItem(storageKey) || '{}')); } catch {}
const persist = () => { try { localStorage.setItem(storageKey, JSON.stringify(state)); } catch {} };
const el = (tag, className, text) => {
const node = document.createElement(tag);
if (className) node.className = className;
if (text !== undefined) node.textContent = text;
return node;
};
function makeFilter(label, value, group, active) {
const button = el('button', 'ux-module-filter', label);
button.type = 'button';
button.dataset.uxModuleFilter = value;
button.dataset.uxModuleGroup = group;
button.setAttribute('aria-pressed', active ? 'true' : 'false');
return button;
}
function text(node) {
return String(node?.textContent || '').toLocaleLowerCase('de-DE');
}
function setupModules() {
if (!moduleRoot || moduleRoot.dataset.uxWorkspaceReady === '1') return;
moduleRoot.dataset.uxWorkspaceReady = '1';
const summary = moduleRoot.querySelector('#module-manager-summary');
const safety = moduleRoot.querySelector('#module-manager-safety');
const list = moduleRoot.querySelector('#module-manager-list');
const search = moduleRoot.querySelector('#module-manager-search');
const file = moduleRoot.querySelector('#module-manager-file');
const install = moduleRoot.querySelector('#module-manager-install');
const refresh = moduleRoot.querySelector('#module-manager-refresh');
if (!list) return;
const command = el('section', 'ux-module-command');
command.setAttribute('aria-label', 'Modulverwaltung');
const copy = el('div', 'ux-module-command-copy');
copy.append(el('span', 'ux-module-kicker', 'Systemerweiterungen'), el('h3', '', 'Module kontrolliert verwalten'), el('p', '', 'Abhängigkeiten, Vertrauensstatus und Betriebszustand zuerst prüfen. Änderungen bleiben bewusste Einzelaktionen.'));
const health = el('div', 'ux-module-health');
command.append(copy, health);
const filters = el('div', 'ux-module-filterbar');
[['Alle', 'all'], ['Aktiv', 'active'], ['Deaktiviert', 'disabled'], ['Installiert', 'external'], ['Probleme', 'problems']]
.forEach(([label, value]) => filters.append(makeFilter(label, value, 'modules', state.moduleFilter === value)));
const installDetails = el('details', 'ux-module-install');
const installSummary = el('summary');
installSummary.append(el('strong', '', 'Modulpaket installieren'), el('small', '', 'Nur geprüfte .vmod-Pakete'));
const installBody = el('div', 'ux-module-install-body');
if (file) installBody.append(file);
if (install) installBody.append(install);
installDetails.append(installSummary, installBody);
const tools = el('div', 'ux-module-tools');
if (search) tools.append(search);
if (refresh) tools.append(refresh);
tools.append(installDetails);
const anchor = summary || safety || list;
anchor.insertAdjacentElement('beforebegin', command);
command.insertAdjacentElement('afterend', filters);
filters.insertAdjacentElement('afterend', tools);
function classify(card) {
const value = text(card);
return {
active: value.includes('aktiv') && !value.includes('deaktiviert'),
disabled: value.includes('deaktiviert') || card.classList.contains('is-disabled'),
external: value.includes('installiert'),
problems: value.includes('fehler') || value.includes('ungültig') || value.includes('invalid') || card.classList.contains('is-invalid') || Boolean(card.querySelector('.module-error')),
};
}
function refresh() {
const cards = [...list.querySelectorAll('.module-card')];
const counts = { total: cards.length, active: 0, disabled: 0, external: 0, problems: 0 };
cards.forEach(card => {
const flags = classify(card);
Object.keys(flags).forEach(key => { if (flags[key]) counts[key] += 1; });
const visible = state.moduleFilter === 'all' || flags[state.moduleFilter];
card.hidden = !visible;
card.tabIndex = visible ? 0 : -1;
});
health.replaceChildren();
[['Gesamt', counts.total], ['Aktiv', counts.active], ['Installiert', counts.external], ['Probleme', counts.problems]]
.forEach(([label, value]) => {
const item = el('div', 'ux-module-health-item');
item.append(el('small', '', label), el('strong', '', String(value)));
if (label === 'Probleme' && value > 0) item.dataset.tone = 'danger';
health.append(item);
});
filters.querySelectorAll('[data-ux-module-filter]').forEach(button => button.setAttribute('aria-pressed', button.dataset.uxModuleFilter === state.moduleFilter ? 'true' : 'false'));
}
filters.addEventListener('click', event => {
const button = event.target.closest('[data-ux-module-filter]');
if (!button) return;
state.moduleFilter = button.dataset.uxModuleFilter;
persist();
refresh();
list.querySelector('.module-card:not([hidden])')?.focus({ preventScroll: true });
});
list.addEventListener('keydown', event => {
const card = event.target.closest('.module-card');
if (!card || !['Enter', ' '].includes(event.key)) return;
if (event.target.closest('button, a, input, select')) return;
event.preventDefault();
card.querySelector('.module-card-actions button:not(:disabled), .module-card-actions a')?.focus();
});
const observer = new MutationObserver(() => requestAnimationFrame(refresh));
observer.observe(list, { childList: true });
refresh();
window.addEventListener('beforeunload', () => observer.disconnect(), { once: true });
}
function setupExtensions() {
if (!extensionRoot || extensionRoot.dataset.uxWorkspaceReady === '1') return;
extensionRoot.dataset.uxWorkspaceReady = '1';
const grid = extensionRoot.querySelector('#extensions-grid');
const activeBrowser = extensionRoot.querySelector('#extensions-active-browser');
const prepare = extensionRoot.querySelector('#extensions-prepare-active');
if (!grid) return;
const board = el('section', 'ux-extension-board');
const copy = el('div');
copy.append(el('span', 'ux-module-kicker', 'Browser-Verbindungen'), el('h3', '', 'Extensions vorbereiten und prüfen'), el('p', '', 'Vendoo bereitet Pakete und Pfade vor. Die endgültige Browserfreigabe bleibt immer manuell.'));
const status = el('div', 'ux-extension-status');
board.append(copy, status);
const filters = el('div', 'ux-module-filterbar ux-extension-filterbar');
[['Alle', 'all'], ['Bereit', 'ready'], ['Verbunden', 'connected'], ['Aktion nötig', 'attention']]
.forEach(([label, value]) => filters.append(makeFilter(label, value, 'extensions', state.extensionFilter === value)));
grid.insertAdjacentElement('beforebegin', filters);
filters.insertAdjacentElement('beforebegin', board);
if (activeBrowser) status.append(activeBrowser);
if (prepare) status.append(prepare);
function classify(card) {
const value = text(card);
return {
connected: value.includes('verbunden') || value.includes('aktiv'),
ready: value.includes('bereit') || value.includes('vorbereitet') || value.includes('installiert'),
attention: value.includes('nicht') || value.includes('fehler') || value.includes('aktion') || value.includes('installieren'),
};
}
function refresh() {
const cards = [...grid.children].filter(node => node.nodeType === 1);
let connected = 0;
let attention = 0;
cards.forEach(card => {
const flags = classify(card);
if (flags.connected) connected += 1;
if (flags.attention) attention += 1;
card.hidden = !(state.extensionFilter === 'all' || flags[state.extensionFilter]);
});
board.dataset.connected = String(connected);
board.dataset.attention = String(attention);
filters.querySelectorAll('[data-ux-module-filter]').forEach(button => button.setAttribute('aria-pressed', button.dataset.uxModuleFilter === state.extensionFilter ? 'true' : 'false'));
}
filters.addEventListener('click', event => {
const button = event.target.closest('[data-ux-module-filter]');
if (!button) return;
state.extensionFilter = button.dataset.uxModuleFilter;
persist();
refresh();
});
const observer = new MutationObserver(() => requestAnimationFrame(refresh));
observer.observe(grid, { childList: true, subtree: true, characterData: true });
refresh();
window.addEventListener('beforeunload', () => observer.disconnect(), { once: true });
}
setupModules();
setupExtensions();
})();