Add draggable splitter to adjust width of missing model detail pane in linker UI

This commit is contained in:
Dariusz L
2026-05-29 15:23:28 +02:00
parent 53cdee3f61
commit ebe8c0a4ea
2 changed files with 171 additions and 3 deletions
+35 -2
View File
@@ -273,7 +273,7 @@
--ml-missing-line-strong: rgba(255,255,255,0.13);
--ml-missing-radius: 6px;
display: grid;
grid-template-columns: minmax(360px, 1.1fr) minmax(320px, 0.9fr);
grid-template-columns: minmax(360px, 1fr) 10px var(--ml-missing-detail-track, minmax(320px, 0.9fr));
gap: 0;
height: 100%;
min-height: 0;
@@ -291,9 +291,39 @@
.ml-missing-list-pane {
display: grid;
grid-template-rows: auto auto minmax(0, 1fr);
border-right: 1px solid var(--ml-missing-line);
border-right: 0;
background: var(--ml-missing-bg);
}
.ml-missing-browser-splitter {
position: relative;
min-width: 10px;
width: 10px;
cursor: col-resize;
user-select: none;
touch-action: none;
background: var(--ml-missing-bg);
border-left: 1px solid var(--ml-missing-line);
border-right: 1px solid var(--ml-missing-line);
outline: none;
}
.ml-missing-browser-splitter::before {
content: "";
position: absolute;
top: 16px;
bottom: 16px;
left: 50%;
width: 2px;
border-radius: 999px;
background: rgba(255,255,255,0.13);
transform: translateX(-50%);
transition: background 0.15s ease, box-shadow 0.15s ease;
}
.ml-missing-browser-splitter:hover::before,
.ml-missing-browser-splitter:focus-visible::before,
.ml-missing-browser.is-resizing .ml-missing-browser-splitter::before {
background: rgba(114,211,138,0.78);
box-shadow: 0 0 0 3px rgba(114,211,138,0.12);
}
.ml-missing-list-toolbar {
display: flex;
align-items: center;
@@ -651,6 +681,9 @@
grid-template-columns: 1fr;
overflow-y: auto;
}
.ml-missing-browser-splitter {
display: none;
}
.ml-missing-list-pane {
max-height: 46vh;
border-right: 0;
+136 -1
View File
@@ -39,6 +39,7 @@ class LinkerManagerDialog extends ComfyDialog {
this.pendingDockToSidebar = false;
this.sidebarTabId = "comfyui-model-linker";
this.sidebarOpenModeStorageKey = "model_linker_sidebar_open_mode";
this.missingBrowserSplitStorageKey = "model_linker_missing_browser_detail_w";
this.dockButton = null;
this.undockButton = null;
this._floatingRectBeforeDock = null;
@@ -4837,7 +4838,14 @@ class LinkerManagerDialog extends ComfyDialog {
? `${activeCount} downloading`
: (hasAny100Match ? 'Auto-link ready for exact matches' : 'Review matches or search online');
const listLayout = this.getMissingModelsListLayout(missingModels);
const listStyle = `--ml-missing-model-col:${listLayout.modelPx}px;--ml-missing-type-col:${listLayout.typePx}px;`;
let savedDetailWidth = null;
try {
savedDetailWidth = parseInt(localStorage.getItem(this.missingBrowserSplitStorageKey) || '', 10);
} catch (e) {}
const splitStyle = Number.isFinite(savedDetailWidth) && savedDetailWidth > 0
? `--ml-missing-detail-track:${savedDetailWidth}px;`
: '';
const listStyle = `${splitStyle}--ml-missing-model-col:${listLayout.modelPx}px;--ml-missing-type-col:${listLayout.typePx}px;`;
let html = `
<div class="ml-missing-browser" style="${listStyle}">
@@ -4906,6 +4914,7 @@ class LinkerManagerDialog extends ComfyDialog {
html += `
</div>
</section>
<div class="ml-missing-browser-splitter" role="separator" aria-orientation="vertical" aria-label="Resize missing model panes" tabindex="0" data-tooltip="Drag to resize panes"></div>
<section class="ml-missing-detail-pane" aria-label="Missing model details">
${detailMissing ? this.renderMissingModel(detailMissing, detailIndex) : this.renderStatusMessage('Select a missing model to inspect details.', 'info')}
</section>
@@ -4915,6 +4924,8 @@ class LinkerManagerDialog extends ComfyDialog {
}
wireMissingModelsBrowser(container, data, sortedMissingModels) {
this.wireMissingBrowserSplitter(container);
const selectRow = (row) => {
const key = row.dataset.missingKey;
if (!key || key === this.selectedMissingModelKey) return;
@@ -4953,6 +4964,130 @@ class LinkerManagerDialog extends ComfyDialog {
this.wireMissingModelDetail(container, selectedMissing, selectedIndex);
}
wireMissingBrowserSplitter(container) {
const browser = container.querySelector('.ml-missing-browser');
const splitter = browser?.querySelector('.ml-missing-browser-splitter');
if (!(browser instanceof HTMLElement) || !(splitter instanceof HTMLElement)) return;
this.restoreMissingBrowserSplitWidth(browser);
splitter.addEventListener('mousedown', (event) => {
if (event.button !== 0) return;
this.startMissingBrowserSplitDrag(event, browser);
});
splitter.addEventListener('dblclick', () => {
browser.style.removeProperty('--ml-missing-detail-track');
try {
localStorage.removeItem(this.missingBrowserSplitStorageKey);
} catch (e) {}
});
splitter.addEventListener('keydown', (event) => {
if (event.key !== 'ArrowLeft' && event.key !== 'ArrowRight') return;
event.preventDefault();
this.resizeMissingBrowserDetailBy(browser, event.key === 'ArrowLeft' ? 32 : -32);
});
this._missingBrowserResizeObserver?.disconnect?.();
if (typeof ResizeObserver === 'function') {
this._missingBrowserResizeObserver = new ResizeObserver(() => {
if (!this._missingBrowserSplitDragging) {
this.restoreMissingBrowserSplitWidth(browser);
}
});
this._missingBrowserResizeObserver.observe(browser);
}
}
restoreMissingBrowserSplitWidth(browser) {
try {
const savedWidth = parseInt(localStorage.getItem(this.missingBrowserSplitStorageKey) || '', 10);
if (Number.isFinite(savedWidth) && savedWidth > 0) {
this.setMissingBrowserDetailWidth(browser, savedWidth, { persist: false });
}
} catch (e) {}
}
startMissingBrowserSplitDrag(event, browser) {
if (!(browser instanceof HTMLElement)) return;
const detailPane = browser.querySelector('.ml-missing-detail-pane');
if (!(detailPane instanceof HTMLElement)) return;
event.preventDefault();
this._missingBrowserSplitDragging = true;
this._missingBrowserSplitStart = {
x: event.clientX,
width: detailPane.getBoundingClientRect().width
};
this._missingBrowserSplitBrowser = browser;
this._missingBrowserPrevUserSelect = document.body.style.userSelect;
this._missingBrowserPrevCursor = document.body.style.cursor;
document.body.style.userSelect = 'none';
document.body.style.cursor = 'col-resize';
browser.classList.add('is-resizing');
this._onMissingBrowserSplitMove = (moveEvent) => this.onMissingBrowserSplitDrag(moveEvent);
this._onMissingBrowserSplitUp = () => this.endMissingBrowserSplitDrag();
document.addEventListener('mousemove', this._onMissingBrowserSplitMove);
document.addEventListener('mouseup', this._onMissingBrowserSplitUp, { once: true });
}
onMissingBrowserSplitDrag(event) {
if (!this._missingBrowserSplitDragging || !this._missingBrowserSplitStart || !this._missingBrowserSplitBrowser) return;
const nextWidth = this._missingBrowserSplitStart.width - (event.clientX - this._missingBrowserSplitStart.x);
this.setMissingBrowserDetailWidth(this._missingBrowserSplitBrowser, nextWidth);
}
endMissingBrowserSplitDrag() {
if (!this._missingBrowserSplitDragging) return;
this._missingBrowserSplitDragging = false;
document.removeEventListener('mousemove', this._onMissingBrowserSplitMove);
this._missingBrowserSplitBrowser?.classList.remove('is-resizing');
this._missingBrowserSplitBrowser = null;
this._missingBrowserSplitStart = null;
try {
document.body.style.userSelect = this._missingBrowserPrevUserSelect || '';
document.body.style.cursor = this._missingBrowserPrevCursor || '';
} catch (e) {}
}
resizeMissingBrowserDetailBy(browser, delta) {
const detailPane = browser.querySelector('.ml-missing-detail-pane');
if (!(detailPane instanceof HTMLElement)) return;
this.setMissingBrowserDetailWidth(browser, detailPane.getBoundingClientRect().width + delta);
}
setMissingBrowserDetailWidth(browser, width, { persist = true } = {}) {
const bounds = this.getMissingBrowserSplitBounds(browser);
const nextWidth = Math.round(Math.max(bounds.min, Math.min(bounds.max, width)));
browser.style.setProperty('--ml-missing-detail-track', `${nextWidth}px`);
if (persist) {
try {
localStorage.setItem(this.missingBrowserSplitStorageKey, String(nextWidth));
} catch (e) {}
}
}
getMissingBrowserSplitBounds(browser) {
const browserRect = browser.getBoundingClientRect();
const splitter = browser.querySelector('.ml-missing-browser-splitter');
const splitterWidth = splitter instanceof HTMLElement
? splitter.getBoundingClientRect().width
: 10;
const available = Math.max(1, browserRect.width - splitterWidth);
const minListWidth = Math.min(360, Math.max(240, Math.floor(available * 0.28)));
const max = Math.max(1, available - minListWidth);
const min = Math.min(max, Math.min(420, Math.max(300, Math.floor(available * 0.3))));
return { min, max };
}
wireMissingModelDetail(container, missing, missingIndex) {
this.wireLocalMatchButtons(container, missing, missingIndex);
this.wireDownloadSearchPanel(container, missing);