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