diff --git a/tests/test_downloads_tab_workflow_route.mjs b/tests/test_downloads_tab_workflow_route.mjs index fd5d70c..5d671a6 100644 --- a/tests/test_downloads_tab_workflow_route.mjs +++ b/tests/test_downloads_tab_workflow_route.mjs @@ -329,16 +329,12 @@ test('missing browser splitter keeps a compositor preview between live layout fr /\.mr-missing-browser\.is-resizing \.mr-missing-(?:list|detail)-pane/ ); assert.doesNotMatch(missingBrowserMethodsSource, /deferMissingBrowserSplitReleaseUi/); - assert.match(missingBrowserMethodsSource, /observedWidth\s*<\s*760/); - assert.match(missingBrowserMethodsSource, /observedWidth\s*<\s*700/); assert.match( missingBrowserMethodsSource, /scheduleMissingBrowserExternalResizeRestore\(browser,\s*observedWidth\)/ ); - assert.match( - resolverMainCssSource, - /\.mr-missing-browser\.is-detail-collapsed \.mr-missing-detail-pane\s*\{[^}]*display:\s*none/s - ); + assert.doesNotMatch(missingBrowserMethodsSource, /is-detail-collapsed/); + assert.doesNotMatch(resolverMainCssSource, /is-detail-collapsed/); assert.doesNotMatch(missingBrowserMethodsSource, /startMissingBrowserExternalResizeLock/); assert.doesNotMatch(resolverMainCssSource, /mr-resolver-external-resizing/); assert.doesNotMatch( @@ -423,43 +419,92 @@ test('docked ComfyUI splitter coalesces live layout while its gutter stays respo assert.doesNotMatch(dialogShellMethodsSource, /mr-resolver-external-resizing/); }); -test('missing browser detail collapse follows container width with hysteresis', () => { +test('missing browser detail remains available at narrow widths', () => { + assert.doesNotMatch(missingBrowserMethodsSource, /updateMissingBrowserDetailCollapse/); + assert.doesNotMatch(missingBrowserMethodsSource, /is-detail-collapsed/); + assert.doesNotMatch(resolverMainCssSource, /is-detail-collapsed/); +}); + +test('missing browser detail follows external splitter resizing every animation frame', () => { + const scheduleRestore = eval( + `(${extractMethod(missingBrowserMethodsSource, 'scheduleMissingBrowserExternalResizeRestore')})` + ); const previousHTMLElement = globalThis.HTMLElement; + const previousRequestAnimationFrame = globalThis.requestAnimationFrame; + const frames = []; + const restores = []; + class FakeHTMLElement {} globalThis.HTMLElement = FakeHTMLElement; + globalThis.requestAnimationFrame = callback => { + frames.push(callback); + return frames.length; + }; try { - const classes = new Set(); const browser = new FakeHTMLElement(); - browser.classList = { - contains(name) { - return classes.has(name); - }, - toggle(name, enabled) { - if (enabled) classes.add(name); - else classes.delete(name); - }, - }; - + browser.isConnected = true; const context = { - _missingBrowserRestoreFrame: null, - cancelMissingBrowserExternalResizeRestore() {}, + _missingBrowserExternalResizeFrame: null, + _pendingMissingBrowserExternalResizeBrowser: null, + _pendingMissingBrowserExternalResizeWidth: null, + _missingBrowserSplitDragging: false, + restoreMissingBrowserSplitWidth(target, options) { + restores.push([target, options]); + }, }; - const updateDetailCollapse = eval( - `(${extractMethod(missingBrowserMethodsSource, 'updateMissingBrowserDetailCollapse')})` - ); - assert.equal(updateDetailCollapse.call(context, browser, 690), true); - assert.equal(classes.has('is-detail-collapsed'), true); + scheduleRestore.call(context, browser, 620); + scheduleRestore.call(context, browser, 540); - assert.equal(updateDetailCollapse.call(context, browser, 730), false); - assert.equal(classes.has('is-detail-collapsed'), true); + assert.equal(frames.length, 1); + assert.equal(restores.length, 0); - assert.equal(updateDetailCollapse.call(context, browser, 770), true); - assert.equal(classes.has('is-detail-collapsed'), false); + frames.shift()(); + + assert.equal(restores.length, 1); + assert.equal(restores[0][0], browser); + assert.deepEqual(restores[0][1], { + browserWidth: 540, + useDefault: false, + }); } finally { globalThis.HTMLElement = previousHTMLElement; + globalThis.requestAnimationFrame = previousRequestAnimationFrame; } + + assert.doesNotMatch( + extractMethod(missingBrowserMethodsSource, 'scheduleMissingBrowserExternalResizeRestore'), + /setTimeout|120/ + ); +}); + +test('missing browser splitter preserves the table through the Type column', () => { + const getSplitBounds = eval( + `(${extractMethod(missingBrowserMethodsSource, 'getMissingBrowserSplitBoundsForWidth')})` + ); + const regularBounds = getSplitBounds(1000); + const narrowBounds = getSplitBounds(620); + + assert.equal(regularBounds.available - regularBounds.max, 340); + assert.equal(narrowBounds.available - narrowBounds.max, 340); + assert.equal(narrowBounds.min, narrowBounds.max); + assert.match( + extractMethod(missingBrowserMethodsSource, 'getMissingBrowserSplitBoundsForWidth'), + /Math\.min\(340,\s*available\)/ + ); + assert.match( + resolverMainCssSource, + /\.mr-missing-browser\s*\{[^}]*--mr-missing-list-min-width:\s*340px;/s + ); + assert.match( + resolverMainCssSource, + /\.mr-missing-list-pane\s*\{[^}]*min-width:\s*var\(--mr-missing-list-min-width\);/s + ); + assert.doesNotMatch( + resolverMainCssSource, + /\.mr-missing-row-best,[\s\S]*?\.mr-missing-row-sources\s*\{[^}]*display:\s*none;/ + ); }); test('local and loaded model tooltips include preview image routes above full names', () => { diff --git a/web/css/resolver-main.css b/web/css/resolver-main.css index 816a065..b32df72 100644 --- a/web/css/resolver-main.css +++ b/web/css/resolver-main.css @@ -503,6 +503,7 @@ --mr-missing-line-strong: rgba(255, 255, 255, 0.13); --mr-missing-radius: 6px; --mr-missing-splitter-track: 10px; + --mr-missing-list-min-width: 340px; --mr-missing-list-track: calc(100% - var(--mr-missing-splitter-track) - var(--mr-missing-detail-track, 42%)); position: relative; display: flex; @@ -527,6 +528,7 @@ .mr-missing-list-pane { flex: 1 1 0; + min-width: var(--mr-missing-list-min-width); display: grid; grid-template-rows: auto minmax(0, 1fr); border-right: 0; @@ -1156,54 +1158,6 @@ box-shadow: none; } -.mr-missing-browser.is-detail-collapsed .mr-missing-browser-splitter, -.mr-missing-browser.is-detail-collapsed .mr-missing-detail-pane { - display: none; -} - -.mr-missing-browser.is-detail-collapsed .mr-missing-list-pane { - flex: 1 1 100% !important; - width: 100%; - border-right: 0; -} - -@media (max-width: 760px) { - .mr-missing-list-head { - display: none; - } - - #model-resolver-modal .mr-missing-list-row { - grid-template-columns: 24px 18px minmax(0, 1fr); - content-visibility: visible; - contain-intrinsic-size: none; - height: auto; - min-height: 64px; - overflow: visible; - gap: 7px; - } - - .mr-missing-row-select { - grid-row: 1 / span 4; - align-self: start; - margin-top: 1px; - } - - .mr-missing-row-index { - grid-row: 1 / span 4; - align-self: start; - margin-top: 1px; - text-align: center; - } - - .mr-missing-row-model, - .mr-missing-row-type, - .mr-missing-row-best, - .mr-missing-row-match, - .mr-missing-row-sources { - grid-column: 3; - } -} - /* Card Styles */ .mr-card { background: var(--mr-card-bg); diff --git a/web/resolver/resolver_dialog.js b/web/resolver/resolver_dialog.js index e33354d..e5d395e 100644 --- a/web/resolver/resolver_dialog.js +++ b/web/resolver/resolver_dialog.js @@ -73,7 +73,7 @@ export class ResolverManagerDialog extends ComfyDialog { this.sidebarTabId = "comfyui-model-resolver"; this.sidebarOpenModeStorageKey = "model_resolver_sidebar_open_mode"; this.missingBrowserSplitStorageKey = "model_resolver_missing_browser_detail_w"; - this._missingBrowserExternalResizeTimer = null; + this._missingBrowserExternalResizeFrame = null; this._pendingMissingBrowserExternalResizeBrowser = null; this._pendingMissingBrowserExternalResizeWidth = null; this._missingBrowserObservedHostWidth = null; diff --git a/web/resolver/views/missing_browser_methods.js b/web/resolver/views/missing_browser_methods.js index fd438c1..4eda5d9 100644 --- a/web/resolver/views/missing_browser_methods.js +++ b/web/resolver/views/missing_browser_methods.js @@ -975,7 +975,7 @@ export const missingBrowserMethods = { this.rememberMissingBrowserWidth(observedWidth); if (!Number.isFinite(previousHostWidth) || previousHostWidth <= 0) { - this.updateMissingBrowserDetailCollapse(browser, observedWidth); + this.scheduleMissingBrowserSplitRestore(browser, observedWidth, { useDefault: false }); return; } if (Math.abs(previousHostWidth - observedWidth) < 0.5) return; @@ -1000,10 +1000,7 @@ export const missingBrowserMethods = { if (this._missingBrowserSplitDragging) return; const measuredWidth = Number(browser.clientWidth || browser.getBoundingClientRect().width); this.rememberMissingBrowserWidth(measuredWidth); - this.updateMissingBrowserDetailCollapse(browser, measuredWidth); - if (!this._missingBrowserSplitDragging && !browser.classList.contains('is-detail-collapsed')) { - this.scheduleMissingBrowserSplitRestore(browser, measuredWidth, { useDefault: false }); - } + this.scheduleMissingBrowserSplitRestore(browser, measuredWidth, { useDefault: false }); }); } }, @@ -1014,51 +1011,24 @@ export const missingBrowserMethods = { this._missingBrowserPrewarmFrame = null; }, - updateMissingBrowserDetailCollapse(browser, width) { - if (!(browser instanceof HTMLElement)) return false; - const observedWidth = Number(width); - if (!Number.isFinite(observedWidth) || observedWidth <= 0) return false; - - const wasCollapsed = browser.classList.contains('is-detail-collapsed'); - const shouldCollapse = wasCollapsed - ? observedWidth < 760 - : observedWidth < 700; - if (shouldCollapse === wasCollapsed) return false; - - browser.classList.toggle('is-detail-collapsed', shouldCollapse); - if (shouldCollapse) { - this.cancelMissingBrowserExternalResizeRestore(); - if (this._missingBrowserRestoreFrame) { - cancelAnimationFrame(this._missingBrowserRestoreFrame); - this._missingBrowserRestoreFrame = null; - } - this._pendingMissingBrowserRestoreBrowser = null; - this._pendingMissingBrowserRestoreWidth = null; - this._pendingMissingBrowserRestoreUseDefault = false; - } - return true; - }, - cancelMissingBrowserExternalResizeRestore() { - if (this._missingBrowserExternalResizeTimer) { - clearTimeout(this._missingBrowserExternalResizeTimer); + if (this._missingBrowserExternalResizeFrame) { + cancelAnimationFrame(this._missingBrowserExternalResizeFrame); } - this._missingBrowserExternalResizeTimer = null; + this._missingBrowserExternalResizeFrame = null; this._pendingMissingBrowserExternalResizeBrowser = null; this._pendingMissingBrowserExternalResizeWidth = null; }, - scheduleMissingBrowserExternalResizeRestore(browser, browserWidth, delay = 120) { + scheduleMissingBrowserExternalResizeRestore(browser, browserWidth) { if (!(browser instanceof HTMLElement)) return; this._pendingMissingBrowserExternalResizeBrowser = browser; this._pendingMissingBrowserExternalResizeWidth = Number(browserWidth) || null; - if (this._missingBrowserExternalResizeTimer) { - clearTimeout(this._missingBrowserExternalResizeTimer); - } + if (this._missingBrowserExternalResizeFrame) return; - this._missingBrowserExternalResizeTimer = setTimeout(() => { - this._missingBrowserExternalResizeTimer = null; + this._missingBrowserExternalResizeFrame = requestAnimationFrame(() => { + this._missingBrowserExternalResizeFrame = null; const targetBrowser = this._pendingMissingBrowserExternalResizeBrowser; const targetWidth = this._pendingMissingBrowserExternalResizeWidth; this._pendingMissingBrowserExternalResizeBrowser = null; @@ -1070,10 +1040,11 @@ export const missingBrowserMethods = { ) { return; } - this.updateMissingBrowserDetailCollapse(targetBrowser, targetWidth); - if (targetBrowser.classList.contains('is-detail-collapsed')) return; - this.scheduleMissingBrowserSplitRestore(targetBrowser, targetWidth, { useDefault: false }); - }, Math.max(0, Number(delay) || 0)); + this.restoreMissingBrowserSplitWidth(targetBrowser, { + browserWidth: targetWidth, + useDefault: false + }); + }); }, getStoredMissingBrowserSplitWidth() { @@ -1125,7 +1096,7 @@ export const missingBrowserMethods = { getMissingBrowserSplitBoundsForWidth(width) { const safeWidth = Math.max(1, Number(width) || 1); const available = Math.max(1, safeWidth - 10); - const minListWidth = Math.min(360, Math.max(240, Math.floor(available * 0.28))); + const minListWidth = Math.min(340, available); const max = Math.max(1, available - minListWidth); const min = Math.min(max, Math.min(420, Math.max(300, Math.floor(available * 0.3)))); @@ -1142,7 +1113,7 @@ export const missingBrowserMethods = { }, scheduleMissingBrowserSplitRestore(browser, browserWidth = null, { useDefault = false } = {}) { - if (!(browser instanceof HTMLElement) || browser.classList.contains('is-detail-collapsed')) return; + if (!(browser instanceof HTMLElement)) return; this._pendingMissingBrowserRestoreBrowser = browser; this._pendingMissingBrowserRestoreWidth = Number.isFinite(Number(browserWidth)) && Number(browserWidth) > 0 @@ -1182,7 +1153,7 @@ export const missingBrowserMethods = { }, startMissingBrowserSplitDrag(event, browser, splitter = null, panes = {}) { - if (!(browser instanceof HTMLElement) || browser.classList.contains('is-detail-collapsed')) return; + if (!(browser instanceof HTMLElement)) return; event.preventDefault(); event.stopPropagation?.();