From a0e00b8044492f4b7f8e4849a8502f64448c770a Mon Sep 17 00:00:00 2001 From: Oier Bravo Urtasun Date: Mon, 18 May 2026 01:20:20 +0200 Subject: [PATCH] feat(fase-9): cap reading column at max-w-2xl on desktop detail routes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes 9.2 (desktop reading width). - (app)/+layout.svelte wraps detail routes (/lists/[id], /tasks/[id], /notes/[id]) in a max-w-2xl mx-auto container at the md breakpoint and up. Mobile ( --- apps/web/src/routes/(app)/+layout.svelte | 19 +++++- apps/web/tests/e2e/reading-width.test.ts | 78 ++++++++++++++++++++++++ 2 files changed, 96 insertions(+), 1 deletion(-) create mode 100644 apps/web/tests/e2e/reading-width.test.ts diff --git a/apps/web/src/routes/(app)/+layout.svelte b/apps/web/src/routes/(app)/+layout.svelte index c180e08..f3e832a 100644 --- a/apps/web/src/routes/(app)/+layout.svelte +++ b/apps/web/src/routes/(app)/+layout.svelte @@ -25,6 +25,13 @@ const detailRoutePattern = /^\/(lists\/[0-9a-f-]+(?:\/session)?|tasks\/[0-9a-f-]+|notes\/[0-9a-f-]+)\/?$/; const isDetailRoute = $derived(detailRoutePattern.test($page.url.pathname)); + // Fase 9.2: cap the reading column at max-w-2xl on >=md viewports for + // content detail routes (lists/[id], tasks/[id], notes/[id]). Mobile + // stays full-width. Shopping session is explicitly excluded — the mode + // is immersive and fills the viewport. + const readingRoutePattern = /^\/(lists\/[0-9a-f-]+|tasks\/[0-9a-f-]+|notes\/[0-9a-f-]+)\/?$/; + const isReadingRoute = $derived(readingRoutePattern.test($page.url.pathname)); + // When auth resolves as unauthenticated, redirect to Keycloak. // This runs after onAuthStateChange fires (authLoading = false), so there // is no race with Supabase's async localStorage initialisation. @@ -82,7 +89,17 @@
- {@render children()} + {#if isReadingRoute} + +
+ {@render children()} +
+ {:else} + {@render children()} + {/if}
diff --git a/apps/web/tests/e2e/reading-width.test.ts b/apps/web/tests/e2e/reading-width.test.ts new file mode 100644 index 0000000..83dacd8 --- /dev/null +++ b/apps/web/tests/e2e/reading-width.test.ts @@ -0,0 +1,78 @@ +/** + * RW-series: Desktop reading-width cap (Fase 9.2). + * + * RW-01 detail routes (/lists/[id], /tasks/[id], /notes/[id]) cap the + * content column at max-w-2xl on >=md viewports — measured by the + * main element's bounding box being narrower than a 1280px viewport. + * RW-02 mobile viewports ( { + await page.goto('/lists'); + // Click any list card to get into a detail route. + const card = page.getByRole('heading', { level: 3 }).or(page.getByRole('heading', { level: 2 })).first(); + await expect(card).toBeVisible({ timeout: 10_000 }); + await card.click(); + await page.waitForURL(/\/lists\/[0-9a-f-]+\/?$/, { timeout: 10_000 }); + const url = new URL(page.url()); + const segments = url.pathname.split('/'); + return segments[segments.indexOf('lists') + 1]; +} + +test.describe('Reading width — desktop max-w-2xl cap', () => { + test('RW-01: list detail page caps content at max-w-2xl on desktop', async ({ browser }) => { + const ctx = await browser.newContext({ viewport: DESKTOP }); + const page = await ctx.newPage(); + await loginAs(page, USERS.ana); + await firstListId(page); + + // The detail content should live inside an element whose width is + // capped at max-w-2xl (672px). We assert the inner scroll container + // reports a width <= 672 + slack for padding. + const wrapper = page.locator('[data-testid="reading-column"]').first(); + await expect(wrapper).toBeVisible({ timeout: 10_000 }); + const box = await wrapper.boundingBox(); + expect(box).not.toBeNull(); + expect(box!.width).toBeLessThanOrEqual(READING_WIDTH + 4); + + await ctx.close(); + }); + + test('RW-02: list detail page is full-width on mobile ( { + const ctx = await browser.newContext({ viewport: MOBILE }); + const page = await ctx.newPage(); + await loginAs(page, USERS.ana); + await firstListId(page); + + const wrapper = page.locator('[data-testid="reading-column"]').first(); + await expect(wrapper).toBeVisible({ timeout: 10_000 }); + const box = await wrapper.boundingBox(); + expect(box).not.toBeNull(); + // Mobile viewport is 390 — wrapper should fill it (allow some padding). + expect(box!.width).toBeGreaterThan(380); + await ctx.close(); + }); + + test('RW-03: shopping session stays full-width on desktop (immersive)', async ({ browser }) => { + const ctx = await browser.newContext({ viewport: DESKTOP }); + const page = await ctx.newPage(); + await loginAs(page, USERS.ana); + const id = await firstListId(page); + await page.goto(`/lists/${id}/session`); + // The session view is allowed to occupy the full viewport. The + // reading-column wrapper must NOT be present in this route. + await expect(page.locator('[data-testid="reading-column"]')).toHaveCount(0); + await ctx.close(); + }); +});