fix(ui): keep gallery thumbnails large and adjustable
This commit is contained in:
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 |
@@ -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');
|
||||
|
||||
@@ -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 => {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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; }
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user