From 7eebb4257049364b94a7e5b34b774954dc975e7e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Almeida?= <57680069+zeval@users.noreply.github.com> Date: Thu, 13 Aug 2026 18:02:58 +0100 Subject: [PATCH] fix(web): align empty sidebar task message (#2632) --- .../app-sidebar/sections/tasks-section.tsx | 2 +- apps/web/components/task/task-switcher.tsx | 7 +- .../task/mobile-task-listing-display.spec.ts | 1 + .../task/sidebar-scroll-preservation.spec.ts | 20 +++- .../sidebar-empty-task-alignment/plan.md | 83 ++++++++++++++ .../task-01-align-empty-task-message.md | 103 ++++++++++++++++++ docs/specs/INDEX.md | 1 + docs/specs/ui/sidebar-empty-task-alignment.md | 34 ++++++ 8 files changed, 244 insertions(+), 7 deletions(-) create mode 100644 docs/plans/sidebar-empty-task-alignment/plan.md create mode 100644 docs/plans/sidebar-empty-task-alignment/task-01-align-empty-task-message.md create mode 100644 docs/specs/ui/sidebar-empty-task-alignment.md diff --git a/apps/web/components/app-sidebar/sections/tasks-section.tsx b/apps/web/components/app-sidebar/sections/tasks-section.tsx index 03cf239934..414696aba2 100644 --- a/apps/web/components/app-sidebar/sections/tasks-section.tsx +++ b/apps/web/components/app-sidebar/sections/tasks-section.tsx @@ -26,7 +26,7 @@ export function TasksSection({ collapsed }: TasksSectionProps) { headerAction={} grow > -
+
diff --git a/apps/web/components/task/task-switcher.tsx b/apps/web/components/task/task-switcher.tsx index 8f6652548e..bd9dc76a91 100644 --- a/apps/web/components/task/task-switcher.tsx +++ b/apps/web/components/task/task-switcher.tsx @@ -117,7 +117,12 @@ export const TaskSwitcher = memo(function TaskSwitcher(props: TaskSwitcherProps) return ( <> {loadErrorNotice} -
{t("sidebar:noTasksYet")}
+
+ {t("sidebar:noTasksYet")} +
); } diff --git a/apps/web/e2e/tests/task/mobile-task-listing-display.spec.ts b/apps/web/e2e/tests/task/mobile-task-listing-display.spec.ts index 698ced1db1..a8aa3ee288 100644 --- a/apps/web/e2e/tests/task/mobile-task-listing-display.spec.ts +++ b/apps/web/e2e/tests/task/mobile-task-listing-display.spec.ts @@ -70,6 +70,7 @@ test.describe("Mobile task listing display preferences", () => { ); await testPage.goto("/"); await expect(testPage.getByTestId("mobile-kanban-layout")).toBeVisible(); + await expect(testPage.getByTestId("app-sidebar-layout")).toBeHidden(); await expect( testPage.evaluate((key) => window.localStorage.getItem(key), VIEW_STORAGE_KEY), ).resolves.toBe(JSON.stringify("pipeline")); diff --git a/apps/web/e2e/tests/task/sidebar-scroll-preservation.spec.ts b/apps/web/e2e/tests/task/sidebar-scroll-preservation.spec.ts index 3e59f7a5f1..fc6f2d4dde 100644 --- a/apps/web/e2e/tests/task/sidebar-scroll-preservation.spec.ts +++ b/apps/web/e2e/tests/task/sidebar-scroll-preservation.spec.ts @@ -24,23 +24,33 @@ test.describe("sidebar scrolling", () => { const appSidebar = testPage.getByTestId("app-sidebar"); const taskSidebar = testPage.getByTestId("task-sidebar"); const scrollRoot = taskSidebar.locator("[data-slot='scroll-area']"); - await expect(taskSidebar.getByText("No tasks yet.")).toBeVisible(); + const emptyMessage = taskSidebar.locator("[data-slot='task-switcher-empty-state']"); + await expect(emptyMessage).toHaveText("No tasks yet."); await expect .poll(() => scrollRoot.evaluate((element) => getComputedStyle(element).backgroundColor)) .toBe("rgba(0, 0, 0, 0)"); - const [navigationBox, taskSidebarBox] = await Promise.all([ - appSidebar.locator("nav").boundingBox(), - taskSidebar.boundingBox(), - ]); + const [navigationBox, taskSidebarBox, tasksTitleBox, emptyMessageBox, emptyPaddingLeft] = + await Promise.all([ + appSidebar.locator("nav").boundingBox(), + taskSidebar.boundingBox(), + tasksToggle.locator("span").first().boundingBox(), + emptyMessage.boundingBox(), + emptyMessage.evaluate((element) => parseFloat(getComputedStyle(element).paddingLeft)), + ]); expect(navigationBox).not.toBeNull(); expect(taskSidebarBox).not.toBeNull(); + expect(tasksTitleBox).not.toBeNull(); + expect(emptyMessageBox).not.toBeNull(); expect(taskSidebarBox!.x).toBeCloseTo(navigationBox!.x, 0); expect(taskSidebarBox!.x + taskSidebarBox!.width).toBeCloseTo( navigationBox!.x + navigationBox!.width, 0, ); + expect(Math.abs(emptyMessageBox!.x + emptyPaddingLeft - tasksTitleBox!.x)).toBeLessThanOrEqual( + 0.5, + ); }); test("fades overflowing tasks and reveals the scrollbar on hover", async ({ diff --git a/docs/plans/sidebar-empty-task-alignment/plan.md b/docs/plans/sidebar-empty-task-alignment/plan.md new file mode 100644 index 0000000000..1a27db0f4f --- /dev/null +++ b/docs/plans/sidebar-empty-task-alignment/plan.md @@ -0,0 +1,83 @@ +--- +spec: docs/specs/ui/sidebar-empty-task-alignment.md +created: 2026-08-13 +status: complete +--- + +# Implementation Plan: Sidebar empty task alignment + +## Overview + +Add a stable slot marker to the shared task-switcher empty state, then override only that slot's horizontal padding inside the full-width desktop Tasks host. Extend the existing empty-sidebar E2E with a text-anchor geometry assertion and add a mobile preservation assertion for the separate phone composition. + +## Confirmed root cause + +`apps/web/components/app-sidebar/app-sidebar.tsx` gives the navigation `px-2`. The Tasks section title adds another `px-2`, so its text starts at x=16 in the rendered 1280px diagnostic viewport. `apps/web/components/app-sidebar/sections/tasks-section.tsx` intentionally uses `-mx-2` so the task scroll surface still spans the full navigation width, but `apps/web/components/task/task-switcher.tsx` gives the empty message only `px-3`. Its text therefore starts at x=12, four pixels left of the title. + +Browser evidence from an isolated mock instance confirmed the mismatch: Tasks title x=16, empty-message text anchor x=12. At 393px the desktop app sidebar had `display: none` and no visible sidebar empty message. + +--- + +## Frontend + +### Shared task-switcher empty state + +- `apps/web/components/task/task-switcher.tsx`: add `data-slot="task-switcher-empty-state"` to the existing empty-state element. Keep its base `px-3` class so standalone and mobile task-switcher hosts retain their current inset. + +### Desktop app sidebar Tasks host + +- `apps/web/components/app-sidebar/sections/tasks-section.tsx`: extend the existing full-width wrapper with `[&_[data-slot=task-switcher-empty-state]]:px-4`. Within its `-mx-2` geometry, 16px padding places the message at the same x-coordinate as the section title without shifting task rows, group headers, loading/error states, or the shared mobile surface. + +No API, store, state, copy, or backend change is required. + +## Mobile parity + +- **Desktop outcome:** the empty message aligns with the Tasks title while the list remains full-width. +- **Mobile entry point and exemplar:** phone task navigation remains the existing `SessionTaskSwitcherSheet` in `apps/web/components/task/mobile/session-task-switcher-sheet.tsx`, whose `p-2` list wrapper and shared `px-3` task content remain unchanged. +- **Presentation, hierarchy, scrolling, and touch behavior:** unchanged. The desktop AppSidebar remains hidden below `md`; the mobile drawer remains the scroll owner. +- **Coverage:** extend the existing `mobile-task-listing-display.spec.ts` phone scenario to assert the desktop app sidebar is hidden. The plan-phase rendered check already confirmed `display: none` at 393px; implementation reruns the mobile project against the fresh production build. + +## Tests + +- **What:** the empty message's left text anchor equals the Tasks title's left edge while the task sidebar remains transparent and full-width. + **File:** `apps/web/e2e/tests/task/sidebar-scroll-preservation.spec.ts`. + **How:** extend the existing empty-list test. Read the Tasks title bounding box, compute the empty message text anchor from its box plus rendered left padding, and compare the x-coordinates with subpixel tolerance. Write and run this assertion before the host override; it must fail with the confirmed 12px versus 16px coordinates, then pass after the fix. +- **What:** phone rendering continues to use the mobile composition rather than the desktop sidebar. + **File:** `apps/web/e2e/tests/task/mobile-task-listing-display.spec.ts`. + **How:** extend the existing phone Kanban scenario with a visibility assertion for `app-sidebar-layout`. This is preservation coverage; the desktop geometry assertion is the required RED regression. + +No unit test is needed because the defect is a rendered alignment relationship with no domain logic. + +## E2E Tests + +- **Scenario:** GIVEN the expanded desktop app sidebar has no tasks, WHEN the Tasks section renders, THEN **No tasks yet.** and **Tasks** share the same left text anchor while the transparent list still spans the navigation width. + **File:** `apps/web/e2e/tests/task/sidebar-scroll-preservation.spec.ts`. + **What to verify:** x-coordinate equality, transparent scroll background, and unchanged full-width bounds. +- **Scenario:** GIVEN a phone viewport, WHEN the task listing renders, THEN the desktop app sidebar remains hidden and mobile Kanban remains visible. + **File:** `apps/web/e2e/tests/task/mobile-task-listing-display.spec.ts`. + **What to verify:** `mobile-kanban-layout` is visible and `app-sidebar-layout` is hidden. + +## Verification Results + +- **RED:** the focused Chromium empty-sidebar test discovered one test and failed on the new geometry assertion with a 4px mismatch (`received: 4`, tolerance `0.5`). +- **GREEN:** the same production-build Chromium command passed its one test; the focused mobile Chrome preservation command also passed its one test. +- **Rendered geometry:** an isolated 1280x800 mock instance measured the Tasks title at x=16 and the empty-message text at x=16 (delta 0). The corrected desktop state was captured in `apps/web/.pr-assets/sidebar-empty-task-alignment.png` for PR publication. +- **Static checks:** web typecheck, targeted ESLint, targeted Prettier, the i18n ratchet, and `git diff --check` passed. +- **Cleanup:** the named browser session and isolated backend/frontend processes were stopped after capture. + +## Implementation Waves And Parallel Candidates + +Wave 1 - sequential: + +- [x] [task-01-align-empty-task-message](task-01-align-empty-task-message.md) + +The marker, desktop host override, and geometry regression describe one rendered behavior and share the same verification pass. + +## Risks + +- A global `px-4` change in `TaskSwitcher` would also shift the mobile drawer's empty state. The host-scoped selector avoids that regression. +- Testing only element boxes would measure the full-width empty-state container rather than its text. The E2E must include rendered left padding when deriving the text anchor. + +## Open Questions + +None. diff --git a/docs/plans/sidebar-empty-task-alignment/task-01-align-empty-task-message.md b/docs/plans/sidebar-empty-task-alignment/task-01-align-empty-task-message.md new file mode 100644 index 0000000000..e5571a74ef --- /dev/null +++ b/docs/plans/sidebar-empty-task-alignment/task-01-align-empty-task-message.md @@ -0,0 +1,103 @@ +--- +id: "01-align-empty-task-message" +title: "Align empty task message" +status: done +wave: 1 +depends_on: [] +plan: "plan.md" +spec: "../../specs/ui/sidebar-empty-task-alignment.md" +--- + +# Task 01: Align empty task message + +## Intent + +Align the desktop app sidebar's empty task message with its Tasks section title without changing the shared mobile task-switcher inset or the task list's full-width surface. + +## Root cause + +The desktop navigation starts with `px-2`; its Tasks title adds `px-2` and renders at x=16. The Tasks content wrapper cancels the navigation inset with `-mx-2`, while the shared empty state uses `px-3`, rendering its text at x=12. The four-pixel difference is the reported drift. + +## Acceptance + +- On the expanded desktop app sidebar, the **No tasks yet.** text anchor matches the **Tasks** title x-coordinate within subpixel tolerance. +- The empty task scroll surface stays transparent and spans the full navigation width; task rows, group headers, load errors, and other sidebar sections keep their current geometry. +- At the mobile Playwright viewport, the desktop app sidebar remains hidden and the existing mobile task composition remains visible and unchanged. + +## Regression tests (write first) + +- `apps/web/e2e/tests/task/sidebar-scroll-preservation.spec.ts`: extend the existing `keeps the empty list transparent and uses the full sidebar width` test. Locate the empty state through `data-slot="task-switcher-empty-state"`, derive its text x-coordinate as `getBoundingClientRect().x + parseFloat(getComputedStyle(element).paddingLeft)`, and compare it with the Tasks title span's bounding-box x-coordinate. Add the slot marker first, run the test before the padding override, and record the expected RED mismatch (currently x=12 versus x=16). +- `apps/web/e2e/tests/task/mobile-task-listing-display.spec.ts`: extend `uses Kanban on a phone without overwriting a saved Pipeline preference` to assert `app-sidebar-layout` is hidden while `mobile-kanban-layout` is visible. This guards the viewport boundary and is expected to remain green. + +## Implementation + +- `apps/web/components/task/task-switcher.tsx`: add `data-slot="task-switcher-empty-state"` to the existing empty-message element. Preserve its base `px-3` class. +- `apps/web/components/app-sidebar/sections/tasks-section.tsx`: add `[&_[data-slot=task-switcher-empty-state]]:px-4` to the wrapper that already owns `-mx-2`. Do not change shared task rows or mobile wrappers. + +## Files likely touched + +- `apps/web/components/task/task-switcher.tsx` +- `apps/web/components/app-sidebar/sections/tasks-section.tsx` +- `apps/web/e2e/tests/task/sidebar-scroll-preservation.spec.ts` +- `apps/web/e2e/tests/task/mobile-task-listing-display.spec.ts` +- `docs/plans/sidebar-empty-task-alignment/plan.md` +- `docs/plans/sidebar-empty-task-alignment/task-01-align-empty-task-message.md` + +## Dependencies + +None. + +## Parallelism + +Sequential. The RED geometry assertion, host-scoped class change, and desktop/mobile checks share one behavior and one production build. + +## Inputs + +- Repair behavior in `docs/specs/ui/sidebar-empty-task-alignment.md`. +- Root-cause geometry and host-scoping design in `docs/plans/sidebar-empty-task-alignment/plan.md`. +- Existing empty-list coverage in `apps/web/e2e/tests/task/sidebar-scroll-preservation.spec.ts`. +- Existing phone composition coverage in `apps/web/e2e/tests/task/mobile-task-listing-display.spec.ts`. + +## Verification + +Bootstrap a fresh worktree once if dependencies are absent: + +```bash +(cd apps && pnpm install --frozen-lockfile) +``` + +Add the slot marker and geometry assertion, then run the desktop regression before the host override (RED): + +```bash +(cd apps/web && pnpm e2e:run --project chromium tests/task/sidebar-scroll-preservation.spec.ts -- --grep "keeps the empty list transparent") +``` + +Apply the minimal host override, rerun the same command (GREEN), then run mobile preservation coverage against the fresh build: + +```bash +(cd apps/web && pnpm e2e:run --no-build --project mobile-chrome tests/task/mobile-task-listing-display.spec.ts -- --grep "uses Kanban on a phone") +``` + +Run targeted static checks: + +```bash +(cd apps/web && pnpm run typecheck) +(cd apps/web && pnpm exec eslint components/task/task-switcher.tsx components/app-sidebar/sections/tasks-section.tsx e2e/tests/task/sidebar-scroll-preservation.spec.ts e2e/tests/task/mobile-task-listing-display.spec.ts) +(cd apps/web && pnpm exec prettier --check components/task/task-switcher.tsx components/app-sidebar/sections/tasks-section.tsx e2e/tests/task/sidebar-scroll-preservation.spec.ts e2e/tests/task/mobile-task-listing-display.spec.ts) +(cd apps/web && pnpm run i18n:ratchet) +git diff --check +``` + +The managed desktop E2E run rebuilds the production web assets. Confirm each focused command discovers one test before treating it as evidence. + +## Output contract + +Report the RED and GREEN x-coordinates, exact commands and test counts, changed files, mobile preservation result, generated artifacts, cleanup evidence, risks, and synchronized task/plan statuses in the same conversation. + +## Results + +- Added a stable empty-state slot while preserving the shared `px-3` default, then applied `px-4` only from the desktop Tasks host. +- Extended the existing desktop E2E with text-anchor geometry coverage. Its RED run found the expected 4px drift; its GREEN run passed one test after the host override. +- Extended the existing phone scenario to prove the desktop sidebar remains hidden; the focused mobile Chrome run passed one test. +- Confirmed the final desktop geometry in an isolated mock instance: Tasks title x=16, empty text x=16, delta 0. +- Passed web typecheck, targeted ESLint and Prettier, the i18n ratchet, and `git diff --check`. Captured and inspected a synthetic 1280x800 PR screenshot, then stopped the browser and isolated app. diff --git a/docs/specs/INDEX.md b/docs/specs/INDEX.md index 4b001eb69f..334f23c39a 100644 --- a/docs/specs/INDEX.md +++ b/docs/specs/INDEX.md @@ -184,6 +184,7 @@ Subscription quota tracking and per-agent cheap-model profile routing. | [review-markdown-preview](ui/review-markdown-preview.md) | draft | | [sidebar-view-creation](ui/sidebar-view-creation.md) | shipped | | [command-panel sidebar task reveal](ui/command-panel-sidebar-task-reveal.md) | draft | +| [sidebar-empty-task-alignment](ui/sidebar-empty-task-alignment.md) | building | | [sidebar-task-completion-icons](ui/sidebar-task-completion-icons.md) | shipped | | [sidebar-queued-prompt-count](ui/sidebar-queued-prompt-count.md) | shipped | | [session-tab-delete-feedback](ui/session-tab-delete-feedback.md) | shipped | diff --git a/docs/specs/ui/sidebar-empty-task-alignment.md b/docs/specs/ui/sidebar-empty-task-alignment.md new file mode 100644 index 0000000000..05ae01bf7f --- /dev/null +++ b/docs/specs/ui/sidebar-empty-task-alignment.md @@ -0,0 +1,34 @@ +--- +status: building +created: 2026-08-13 +owner: kandev +--- + +# Sidebar empty task alignment + +## Why + +When the expanded desktop app sidebar has no tasks, its empty-state message starts farther left than the surrounding sidebar section titles. The inconsistent inset makes the task section look detached from the sidebar hierarchy. + +## What + +- In the expanded desktop app sidebar, the **No tasks yet.** message aligns its left text edge with the **Tasks** section title. +- The task list keeps its full sidebar width and transparent background while empty. +- Task rows, task-group headers, other sidebar sections, and other task-switcher hosts keep their existing insets. +- Phone navigation keeps its separate mobile composition and existing task-list inset. + +## Scenarios + +- **GIVEN** the expanded desktop app sidebar contains no tasks, **WHEN** the Tasks section renders, **THEN** the left text edge of **No tasks yet.** aligns with the left text edge of the **Tasks** title. +- **GIVEN** the desktop task list is empty, **WHEN** its layout is measured, **THEN** its transparent scroll surface still spans the full width of the sidebar navigation area. +- **GIVEN** a phone-sized viewport, **WHEN** the task listing renders, **THEN** the desktop app sidebar remains hidden and the mobile task surface keeps its existing composition and inset. + +## Out of scope + +- Changing task-row, task-group, filter, or non-task sidebar spacing. +- Changing empty states outside the desktop app sidebar Tasks section. +- Redesigning mobile task navigation or introducing a new mobile surface. + +## Implementation plan + +- [Sidebar empty task alignment plan](../../plans/sidebar-empty-task-alignment/plan.md)