diff --git a/TESTING.md b/TESTING.md index b146454..543d494 100644 --- a/TESTING.md +++ b/TESTING.md @@ -838,9 +838,15 @@ Recorded deviations on this page: star wherever the read returns `favorite`, no checkbox or selection bar (Images only, #259), the image opens the Viewer. A saved variant with no images shows the concept's `.empty` "Saved variant · no runs yet." beside the grid (the concept puts it inside - `.image-grid`, which only holds positioned cards here). The embedded collection keeps - production's layout row, "Linked images" caption with "N loaded · N linked images" - and the selected-image side panel, which the concept's gallery does not show. With no + `.image-grid`, which only holds positioned cards here). The concept's "Shared gallery" + heading is the gallery's only heading and names its region: the embedded collection's + "Linked images" heading and "N loaded · N linked images" count are removed, since they + repeated it (maintainer ruling, 2026-10-21). Two production pieces stay, which the + concept's gallery does not show: the layout row (Grid/List/Masonry and Thumbnail size), + because the legacy gallery let people change layout and density and nothing else on the + page offers it, and the selected-image side panel, the one shared inspector the Images + page uses (spec §3.3), so an image's details stay reachable without opening the viewer. + The All images and Repeat run dialogs keep their collection caption. With no saved variant the page shows every image of the positive under "All images for this positive, including unassigned images. …" and no empty copy (a state the concept never reaches). diff --git a/tests/browser/workspace-history.spec.mjs b/tests/browser/workspace-history.spec.mjs index e7352f7..054b977 100644 --- a/tests/browser/workspace-history.spec.mjs +++ b/tests/browser/workspace-history.spec.mjs @@ -969,6 +969,17 @@ test('shared gallery uses concept cards, image opens the viewer, empty copy',asy await expect(heading.locator(':scope > span.small')).toHaveText('No negative · same positive prompt'); await expect(section.locator('.empty')).toHaveText('Saved variant · no runs yet.');await expect(section.locator('.empty')).toBeVisible(); await expect(section.locator('article[data-image-key]')).toHaveCount(0); + // D47 (maintainer ruling 2026-10-21): the concept's "Shared gallery" heading is the gallery's only heading, so the + // collection's "Linked images" heading and its "N loaded · N linked images" count are gone; the layout row and the + // selected-image side panel stay. + await expect(section.locator('.section-caption')).toHaveCount(0); + await expect(section.getByRole('heading',{name:'Linked images',exact:true})).toHaveCount(0); + await expect(section.getByText(/ loaded · /)).toHaveCount(0); + // Outside the side panel (whose own heading names the selected image), the gallery has one section heading. + expect(await section.evaluate(node=>[...node.querySelectorAll('h1, h2')].filter(item=>!item.closest('aside')).map(item=>item.textContent))).toEqual(['Shared gallery']); + await expect(section.getByRole('group',{name:'Image layout',exact:true})).toBeVisible(); + await expect(section.getByRole('combobox',{name:'Thumbnail size',exact:true})).toBeVisible(); + await expect(section.getByRole('complementary',{name:'Selected image details',exact:true})).toBeVisible(); // As in the concept, the gallery follows the variants panel; recorded executions (#209) stay below it. const order=await page.locator('.library-history').evaluate(node=>[...node.querySelectorAll('section[aria-label]')].map(item=>item.getAttribute('aria-label')).filter(name=>['Saved negative variants','Shared gallery','Recorded executions'].includes(name))); expect(order).toEqual(['Saved negative variants','Shared gallery','Recorded executions']); @@ -997,6 +1008,11 @@ test('shared gallery uses concept cards, image opens the viewer, empty copy',asy await page.keyboard.press('Escape');await expect(viewer).toBeHidden();await expect(openButton).toBeFocused(); await expect(section.locator(':scope > .panel-heading')).toHaveCSS('justify-content','space-between'); await expect(section).toHaveCSS('margin-top','24px'); + // The D47 ruling covers the shared gallery only: the All images dialog's collection keeps its own caption. + await variantsPanel(page).getByRole('button',{name:'All images for this positive',exact:true}).click(); + const all=page.getByRole('dialog',{name:'All images for this positive',exact:true}); + await expect(all.locator('.section-caption')).toContainText(/ loaded · /); + await page.keyboard.press('Escape');await expect(all).toHaveCount(0); // At 390px the heading stays inside the gallery and the page never scrolls sideways. await page.setViewportSize({width:390,height:844}); await expect.poll(()=>page.evaluate(()=>document.documentElement.scrollWidth-document.documentElement.clientWidth)).toBeLessThanOrEqual(1); diff --git a/tests/browser/workspace-library-editor.spec.mjs b/tests/browser/workspace-library-editor.spec.mjs index 97cc644..3101c4a 100644 --- a/tests/browser/workspace-library-editor.spec.mjs +++ b/tests/browser/workspace-library-editor.spec.mjs @@ -708,7 +708,8 @@ test('archive hides a pair, Show archived finds it and Restore keeps it selected // #249 D8 ruling (a): the Library-embedded gallery has no select checkbox and no selection bar. await expect(page.getByRole('region',{name:'Shared gallery',exact:true}).locator('.image-overlays input[type="checkbox"]')).toHaveCount(0); await expect(page.locator('#bulkbar, .bulkbar')).toHaveCount(0); - await expect(page.getByRole('region',{name:'Shared gallery',exact:true}).getByText('1 loaded · 1 linked image',{exact:true})).toBeVisible(); + // D47: the archived pair's gallery is named by its "Shared gallery" heading, with no separate loaded count. + await expect(page.getByRole('region',{name:'Shared gallery',exact:true}).locator('.panel-heading .small')).toHaveText(/ · Archived · same positive prompt$/); expect(galleryReads.at(-1).get('archive')).toBe('archived');expect(galleryReads.at(-1).get('pair_id')).toBe(String(f.pair_id)); await expect(headingActions(page).getByRole('button',{name:/^Send prompt pair to ComfyUI/})).toBeEnabled(); const writes=page.writes.length; diff --git a/web/js/workspace/images.mjs b/web/js/workspace/images.mjs index 6405bf4..2e7923d 100644 --- a/web/js/workspace/images.mjs +++ b/web/js/workspace/images.mjs @@ -96,7 +96,11 @@ function deliverFavorite(key, favorite) { for (const target of embeddedFavoriteTargets) target(key, favorite); } -export function mountImages(main, api, parentScope, {embedded = false, fetchPage, emptyText} = {}) { +/** + * `caption: false` (the history page's shared gallery, D47) leaves out the embedded collection's "Linked images" heading + * and count, whose page heading already names it; `focusTarget` then takes focus where that heading did. + */ +export function mountImages(main, api, parentScope, {embedded = false, fetchPage, emptyText, caption: showCaption = true, focusTarget = null} = {}) { const scope = createScope(); const dispose = () => scope.dispose(); parentScope.signal.addEventListener('abort', dispose, {once:true}); @@ -194,7 +198,7 @@ export function mountImages(main, api, parentScope, {embedded = false, fetchPage collection.setAttribute('aria-label', 'Images'); // The concept's caption replaces the old "All images" heading; the embedded gallery keeps its own heading. const caption = el('div', undefined, 'section-caption'); - const collectionHeading = embedded ? el('h2', 'Linked images') : null; + const collectionHeading = embedded && showCaption ? el('h2', 'Linked images') : null; if (collectionHeading) { collectionHeading.tabIndex = -1; caption.append(collectionHeading); } const count = el('span', 'Loading images…'); if (!embedded) count.id = 'result-count'; @@ -202,7 +206,7 @@ export function mountImages(main, api, parentScope, {embedded = false, fetchPage captionDay.hidden = true; caption.append(count); if (!embedded) caption.append(captionDay); - const focusFallback = collectionHeading || heading?.h1 || main; + const focusFallback = collectionHeading || focusTarget || heading?.h1 || main; const grid = el('div', undefined, 'image-grid'); grid.setAttribute('role', 'list'); grid.setAttribute('aria-label', 'Image collection'); const continuation = el('div', undefined, 'infinite-continuation'); @@ -217,7 +221,9 @@ export function mountImages(main, api, parentScope, {embedded = false, fetchPage const showsEmpty = !embedded || typeof emptyText === 'string'; const empty = el('div', embedded ? emptyText : 'No matching images. Try another search or category.', 'empty'); empty.hidden = true; - collection.append(caption, grid); + // D47: the history page's shared gallery is named by its own "Shared gallery" heading, so it has no caption. + if (showCaption) collection.append(caption); + collection.append(grid); if (showsEmpty) collection.append(empty); collection.append(continuation); // After a Load more or Retry press, the index of the first image that batch appends (concept: focus it). diff --git a/web/js/workspace/library.mjs b/web/js/workspace/library.mjs index 2db39e9..7fa3291 100644 --- a/web/js/workspace/library.mjs +++ b/web/js/workspace/library.mjs @@ -616,7 +616,8 @@ export function mountLibrary(main,api,parentScope,{positiveId}={}) { // The concept's shared gallery (app.js a616-621): its heading names the selected variant; the embedded // collection renders the PR C media cards. Recorded executions (#209) stay below it. /* D47 */ const context=el('span','','small');context.setAttribute('role','status'); - const galleryHeading=el('div',undefined,'panel-heading');galleryHeading.append(el('h2','Shared gallery'),context); + const galleryTitle=el('h2','Shared gallery');galleryTitle.tabIndex=-1; + const galleryHeading=el('div',undefined,'panel-heading');galleryHeading.append(galleryTitle,context); const gallery=el('section',undefined,'history-gallery');gallery.setAttribute('aria-label','Shared gallery');gallery.append(galleryHeading); const images=el('div',undefined,'library-linked-images');gallery.append(images); const recorded=el('div',undefined,'library-recorded-container'); @@ -690,7 +691,7 @@ export function mountLibrary(main,api,parentScope,{positiveId}={}) { updateGalleryContext(); galleryScope?.dispose();galleryScope=createScope();images.replaceChildren(); // An archived pair's images are read in the archived scope; the gallery stays authoritative. - mountImages(images,api,galleryScope,{embedded:true,emptyText:pairId===null?undefined:'Saved variant · no runs yet.',fetchPage:({parameters,signal})=>{const query=new URLSearchParams(parameters);if(pairId!==null) query.set('pair_id',String(pairId));if(archived) query.set('archive','archived');return api.get(`/prompt_manager/library/${positiveId}/images?${query}`,{signal});}}); + mountImages(images,api,galleryScope,{embedded:true,caption:false,focusTarget:galleryTitle,emptyText:pairId===null?undefined:'Saved variant · no runs yet.',fetchPage:({parameters,signal})=>{const query=new URLSearchParams(parameters);if(pairId!==null) query.set('pair_id',String(pairId));if(archived) query.set('archive','archived');return api.get(`/prompt_manager/library/${positiveId}/images?${query}`,{signal});}}); updateSend();updateCompare(); } /** A metadata receipt names no pair, so the page re-reads its own selection through the exact-ID route. */