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).
This commit is contained in:
Vito Sansevero
2026-10-20 21:19:55 -07:00
parent 693ecd5770
commit 14992a4c6b
4 changed files with 53 additions and 3 deletions
+7
View File
@@ -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:
+33
View File
@@ -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({
+10 -2
View File
@@ -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.
+3 -1
View File
@@ -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. */