diff --git a/apps/web/e2e/mocked/browser-behavior.spec.ts b/apps/web/e2e/mocked/browser-behavior.spec.ts index 05eb571..154cf48 100644 --- a/apps/web/e2e/mocked/browser-behavior.spec.ts +++ b/apps/web/e2e/mocked/browser-behavior.spec.ts @@ -34,6 +34,38 @@ test('dashboard stays available when the viewport changes to mobile', async ({ p assertNoUnexpected() }) +test('recent journal cards fit narrow screens with long entry titles', async ({ page }) => { + const longTitle = 'database-migration-'.repeat(24) + const assertNoUnexpected = await mockApi(page, (request, url) => { + if (request.method() === 'GET' && url.pathname === '/api/technical-entry') { + return { body: { data: [{ ...entry, title: longTitle }], meta: { currentPage: 1, perPage: 5, lastPage: 1, total: 1 } } } + } + return authenticatedApi(request, url) + }) + + for (const width of [320, 390]) { + await page.setViewportSize({ width, height: 844 }) + await page.goto('/dashboard') + const section = page.getByRole('region', { name: 'Recent journal entries' }) + await expect(section.getByText(longTitle)).toBeVisible() + + const layout = await section.evaluate((element) => ({ + sectionWidth: element.clientWidth, + contentWidth: element.scrollWidth, + sectionRight: element.getBoundingClientRect().right, + headerWidth: element.querySelector('header')?.clientWidth ?? 0, + headerContentWidth: element.querySelector('header')?.scrollWidth ?? 0, + viewportWidth: document.documentElement.clientWidth, + documentWidth: document.documentElement.scrollWidth, + })) + expect(layout.contentWidth, JSON.stringify(layout)).toBeLessThanOrEqual(layout.sectionWidth) + expect(layout.headerContentWidth, JSON.stringify(layout)).toBeLessThanOrEqual(layout.headerWidth) + expect(layout.sectionRight, JSON.stringify(layout)).toBeLessThanOrEqual(layout.viewportWidth) + expect(layout.documentWidth, JSON.stringify(layout)).toBeLessThanOrEqual(layout.viewportWidth) + } + assertNoUnexpected() +}) + test('desktop sidebar collapses and identifies active entry scope', async ({ page }) => { const assertNoUnexpected = await mockApi(page, authenticatedApi) await page.goto('/technical-entries/archived') diff --git a/apps/web/e2e/mocked/project-mobile-cards.spec.ts b/apps/web/e2e/mocked/project-mobile-cards.spec.ts new file mode 100644 index 0000000..0e3bb0a --- /dev/null +++ b/apps/web/e2e/mocked/project-mobile-cards.spec.ts @@ -0,0 +1,76 @@ +import { expect, test, type Locator, type Page } from '@playwright/test' +import { authenticatedApi, entry, mockApi, project, projectId } from './mock-api' + +const longWord = 'MobileLayoutWithoutSpaces'.repeat(12) + +async function expectCardFitsViewport(page: Page, card: Locator) { + await page.evaluate(() => document.fonts.ready) + const layout = await card.evaluate((element) => { + const viewport = document.documentElement.clientWidth + const cardBounds = element.getBoundingClientRect() + const title = element.querySelector('h3') + const titleBounds = title?.getBoundingClientRect() + return { + viewport, + document: document.documentElement.scrollWidth, + cardLeft: cardBounds.left, + cardRight: cardBounds.right, + titleRight: titleBounds?.right, + titleScrollWidth: title?.scrollWidth, + titleClientWidth: title?.clientWidth, + } + }) + + // Measure real layout: an implicit grid track may grow to a long word's + // min-content width even though the card appears on a single mobile column. + expect(layout.document, JSON.stringify(layout)).toBeLessThanOrEqual(layout.viewport) + expect(layout.cardLeft, JSON.stringify(layout)).toBeGreaterThanOrEqual(0) + expect(layout.cardRight, JSON.stringify(layout)).toBeLessThanOrEqual(layout.viewport) + expect(layout.titleRight, JSON.stringify(layout)).toBeLessThanOrEqual(layout.cardRight) + expect(layout.titleScrollWidth, JSON.stringify(layout)).toBeLessThanOrEqual(layout.titleClientWidth! + 1) +} + +for (const width of [320, 390]) { + test(`project list card fits a ${width}px viewport with long content`, async ({ page }) => { + await page.setViewportSize({ width, height: 844 }) + const assertNoUnexpected = await mockApi(page, (request, url) => { + if (request.method() === 'GET' && url.pathname === '/api/project') { + return { + body: { + data: [{ ...project, name: longWord, description: longWord }], + meta: { currentPage: 1, perPage: 10, lastPage: 1, total: 1 }, + }, + } + } + return authenticatedApi(request, url) + }) + + await page.goto('/projects') + const card = page.locator('section[aria-labelledby="projects-list-title"] article') + await expect(card.getByRole('heading', { name: longWord })).toBeVisible() + await expectCardFitsViewport(page, card) + assertNoUnexpected() + }) + + test(`project's linked entry card fits a ${width}px viewport with long content`, async ({ page }) => { + await page.setViewportSize({ width, height: 844 }) + const assertNoUnexpected = await mockApi(page, (request, url) => { + if (request.method() === 'GET' && url.pathname === `/api/project/${projectId}/technical-entries`) { + return { + body: { + data: [{ ...entry, title: longWord, context: longWord, tags: [{ id: 'tag-1', name: longWord }] }], + meta: { currentPage: 1, perPage: 6, lastPage: 1, total: 1 }, + }, + } + } + return authenticatedApi(request, url) + }) + + await page.goto(`/projects/${projectId}`) + await page.getByRole('tab', { name: 'Technical entries', exact: true }).click() + const card = page.getByRole('tabpanel', { name: 'Technical entries' }).locator('article') + await expect(card.getByRole('heading', { name: longWord })).toBeVisible() + await expectCardFitsViewport(page, card) + assertNoUnexpected() + }) +} diff --git a/apps/web/src/features/home/components/recent-entries-section.tsx b/apps/web/src/features/home/components/recent-entries-section.tsx index fb17811..09be1d0 100644 --- a/apps/web/src/features/home/components/recent-entries-section.tsx +++ b/apps/web/src/features/home/components/recent-entries-section.tsx @@ -34,7 +34,9 @@ function RecentEntryRow({ entry }: { entry: TechnicalEntry }) { - {entry.title} + + {entry.title} + {status ? ( -
+

Latest activity diff --git a/apps/web/src/features/home/components/recent-projects-section.tsx b/apps/web/src/features/home/components/recent-projects-section.tsx index 12d2098..de4c64a 100644 --- a/apps/web/src/features/home/components/recent-projects-section.tsx +++ b/apps/web/src/features/home/components/recent-projects-section.tsx @@ -46,7 +46,7 @@ export function RecentProjectsSection({ return (

diff --git a/apps/web/src/features/home/pages/home-page.tsx b/apps/web/src/features/home/pages/home-page.tsx index f853e8d..f3d8db0 100644 --- a/apps/web/src/features/home/pages/home-page.tsx +++ b/apps/web/src/features/home/pages/home-page.tsx @@ -65,7 +65,9 @@ function HomePage() { tags={{ isPending: tags.isPending, total: tags.data?.meta.total }} /> -
+ {/* An explicit zero-minimum mobile track lets long journal titles wrap + inside their card instead of widening the whole dashboard. */} +
-
+
{entries.map((entry) => { const type = presentTechnicalEntryType(entry.type); const TypeIcon = type.icon; @@ -57,22 +57,22 @@ export function TechnicalEntriesSection({ // navigation target and avoids nesting the title link inside it. -
-
-
+
+
+
-
+

{type.label}

-

- {entry.title} +

+ {entry.title}

-
    + {/* A zero-minimum column and item let long project identifiers wrap + inside a phone-width card instead of sizing the entire grid. */} +
      {projects.map((project) => { const status = presentProjectStatus(project.status); return ( -
    • +
    • -
      -

      - {project.name} +
      +

      + {project.name}

      {project.description ? ( - + {project.description} ) : ( diff --git a/docs/images/issue-13-dashboard-mobile.png b/docs/images/issue-13-dashboard-mobile.png new file mode 100644 index 0000000..ef751cb Binary files /dev/null and b/docs/images/issue-13-dashboard-mobile.png differ diff --git a/docs/images/issue-13-linked-entries-mobile.png b/docs/images/issue-13-linked-entries-mobile.png new file mode 100644 index 0000000..65418c9 Binary files /dev/null and b/docs/images/issue-13-linked-entries-mobile.png differ diff --git a/docs/images/issue-13-projects-mobile.png b/docs/images/issue-13-projects-mobile.png new file mode 100644 index 0000000..b996de9 Binary files /dev/null and b/docs/images/issue-13-projects-mobile.png differ