fix(ui): keep gallery thumbnails large and adjustable

This commit is contained in:
Vito Sansevero
2026-10-16 11:03:33 -07:00
parent 2e7fac4b70
commit 9e2b271824
9 changed files with 74 additions and 6 deletions
Binary file not shown.

Before

Width:  |  Height:  |  Size: 89 KiB

After

Width:  |  Height:  |  Size: 94 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 92 KiB

After

Width:  |  Height:  |  Size: 97 KiB

+26
View File
@@ -92,6 +92,32 @@ test('a slower directory response cannot overwrite the latest folder', async ({
await expect(page.getByAltText('cat & dog.png', { exact: true })).toHaveCount(0);
});
test('large galleries scroll instead of squeezing thumbnails and size is adjustable', async ({ page, request }) => {
await expect(await request.post('/__test__/reset', { data: { extra_images: 52 } })).toBeOK();
await openHost(page);
await createNode(page, 'KikoLocalImageLoader');
await page.locator('.kiko-folder-name').filter({ hasText: /^bulk$/ }).click();
await expect(page.locator('.kiko-card-info-panel')).toHaveCount(50);
const loader = page.locator('.ca-image-browser');
await loader.evaluate(element => { element.style.width = '800px'; element.style.height = '650px'; });
const size = loader.getByRole('slider', { name: 'Thumbnail size' });
await expect(size).toHaveValue('224');
const media = loader.locator('.kiko-card-media-wrapper').nth(1);
const before = await media.boundingBox();
expect(before.height).toBeGreaterThanOrEqual(200);
expect(await loader.locator('.kiko-cardholder').evaluate(element => element.scrollHeight > element.clientHeight)).toBe(true);
await size.fill('320');
await size.dispatchEvent('input');
expect((await media.boundingBox()).height).toBeGreaterThan(before.height);
await expect(loader.locator('.ca-range output')).toHaveText('320 px');
await size.focus();
await page.keyboard.press('ArrowLeft');
await expect(size).toHaveValue('304');
await loader.evaluate(element => { element.style.width = '320px'; });
expect(await loader.evaluate(element => element.scrollWidth - element.clientWidth)).toBeLessThanOrEqual(1);
await expect(media.locator('img')).toHaveCSS('object-fit', 'contain');
});
test('removing a loader cancels delayed autocomplete and detaches theme observers', async ({ page }) => {
await openHost(page);
const id = await createNode(page, 'KikoLocalImageLoader');
+11 -1
View File
@@ -47,7 +47,8 @@ test.beforeEach(async ({ page, context, request, baseURL }, testInfo) => {
});
});
test('actual host preserves the widget through renderer changes, theme changes and save/reload', async ({ page }, testInfo) => {
test('actual host preserves the widget through renderer changes, theme changes and save/reload', async ({ page, request }, testInfo) => {
await expect(await request.post(`${fixture}/__test__/reset`, { data: { extra_images: 52 } })).toBeOK();
await page.evaluate(async () => {
const { app } = await import('/scripts/app.js');
await app.ui.settings.setSettingValue('Comfy.VueNodes.Enabled', false);
@@ -75,7 +76,16 @@ test('actual host preserves the widget through renderer changes, theme changes a
});
expect(node).toBeTruthy();
expect(node.widgets_values || []).not.toContainEqual(expect.objectContaining({ className: 'ca-image-browser' }));
await loader.getByRole('button', { name: 'Open folder bulk', exact: true }).click();
await expect(loader.locator('.kiko-card-info-panel')).toHaveCount(50);
const media = await loader.locator('.kiko-card-media-wrapper').nth(1).boundingBox();
expect(media.height / media.width).toBeCloseTo(1, 1);
expect(media.height).toBeGreaterThan(150);
expect(await loader.locator('.kiko-cardholder').evaluate(element => element.clientHeight)).toBeGreaterThanOrEqual(await loader.locator('.kiko-card-media-wrapper').nth(1).evaluate(element => element.offsetHeight));
expect(await loader.locator('.kiko-cardholder').evaluate(element => element.scrollHeight > element.clientHeight)).toBe(true);
await page.screenshot({ path: testInfo.outputPath(renderer ? 'nodes2.png' : 'classic.png') });
await loader.getByRole('button', { name: 'Open folder ..', exact: true }).click();
await expect(loader.getByAltText('cat & dog.png', { exact: true })).toBeVisible();
}
for (const theme of ['light', 'dark']) {
await page.evaluate(async theme => {
+1 -1
View File
@@ -56,7 +56,7 @@ app.registerExtension({
});
// Set initial size for the node
this.size = [800, 600];
this.size = [800, 900];
this.setSize(this.size);
// Initialize gallery functionality
+7
View File
@@ -5,6 +5,13 @@ blue controls and host-following light/dark themes. `../views/image-browser.js`
composes these primitives; `../kiko_local_image_loader.js` retains the controller,
API calls and existing selection persistence.
Grid thumbnails default to 224 px with square preview areas and preserve the
entire image. The Thumbnail size slider adjusts the grid from 128–384 px. A full
page scrolls within the gallery instead of compressing the rows to fit the node.
Thumbnail size is a per-node view setting for the current session.
New nodes start at 800 × 900 to leave room for the larger previews. Existing
workflows retain their saved node dimensions and can be resized normally.
`createScope()` owns listeners, delayed work and a fetch cancellation signal.
Dispose it on node removal. `mountTheme()` observes host palette changes and
removes its observers/listeners with that scope. The module has no ComfyUI imports;
+6 -3
View File
@@ -59,7 +59,7 @@
.ca-tools .ca-segments input:checked + span { background: var(--ca-accent); border-color: var(--ca-accent); color: #fff; }
.ca-tools .ca-segments input:focus-visible + span { outline: 2px solid var(--ca-focus); outline-offset: 2px; }
.ca-tools .ca-visually-hidden { position: absolute; inline-size: 1px; block-size: 1px; clip-path: inset(50%); overflow: hidden; white-space: nowrap; }
.ca-image-browser { inline-size: 100%; block-size: 100%; min-block-size: 350px; overflow: hidden; border-radius: 6px; container-type: inline-size; }
.ca-image-browser { inline-size: 100%; block-size: 100%; min-block-size: 350px; overflow: hidden; border-radius: 6px; container-type: inline-size; contain: size; }
.ca-image-browser .kiko-container-wrapper { display: flex; flex-direction: column; gap: 12px; padding: 12px; block-size: 100%; min-block-size: 350px; overflow: auto; }
.ca-image-browser .ca-browser-header { display: flex; justify-content: space-between; align-items: center; gap: 8px; border-block-end: 1px solid var(--ca-line); padding-block-end: 8px; }
.ca-image-browser .ca-browser-header strong { font-size: 13px; font-weight: 550; }
@@ -74,11 +74,11 @@
.ca-image-browser .ca-sort select { font-size: 12px; }
.ca-image-browser .ca-gallery-tools { flex-wrap: wrap; }
.ca-image-browser .ca-file-filter { flex: 1; inline-size: 100px; }
.ca-image-browser .kiko-cardholder { display: grid; grid-template-columns: repeat(auto-fill, minmax(min(100%, 140px), 1fr)); gap: 8px; align-content: start; overflow: auto; min-block-size: 100px; flex: 1; padding: 3px; }
.ca-image-browser .kiko-cardholder { display: grid; grid-template-columns: repeat(auto-fill, minmax(min(100%, var(--ca-thumbnail-size, 224px)), 1fr)); grid-auto-rows: max-content; gap: 8px; align-items: start; align-content: start; overflow: auto; min-block-size: 100px; flex: 1; padding: 3px; }
.ca-image-browser .ca-empty { grid-column: 1 / -1; text-align: center; padding: 24px 8px; color: var(--ca-muted); font-size: 12px; }
.ca-image-browser button.kiko-gallery-card { display: flex; flex-direction: column; justify-content: start; min-inline-size: 0; overflow: hidden; border: 1px solid var(--ca-border); padding: 4px; white-space: normal; background: var(--ca-field); }
.ca-image-browser button.kiko-gallery-card.kiko-selected { border: 2px solid var(--ca-focus); padding: 3px; }
.ca-image-browser .kiko-card-media-wrapper { position: relative; inline-size: 100%; aspect-ratio: 1.5; overflow: hidden; border-radius: 3px; }
.ca-image-browser .kiko-card-media-wrapper { position: relative; inline-size: 100%; aspect-ratio: 1; flex-shrink: 0; overflow: hidden; border-radius: 3px; }
.ca-image-browser .kiko-card-media-wrapper img { display: block; inline-size: 100%; block-size: 100%; object-fit: contain; }
.ca-image-browser .kiko-card-info-panel { inline-size: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; text-align: start; padding: 6px 3px 3px; font-size: 11px; }
.ca-image-browser :where(.kiko-folder-card, .kiko-audio-card) { display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 4px; block-size: 100%; inline-size: 100%; background: var(--ca-field); }
@@ -94,6 +94,9 @@
.ca-image-browser .kiko-pagination button { font-size: 11px; min-inline-size: 30px; padding-inline: 5px; }
.ca-image-browser .ca-selection { border-block-start: 1px solid var(--ca-line); padding-block-start: 8px; }
.ca-image-browser .ca-selection strong { font-size: 12px; font-weight: 500; overflow-wrap: anywhere; }
.ca-tools .ca-range { display: flex; align-items: center; gap: 10px; flex-shrink: 0; color: var(--ca-muted); font-size: 11px; }
.ca-tools .ca-range input[type="range"] { flex: 1; inline-size: 60px; min-inline-size: 0; margin: 0; padding: 0; border: 0; min-block-size: 30px; cursor: pointer; }
.ca-tools .ca-range output { min-inline-size: 44px; font-variant-numeric: tabular-nums; }
.ca-image-browser .kiko-status-message { padding: 10px; color: var(--ca-muted); border: 1px solid var(--ca-line); border-radius: 5px; font-size: 12px; }
.ca-image-browser .kiko-status-message[data-state="error"] { color: var(--ca-error-text); background: var(--ca-error-bg); border-color: var(--ca-error); }
.ca-image-browser .kiko-status-message code { overflow-wrap: anywhere; }
+19
View File
@@ -66,6 +66,25 @@ export function createButton(text, className = '') {
return element;
}
export function createSlider(label, { id, min, max, step, value, unit }, scope, onInput) {
const field = document.createElement('div');
field.className = 'ca-range';
const caption = document.createElement('label');
caption.htmlFor = id;
caption.textContent = label;
const input = document.createElement('input');
Object.assign(input, { type: 'range', id, min, max, step, value });
const output = document.createElement('output');
output.setAttribute('for', id);
output.value = `${value} ${unit}`;
scope.listen(input, 'input', () => {
output.value = `${input.value} ${unit}`;
onInput(Number(input.value));
});
field.append(caption, input, output);
return field;
}
export function createField(label, input, id) {
const field = document.createElement('label');
field.className = 'ca-field';
+4 -1
View File
@@ -1,4 +1,4 @@
import { createButton, createField, createSelect, createSegments, createSwitch, mountTheme } from '../ui/core.js';
import { createButton, createField, createSelect, createSegments, createSlider, createSwitch, mountTheme } from '../ui/core.js';
export function createImageBrowser(id, settings, scope) {
const element = document.createElement('div');
@@ -37,6 +37,9 @@ export function createImageBrowser(id, settings, scope) {
filter.className = 'ca-file-filter';
filter.setAttribute('aria-label', 'Filter this page');
element.querySelector('.ca-gallery-tools').append(filter, createSegments('Gallery layout', [['grid', 'Grid'], ['list', 'List']], `${id}-view`, scope, value => { element.querySelector('.kiko-cardholder').dataset.view = value; }));
element.querySelector('.ca-browser-footer').after(createSlider('Thumbnail size', { id: `${id}-thumbnail-size`, min: 128, max: 384, step: 16, value: 224, unit: 'px' }, scope, value => {
element.style.setProperty('--ca-thumbnail-size', `${value}px`);
}));
mountTheme(element, settings, scope);
return element;
}