feat(ui): polish accessibility and German interface for v2.0.2
Improve the account empty state and modal ergonomics, add complete keyboard focus handling and accessible labels, localize the core upload UI, and align the product version with the v2.0.2 release.
This commit is contained in:
parent
ae8d82e42b
commit
3709b18930
4
package-lock.json
generated
4
package-lock.json
generated
@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "multi-hoster-uploader",
|
||||
"version": "2.0.1",
|
||||
"version": "2.0.2",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "multi-hoster-uploader",
|
||||
"version": "2.0.1",
|
||||
"version": "2.0.2",
|
||||
"dependencies": {
|
||||
"chokidar": "^3.6.0",
|
||||
"undici": "^7.29.0",
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "multi-hoster-uploader",
|
||||
"version": "2.0.1",
|
||||
"version": "2.0.2",
|
||||
"description": "Upload files to doodstream, voe, vidmoly, byse simultaneously",
|
||||
"main": "main.js",
|
||||
"scripts": {
|
||||
|
||||
110
renderer/app.js
110
renderer/app.js
@ -353,10 +353,14 @@ function _isHistoryTabActive() {
|
||||
if (!tab || tab === activeTab) return;
|
||||
if (activeTab) {
|
||||
activeTab.classList.remove('active');
|
||||
activeTab.setAttribute('aria-selected', 'false');
|
||||
activeTab.tabIndex = -1;
|
||||
const prevView = viewsById[`${activeTab.dataset.view}-view`];
|
||||
if (prevView) prevView.classList.remove('active');
|
||||
}
|
||||
tab.classList.add('active');
|
||||
tab.setAttribute('aria-selected', 'true');
|
||||
tab.tabIndex = 0;
|
||||
const nextView = viewsById[`${tab.dataset.view}-view`];
|
||||
if (nextView) nextView.classList.add('active');
|
||||
activeTab = tab;
|
||||
@ -368,6 +372,18 @@ function _isHistoryTabActive() {
|
||||
const tabBar = tabs[0] && tabs[0].parentElement;
|
||||
if (tabBar) {
|
||||
tabBar.addEventListener('click', (e) => handle(e.target));
|
||||
tabBar.addEventListener('keydown', (e) => {
|
||||
if (!['ArrowLeft', 'ArrowRight', 'Home', 'End'].includes(e.key)) return;
|
||||
e.preventDefault();
|
||||
const current = Math.max(0, tabs.indexOf(activeTab));
|
||||
const next = e.key === 'Home'
|
||||
? 0
|
||||
: e.key === 'End'
|
||||
? tabs.length - 1
|
||||
: (current + (e.key === 'ArrowRight' ? 1 : -1) + tabs.length) % tabs.length;
|
||||
tabs[next].focus();
|
||||
handle(tabs[next]);
|
||||
});
|
||||
} else {
|
||||
// Fallback: bind per-tab if somehow no common parent
|
||||
tabs.forEach(t => t.addEventListener('click', () => handle(t)));
|
||||
@ -1920,9 +1936,20 @@ document.addEventListener('click', (e) => {
|
||||
if (!e.target.closest('.context-menu')) hideContextMenu();
|
||||
});
|
||||
document.addEventListener('keydown', (e) => {
|
||||
const accountModal = document.getElementById('accountModal');
|
||||
if (e.key === 'Tab' && accountModal && accountModal.style.display !== 'none') {
|
||||
const focusable = Array.from(accountModal.querySelectorAll('button:not([disabled]), input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])'));
|
||||
const first = focusable[0];
|
||||
const last = focusable[focusable.length - 1];
|
||||
if (first && last && ((!e.shiftKey && document.activeElement === last) || (e.shiftKey && document.activeElement === first))) {
|
||||
e.preventDefault();
|
||||
(e.shiftKey ? last : first).focus();
|
||||
}
|
||||
}
|
||||
if (e.key === 'Escape') {
|
||||
hideContextMenu();
|
||||
cancelHosterModal();
|
||||
if (accountModal && accountModal.style.display !== 'none') closeAccountModal();
|
||||
}
|
||||
if (e.target.closest('input, textarea, select')) return;
|
||||
const activeView = document.querySelector('.view.active');
|
||||
@ -2951,12 +2978,12 @@ function updateStatusBar() {
|
||||
const totalSize = Math.max(stats.totalSize, _sessionTotalBytes);
|
||||
document.getElementById('sbTotal').textContent = `${formatSize(uploadedSize)} / ${formatSize(totalSize)}`;
|
||||
document.getElementById('sbEta').textContent = `ETA ${etaSeconds > 0 ? formatTime(etaSeconds) : '--:--'}`;
|
||||
document.getElementById('sbConnections').textContent = `Connections: ${lastUploadStats.activeJobs || 0}`;
|
||||
document.getElementById('sbQueueCount').textContent = `Total: ${stats.total}`;
|
||||
document.getElementById('sbRemainingCount').textContent = `Remaining: ${stats.remaining}`;
|
||||
document.getElementById('sbInProgressCount').textContent = `In Progress: ${stats.inProgress}`;
|
||||
document.getElementById('sbDoneCount').textContent = `Done: ${_sessionDoneCount}`;
|
||||
document.getElementById('sbErrorCount').textContent = `Error: ${_sessionErrorCount}`;
|
||||
document.getElementById('sbConnections').textContent = `Verbindungen ${lastUploadStats.activeJobs || 0}`;
|
||||
document.getElementById('sbQueueCount').textContent = `Gesamt ${stats.total}`;
|
||||
document.getElementById('sbRemainingCount').textContent = `Verbleibend ${stats.remaining}`;
|
||||
document.getElementById('sbInProgressCount').textContent = `Läuft ${stats.inProgress}`;
|
||||
document.getElementById('sbDoneCount').textContent = `Fertig ${_sessionDoneCount}`;
|
||||
document.getElementById('sbErrorCount').textContent = `Fehler ${_sessionErrorCount}`;
|
||||
}
|
||||
|
||||
// --- Health Check ---
|
||||
@ -3864,8 +3891,10 @@ function renderAccounts() {
|
||||
if (allAccounts.length === 0) {
|
||||
container.innerHTML = `
|
||||
<div class="accounts-empty">
|
||||
<p>Keine Accounts vorhanden</p>
|
||||
<span class="hint">Klicke auf "Account hinzufügen", um einen Hoster einzurichten.</span>
|
||||
<div class="accounts-empty-icon" aria-hidden="true">+</div>
|
||||
<h3>Noch keine Accounts</h3>
|
||||
<p>Füge deinen ersten Hoster-Account hinzu. Die Zugangsdaten werden vor dem Speichern geprüft.</p>
|
||||
<button class="btn btn-primary" type="button" data-account-empty-add>Ersten Account hinzufügen</button>
|
||||
</div>`;
|
||||
if (footer) footer.style.display = 'none';
|
||||
if (!_accountListenersBound) bindAccountListeners(container);
|
||||
@ -4106,6 +4135,7 @@ function bindAccountListeners(container) {
|
||||
}
|
||||
const btn = e.target.closest('button');
|
||||
if (!btn) return;
|
||||
if (btn.hasAttribute('data-account-empty-add')) return openAccountModal(null);
|
||||
if (btn.dataset.accountToggle) return toggleAccount(btn.dataset.accountToggle);
|
||||
if (btn.dataset.accountEdit) return openAccountModal(btn.dataset.accountEdit);
|
||||
if (btn.dataset.accountDelete) return openDeleteAccountModal(btn.dataset.accountDelete);
|
||||
@ -4240,25 +4270,42 @@ function getCredsFieldsHtml(authType, account, hoster) {
|
||||
};
|
||||
return `
|
||||
<div class="settings-row">
|
||||
<label>${escapeHtml(fld.label)}</label>
|
||||
<input type="${fld.inputType}" class="key-input" id="accField_username" value="${escapeAttr(account.username || '')}" placeholder="${escapeAttr(fld.placeholder)}">
|
||||
<label for="accField_username">${escapeHtml(fld.label)}</label>
|
||||
<input type="${fld.inputType}" class="key-input" id="accField_username" name="username" autocomplete="username" spellcheck="false" value="${escapeAttr(account.username || '')}" placeholder="${escapeAttr(fld.placeholder)}">
|
||||
</div>
|
||||
<div class="settings-row">
|
||||
<label>Passwort</label>
|
||||
<input type="password" class="key-input" id="accField_password" value="${escapeAttr(account.password || '')}" placeholder="Passwort">
|
||||
<button class="toggle-vis" type="button" title="Anzeigen">👁</button>
|
||||
<label for="accField_password">Passwort</label>
|
||||
<input type="password" class="key-input" id="accField_password" name="password" autocomplete="current-password" value="${escapeAttr(account.password || '')}" placeholder="Passwort">
|
||||
<button class="toggle-vis" type="button" title="Passwort anzeigen" aria-label="Passwort anzeigen" aria-pressed="false">👁</button>
|
||||
</div>`;
|
||||
}
|
||||
// API key
|
||||
return `
|
||||
<div class="settings-row">
|
||||
<label>API Key</label>
|
||||
<input type="password" class="key-input" id="accField_apiKey" value="${escapeAttr(account.apiKey || '')}" placeholder="API Key">
|
||||
<button class="toggle-vis" type="button" title="Anzeigen">👁</button>
|
||||
<label for="accField_apiKey">API-Key</label>
|
||||
<input type="password" class="key-input" id="accField_apiKey" name="apiKey" autocomplete="off" spellcheck="false" value="${escapeAttr(account.apiKey || '')}" placeholder="API-Key">
|
||||
<button class="toggle-vis" type="button" title="API-Key anzeigen" aria-label="API-Key anzeigen" aria-pressed="false">👁</button>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
function wireCredentialVisibilityButtons(container) {
|
||||
container.querySelectorAll('.toggle-vis').forEach(btn => {
|
||||
btn.addEventListener('click', () => {
|
||||
const input = btn.previousElementSibling;
|
||||
const visible = input.type === 'password';
|
||||
const fieldName = input.id === 'accField_apiKey' ? 'API-Key' : 'Passwort';
|
||||
input.type = visible ? 'text' : 'password';
|
||||
btn.setAttribute('aria-pressed', String(visible));
|
||||
btn.setAttribute('aria-label', `${fieldName} ${visible ? 'verbergen' : 'anzeigen'}`);
|
||||
btn.title = `${fieldName} ${visible ? 'verbergen' : 'anzeigen'}`;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
let _accountModalReturnFocus = null;
|
||||
|
||||
function openAccountModal(editAccountId) {
|
||||
_accountModalReturnFocus = document.activeElement instanceof HTMLElement ? document.activeElement : null;
|
||||
editingAccountId = editAccountId || null;
|
||||
_resetAccountModalState();
|
||||
const modal = document.getElementById('accountModal');
|
||||
@ -4298,17 +4345,17 @@ function openAccountModal(editAccountId) {
|
||||
credsContainer.innerHTML = getCredsFieldsHtml(firstOpt.authType, {}, firstOpt.value);
|
||||
}
|
||||
|
||||
// Toggle visibility buttons
|
||||
credsContainer.querySelectorAll('.toggle-vis').forEach(btn => {
|
||||
btn.addEventListener('click', () => {
|
||||
const input = btn.previousElementSibling;
|
||||
input.type = input.type === 'password' ? 'text' : 'password';
|
||||
});
|
||||
});
|
||||
wireCredentialVisibilityButtons(credsContainer);
|
||||
|
||||
_wireCredFieldInvalidation();
|
||||
|
||||
modal.style.display = 'flex';
|
||||
requestAnimationFrame(() => {
|
||||
const firstControl = editingAccountId
|
||||
? document.getElementById('accField_label')
|
||||
: hosterSelect;
|
||||
if (firstControl) firstControl.focus();
|
||||
});
|
||||
}
|
||||
|
||||
function closeAccountModal() {
|
||||
@ -4316,6 +4363,12 @@ function closeAccountModal() {
|
||||
_hideOtpField();
|
||||
editingAccountId = null;
|
||||
_resetAccountModalState();
|
||||
const returnFocus = _accountModalReturnFocus;
|
||||
_accountModalReturnFocus = null;
|
||||
const focusTarget = returnFocus && returnFocus.isConnected
|
||||
? returnFocus
|
||||
: document.getElementById('addAccountBtn');
|
||||
if (focusTarget) focusTarget.focus();
|
||||
}
|
||||
|
||||
function openDeleteAccountModal(accountId) {
|
||||
@ -4879,7 +4932,7 @@ function renderHistoryTable(container) {
|
||||
};
|
||||
|
||||
container.innerHTML = `<table class="results-table history-table"><thead><tr>
|
||||
${headerCell('date', 'Date')}${headerCell('filename', 'Filename')}${headerCell('host', 'Host')}${headerCell('link', 'Link')}
|
||||
${headerCell('date', 'Datum')}${headerCell('filename', 'Dateiname')}${headerCell('host', 'Hoster')}${headerCell('link', 'Link')}
|
||||
</tr></thead><tbody id="historyBody"></tbody></table>`;
|
||||
|
||||
if (!_historyListenersBound) {
|
||||
@ -5136,12 +5189,7 @@ function setupListeners() {
|
||||
const authType = opt ? opt.authType : 'login';
|
||||
const credsContainer = document.getElementById('accountCredsFields');
|
||||
credsContainer.innerHTML = getCredsFieldsHtml(authType, {}, e.target.value);
|
||||
credsContainer.querySelectorAll('.toggle-vis').forEach(btn => {
|
||||
btn.addEventListener('click', () => {
|
||||
const input = btn.previousElementSibling;
|
||||
input.type = input.type === 'password' ? 'text' : 'password';
|
||||
});
|
||||
});
|
||||
wireCredentialVisibilityButtons(credsContainer);
|
||||
_wireCredFieldInvalidation();
|
||||
});
|
||||
|
||||
@ -5185,7 +5233,7 @@ function showUpdateBanner(info) {
|
||||
function handleUpdateProgress(data) {
|
||||
const msg = document.getElementById('updateMessage');
|
||||
if (!msg) return;
|
||||
if (data.stage === 'downloading') msg.textContent = `Downloading... ${data.percent || 0}%`;
|
||||
if (data.stage === 'downloading') msg.textContent = `Wird heruntergeladen… ${data.percent || 0}%`;
|
||||
else if (data.stage === 'verifying') msg.textContent = 'Verifiziere...';
|
||||
else if (data.stage === 'launching') msg.textContent = 'Setup wird gestartet...';
|
||||
else if (data.stage === 'done') msg.textContent = 'Update installiert. App wird neu gestartet...';
|
||||
|
||||
@ -66,11 +66,11 @@
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<nav class="tab-bar">
|
||||
<button class="tab active" data-view="upload">Upload</button>
|
||||
<button class="tab" data-view="accounts">Accounts</button>
|
||||
<button class="tab" data-view="settings">Einstellungen</button>
|
||||
<button class="tab" data-view="history">Verlauf</button>
|
||||
<nav class="tab-bar" role="tablist" aria-label="Hauptbereiche">
|
||||
<button class="tab active" id="upload-tab" role="tab" aria-selected="true" aria-controls="upload-view" tabindex="0" data-view="upload">Upload</button>
|
||||
<button class="tab" id="accounts-tab" role="tab" aria-selected="false" aria-controls="accounts-view" tabindex="-1" data-view="accounts">Accounts</button>
|
||||
<button class="tab" id="settings-tab" role="tab" aria-selected="false" aria-controls="settings-view" tabindex="-1" data-view="settings">Einstellungen</button>
|
||||
<button class="tab" id="history-tab" role="tab" aria-selected="false" aria-controls="history-view" tabindex="-1" data-view="history">Verlauf</button>
|
||||
<span class="version-label" id="versionLabel"></span>
|
||||
</nav>
|
||||
|
||||
@ -80,7 +80,7 @@
|
||||
<button class="btn btn-sm btn-secondary" id="dismissUpdateBtn">×</button>
|
||||
</div>
|
||||
|
||||
<div id="upload-view" class="view active">
|
||||
<div id="upload-view" class="view active" role="tabpanel" aria-labelledby="upload-tab">
|
||||
<div class="upload-toolbar">
|
||||
<div class="toolbar-left">
|
||||
<span class="hoster-summary" id="hosterSummary" style="display:none"></span>
|
||||
@ -99,35 +99,35 @@
|
||||
|
||||
<div class="queue-shell" id="queueShell" style="display:none">
|
||||
<div class="queue-command-bar" id="queueCommandBar">
|
||||
<button class="toolbar-btn" id="startUploadBtn" title="Start all" disabled>
|
||||
<svg width="16" height="16" viewBox="0 0 16 16"><path d="M4 2l10 6-10 6z" fill="#4caf50"/></svg>
|
||||
<button class="toolbar-btn" id="startUploadBtn" title="Alle Uploads starten" aria-label="Alle Uploads starten" disabled>
|
||||
<svg aria-hidden="true" width="16" height="16" viewBox="0 0 16 16"><path d="M4 2l10 6-10 6z" fill="#4caf50"/></svg>
|
||||
</button>
|
||||
<button class="toolbar-btn" id="startSelectedBtn" title="Start selected" disabled>
|
||||
<svg width="16" height="16" viewBox="0 0 16 16"><path d="M6 3l8 5-8 5z" fill="#4caf50"/><rect x="1" y="3" width="3" height="10" rx="0.5" fill="#4caf50"/></svg>
|
||||
<button class="toolbar-btn" id="startSelectedBtn" title="Ausgewählte Uploads starten" aria-label="Ausgewählte Uploads starten" disabled>
|
||||
<svg aria-hidden="true" width="16" height="16" viewBox="0 0 16 16"><path d="M6 3l8 5-8 5z" fill="#4caf50"/><rect x="1" y="3" width="3" height="10" rx="0.5" fill="#4caf50"/></svg>
|
||||
</button>
|
||||
<button class="toolbar-btn" id="reuploadSelectedBtn" title="Reupload selected file">
|
||||
<svg width="16" height="16" viewBox="0 0 16 16"><path d="M8 1a7 7 0 0 0-5 2.1V1H2v4h4V4H3.7A5.5 5.5 0 1 1 2.5 8H1a7 7 0 1 0 7-7z" fill="#4caf50"/></svg>
|
||||
<button class="toolbar-btn" id="reuploadSelectedBtn" title="Ausgewählte Datei erneut hochladen" aria-label="Ausgewählte Datei erneut hochladen">
|
||||
<svg aria-hidden="true" width="16" height="16" viewBox="0 0 16 16"><path d="M8 1a7 7 0 0 0-5 2.1V1H2v4h4V4H3.7A5.5 5.5 0 1 1 2.5 8H1a7 7 0 1 0 7-7z" fill="#4caf50"/></svg>
|
||||
</button>
|
||||
<button class="toolbar-btn" id="abortSelectedBtn" title="Abort selected file">
|
||||
<svg width="16" height="16" viewBox="0 0 16 16"><rect x="3" y="3" width="10" height="10" rx="1" fill="#e53935"/><path d="M5.5 5.5l5 5M10.5 5.5l-5 5" stroke="#fff" stroke-width="1.5" stroke-linecap="round"/></svg>
|
||||
<button class="toolbar-btn" id="abortSelectedBtn" title="Ausgewählten Upload abbrechen" aria-label="Ausgewählten Upload abbrechen">
|
||||
<svg aria-hidden="true" width="16" height="16" viewBox="0 0 16 16"><rect x="3" y="3" width="10" height="10" rx="1" fill="#e53935"/><path d="M5.5 5.5l5 5M10.5 5.5l-5 5" stroke="#fff" stroke-width="1.5" stroke-linecap="round"/></svg>
|
||||
</button>
|
||||
<button class="toolbar-btn" id="finishStopBtn" title="Finish Uploads in Progress and Stop">
|
||||
<svg width="16" height="16" viewBox="0 0 16 16"><path d="M2 8l4 4 8-8" stroke="#4caf50" stroke-width="2" fill="none" stroke-linecap="round" stroke-linejoin="round"/><rect x="11" y="9" width="5" height="5" rx="1" fill="#e53935"/></svg>
|
||||
<button class="toolbar-btn" id="finishStopBtn" title="Aktive Uploads beenden und stoppen" aria-label="Aktive Uploads beenden und stoppen">
|
||||
<svg aria-hidden="true" width="16" height="16" viewBox="0 0 16 16"><path d="M2 8l4 4 8-8" stroke="#4caf50" stroke-width="2" fill="none" stroke-linecap="round" stroke-linejoin="round"/><rect x="11" y="9" width="5" height="5" rx="1" fill="#e53935"/></svg>
|
||||
</button>
|
||||
<button class="toolbar-btn toolbar-btn-danger" id="abortAllBtn" title="Abort all Downloads">
|
||||
<svg width="16" height="16" viewBox="0 0 16 16"><rect x="1" y="1" width="14" height="14" rx="2" fill="#e53935"/><path d="M4.5 4.5l7 7M11.5 4.5l-7 7" stroke="#fff" stroke-width="2" stroke-linecap="round"/></svg>
|
||||
<button class="toolbar-btn toolbar-btn-danger" id="abortAllBtn" title="Alle Uploads abbrechen" aria-label="Alle Uploads abbrechen">
|
||||
<svg aria-hidden="true" width="16" height="16" viewBox="0 0 16 16"><rect x="1" y="1" width="14" height="14" rx="2" fill="#e53935"/><path d="M4.5 4.5l7 7M11.5 4.5l-7 7" stroke="#fff" stroke-width="2" stroke-linecap="round"/></svg>
|
||||
</button>
|
||||
<span class="toolbar-sep"></span>
|
||||
<button class="toolbar-btn" id="moveTopBtn" title="Move to the top">
|
||||
<button class="toolbar-btn" id="moveTopBtn" title="Ganz nach oben" aria-label="Ganz nach oben">
|
||||
<svg width="16" height="16" viewBox="0 0 16 16"><rect x="4" y="1" width="8" height="2" rx="0.5" fill="#4caf50"/><path d="M8 5l-4 5h8z" fill="#4caf50"/><path d="M8 9l-4 5h8z" fill="#4caf50"/></svg>
|
||||
</button>
|
||||
<button class="toolbar-btn" id="moveUpBtn" title="Move up">
|
||||
<button class="toolbar-btn" id="moveUpBtn" title="Nach oben" aria-label="Nach oben">
|
||||
<svg width="16" height="16" viewBox="0 0 16 16"><path d="M8 2l-5 6h10z" fill="#4caf50"/><rect x="6" y="8" width="4" height="6" rx="0.5" fill="#4caf50"/></svg>
|
||||
</button>
|
||||
<button class="toolbar-btn" id="moveDownBtn" title="Move down">
|
||||
<button class="toolbar-btn" id="moveDownBtn" title="Nach unten" aria-label="Nach unten">
|
||||
<svg width="16" height="16" viewBox="0 0 16 16"><rect x="6" y="2" width="4" height="6" rx="0.5" fill="#4caf50"/><path d="M8 14l-5-6h10z" fill="#4caf50"/></svg>
|
||||
</button>
|
||||
<button class="toolbar-btn" id="moveBottomBtn" title="Move to the bottom">
|
||||
<button class="toolbar-btn" id="moveBottomBtn" title="Ganz nach unten" aria-label="Ganz nach unten">
|
||||
<svg width="16" height="16" viewBox="0 0 16 16"><path d="M8 7l-4-5h8z" fill="#4caf50"/><path d="M8 11l-4-5h8z" fill="#4caf50"/><rect x="4" y="13" width="8" height="2" rx="0.5" fill="#4caf50"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
@ -136,14 +136,14 @@
|
||||
<table class="queue-table" id="queueTable">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="col-filename sortable" data-col="filename" data-sort="filename">Filename<span class="col-resizer"></span></th>
|
||||
<th class="col-size sortable" data-col="size" data-sort="size">Uploaded / Size<span class="col-resizer"></span></th>
|
||||
<th class="col-host sortable" data-col="host" data-sort="host">Host<span class="col-resizer"></span></th>
|
||||
<th class="col-filename sortable" data-col="filename" data-sort="filename">Dateiname<span class="col-resizer"></span></th>
|
||||
<th class="col-size sortable" data-col="size" data-sort="size">Hochgeladen / Größe<span class="col-resizer"></span></th>
|
||||
<th class="col-host sortable" data-col="host" data-sort="host">Hoster<span class="col-resizer"></span></th>
|
||||
<th class="col-status sortable" data-col="status" data-sort="status">Status<span class="col-resizer"></span></th>
|
||||
<th class="col-elapsed" data-col="elapsed">Zeit<span class="col-resizer"></span></th>
|
||||
<th class="col-remaining" data-col="remaining">Rest<span class="col-resizer"></span></th>
|
||||
<th class="col-speed sortable" data-col="speed" data-sort="speed">Speed<span class="col-resizer"></span></th>
|
||||
<th class="col-progress sortable" data-col="progress" data-sort="progress">Progress</th>
|
||||
<th class="col-speed sortable" data-col="speed" data-sort="speed">Geschwindigkeit<span class="col-resizer"></span></th>
|
||||
<th class="col-progress sortable" data-col="progress" data-sort="progress">Fortschritt</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="queueBody"></tbody>
|
||||
@ -168,8 +168,8 @@
|
||||
<div class="recent-files-panel" id="recentFilesPanel">
|
||||
<div class="recent-files-header">
|
||||
<div class="recent-tabs">
|
||||
<button class="recent-tab active" data-panel="filesTab">Files</button>
|
||||
<button class="recent-tab" data-panel="statsTab">Stats</button>
|
||||
<button class="recent-tab active" data-panel="filesTab">Dateien</button>
|
||||
<button class="recent-tab" data-panel="statsTab">Statistik</button>
|
||||
</div>
|
||||
<span class="recent-files-hint" id="recentFilesHint">Zuletzt erzeugte Upload-Links</span>
|
||||
<button class="btn btn-xs btn-secondary" id="exportRecentFilesBtn" title="Alle Zeilen als Datei exportieren (Zeit, Hoster, Link, Dateiname)">Exportieren</button>
|
||||
@ -181,8 +181,8 @@
|
||||
<thead id="recentFilesHead">
|
||||
<tr>
|
||||
<th class="col-date sortable" data-recent-sort="date">Datum<span class="sort-indicator">▼</span></th>
|
||||
<th class="col-filename sortable" data-recent-sort="filename">Filename<span class="sort-indicator">↕</span></th>
|
||||
<th class="col-host sortable" data-recent-sort="host">Host<span class="sort-indicator">↕</span></th>
|
||||
<th class="col-filename sortable" data-recent-sort="filename">Dateiname<span class="sort-indicator">↕</span></th>
|
||||
<th class="col-host sortable" data-recent-sort="host">Hoster<span class="sort-indicator">↕</span></th>
|
||||
<th class="col-link sortable" data-recent-sort="link">Link<span class="sort-indicator">↕</span></th>
|
||||
</tr>
|
||||
</thead>
|
||||
@ -193,24 +193,24 @@
|
||||
<div class="recent-tab-body" id="statsTab">
|
||||
<div class="stats-grid">
|
||||
<div class="stats-col">
|
||||
<h4>Files in queue (count)</h4>
|
||||
<div class="stats-row"><span>total:</span><span id="statQueueTotal">0</span></div>
|
||||
<div class="stats-row"><span>done:</span><span id="statQueueDone">0</span></div>
|
||||
<div class="stats-row"><span>remaining:</span><span id="statQueueRemaining">0</span></div>
|
||||
<div class="stats-row"><span>in progress:</span><span id="statQueueInProgress">0</span></div>
|
||||
<div class="stats-row"><span>error:</span><span id="statQueueError">0</span></div>
|
||||
<h4>Dateien in der Warteschlange</h4>
|
||||
<div class="stats-row"><span>Gesamt:</span><span id="statQueueTotal">0</span></div>
|
||||
<div class="stats-row"><span>Fertig:</span><span id="statQueueDone">0</span></div>
|
||||
<div class="stats-row"><span>Verbleibend:</span><span id="statQueueRemaining">0</span></div>
|
||||
<div class="stats-row"><span>Läuft:</span><span id="statQueueInProgress">0</span></div>
|
||||
<div class="stats-row"><span>Fehler:</span><span id="statQueueError">0</span></div>
|
||||
</div>
|
||||
<div class="stats-col">
|
||||
<h4>File size in queue</h4>
|
||||
<div class="stats-row"><span>total:</span><span id="statSizeTotal">0 B</span></div>
|
||||
<div class="stats-row"><span>remaining:</span><span id="statSizeRemaining">0 B</span></div>
|
||||
<h4>Dateigröße in der Warteschlange</h4>
|
||||
<div class="stats-row"><span>Gesamt:</span><span id="statSizeTotal">0 B</span></div>
|
||||
<div class="stats-row"><span>Verbleibend:</span><span id="statSizeRemaining">0 B</span></div>
|
||||
</div>
|
||||
<div class="stats-col">
|
||||
<h4>Session</h4>
|
||||
<div class="stats-row"><span>Upload speed:</span><span id="statSpeed">0 B/s</span></div>
|
||||
<div class="stats-row"><span>Remaining time:</span><span id="statEta">--:--</span></div>
|
||||
<div class="stats-row"><span>Run time:</span><span id="statRunTime">00:00:00</span></div>
|
||||
<div class="stats-row"><span>Uploaded (this run):</span><span id="statSessionBytes">0 B</span></div>
|
||||
<h4>Sitzung</h4>
|
||||
<div class="stats-row"><span>Upload-Geschwindigkeit:</span><span id="statSpeed">0 B/s</span></div>
|
||||
<div class="stats-row"><span>Restzeit:</span><span id="statEta">--:--</span></div>
|
||||
<div class="stats-row"><span>Laufzeit:</span><span id="statRunTime">00:00:00</span></div>
|
||||
<div class="stats-row"><span>In diesem Lauf hochgeladen:</span><span id="statSessionBytes">0 B</span></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -219,7 +219,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="accounts-view" class="view">
|
||||
<div id="accounts-view" class="view" role="tabpanel" aria-labelledby="accounts-tab">
|
||||
<div class="accounts-container">
|
||||
<div class="accounts-header">
|
||||
<div>
|
||||
@ -232,7 +232,7 @@
|
||||
<input type="checkbox" id="autoHealthCheckToggle" checked>
|
||||
<span>Auto-Check vor Upload</span>
|
||||
</label>
|
||||
<button class="btn btn-primary" id="addAccountBtn">+ Account hinzufügen</button>
|
||||
<button class="btn btn-primary" id="addAccountBtn">Account hinzufügen</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="health-check-results account-health-results" id="healthCheckResults"></div>
|
||||
@ -244,7 +244,7 @@
|
||||
</div>
|
||||
|
||||
<div class="modal-overlay" id="accountModal" style="display:none">
|
||||
<div class="modal-card">
|
||||
<div class="modal-card" role="dialog" aria-modal="true" aria-labelledby="accountModalTitle" aria-describedby="accountModalSubtitle">
|
||||
<div class="modal-header">
|
||||
<div>
|
||||
<h3 id="accountModalTitle">Account hinzufügen</h3>
|
||||
@ -254,15 +254,15 @@
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="settings-row" id="accountHosterRow">
|
||||
<label>Hoster</label>
|
||||
<select class="key-input" id="accountHosterSelect" style="max-width:300px"></select>
|
||||
<label for="accountHosterSelect">Hoster</label>
|
||||
<select class="key-input" id="accountHosterSelect" name="hoster" autocomplete="off" style="max-width:300px"></select>
|
||||
</div>
|
||||
<div class="settings-row">
|
||||
<label>Label (optional)</label>
|
||||
<input type="text" class="key-input" id="accField_label" placeholder="z.B. Hauptaccount, Premium, Kunde XY" maxlength="60">
|
||||
<label for="accField_label">Label (optional)</label>
|
||||
<input type="text" class="key-input" id="accField_label" name="accountLabel" autocomplete="off" placeholder="z. B. Hauptaccount, Premium, Kunde XY" maxlength="60">
|
||||
</div>
|
||||
<div id="accountCredsFields"></div>
|
||||
<div class="account-modal-status" id="accountModalStatus"></div>
|
||||
<div class="account-modal-status" id="accountModalStatus" role="status" aria-live="polite"></div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button class="btn btn-secondary" id="cancelAccountModalBtn">Abbrechen</button>
|
||||
@ -303,7 +303,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="settings-view" class="view">
|
||||
<div id="settings-view" class="view" role="tabpanel" aria-labelledby="settings-tab">
|
||||
<div class="settings-container">
|
||||
<h2>Upload-Einstellungen</h2>
|
||||
<p class="settings-hint">Hoster-Einstellungen erscheinen erst, sobald ein Account hinterlegt ist. Änderungen werden automatisch gespeichert.</p>
|
||||
@ -315,7 +315,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="history-view" class="view">
|
||||
<div id="history-view" class="view" role="tabpanel" aria-labelledby="history-tab">
|
||||
<div class="history-container">
|
||||
<div class="history-header">
|
||||
<h2>Upload-Verlauf</h2>
|
||||
@ -368,17 +368,17 @@
|
||||
<span class="sb-separator">|</span>
|
||||
<span class="sb-eta" id="sbEta">ETA --:--</span>
|
||||
<span class="sb-separator">|</span>
|
||||
<span class="sb-connections" id="sbConnections">Aktive Verbindungen 0</span>
|
||||
<span class="sb-connections" id="sbConnections">Verbindungen 0</span>
|
||||
<span class="sb-separator">|</span>
|
||||
<span class="sb-queue-count" id="sbQueueCount">Gesamt 0</span>
|
||||
<span class="sb-separator">|</span>
|
||||
<span class="sb-remaining-count" id="sbRemainingCount">Remaining 0</span>
|
||||
<span class="sb-remaining-count" id="sbRemainingCount">Verbleibend 0</span>
|
||||
<span class="sb-separator">|</span>
|
||||
<span class="sb-progress-count" id="sbInProgressCount">In Progress 0</span>
|
||||
<span class="sb-progress-count" id="sbInProgressCount">Läuft 0</span>
|
||||
<span class="sb-separator">|</span>
|
||||
<span class="sb-done-count" id="sbDoneCount">Done 0</span>
|
||||
<span class="sb-done-count" id="sbDoneCount">Fertig 0</span>
|
||||
<span class="sb-separator">|</span>
|
||||
<span class="sb-error-count" id="sbErrorCount">Error 0</span>
|
||||
<span class="sb-error-count" id="sbErrorCount">Fehler 0</span>
|
||||
</div>
|
||||
|
||||
<div class="copy-toast" id="copyToast"></div>
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
:root {
|
||||
color-scheme: dark;
|
||||
--bg-primary: #16181c;
|
||||
--bg-secondary: #20242b;
|
||||
--bg-card: #262b33;
|
||||
@ -8,7 +9,7 @@
|
||||
--border-hover: rgba(255, 255, 255, 0.18);
|
||||
--text: #edf1f7;
|
||||
--text-muted: #9ea7b3;
|
||||
--text-dim: #727b88;
|
||||
--text-dim: #8490a0;
|
||||
--accent: #3ea7ff;
|
||||
--accent-end: #65d8ff;
|
||||
--success: #43c788;
|
||||
@ -216,7 +217,7 @@ body {
|
||||
text-transform: uppercase;
|
||||
cursor: pointer;
|
||||
border-bottom: 2px solid transparent;
|
||||
transition: all 0.2s;
|
||||
transition: color 0.2s, background-color 0.2s, border-color 0.2s;
|
||||
}
|
||||
.tab:hover { color: var(--text); }
|
||||
.tab.active { color: var(--text); border-bottom-color: var(--accent); background: rgba(255, 255, 255, 0.03); }
|
||||
@ -256,7 +257,7 @@ body {
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.02em;
|
||||
transition: all 0.2s;
|
||||
transition: color 0.2s, background-color 0.2s, border-color 0.2s, filter 0.2s, transform 0.2s;
|
||||
}
|
||||
.btn-xs { padding: 4px 10px; font-size: 11px; border-radius: 4px; }
|
||||
.btn-sm { padding: 5px 12px; font-size: 12px; border-radius: 5px; }
|
||||
@ -319,7 +320,7 @@ body {
|
||||
border: 1px dashed rgba(126, 220, 255, 0.28);
|
||||
border-radius: 18px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
transition: color 0.3s, background-color 0.3s, border-color 0.3s;
|
||||
color: var(--text-muted);
|
||||
min-height: 200px;
|
||||
background:
|
||||
@ -359,7 +360,7 @@ body {
|
||||
background: transparent;
|
||||
cursor: pointer;
|
||||
color: var(--text-muted);
|
||||
transition: all 0.15s;
|
||||
transition: color 0.15s, background-color 0.15s, border-color 0.15s, transform 0.15s;
|
||||
}
|
||||
.toolbar-btn:hover {
|
||||
background: rgba(255,255,255,0.1);
|
||||
@ -571,7 +572,7 @@ body.col-resizing, body.col-resizing * { cursor: col-resize !important; user-sel
|
||||
border-bottom: none;
|
||||
color: var(--text-muted);
|
||||
cursor: pointer;
|
||||
transition: all 0.15s;
|
||||
transition: color 0.15s, background-color 0.15s, border-color 0.15s;
|
||||
}
|
||||
.recent-tab:first-child { border-radius: 4px 0 0 0; }
|
||||
.recent-tab:last-child { border-radius: 0 4px 0 0; }
|
||||
@ -677,6 +678,7 @@ body.col-resizing, body.col-resizing * { cursor: col-resize !important; user-sel
|
||||
background: rgba(5, 7, 16, 0.72);
|
||||
z-index: 2500;
|
||||
padding: 24px;
|
||||
overscroll-behavior: contain;
|
||||
}
|
||||
.modal-card {
|
||||
width: min(560px, 100%);
|
||||
@ -713,6 +715,8 @@ body.col-resizing, body.col-resizing * { cursor: col-resize !important; user-sel
|
||||
padding: 14px 16px 10px;
|
||||
overflow: auto;
|
||||
}
|
||||
.modal-body .settings-row { gap: 10px; }
|
||||
.modal-body .key-input { min-height: 36px; }
|
||||
.modal-actions-inline {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
@ -1190,12 +1194,29 @@ select.hs-input { max-width: none; width: auto; min-width: 140px; }
|
||||
}
|
||||
|
||||
.accounts-empty {
|
||||
width: min(520px, 100%);
|
||||
margin: clamp(56px, 12vh, 120px) auto 0;
|
||||
padding: 32px;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 16px;
|
||||
background: linear-gradient(180deg, rgba(62, 167, 255, 0.06), rgba(255, 255, 255, 0.025));
|
||||
box-shadow: var(--panel-shadow);
|
||||
text-align: center;
|
||||
padding: 48px 16px;
|
||||
color: var(--text-dim);
|
||||
}
|
||||
.accounts-empty p { font-size: 14px; margin-bottom: 4px; }
|
||||
.accounts-empty .hint { font-size: 12px; }
|
||||
.accounts-empty-icon {
|
||||
display: grid;
|
||||
place-items: center;
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
margin: 0 auto 14px;
|
||||
border-radius: 14px;
|
||||
background: rgba(62, 167, 255, 0.14);
|
||||
color: var(--accent-end);
|
||||
font-size: 30px;
|
||||
font-weight: 300;
|
||||
}
|
||||
.accounts-empty h3 { font-size: 17px; margin-bottom: 6px; }
|
||||
.accounts-empty p { max-width: 390px; margin: 0 auto 18px; color: var(--text-muted); font-size: 13px; line-height: 1.55; }
|
||||
|
||||
/* History View */
|
||||
.history-container { padding: 16px; overflow: auto; flex: 1; background: linear-gradient(180deg, rgba(255,255,255,0.015), transparent 24%); }
|
||||
@ -1292,7 +1313,7 @@ select.hs-input { max-width: none; width: auto; min-width: 140px; }
|
||||
font-size: 12px;
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
transition: all 0.3s;
|
||||
transition: opacity 0.3s, transform 0.3s;
|
||||
z-index: 2000;
|
||||
}
|
||||
.copy-toast.show { opacity: 1; transform: translateX(-50%) translateY(0); }
|
||||
@ -1339,3 +1360,29 @@ select.hs-input { max-width: none; width: auto; min-width: 140px; }
|
||||
::-webkit-scrollbar-track { background: transparent; }
|
||||
::-webkit-scrollbar-thumb { background: rgba(255, 255, 255, 0.1); border-radius: 4px; }
|
||||
::-webkit-scrollbar-thumb:hover { background: rgba(255, 255, 255, 0.2); }
|
||||
|
||||
:where(button, input, select, textarea, [tabindex]):focus-visible {
|
||||
outline: 2px solid var(--accent-end);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
.key-input:focus-visible,
|
||||
.hs-input:focus-visible {
|
||||
border-color: var(--accent);
|
||||
outline: 2px solid rgba(101, 216, 255, 0.45);
|
||||
outline-offset: 1px;
|
||||
}
|
||||
|
||||
.menu-spinner input[type="text"]:focus-visible {
|
||||
outline: 2px solid var(--accent-end);
|
||||
outline-offset: -2px;
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
*, *::before, *::after {
|
||||
scroll-behavior: auto !important;
|
||||
transition-duration: 0.01ms !important;
|
||||
animation-duration: 0.01ms !important;
|
||||
animation-iteration-count: 1 !important;
|
||||
}
|
||||
}
|
||||
|
||||
@ -53,9 +53,15 @@ setTimeout(async () => {
|
||||
const tabLabels = await wc.executeJavaScript('[...document.querySelectorAll(".tab")].map(el => el.textContent.trim()).join("|")');
|
||||
check('Current tab labels present', tabLabels === 'Upload|Accounts|Einstellungen|Verlauf');
|
||||
|
||||
const tabSemantics = await wc.executeJavaScript('document.querySelector(".tab-bar")?.getAttribute("role") + "|" + document.querySelector(".tab.active")?.getAttribute("aria-selected")');
|
||||
check('Tab navigation exposes active state', tabSemantics === 'tablist|true');
|
||||
|
||||
const activeTab = await wc.executeJavaScript('document.querySelector(".tab.active")?.textContent?.trim()');
|
||||
check('Upload tab active by default', activeTab === 'Upload');
|
||||
|
||||
const tabStops = await wc.executeJavaScript('[...document.querySelectorAll(".tab")].map(el => el.tabIndex).join("|")');
|
||||
check('Tab navigation exposes one keyboard stop', tabStops === '0|-1|-1|-1');
|
||||
|
||||
const dropVisible = await wc.executeJavaScript('document.getElementById("dropZone")?.style.display !== "none"');
|
||||
check('Drop zone visible (no files)', dropVisible);
|
||||
|
||||
@ -71,6 +77,21 @@ setTimeout(async () => {
|
||||
const version = await wc.executeJavaScript('document.getElementById("versionLabel")?.textContent');
|
||||
check('Version label present', version && version.startsWith('v'));
|
||||
|
||||
const localizedQueueHeaders = await wc.executeJavaScript('[...document.querySelectorAll("#queueTable thead th")].map(el => el.childNodes[0]?.textContent.trim()).join("|")');
|
||||
check('Upload table labels are consistently German', localizedQueueHeaders === 'Dateiname|Hochgeladen / Größe|Hoster|Status|Zeit|Rest|Geschwindigkeit|Fortschritt');
|
||||
|
||||
const localizedRecentTabs = await wc.executeJavaScript('[...document.querySelectorAll(".recent-tab")].map(el => el.textContent.trim()).join("|")');
|
||||
check('Recent panel labels are consistently German', localizedRecentTabs === 'Dateien|Statistik');
|
||||
|
||||
const localizedStatusbar = await wc.executeJavaScript('["sbConnections", "sbQueueCount", "sbRemainingCount", "sbInProgressCount", "sbDoneCount", "sbErrorCount"].map(id => document.getElementById(id)?.textContent).join("|")');
|
||||
check('Statusbar labels are consistently German', localizedStatusbar === 'Verbindungen 0|Gesamt 0|Verbleibend 0|Läuft 0|Fertig 0|Fehler 0');
|
||||
|
||||
const toolbarLabels = await wc.executeJavaScript('[...document.querySelectorAll("#queueCommandBar .toolbar-btn")].map(el => el.getAttribute("aria-label")).join("|")');
|
||||
check('Upload toolbar actions have German accessible names', toolbarLabels === 'Alle Uploads starten|Ausgewählte Uploads starten|Ausgewählte Datei erneut hochladen|Ausgewählten Upload abbrechen|Aktive Uploads beenden und stoppen|Alle Uploads abbrechen|Ganz nach oben|Nach oben|Nach unten|Ganz nach unten');
|
||||
|
||||
const keyboardTab = await wc.executeJavaScript('document.getElementById("upload-tab").focus(); document.getElementById("upload-tab").dispatchEvent(new KeyboardEvent("keydown", { key: "ArrowRight", bubbles: true })); document.querySelector(".tab.active")?.textContent?.trim() + "|" + document.activeElement?.id');
|
||||
check('Arrow keys move and activate main tabs', keyboardTab === 'Accounts|accounts-tab');
|
||||
|
||||
const ctxHidden = await wc.executeJavaScript('document.getElementById("contextMenu")?.style.display');
|
||||
check('Context menu hidden', ctxHidden === 'none');
|
||||
|
||||
@ -88,7 +109,10 @@ setTimeout(async () => {
|
||||
const addAccountEnabled = await wc.executeJavaScript('document.getElementById("addAccountBtn")?.disabled === false');
|
||||
check('Add account button enabled', addAccountEnabled);
|
||||
|
||||
await wc.executeJavaScript('document.getElementById("addAccountBtn").click()');
|
||||
const emptyAccountAction = await wc.executeJavaScript('document.querySelector("[data-account-empty-add]")?.textContent?.trim()');
|
||||
check('Empty account state offers direct action', emptyAccountAction === 'Ersten Account hinzufügen');
|
||||
|
||||
await wc.executeJavaScript('document.querySelector("[data-account-empty-add]").focus(); document.querySelector("[data-account-empty-add]").click()');
|
||||
await new Promise(r => setTimeout(r, 200));
|
||||
|
||||
const accountModalVisible = await wc.executeJavaScript('document.getElementById("accountModal")?.style.display');
|
||||
@ -97,6 +121,21 @@ setTimeout(async () => {
|
||||
const accountModalTitle = await wc.executeJavaScript('document.getElementById("accountModalTitle")?.textContent');
|
||||
check('Account modal is in add mode', accountModalTitle === 'Account hinzufügen');
|
||||
|
||||
const accountModalSemantics = await wc.executeJavaScript('document.querySelector("#accountModal .modal-card")?.getAttribute("role") + "|" + document.querySelector("#accountModal .modal-card")?.getAttribute("aria-modal")');
|
||||
check('Account modal exposes dialog semantics', accountModalSemantics === 'dialog|true');
|
||||
|
||||
const accountFormLabels = await wc.executeJavaScript('["accountHosterSelect", "accField_label", "accField_username", "accField_password"].every(id => document.getElementById(id)?.labels?.length === 1)');
|
||||
check('Account form controls have linked labels', accountFormLabels);
|
||||
|
||||
const accountStatusLive = await wc.executeJavaScript('document.getElementById("accountModalStatus")?.getAttribute("aria-live")');
|
||||
check('Account validation status is announced', accountStatusLive === 'polite');
|
||||
|
||||
const initialAccountFocus = await wc.executeJavaScript('document.activeElement?.id');
|
||||
check('Account modal focuses first control', initialAccountFocus === 'accountHosterSelect');
|
||||
|
||||
const trappedAccountFocus = await wc.executeJavaScript('document.getElementById("saveAccountBtn").focus(); document.dispatchEvent(new KeyboardEvent("keydown", { key: "Tab", bubbles: true })); document.activeElement?.id');
|
||||
check('Account modal keeps keyboard focus inside', trappedAccountFocus === 'closeAccountModalBtn');
|
||||
|
||||
const authOptionCount = await wc.executeJavaScript('document.querySelectorAll("#accountHosterSelect option").length');
|
||||
check('7 hoster authentication options exist', authOptionCount === 7);
|
||||
|
||||
@ -109,9 +148,18 @@ setTimeout(async () => {
|
||||
const credentialInputs = await wc.executeJavaScript('document.querySelectorAll("#accountCredsFields .key-input").length');
|
||||
check('Credential inputs rendered', credentialInputs === 2);
|
||||
|
||||
await wc.executeJavaScript('document.getElementById("cancelAccountModalBtn").click()');
|
||||
const passwordToggleState = await wc.executeJavaScript('document.querySelector("#accountCredsFields .toggle-vis").click(); document.querySelector("#accountCredsFields .toggle-vis").getAttribute("aria-label") + "|" + document.querySelector("#accountCredsFields .toggle-vis").getAttribute("aria-pressed")');
|
||||
check('Password visibility action exposes its state', passwordToggleState === 'Passwort verbergen|true');
|
||||
|
||||
await wc.executeJavaScript('document.dispatchEvent(new KeyboardEvent("keydown", { key: "Escape", bubbles: true }))');
|
||||
const accountModalHidden = await wc.executeJavaScript('document.getElementById("accountModal")?.style.display');
|
||||
check('Account modal closes', accountModalHidden === 'none');
|
||||
check('Escape closes account modal', accountModalHidden === 'none');
|
||||
|
||||
const restoredAccountFocus = await wc.executeJavaScript('document.activeElement?.hasAttribute("data-account-empty-add")');
|
||||
check('Account modal restores trigger focus', restoredAccountFocus === true);
|
||||
|
||||
const fallbackAccountFocus = await wc.executeJavaScript('document.querySelector("[data-account-empty-add]").focus(); document.querySelector("[data-account-empty-add]").click(); document.querySelector("[data-account-empty-add]").remove(); document.dispatchEvent(new KeyboardEvent("keydown", { key: "Escape", bubbles: true })); document.activeElement?.id');
|
||||
check('Account modal restores stable focus after list rerender', fallbackAccountFocus === 'addAccountBtn');
|
||||
|
||||
console.log('\\n=== Settings View ===');
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user