diff --git a/apps/web/components/task/workflow-step-disclosure.tsx b/apps/web/components/task/workflow-step-disclosure.tsx index 9573db669b..986dacb356 100644 --- a/apps/web/components/task/workflow-step-disclosure.tsx +++ b/apps/web/components/task/workflow-step-disclosure.tsx @@ -320,7 +320,7 @@ function CompactWorkflowDisclosureSurface({ aria-label={t("task:moveTo")} side="bottom" align="center" - className="w-72 max-w-[calc(100vw-1rem)] p-2" + className="w-[28rem] max-w-[calc(100vw-1rem)] p-2" onOpenAutoFocus={controls.handleOpenAutoFocus} onCloseAutoFocus={controls.handleCloseAutoFocus} onEscapeKeyDown={(event) => event.stopPropagation()} diff --git a/apps/web/e2e/tests/layout/task-topbar-workflow-stepper.spec.ts b/apps/web/e2e/tests/layout/task-topbar-workflow-stepper.spec.ts index 9bdaf7c687..7d294c5516 100644 --- a/apps/web/e2e/tests/layout/task-topbar-workflow-stepper.spec.ts +++ b/apps/web/e2e/tests/layout/task-topbar-workflow-stepper.spec.ts @@ -3,6 +3,33 @@ import { waitForFiniteAnimations } from "../../helpers/animations"; import { SessionPage } from "../../pages/session-page"; const COMPACT_TASK_TITLE = `Compact workflow navigation ${"W".repeat(90)}`; +const WIDTH_REGRESSION_WORKFLOW_NAME = "Compact disclosure width workflow"; +const WIDTH_REGRESSION_WORKFLOW_YAML = `version: 1 +type: kandev_workflow +workflows: + - name: ${WIDTH_REGRESSION_WORKFLOW_NAME} + steps: + - name: Backlog + position: 0 + color: bg-neutral-400 + allow_manual_move: true + events: + on_turn_start: + - type: move_to_next + - name: Implementation + position: 1 + color: bg-blue-500 + allow_manual_move: true + events: + on_enter: + - type: auto_start_agent + on_turn_complete: + - type: move_to_next + - name: Review + position: 2 + color: bg-yellow-500 + is_start_step: true + allow_manual_move: true`; function adjacentStep( steps: Array<{ id: string; position: number }>, @@ -16,6 +43,123 @@ function adjacentStep( } test.describe("Compact task topbar workflow stepper", () => { + test("compact disclosure keeps ordinary step names readable", async ({ + testPage, + apiClient, + seedData, + prCapture, + }) => { + const imported = await apiClient.importWorkflows( + seedData.workspaceId, + WIDTH_REGRESSION_WORKFLOW_YAML, + ); + expect(imported.created).toContain(WIDTH_REGRESSION_WORKFLOW_NAME); + + const { workflows } = await apiClient.listWorkflows(seedData.workspaceId); + const workflow = workflows.find(({ name }) => name === WIDTH_REGRESSION_WORKFLOW_NAME); + if (!workflow) throw new Error("compact disclosure width workflow was not imported"); + + const { steps } = await apiClient.listWorkflowSteps(workflow.id); + const backlog = steps.find(({ name }) => name === "Backlog"); + const implementation = steps.find(({ name }) => name === "Implementation"); + const review = steps.find(({ name }) => name === "Review"); + if (!backlog || !implementation || !review) { + throw new Error("compact disclosure width workflow is missing its regression steps"); + } + + await apiClient.saveUserSettings({ + workspace_id: seedData.workspaceId, + workflow_filter_id: workflow.id, + }); + const task = await apiClient.seedTask(seedData.workspaceId, COMPACT_TASK_TITLE, { + workflow_id: workflow.id, + workflow_step_id: review.id, + state: "REVIEW", + }); + + await testPage.setViewportSize({ width: 900, height: 800 }); + await testPage.goto(`/t/${task.task_id}`); + const session = new SessionPage(testPage); + await session.waitForLoad(); + + for (const locale of ["en", "pt-pt"] as const) { + if (locale === "pt-pt") { + await testPage.evaluate((nextLocale) => { + document.cookie = `kandev_locale=${nextLocale}; path=/; max-age=31536000; SameSite=Lax`; + }, locale); + await testPage.reload(); + await session.waitForLoad(); + } + await expect(testPage.locator("html")).toHaveAttribute("lang", locale); + + const trigger = testPage.getByTestId("workflow-stepper-minimal"); + await expect(trigger).toBeVisible(); + await trigger.hover(); + + const disclosure = testPage.getByTestId("workflow-step-disclosure"); + const disclosureSurface = testPage + .locator('[role="dialog"]:visible') + .filter({ has: disclosure }); + await expect(disclosure).toBeVisible(); + await expect(disclosureSurface).toHaveCount(1); + + await waitForFiniteAnimations(disclosureSurface.first()); + const surfaceBox = await disclosureSurface.boundingBox(); + expect(surfaceBox).not.toBeNull(); + if (!surfaceBox) return; + const viewport = await testPage.evaluate(() => ({ + height: window.innerHeight, + width: window.innerWidth, + })); + expect(surfaceBox.x).toBeGreaterThanOrEqual(0); + expect(surfaceBox.y).toBeGreaterThanOrEqual(0); + expect(surfaceBox.x + surfaceBox.width).toBeLessThanOrEqual(viewport.width); + expect(surfaceBox.y + surfaceBox.height).toBeLessThanOrEqual(viewport.height); + + for (const { step, capabilityCount } of [ + { step: backlog, capabilityCount: 1 }, + { step: implementation, capabilityCount: 2 }, + ]) { + const row = disclosure.getByTestId(`workflow-step-disclosure-row-${step.id}`); + const label = row.locator("span.truncate").first(); + await expect(label).toHaveText(step.name); + const labelWidths = await label.evaluate((element) => ({ + clientWidth: element.clientWidth, + scrollWidth: element.scrollWidth, + })); + expect( + labelWidths.scrollWidth, + `${locale} ${step.name} label should not be truncated`, + ).toBeLessThanOrEqual(labelWidths.clientWidth + 1); + + await expect(row.locator('[data-slot="tooltip-trigger"]')).toHaveCount(capabilityCount); + const actionButtons = row.locator( + '[data-testid^="workflow-step-disclosure-options-"], [data-testid^="workflow-step-disclosure-move-"]', + ); + await expect(actionButtons).toHaveCount(2); + for (let index = 0; index < (await actionButtons.count()); index += 1) { + const actionBox = await actionButtons.nth(index).boundingBox(); + expect(actionBox).not.toBeNull(); + if (!actionBox) return; + expect(actionBox.x).toBeGreaterThanOrEqual(surfaceBox.x); + expect(actionBox.y).toBeGreaterThanOrEqual(surfaceBox.y); + expect(actionBox.x + actionBox.width).toBeLessThanOrEqual( + surfaceBox.x + surfaceBox.width, + ); + expect(actionBox.y + actionBox.height).toBeLessThanOrEqual( + surfaceBox.y + surfaceBox.height, + ); + } + } + + await prCapture.screenshot(`compact-disclosure-${locale}`, { + caption: `Compact workflow disclosure with ${locale} labels and actions`, + }); + await testPage.keyboard.press("Escape"); + await expect(disclosureSurface).toBeHidden(); + } + }); + test("opens ordered steps on hover and moves the task", async ({ testPage, apiClient, diff --git a/docs/plans/workflow-hover-width/plan.md b/docs/plans/workflow-hover-width/plan.md new file mode 100644 index 0000000000..510f4b5df6 --- /dev/null +++ b/docs/plans/workflow-hover-width/plan.md @@ -0,0 +1,68 @@ +--- +created: 2026-09-13 +status: complete +requirements: + - REQ-UI-COMPACT-WORKFLOW-STEP-NAVIGATION-001 +system_design: + - ../../specs/ui/system-design/compact-workflow-step-navigation.md +legacy_specs: [] +--- + +# Implementation Plan: Workflow hover width + +## Overview + +Widen the compact workflow hover so ordinary step names remain readable. One sequential work order covers the CSS correction and rendered checks. + +## Scope + +Include desktop disclosure width and regression coverage. Exclude move policy, backend changes, new copy, full-stepper changes, and phone navigation changes. + +## Technical approach + +The screenshot showed truncated labels beside fixed-size controls. `CompactWorkflowDisclosureSurface` now uses `w-[28rem]` (448px) instead of `w-72` (288px). `StepDisclosureRow` still gives the label remaining flex space and applies `truncate`; actions do not shrink. + +Replace `w-72` with `w-[28rem]` in `apps/web/components/task/workflow-step-disclosure.tsx`. Preserve the viewport cap, collision handling, and touch Drawer branch. The existing UI specification owns this responsive disclosure; tasks retain movement policy ownership. + +## ASCII UI preview + +UI-01: Compact stepper hover, eligible step. AC-001.11 and AC-001.6 below refer to the full acceptance IDs in the work order. + +```text +Before: [o I... icons options Move here] +After: [o Implementation icons options Move here] +Phone: [Task actions] -> [Move to] -> [step choices] +Tablet: [Current step v] -> [inset drawer with step choices] +``` + +Keep the row order and right-hand actions. Width is 28rem, capped to the viewport; ASCII spacing is illustrative. +The existing list owns vertical scrolling. Keep safe-area padding and touch targets in the existing drawer. +Phone navigation stays as shipped; the nearest exemplar is the existing compact disclosure Drawer and phone task actions. + +## Tests + +Run existing workflow-stepper component tests to check interaction semantics. Do not add a unit test that only repeats a CSS class. + +## E2E tests + +Extend `apps/web/e2e/tests/layout/task-topbar-workflow-stepper.spec.ts` with a test named `compact disclosure keeps ordinary step names readable` for AC-001.11. Seed Backlog and Implementation with capability icons and enabled move controls. Assert label scrollWidth does not exceed clientWidth, actions remain contained, and the dialog stays within the viewport. Cover English and pt-pt action widths. + +Retain existing desktop keyboard and tablet Drawer checks for AC-001.6 and AC-001.3. Run the existing phone task-drawer move scenario for AC-001.9. Capture desktop and phone screenshots during these focused checks. + +## Work orders + +- [x] [Task 01: Widen compact disclosure](task-01-widen-disclosure.md) + +## Verification results + +Implementation and rendered checks passed. The desktop Popover uses `w-[28rem]` with the existing viewport cap; the touch Drawer branch is unchanged. The regression covers Backlog and Implementation labels, capability icons, action containment, English and pt-pt, and viewport containment. + +Validation passed: `pnpm exec vitest run components/task/workflow-stepper.test.tsx` (20 tests), targeted ESLint, the desktop workflow-stepper E2E (3 tests), the phone task-drawer move E2E (1 test), and `git diff --check`. The host E2E run built the backend and pseudo-locale Vite bundle successfully. Documentation validation also passed: `list-docs.py validate` (267 decisions, 895 specifications), `lint-spec-files.py --all`, and `lint-spec-files.test.py` (36 tests). + +## Risks + +Long custom names may still truncate. Localized action labels consume more width; rendered checks must use real icons and controls. + +## Documentation impact + +Internal requirements and design updated. Public instructions and terminology remain accurate because only spacing changes. diff --git a/docs/plans/workflow-hover-width/task-01-widen-disclosure.md b/docs/plans/workflow-hover-width/task-01-widen-disclosure.md new file mode 100644 index 0000000000..648b18fe35 --- /dev/null +++ b/docs/plans/workflow-hover-width/task-01-widen-disclosure.md @@ -0,0 +1,106 @@ +--- +id: "01-widen-disclosure" +title: "Widen compact workflow disclosure" +status: done +wave: 1 +depends_on: [] +plan: "plan.md" +requirements: + - REQ-UI-COMPACT-WORKFLOW-STEP-NAVIGATION-001 +acceptance_criteria: + - AC-UI-COMPACT-WORKFLOW-STEP-NAVIGATION-001.3 + - AC-UI-COMPACT-WORKFLOW-STEP-NAVIGATION-001.6 + - AC-UI-COMPACT-WORKFLOW-STEP-NAVIGATION-001.9 + - AC-UI-COMPACT-WORKFLOW-STEP-NAVIGATION-001.11 +system_design: + - ../../specs/ui/system-design/compact-workflow-step-navigation.md +--- + +# Task 01: Widen compact workflow disclosure + +## Summary + +Use a 28rem desktop hover width with the existing viewport cap. Prove normal step names fit beside the actual controls. + +## In scope + +- Add the rendered regression described in the plan, run it to demonstrate failure, then change the Popover width. +- Check constrained viewport containment, desktop keyboard operation, tablet Drawer, and the existing phone move path. + +## Out of scope + +Backend, move semantics, full-stepper layout, translations, and new mobile surfaces. + +## Acceptance + +- Backlog and Implementation fit with capability icons and move/options controls in English and pt-pt at a desktop viewport. +- The desktop dialog stays viewport-contained; unusually long labels still truncate safely. +- Existing tablet touch and phone move paths pass their checks. + +## ASCII UI preview + +UI-01: Compact stepper hover, eligible step. AC-001.11 and AC-001.6 below refer to the full acceptance IDs in the work order. + +```text +Before: [o I... icons options Move here] +After: [o Implementation icons options Move here] +Phone: [Task actions] -> [Move to] -> [step choices] +Tablet: [Current step v] -> [inset drawer with step choices] +``` + +Keep the row order and right-hand actions. Width is 28rem, capped to the viewport; ASCII spacing is illustrative. +The existing list owns vertical scrolling. Keep safe-area padding and touch targets in the existing drawer. +Phone navigation stays as shipped; the nearest exemplar is the existing compact disclosure Drawer and phone task actions. + +See the [combined preview](plan.md#ascii-ui-preview). + +## Verification + +Run from the repository root. Install workspace dependencies if missing. The first E2E command builds current sources; the second reuses that build. + +```bash +(cd apps && pnpm install --frozen-lockfile) +(cd apps/web && pnpm exec vitest run components/task/workflow-stepper.test.tsx) +(cd apps/web && pnpm exec eslint components/task/workflow-step-disclosure.tsx e2e/tests/layout/task-topbar-workflow-stepper.spec.ts) +(cd apps/web && pnpm e2e:run --host --project chromium -- tests/layout/task-topbar-workflow-stepper.spec.ts --workers=1) +(cd apps/web && pnpm e2e:run --host --no-build --project mobile-chrome -- tests/task/mobile-sidebar-task-actions.spec.ts --grep "moves a task to another step from the mobile task drawer" --workers=1) +git diff --check +``` + +## Files likely touched + +- `apps/web/components/task/workflow-step-disclosure.tsx` +- `apps/web/e2e/tests/layout/task-topbar-workflow-stepper.spec.ts` +- This work order and `plan.md` for results. + +## Dependencies + +None. The original compact-navigation work order is already done. + +## Risks + +Check real computed label widths; visible text assertions alone do not detect ellipsis. + +## Parallelism + +`sequential` + +## Inputs + +- [Requirement](../../specs/ui/requirements/compact-workflow-step-navigation.md). +- [System design](../../specs/ui/system-design/compact-workflow-step-navigation.md). +- Existing stepper E2E and component tests; screenshot attached to this task. + +## Results + +Implemented the desktop Popover width as `w-[28rem]` while retaining the existing `max-w-[calc(100vw-1rem)]` cap and the touch Drawer branch. Added the named desktop regression with a disposable workflow that renders Backlog and Implementation capability icons and move/options controls in English and pt-pt. The regression first failed against `w-72` because Implementation measured 82px of content in a 46px label box, then passed with the 28rem width. + +Validation passed: + +- `pnpm exec vitest run components/task/workflow-stepper.test.tsx` (20 tests). +- Targeted ESLint for the disclosure component and desktop E2E. +- Desktop workflow-stepper E2E (3 tests), including the new regression, keyboard hover flow, and tablet touch Drawer. +- Existing Pixel 5 phone task-drawer move E2E (1 test). +- `git diff --check`. + +The host E2E run built the backend and pseudo-locale Vite bundle successfully. No new mobile surface or user-facing copy was added. diff --git a/docs/specs/ui/requirements/compact-workflow-step-navigation.md b/docs/specs/ui/requirements/compact-workflow-step-navigation.md index 81c65c8138..05a0dd44e6 100644 --- a/docs/specs/ui/requirements/compact-workflow-step-navigation.md +++ b/docs/specs/ui/requirements/compact-workflow-step-navigation.md @@ -41,6 +41,8 @@ The UI system owns this responsive disclosure. The task system continues to own - **AC-UI-COMPACT-WORKFLOW-STEP-NAVIGATION-001.9:** When a phone does not show the task top bar, the mobile task drawer shall retain its existing **Move to** path. - **AC-UI-COMPACT-WORKFLOW-STEP-NAVIGATION-001.10:** Fine-pointer movement controls shall retain compact desktop sizing. The 44px minimum shall apply only to coarse-pointer touch hit areas and mobile controls. +- **AC-UI-COMPACT-WORKFLOW-STEP-NAVIGATION-001.11:** On a desktop with sufficient viewport space, the disclosure shall show Backlog and Implementation without truncation alongside capability icons, options, and move controls. Longer names may truncate when constrained by the viewport. + ## Out of scope - Changes to workflow order, task-move permissions, or backend task transitions. diff --git a/docs/specs/ui/system-design/compact-workflow-step-navigation.md b/docs/specs/ui/system-design/compact-workflow-step-navigation.md index 1252837a9a..94ac853f8f 100644 --- a/docs/specs/ui/system-design/compact-workflow-step-navigation.md +++ b/docs/specs/ui/system-design/compact-workflow-step-navigation.md @@ -87,7 +87,7 @@ The current row uses `aria-current="step"`. Eligible movement controls are direc Fine-pointer movement controls use the surrounding compact desktop button density. The 44px minimum applies to the coarse-pointer drawer's action hit areas and rows, not to the desktop button's visual height. -The Popover stays within the viewport through Radix collision handling. The drawer uses internal scrolling and bottom safe-area padding. +The Popover uses a preferred width of 28rem (448px at the default root font size), capped at `calc(100vw - 1rem)`. This gives step names room beside capability icons and move actions. Keep label truncation as the fallback for unusually long names. Apply this width only to the fine-pointer Popover in `CompactWorkflowDisclosureSurface`; retain the existing touch Drawer geometry. The Popover stays within the viewport through Radix collision handling. The drawer uses internal scrolling and bottom safe-area padding. The disclosure supports Escape dismissal and focus return. Touch rows use a minimum active height of 44px.