From 0f0321a3023e68c0f1e1c7225c57ec85e0b0df24 Mon Sep 17 00:00:00 2001 From: FreeShard Bot Date: Tue, 20 Oct 2026 15:43:01 +0200 Subject: [PATCH] fix(#405): Phone: toast covers the selection sheet's last row; 'Move to On phone width the toast's bottom offset now adds --selection-bar-height, which FolderPane already measures on the section that contains the toast, so the toast sits above the fixed selection sheet and 'Move to trash' stays tappable. New mobile E2E test checks the toast and sheet don't overlap and does a trial click on 'Move to trash'; DESIGN.md toast entry updated. Refs #405 Co-Authored-By: Claude Opus 5.5 --- DESIGN.md | 2 +- e2e/toast-selection-bar.spec.ts | 37 +++++++++++++++++++++++++++++++++ web/src/styles/app.css | 7 ++++++- 3 files changed, 44 insertions(+), 2 deletions(-) create mode 100644 e2e/toast-selection-bar.spec.ts diff --git a/DESIGN.md b/DESIGN.md index 56de047..a415412 100644 --- a/DESIGN.md +++ b/DESIGN.md @@ -119,7 +119,7 @@ These follow Calendar's rules (#238). These follow the PDF recipes (#241). -- **Toast (p.16).** The feedback of an action in a folder is a toast at the bottom right of the window (full width less the margin on the phone): surface fill, hairline border, tile radius, no shadow, and a 3 px left edge. The edge is teal (`--data`) for a success and `--danger` for an outcome with failures, such as "0 of 1 copied". A close X sits in the toast's bottom-right corner. A success fades after 5 s; an error stays until it is closed or replaced, and its details stay in the failure box above the list. The text is the page's `role="status"` live region, and an empty toast has no chrome and no size. +- **Toast (p.16).** The feedback of an action in a folder is a toast at the bottom right of the window (full width less the margin on the phone, where it stands above the selection sheet while a selection exists, #405): surface fill, hairline border, tile radius, no shadow, and a 3 px left edge. The edge is teal (`--data`) for a success and `--danger` for an outcome with failures, such as "0 of 1 copied". A close X sits in the toast's bottom-right corner. A success fades after 5 s; an error stays until it is closed or replaced, and its details stay in the failure box above the list. The text is the page's `role="status"` live region, and an empty toast has no chrome and no size. - **Dropdown menu (p.18).** The item menu lists what opens or looks at the item first ("Download", "Share", "Details"), then what changes it ("Rename", "Move", "Copy", the favourite toggle). The destructive "Move to trash" comes last, in `--danger`, under a hairline. The active entry (hover or focus) rests on `--ink` at 5 %, never teal or amber. - **Segmented control (p.19).** "List / Gallery" is one control: a shared hairline around both segments. The current segment takes the surface fill and a hairline of its own; the other is transparent with muted text. On phone width the segments show line icons (lines with dots, four squares) at 44 px width and keep their names as `aria-label`. - **Status pill (data table, p.15).** A status, an access level or a role is an outline pill (hairline, pill radius, no fill) with a 6 px dot. The dot is teal for a working connection, `--danger` (also the text) for one that needs signing in again and for an orphaned share, and `--ink-muted` where the pill only labels: "Can view", "Can edit", "Admin", and a connection that is unavailable or has no library yet. diff --git a/e2e/toast-selection-bar.spec.ts b/e2e/toast-selection-bar.spec.ts new file mode 100644 index 0000000..cd4ce3e --- /dev/null +++ b/e2e/toast-selection-bar.spec.ts @@ -0,0 +1,37 @@ +import { expect, test, type Page, type TestInfo } from '@playwright/test'; + +// #405: on the phone the toast sits above the fixed selection sheet, so every action in the sheet stays reachable +// while it shows. +const tag = (info: TestInfo) => `${info.project.name}-${info.repeatEachIndex}-${info.retry}`; +const crumbs = (page: Page) => page.getByRole('navigation', { name: /Pfad|Path/ }); +const bar = (page: Page) => page.getByRole('group', { name: /^(Aktionen für die Auswahl|Actions for the selection)$/ }); + +test('#405 the toast does not cover the selection sheet on the phone', async ({ page }, info) => { + test.skip(!info.project.name.startsWith('mobile'), 'the sheet is fixed at the bottom only on the phone'); + const dir = `toast-sheet-${tag(info)}`; + await page.goto('/'); + await expect(crumbs(page)).toBeVisible(); + const { csrfToken } = await (await page.request.get('/api/v1/me')).json(); + const headers = { 'x-csrf-token': csrfToken, origin: new URL(page.url()).origin }; + expect((await page.request.post('/api/v1/sources/home/mkdir', { headers, data: { path: `/${dir}` } })).status()).toBe(201); + await page.goto(`/home/${encodeURIComponent(dir)}`); + await expect(crumbs(page)).toContainText(dir); + + await page.getByRole('button', { name: /^(Hochladen|Upload)$/ }).and(page.locator('[aria-haspopup="menu"]')).click(); + await page.getByRole('menuitem', { name: /^(Neuer Ordner|New folder)$/ }).click(); + const d = page.getByRole('dialog', { name: /Neuer Ordner|New folder/ }); + await d.getByRole('textbox').fill('Sub'); + await d.getByRole('textbox').press('Enter'); + await expect(d).toHaveCount(0); + + const toast = page.locator('.toast.shown'); + await expect(toast).toContainText(/Sub/); + await expect(bar(page)).toBeVisible(); + const trash = bar(page).getByRole('button', { name: /^(In den Papierkorb|Move to trash)$/ }); + await expect(trash).toBeVisible(); + + const t = (await toast.boundingBox())!; + const b = (await bar(page).boundingBox())!; + expect(t.y + t.height).toBeLessThanOrEqual(b.y + 0.5); + await trash.click({ trial: true, timeout: 2000 }); +}); diff --git a/web/src/styles/app.css b/web/src/styles/app.css index 66f6820..e7d45ee 100644 --- a/web/src/styles/app.css +++ b/web/src/styles/app.css @@ -907,8 +907,13 @@ a:where(:hover) { text-decoration: underline; } .toast-text { margin: 0; } .toast.shown .toast-text { margin: 0 0 var(--space-1); font-size: var(--fs-small); color: var(--ink); overflow-wrap: anywhere; } .toast-close .icon { width: 16px; height: 16px; background: var(--ink-muted); } +/* On the phone the toast stands above the fixed selection sheet (#405); the folder measures the sheet into + --selection-bar-height, 0 without a selection. */ @media (max-width: 600px) { - .toast { left: var(--space-3); right: var(--space-3); bottom: var(--space-3); max-width: none; } + .toast { + left: var(--space-3); right: var(--space-3); bottom: calc(var(--selection-bar-height, 0px) + var(--space-3)); + max-width: none; + } .toast-text { flex: 1; } }