From 5c14d70b910e1aa0a7304ae73d3d06ddcf94ab53 Mon Sep 17 00:00:00 2001 From: Chris0Jeky Date: Sun, 6 Sep 2026 04:03:54 +0100 Subject: [PATCH 1/2] Fix archive mobile control wrapping --- .../taskdeck-web/src/views/ArchiveView.vue | 41 ++++++ .../tests/e2e/mobile-responsive.spec.ts | 126 +++++++++++++++++- 2 files changed, 166 insertions(+), 1 deletion(-) diff --git a/frontend/taskdeck-web/src/views/ArchiveView.vue b/frontend/taskdeck-web/src/views/ArchiveView.vue index 37c00608d..4dd070630 100644 --- a/frontend/taskdeck-web/src/views/ArchiveView.vue +++ b/frontend/taskdeck-web/src/views/ArchiveView.vue @@ -372,6 +372,14 @@ onMounted(() => { justify-content: space-between; align-items: center; gap: var(--s-3, 12px); + flex-wrap: wrap; +} + +.paper-archive__section-title { min-width: 0; } + +.paper-archive__toggle-hidden { + max-width: 100%; + white-space: normal; } .paper-archive__helper { @@ -382,7 +390,9 @@ onMounted(() => { .paper-archive__toolbar { display: flex; + align-items: center; gap: var(--s-2, 8px); + flex-wrap: wrap; margin-bottom: var(--s-4, 16px); } @@ -455,6 +465,8 @@ onMounted(() => { } .paper-archive__input { + flex: 1 1 12rem; + min-width: 0; padding: var(--s-2, 8px) var(--s-3, 12px); border: 1px solid var(--line, #d8d0bf); border-radius: var(--r-2, 4px); @@ -470,10 +482,39 @@ onMounted(() => { box-shadow: 0 0 0 2px var(--ember-bloom, #a8421f1a); } +.paper-archive__refresh { + max-width: 100%; + white-space: normal; +} + +.paper-archive :deep(.pbtn:focus-visible) { + outline: 2px solid var(--ember, #a8421f); + outline-offset: 2px; +} + /* Keep board and item recovery actions reachable on narrow screens. The * desktop row stays unchanged; phone rows stack their content, while the * four board actions wrap into two comfortable tap-target columns. */ @media (max-width: 640px) { + .paper-archive__section-header { + align-items: flex-start; + flex-direction: column; + } + + .paper-archive__toggle-hidden { + width: 100%; + } + + .paper-archive__toolbar { + align-items: stretch; + flex-direction: column; + } + + .paper-archive__input, + .paper-archive__refresh { + width: 100%; + } + .paper-archive__row { align-items: stretch; flex-direction: column; diff --git a/frontend/taskdeck-web/tests/e2e/mobile-responsive.spec.ts b/frontend/taskdeck-web/tests/e2e/mobile-responsive.spec.ts index b392465dd..454d6022f 100644 --- a/frontend/taskdeck-web/tests/e2e/mobile-responsive.spec.ts +++ b/frontend/taskdeck-web/tests/e2e/mobile-responsive.spec.ts @@ -1,5 +1,5 @@ import { expect, test, type Locator, type Page } from '@playwright/test' -import { registerAndAttachSession } from './support/authSession' +import { API_BASE_URL, registerAndAttachSession } from './support/authSession' import { addCard, addColumn, createBoard } from './support/boardUiHelpers' /** @@ -520,6 +520,130 @@ test('@mobile workspace views should render correctly on small screen', async ({ expect(bodyBox!.width).toBeLessThanOrEqual(viewportSize!.width + 20) }) +test('@mobile archive controls stay inside the viewport with long localized labels', async ({ page, request }) => { + const auth = await registerAndAttachSession(page, request, 'archive-geometry', { theme: 'legacy' }) + const boardResponse = await request.post(`${API_BASE_URL}/boards`, { + headers: { Authorization: `Bearer ${auth.token}` }, + data: { name: `Archivio con nome molto lungo ${Date.now()}`, description: 'Archive geometry fixture' }, + }) + expect(boardResponse.ok()).toBeTruthy() + const board = (await boardResponse.json()) as { id: string } + + const archiveResponse = await request.put(`${API_BASE_URL}/boards/${board.id}`, { + headers: { Authorization: `Bearer ${auth.token}` }, + data: { isArchived: true }, + }) + expect(archiveResponse.ok()).toBeTruthy() + + await page.goto('/workspace/archive') + await expect(page.getByRole('heading', { name: 'Archive', exact: true })).toBeVisible() + await expect(page.locator('.paper-archive__row').first()).toBeVisible() + + await page.locator('.paper-archive__toggle-hidden').evaluate((element) => { + element.textContent = 'Mostra tutte le schede archiviate e nascoste' + }) + await page.locator('.paper-archive__refresh').evaluate((element) => { + element.textContent = 'Aggiorna inventario degli elementi archiviati' + }) + await page.locator('.paper-archive__input').evaluate((element) => { + const option = document.createElement('option') + option.value = 'localized-long-label' + option.textContent = 'Tutti i tipi di elementi archiviati' + element.append(option) + element.value = option.value + }) + + const viewportSize = page.viewportSize() + expect(viewportSize).not.toBeNull() + + await page.setViewportSize({ width: 1280, height: viewportSize!.height }) + const desktopFlow = await page.evaluate(() => { + const rectFor = (selector: string) => { + const rect = document.querySelector(selector)?.getBoundingClientRect() + if (!rect) throw new Error(`Missing geometry target: ${selector}`) + return { top: rect.top, bottom: rect.bottom } + } + + return { + sectionTitle: rectFor('.paper-archive__section-header .paper-archive__section-title'), + hiddenBoardsToggle: rectFor('.paper-archive__toggle-hidden'), + filter: rectFor('.paper-archive__input'), + refresh: rectFor('.paper-archive__refresh'), + } + }) + expect(desktopFlow.hiddenBoardsToggle.top).toBeLessThan(desktopFlow.sectionTitle.bottom) + expect(desktopFlow.hiddenBoardsToggle.bottom).toBeGreaterThan(desktopFlow.sectionTitle.top) + expect(desktopFlow.refresh.top).toBeLessThan(desktopFlow.filter.bottom) + expect(desktopFlow.refresh.bottom).toBeGreaterThan(desktopFlow.filter.top) + + for (const width of [375, 390]) { + await page.setViewportSize({ width, height: viewportSize!.height }) + await expect.poll(async () => page.evaluate(() => document.documentElement.clientWidth)).toBe(width) + + const geometry = await page.evaluate(() => { + const selectors = [ + '.paper-archive__toggle-hidden', + '.paper-archive__input', + '.paper-archive__refresh', + '.paper-archive__actions > *', + ] + const controls = selectors.flatMap((selector) => + Array.from(document.querySelectorAll(selector)), + ) + const rects = controls.map((control) => { + const rect = control.getBoundingClientRect() + return { left: rect.left, right: rect.right, top: rect.top, bottom: rect.bottom } + }) + const rectFor = (selector: string) => { + const rect = document.querySelector(selector)?.getBoundingClientRect() + if (!rect) throw new Error(`Missing geometry target: ${selector}`) + return { left: rect.left, right: rect.right, top: rect.top, bottom: rect.bottom } + } + const actionOrder = Array.from(document.querySelectorAll('.paper-archive__actions > *')) + .map((control) => control.textContent?.trim()) + + return { + clientWidth: document.documentElement.clientWidth, + scrollWidth: document.documentElement.scrollWidth, + rects, + sectionTitle: rectFor('.paper-archive__section-header .paper-archive__section-title'), + hiddenBoardsToggle: rectFor('.paper-archive__toggle-hidden'), + filter: rectFor('.paper-archive__input'), + refresh: rectFor('.paper-archive__refresh'), + actionOrder, + } + }) + + expect(geometry.scrollWidth - geometry.clientWidth).toBeLessThanOrEqual(1) + expect(geometry.rects).toHaveLength(7) + for (const rect of geometry.rects) { + expect(rect.left).toBeGreaterThanOrEqual(-1) + expect(rect.right).toBeLessThanOrEqual(width + 1) + } + expect(geometry.hiddenBoardsToggle.top).toBeGreaterThanOrEqual(geometry.sectionTitle.bottom - 1) + expect(geometry.refresh.top).toBeGreaterThanOrEqual(geometry.filter.bottom - 1) + expect(geometry.actionOrder).toEqual([ + 'View captures', + 'View decisions', + 'Restore Board', + 'Hide', + ]) + + const refresh = page.locator('.paper-archive__refresh') + await refresh.focus() + const focusGeometry = await refresh.evaluate((element) => { + const rect = element.getBoundingClientRect() + const style = getComputedStyle(element) + return { + active: document.activeElement === element, + insideViewport: rect.left >= 0 && rect.right <= window.innerWidth, + focusRing: style.outlineStyle !== 'none' || style.boxShadow !== 'none', + } + }) + expect(focusGeometry).toEqual({ active: true, insideViewport: true, focusRing: true }) + } +}) + test('@mobile board columns stack vertically without horizontal overflow', async ({ page }) => { // FE-19: On mobile the board must switch from a horizontal kanban to a // vertical card list so core navigation remains usable at ~375-412px. From ccd5b3602dd640034ce3e99ae94267bed7c017c0 Mon Sep 17 00:00:00 2001 From: Chris0Jeky Date: Sun, 6 Sep 2026 12:47:08 +0100 Subject: [PATCH 2/2] fix(archive): keep the toolbar geometry change mobile-only; 44px floor for the stacked input and refresh (review MEDIUM-1, MEDIUM-3) --- frontend/taskdeck-web/src/views/ArchiveView.vue | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/frontend/taskdeck-web/src/views/ArchiveView.vue b/frontend/taskdeck-web/src/views/ArchiveView.vue index 4dd070630..0c5bd06e8 100644 --- a/frontend/taskdeck-web/src/views/ArchiveView.vue +++ b/frontend/taskdeck-web/src/views/ArchiveView.vue @@ -390,7 +390,6 @@ onMounted(() => { .paper-archive__toolbar { display: flex; - align-items: center; gap: var(--s-2, 8px); flex-wrap: wrap; margin-bottom: var(--s-4, 16px); @@ -465,8 +464,6 @@ onMounted(() => { } .paper-archive__input { - flex: 1 1 12rem; - min-width: 0; padding: var(--s-2, 8px) var(--s-3, 12px); border: 1px solid var(--line, #d8d0bf); border-radius: var(--r-2, 4px); @@ -513,6 +510,8 @@ onMounted(() => { .paper-archive__input, .paper-archive__refresh { width: 100%; + min-width: 0; + min-height: 44px; } .paper-archive__row {