From 14992a4c6b274d11f99e6616fa9282825b738be3 Mon Sep 17 00:00:00 2001 From: Vito Sansevero Date: Tue, 20 Oct 2026 21:19:55 -0700 Subject: [PATCH] fix(images): keep the discovery alert clear of the Rescan toast A rescan that leaves the Images discovery alert visible could show its fixed bottom toast over the alert on a short window. The concept's toast keeps its placement; the production-only alert now scrolls into view only as far as needed, with a 96px block-end scroll margin (D42). --- TESTING.md | 7 ++++++ tests/browser/workspace-images.spec.mjs | 33 +++++++++++++++++++++++++ web/js/workspace/images.mjs | 12 +++++++-- web/js/workspace/workspace.css | 4 ++- 4 files changed, 53 insertions(+), 3 deletions(-) diff --git a/TESTING.md b/TESTING.md index d7cf4f40..be97cbec 100644 --- a/TESTING.md +++ b/TESTING.md @@ -619,6 +619,13 @@ Recorded deviations on this page (spec §8): and no tag row. `positive_links` on `/__test__/concept/reset` links the newest images to their positive alone, so the inspector comparison sees the positive's category ("Positive prompt category: " hidden prefix, D21) and tag filters. +- **D42**: the concept never shows a toast and a page alert together. Production keeps + the concept's fixed toast (centred, 22px from the bottom, 12px at 700px and below) + and, when an Import → Rescan toast leaves the Images discovery alert + (`.options-status`), scrolls that alert into view only as far as needed, with + `scroll-margin-block-end: 96px`, so it is never under the toast. + `Rescan toast does not cover the discovery alert it leaves` ends the window inside + the toast's band at 1600 and 390 pixels and measures both boxes. `tests/browser/inspector-concept.spec.mjs` compares the side panel and the viewer's full details (#249 D4) for alpine_00001.png, at 1600x1100 and 390x844 in both themes: diff --git a/tests/browser/workspace-images.spec.mjs b/tests/browser/workspace-images.spec.mjs index 5395959d..9bba05ed 100644 --- a/tests/browser/workspace-images.spec.mjs +++ b/tests/browser/workspace-images.spec.mjs @@ -1559,6 +1559,39 @@ test('unreachable sources and categories announce one error', async ({ page }) = await expect(page.getByRole('alert')).toHaveCount(0); }); +// PR D-b Task 3 (D42): the shared toast keeps the concept's fixed bottom placement, so a discovery alert that a rescan +// leaves visible must not stay under it. The window ends just below the alert, inside the toast's band, as on a short +// window or a phone with its browser bars shown. +const boxOf = locator => locator.evaluate(node => { + const { top, right, bottom, left } = node.getBoundingClientRect(); + return { top, right, bottom, left }; +}); +const overlaps = (a, b) => a.left < b.right && b.left < a.right && a.top < b.bottom && b.top < a.bottom; +for (const width of [1600, 390]) { + test(`Rescan toast does not cover the discovery alert it leaves at ${width}px`, async ({ page }) => { + await page.setViewportSize({ width, height: 1100 }); + await page.route('**/prompt_manager/library/options*', route => route.abort('failed')); + await page.goto(workspace); + await expect(cards(page)).toHaveCount(2); + const alert = page.getByRole('alert'); + const failed = 'Categories could not be loaded. Rescan from Import to retry.'; + await expect(alert).toHaveText(failed); + const toastInset = width > 700 ? 22 : 12; + await page.setViewportSize({ width, height: Math.ceil((await boxOf(alert)).bottom) + toastInset + 16 }); + await rescan(page); + await expect(toast(page)).toHaveText(/^Rescan finished\. /); + await expect(alert).toHaveText(failed); + await expect.poll(async () => overlaps(await boxOf(alert), await boxOf(toast(page)))).toBe(false); + const [shown, placed] = [await boxOf(alert), await boxOf(toast(page))]; + expect(shown.top).toBeGreaterThanOrEqual(0); + expect(shown.bottom).toBeLessThanOrEqual(placed.top); + // The concept's toast stays where the concept puts it: centred, its inset from the bottom. + const viewport = page.viewportSize(); + expect(Math.round(viewport.height - placed.bottom)).toBe(toastInset); + expect(Math.abs((placed.left + placed.right) / 2 - viewport.width / 2)).toBeLessThan(1); + }); +} + // #249 final review m8: D36's alphabetical categories hold whatever order the server sends. test('toolbar categories are alphabetical regardless of server order', async ({ page }) => { await page.route('**/prompt_manager/library/options*', route => route.fulfill({ diff --git a/web/js/workspace/images.mjs b/web/js/workspace/images.mjs index 29d480b1..e9d2aeb9 100644 --- a/web/js/workspace/images.mjs +++ b/web/js/workspace/images.mjs @@ -638,15 +638,23 @@ export function mountImages(main, api, parentScope, {embedded = false, fetchPage if (scope.signal.aborted) return; renderLoadStatus(); if (found) reset(); - if (!scope.signal.aborted) showWorkspaceToast(rescanFinished(result)); + if (!scope.signal.aborted) announceRescan(rescanFinished(result)); } catch { // A failed rescan leaves the collection and any discovery alert as they were. - if (!scope.signal.aborted) showWorkspaceToast(RESCAN_FAILED); + if (!scope.signal.aborted) announceRescan(RESCAN_FAILED); } finally { rescanning = false; importDialog?.setRescanBusy(false); } } + /** + * D42: the concept's toast stays fixed at the bottom, so a discovery alert the rescan leaves is scrolled, only when + * it must be, clear of the toast's band (its `scroll-margin-block-end`). + */ + function announceRescan(message) { + showWorkspaceToast(message); + if (!optionsStatus.hidden) optionsStatus.scrollIntoView({block:'nearest', inline:'nearest'}); + } async function refreshInspectionCollection() { if (scope.signal.aborted) return; // Numeric roots may now name another source; retire the old stream first. diff --git a/web/js/workspace/workspace.css b/web/js/workspace/workspace.css index 97270f71..5b9e85a1 100644 --- a/web/js/workspace/workspace.css +++ b/web/js/workspace/workspace.css @@ -174,7 +174,9 @@ main:focus-visible { outline: 2px solid var(--ca-focus); outline-offset: 4px; } @container images-page (inline-size < 600px) { .image-workspace { grid-template-columns: minmax(0, 1fr); } } -.options-status { font-size: 12px; padding-inline: 4px; } +/* #249 D42: a discovery alert a rescan leaves scrolls clear of the concept's fixed toast (22px inset, 12px at 700px and + below, two 12px lines at 390px), never under it; the toast itself keeps the concept's placement. */ +.options-status { font-size: 12px; padding-inline: 4px; scroll-margin-block-end: 96px; /* D42 */ } /* Until their pages rebuild (PR E Library, PR F history): the Library's and the recorded run's prompt blocks keep their spacing, the Archive negative dialog keeps `section.prompt-block`, and the shared prompt sender keeps its dialog and send button. */