diff --git a/tests/test_dom_patchers.mjs b/tests/test_dom_patchers.mjs index 444858c..d9a1cfe 100644 --- a/tests/test_dom_patchers.mjs +++ b/tests/test_dom_patchers.mjs @@ -66,6 +66,7 @@ const scheduleSearchUiRefresh = eval(`(${extractMethod(searchPanelMethodsSource, const wireLocalMatchButtons = eval(`(${extractMethod(searchPanelMethodsSource, 'wireLocalMatchButtons')})`); const patchQueuedSelections = eval(`(${extractMethod(queueMethodsSource, 'patchQueuedSelections')})`); const patchDownloadsPanelElement = eval(`(${extractMethod(queueMethodsSource, 'patchDownloadsPanelElement')})`); +const updateQueueToggleIcon = eval(`(${extractMethod(queueMethodsSource, 'updateQueueToggleIcon')})`); const patchLoadedModelsProgress = eval(`(${extractMethod(renderFormatMethodsSource, 'patchLoadedModelsProgress')})`); const patchAnalysisProgress = eval(`(${extractMethod(renderFormatMethodsSource, 'patchAnalysisProgress')})`); @@ -125,6 +126,31 @@ test('sidebar active helper recognizes supported state attributes and classes', assert.equal(domPatchUtils.isSidebarButtonActive(null), false); }); +test('queue toggle icon shows queued and active download counts', () => { + const window = new Window(); + const queueToggleIcon = window.document.createElement('button'); + queueToggleIcon.innerHTML = ` + + + `; + const dialog = { + queueToggleIcon, + activeTab: 'missing', + queueCollapsed: true, + pendingResolutions: [{}, {}, {}], + getActiveQueuePanelDownloads: () => [{ downloadId: 'download-1' }], + }; + + updateQueueToggleIcon.call(dialog); + + assert.equal(queueToggleIcon.querySelector('.mr-queue-toggle-count-queued').textContent, '3'); + assert.equal(queueToggleIcon.querySelector('.mr-queue-toggle-count-downloads').textContent, '1'); + assert.equal( + queueToggleIcon.getAttribute('aria-label'), + 'Show queue and downloads panel. Queued: 3. Downloads: 1.' + ); +}); + test('instant actions handle pointer and click as one idempotent action', () => { assert.equal(typeof domPatchUtils.bindInstantAction, 'function'); diff --git a/web/css/resolver-shell.css b/web/css/resolver-shell.css index 1c66526..4af4b79 100644 --- a/web/css/resolver-shell.css +++ b/web/css/resolver-shell.css @@ -462,6 +462,7 @@ border: 0; border-radius: 0; box-shadow: none; + overflow: visible; } #queue-toggle-icon::before { @@ -477,6 +478,37 @@ transition: var(--mr-transition-smooth); } +#queue-toggle-icon .mr-queue-toggle-count { + position: absolute; + left: 50%; + z-index: 1; + display: inline-flex; + align-items: center; + justify-content: center; + min-width: 14px; + height: 14px; + padding: 0 2px; + box-sizing: border-box; + color: var(--mr-text-muted, #b1b1b1); + background: var(--mr-bg, #121212); + border: 1px solid rgba(255, 255, 255, 0.16); + border-radius: 999px; + font-size: 9px; + font-variant-numeric: tabular-nums; + font-weight: 800; + line-height: 1; + pointer-events: none; + transform: translateX(-50%); +} + +#queue-toggle-icon .mr-queue-toggle-count-queued { + top: -11px; +} + +#queue-toggle-icon .mr-queue-toggle-count-downloads { + bottom: -11px; +} + #queue-toggle-icon:hover, #queue-toggle-icon:focus-visible { opacity: 1; @@ -490,6 +522,13 @@ box-shadow: var(--mr-shadow-focus-ring); } +#queue-toggle-icon:hover .mr-queue-toggle-count, +#queue-toggle-icon:focus-visible .mr-queue-toggle-count, +#queue-toggle-icon.is-collapsed .mr-queue-toggle-count { + color: var(--mr-accent, #72d38a); + border-color: rgba(114, 211, 138, 0.45); +} + .mr-queue-panel, .mr-queue-stack { display: flex; diff --git a/web/resolver/actions/queue_methods.js b/web/resolver/actions/queue_methods.js index a310218..e1fafa6 100644 --- a/web/resolver/actions/queue_methods.js +++ b/web/resolver/actions/queue_methods.js @@ -61,7 +61,14 @@ export const queueMethods = { type: "button", "aria-label": "Toggle queued selections", onclick: (event) => this.onQueueEdgeClick(event) - }); + }, [ + $el("span.mr-queue-toggle-count.mr-queue-toggle-count-queued", { + "aria-hidden": "true" + }), + $el("span.mr-queue-toggle-count.mr-queue-toggle-count-downloads", { + "aria-hidden": "true" + }) + ]); this.queueToggleIcon.addEventListener( typeof PointerEvent === 'function' ? 'pointerdown' : 'mousedown', (event) => this.startQueueEdgeDrag(event) @@ -152,6 +159,7 @@ export const queueMethods = { }, updateQueuePanel({ force = false } = {}) { + this.updateQueueToggleIcon(); if (!this.queueList || !this.queueHeader) return; if (this._queuePanelUpdateTimer) { clearTimeout(this._queuePanelUpdateTimer); @@ -1639,15 +1647,26 @@ export const queueMethods = { updateQueueToggleIcon() { if (!this.queueToggleIcon) return; this.queueToggleIcon.style.display = this.activeTab === 'missing' ? '' : 'none'; - this.queueToggleIcon.textContent = ''; this.queueToggleIcon.removeAttribute('data-tooltip'); this.queueToggleIcon.removeAttribute('title'); + const queueCount = Array.isArray(this.pendingResolutions) ? this.pendingResolutions.length : 0; + const downloadCount = this.getActiveQueuePanelDownloads().length; + const queuedCountElement = this.queueToggleIcon.querySelector('.mr-queue-toggle-count-queued'); + const downloadsCountElement = this.queueToggleIcon.querySelector('.mr-queue-toggle-count-downloads'); + if (queuedCountElement) queuedCountElement.textContent = String(queueCount); + if (downloadsCountElement) downloadsCountElement.textContent = String(downloadCount); if (this.queueCollapsed) { this.queueToggleIcon.classList.add('is-collapsed'); - this.queueToggleIcon.setAttribute('aria-label', 'Show queue and downloads panel'); + this.queueToggleIcon.setAttribute( + 'aria-label', + `Show queue and downloads panel. Queued: ${queueCount}. Downloads: ${downloadCount}.` + ); } else { this.queueToggleIcon.classList.remove('is-collapsed'); - this.queueToggleIcon.setAttribute('aria-label', 'Hide queue and downloads panel'); + this.queueToggleIcon.setAttribute( + 'aria-label', + `Hide queue and downloads panel. Queued: ${queueCount}. Downloads: ${downloadCount}.` + ); } },