fix(web): polish archive confirmation warning surfaces (#2995)
This commit is contained in:
@@ -9,9 +9,11 @@ const deleteTitle = "Delete item?";
|
||||
function Harness({
|
||||
onConfirm = vi.fn(),
|
||||
confirmDisabled = false,
|
||||
size,
|
||||
}: {
|
||||
onConfirm?: () => void | Promise<void>;
|
||||
confirmDisabled?: boolean;
|
||||
size?: "wide";
|
||||
}) {
|
||||
const [open, setOpen] = useState(true);
|
||||
const [showAnchor, setShowAnchor] = useState(true);
|
||||
@@ -34,6 +36,7 @@ function Harness({
|
||||
cancelLabel="Cancel"
|
||||
confirmLabel="Delete"
|
||||
confirmDisabled={confirmDisabled}
|
||||
size={size}
|
||||
onOpenChange={setOpen}
|
||||
onConfirm={onConfirm}
|
||||
/>
|
||||
@@ -55,6 +58,17 @@ describe("ActionConfirmPopover", () => {
|
||||
).toContain("min-h-11");
|
||||
});
|
||||
|
||||
it("supports a wider bounded surface without changing the default width", async () => {
|
||||
render(<Harness />);
|
||||
expect(screen.getByRole("dialog").className).toContain("w-64");
|
||||
|
||||
cleanup();
|
||||
render(<Harness size="wide" />);
|
||||
const wideDialog = screen.getByRole("dialog");
|
||||
expect(wideDialog.className).toContain("w-72");
|
||||
expect(wideDialog.className).toContain("max-w-[calc(100vw-1rem)]");
|
||||
});
|
||||
|
||||
it("closes before invoking an unresolved confirmation and returns focus on cancel", async () => {
|
||||
const onConfirm = vi.fn(() => new Promise<void>(() => {}));
|
||||
render(<Harness onConfirm={onConfirm} />);
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import { useId, useLayoutEffect, useRef, type ReactNode, type RefObject } from "react";
|
||||
import { Button } from "@kandev/ui/button";
|
||||
import { cn } from "@/lib/utils";
|
||||
import {
|
||||
Popover,
|
||||
PopoverAnchor,
|
||||
@@ -11,8 +12,11 @@ import {
|
||||
PopoverTitle,
|
||||
} from "@kandev/ui/popover";
|
||||
|
||||
export type ActionConfirmPopoverSize = "default" | "wide";
|
||||
|
||||
export type ActionConfirmPopoverProps = {
|
||||
open: boolean;
|
||||
size?: ActionConfirmPopoverSize;
|
||||
disabled?: boolean;
|
||||
anchorRef: RefObject<HTMLElement | null>;
|
||||
focusReturnRef?: RefObject<HTMLElement | null>;
|
||||
@@ -40,6 +44,7 @@ export type ActionConfirmPopoverProps = {
|
||||
*/
|
||||
export function ActionConfirmPopover({
|
||||
open,
|
||||
size = "default",
|
||||
disabled = false,
|
||||
anchorRef,
|
||||
focusReturnRef,
|
||||
@@ -104,6 +109,7 @@ export function ActionConfirmPopover({
|
||||
{/* Radix accepts a null current value at runtime while its public type omits it. */}
|
||||
<PopoverAnchor virtualRef={anchorRef as RefObject<HTMLElement>} />
|
||||
<ActionConfirmPopoverContent
|
||||
size={size}
|
||||
titleId={titleId}
|
||||
descriptionId={descriptionId}
|
||||
title={title}
|
||||
@@ -129,6 +135,7 @@ export function ActionConfirmPopover({
|
||||
}
|
||||
|
||||
type ActionConfirmPopoverContentProps = {
|
||||
size: ActionConfirmPopoverSize;
|
||||
titleId: string;
|
||||
descriptionId: string;
|
||||
title: ReactNode;
|
||||
@@ -151,6 +158,7 @@ type ActionConfirmPopoverContentProps = {
|
||||
};
|
||||
|
||||
function ActionConfirmPopoverContent({
|
||||
size,
|
||||
titleId,
|
||||
descriptionId,
|
||||
title,
|
||||
@@ -181,7 +189,7 @@ function ActionConfirmPopoverContent({
|
||||
side="bottom"
|
||||
align="end"
|
||||
sideOffset={8}
|
||||
className="w-64 gap-3 p-3"
|
||||
className={cn("gap-3 p-3", size === "wide" ? "w-72 max-w-[calc(100vw-1rem)]" : "w-64")}
|
||||
onOpenAutoFocus={(event) => {
|
||||
event.preventDefault();
|
||||
cancelRef.current?.focus();
|
||||
|
||||
@@ -3,6 +3,7 @@ import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
||||
import { render, screen, cleanup, fireEvent, waitFor } from "@testing-library/react";
|
||||
import { StateProvider, useAppStore } from "@/components/state-provider";
|
||||
import { defaultState } from "@/lib/state/default-state";
|
||||
import { expectCompactWarning } from "./task-confirm-dialog.test-helpers";
|
||||
|
||||
const mockGetSubtaskCount = vi.fn();
|
||||
|
||||
@@ -264,6 +265,22 @@ describe("TaskArchiveConfirmDialog cleanup copy", () => {
|
||||
});
|
||||
|
||||
describe("TaskArchiveConfirmDialog still-working guard", () => {
|
||||
it("keeps the in-flight warning visually subordinate to confirmation copy", () => {
|
||||
renderWithTasks(
|
||||
<TaskArchiveConfirmDialog
|
||||
open
|
||||
onOpenChange={() => {}}
|
||||
taskTitle="My task"
|
||||
taskId="task-1"
|
||||
executorType="worktree"
|
||||
onConfirm={() => {}}
|
||||
/>,
|
||||
[{ id: "task-1", foregroundActivity: "generating" }],
|
||||
);
|
||||
|
||||
expectCompactWarning(screen.getByTestId(WARNING_TESTID));
|
||||
});
|
||||
|
||||
it("warns when the task is generating", () => {
|
||||
renderWithTasks(
|
||||
<TaskArchiveConfirmDialog
|
||||
|
||||
@@ -137,6 +137,7 @@ function ArchiveConfirmPopover({
|
||||
return (
|
||||
<ActionConfirmPopover
|
||||
open={open}
|
||||
size="wide"
|
||||
disabled={isArchiving}
|
||||
anchorRef={anchorRef}
|
||||
focusReturnRef={focusReturnRef}
|
||||
@@ -178,6 +179,7 @@ function ArchiveClassifyingPopover({
|
||||
return (
|
||||
<ActionConfirmPopover
|
||||
open
|
||||
size="wide"
|
||||
anchorRef={anchorRef}
|
||||
focusReturnRef={focusReturnRef}
|
||||
focusBoundaryRef={focusBoundaryRef ?? anchorRef}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
import { expect } from "vitest";
|
||||
|
||||
export function expectCompactWarning(warning: HTMLElement) {
|
||||
expect(warning.getAttribute("role")).toBe("alert");
|
||||
expect(warning.className).toContain("gap-1.5");
|
||||
expect(warning.className).toContain("p-2.5");
|
||||
expect(warning.className).toContain("text-xs");
|
||||
expect(warning.className).toContain("leading-5");
|
||||
expect(warning.className).toContain("text-pretty");
|
||||
expect(warning.querySelector("svg")?.getAttribute("class")).toContain("h-3.5");
|
||||
}
|
||||
@@ -10,6 +10,7 @@ vi.mock("@/lib/api", () => ({
|
||||
}));
|
||||
|
||||
import { TaskDeleteConfirmDialog } from "./task-delete-confirm-dialog";
|
||||
import { expectCompactWarning } from "./task-confirm-dialog.test-helpers";
|
||||
|
||||
type SeedTask = { id: string; foregroundActivity?: "generating" | "background" | null };
|
||||
|
||||
@@ -190,6 +191,23 @@ describe("TaskDeleteConfirmDialog executor cleanup copy", () => {
|
||||
});
|
||||
|
||||
describe("TaskDeleteConfirmDialog still-working guard", () => {
|
||||
it("keeps the shared in-flight warning visually subordinate", () => {
|
||||
mockGetSubtaskCount.mockResolvedValue({ count: 0 });
|
||||
renderDialog(
|
||||
<TaskDeleteConfirmDialog
|
||||
open
|
||||
onOpenChange={() => {}}
|
||||
taskTitle="My task"
|
||||
taskId="task-1"
|
||||
executorType="worktree"
|
||||
onConfirm={() => {}}
|
||||
/>,
|
||||
[{ id: "task-1", foregroundActivity: "generating" }],
|
||||
);
|
||||
|
||||
expectCompactWarning(screen.getByTestId(WARNING_TESTID));
|
||||
});
|
||||
|
||||
it("warns when the task is generating", () => {
|
||||
mockGetSubtaskCount.mockResolvedValue({ count: 0 });
|
||||
renderDialog(
|
||||
|
||||
@@ -9,9 +9,9 @@ export function StillWorkingWarning({ count }: { count?: number }) {
|
||||
<div
|
||||
data-testid="still-working-warning"
|
||||
role="alert"
|
||||
className="flex items-start gap-2 rounded-md border border-yellow-500/40 bg-yellow-500/10 p-3 text-sm text-yellow-700 dark:text-yellow-300"
|
||||
className="flex items-start gap-1.5 rounded-md border border-yellow-500/40 bg-yellow-500/10 p-2.5 text-xs leading-5 text-pretty text-yellow-700 dark:text-yellow-300"
|
||||
>
|
||||
<IconAlertTriangle className="mt-0.5 h-4 w-4 shrink-0 text-yellow-500" aria-hidden />
|
||||
<IconAlertTriangle className="mt-0.5 h-3.5 w-3.5 shrink-0 text-yellow-500" aria-hidden />
|
||||
<span>{t("task:stillWorkingWarning", { subject })}</span>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -23,6 +23,7 @@ import {
|
||||
} from "@/components/task/task-move-context-menu";
|
||||
import { TaskNestContextMenuItems } from "@/components/task/task-nest-context-menu";
|
||||
import { useTaskWorkflowMove } from "@/hooks/use-task-workflow-move";
|
||||
import { useResponsiveBreakpoint } from "@/hooks/use-responsive-breakpoint";
|
||||
import { TaskColorMenu } from "./task-switcher-color-menu";
|
||||
import {
|
||||
TaskPluginLinkMenu,
|
||||
@@ -168,22 +169,23 @@ export function TaskItemWithContextMenu(props: ContextMenuProps) {
|
||||
setMenuKey((k) => k + 1);
|
||||
};
|
||||
const { handleOpenChange, triggerProps } = useMenuTouchDragCancel(setContextOpen);
|
||||
const { isFinePointer } = useResponsiveBreakpoint();
|
||||
const archive = useTaskSwitcherArchiveConfirmation({
|
||||
task: menuProps.task,
|
||||
onArchiveTask: menuProps.onArchiveTask,
|
||||
isArchiving: menuProps.isArchiving,
|
||||
closeMenu,
|
||||
});
|
||||
const archiveConfirmation = archive.archiveOpen ? archive.archiveConfirmation : undefined;
|
||||
const inlineArchiveConfirmation = isFinePointer ? undefined : archiveConfirmation;
|
||||
const portaledArchiveConfirmation = isFinePointer ? archiveConfirmation : undefined;
|
||||
|
||||
return (
|
||||
<ContextMenu key={menuKey} onOpenChange={handleOpenChange}>
|
||||
<ContextMenuTrigger asChild>
|
||||
<div ref={archive.archiveAnchorRef} tabIndex={-1} {...triggerProps}>
|
||||
{cloneWithMenuOpen(
|
||||
children,
|
||||
contextOpen,
|
||||
archive.archiveOpen ? archive.archiveConfirmation : undefined,
|
||||
)}
|
||||
{cloneWithMenuOpen(children, contextOpen, inlineArchiveConfirmation)}
|
||||
{portaledArchiveConfirmation}
|
||||
</div>
|
||||
</ContextMenuTrigger>
|
||||
<ContextMenuContent
|
||||
|
||||
@@ -1,7 +1,183 @@
|
||||
import { test, expect } from "../../fixtures/test-base";
|
||||
import { waitForActiveSessionForegroundActivity } from "../../helpers/session-store";
|
||||
import { SessionPage } from "../../pages/session-page";
|
||||
|
||||
test.describe("Archive confirmation preference", () => {
|
||||
test("keeps the fine-pointer archive popover wide and the sidebar row stable", async ({
|
||||
testPage,
|
||||
apiClient,
|
||||
seedData,
|
||||
prCapture,
|
||||
}, testInfo) => {
|
||||
await testPage.setViewportSize({ width: 900, height: 900 });
|
||||
const title = "Fine pointer archive geometry target";
|
||||
const task = await apiClient.seedTask(seedData.workspaceId, title, {
|
||||
workflow_id: seedData.workflowId,
|
||||
workflow_step_id: seedData.startStepId,
|
||||
});
|
||||
|
||||
await testPage.goto(`/t/${task.task_id}`);
|
||||
const session = new SessionPage(testPage);
|
||||
await session.waitForLoad();
|
||||
|
||||
const taskRow = session.sidebarTaskItem(title);
|
||||
await expect(taskRow).toBeVisible({ timeout: 15_000 });
|
||||
const heightBefore = await taskRow.evaluate(
|
||||
(element) => element.getBoundingClientRect().height,
|
||||
);
|
||||
await prCapture.screenshot("before-archive-popover", {
|
||||
caption: "Fine-pointer sidebar row before archive confirmation",
|
||||
});
|
||||
|
||||
await session.openSidebarMenuAndClick(title, "Archive");
|
||||
const popover = testPage.getByTestId("task-archive-confirm-popover");
|
||||
await expect(popover).toBeVisible();
|
||||
await prCapture.screenshot("archive-popover-open", {
|
||||
caption: "Fine-pointer archive confirmation popover",
|
||||
});
|
||||
|
||||
const openRowMetrics = await taskRow.evaluate((element) => {
|
||||
const rowBox = element.getBoundingClientRect();
|
||||
return { rowHeight: rowBox.height };
|
||||
});
|
||||
const openSurfaceMetrics = await popover.evaluate((element) => {
|
||||
const surfaceBox = element.getBoundingClientRect();
|
||||
return {
|
||||
surface: {
|
||||
x: surfaceBox.x,
|
||||
y: surfaceBox.y,
|
||||
width: surfaceBox.width,
|
||||
height: surfaceBox.height,
|
||||
},
|
||||
viewport: { width: window.innerWidth, height: window.innerHeight },
|
||||
documentWidth: document.documentElement.scrollWidth,
|
||||
viewportWidth: document.documentElement.clientWidth,
|
||||
};
|
||||
});
|
||||
const openMetrics = {
|
||||
rowHeight: openRowMetrics.rowHeight,
|
||||
...openSurfaceMetrics,
|
||||
};
|
||||
|
||||
expect(openMetrics.rowHeight).toBeCloseTo(heightBefore, 3);
|
||||
expect(openMetrics.surface.width).toBeGreaterThan(256);
|
||||
expect(openMetrics.surface.x).toBeGreaterThanOrEqual(0);
|
||||
expect(openMetrics.surface.x + openMetrics.surface.width).toBeLessThanOrEqual(
|
||||
openMetrics.viewport.width,
|
||||
);
|
||||
expect(openMetrics.surface.y).toBeGreaterThanOrEqual(0);
|
||||
expect(openMetrics.surface.y + openMetrics.surface.height).toBeLessThanOrEqual(
|
||||
openMetrics.viewport.height,
|
||||
);
|
||||
expect(openMetrics.documentWidth).toBeLessThanOrEqual(openMetrics.viewportWidth);
|
||||
|
||||
await popover.getByRole("button", { name: "Cancel", exact: true }).click();
|
||||
await expect(popover).toBeHidden();
|
||||
const heightAfter = await taskRow.evaluate((element) => element.getBoundingClientRect().height);
|
||||
expect(heightAfter).toBeCloseTo(heightBefore, 3);
|
||||
await testInfo.attach("archive-popover-geometry.json", {
|
||||
body: Buffer.from(
|
||||
JSON.stringify(
|
||||
{
|
||||
heightBefore,
|
||||
heightOpen: openMetrics.rowHeight,
|
||||
heightAfter,
|
||||
popover: openMetrics.surface,
|
||||
viewport: openMetrics.viewport,
|
||||
documentWidth: openMetrics.documentWidth,
|
||||
viewportWidth: openMetrics.viewportWidth,
|
||||
},
|
||||
null,
|
||||
2,
|
||||
),
|
||||
),
|
||||
contentType: "application/json",
|
||||
});
|
||||
});
|
||||
|
||||
test("keeps the full archive warning subordinate to dialog copy", async ({
|
||||
testPage,
|
||||
apiClient,
|
||||
seedData,
|
||||
prCapture,
|
||||
}) => {
|
||||
test.setTimeout(120_000);
|
||||
const title = "Full archive warning geometry target";
|
||||
const task = await apiClient.createTaskWithAgent(
|
||||
seedData.workspaceId,
|
||||
title,
|
||||
seedData.agentProfileId,
|
||||
{
|
||||
description: "/background 60s",
|
||||
workflow_id: seedData.workflowId,
|
||||
workflow_step_id: seedData.startStepId,
|
||||
repository_ids: [seedData.repositoryId],
|
||||
},
|
||||
);
|
||||
await apiClient.seedTask(seedData.workspaceId, "Full archive warning child", {
|
||||
workflow_id: seedData.workflowId,
|
||||
workflow_step_id: seedData.startStepId,
|
||||
parent_id: task.id,
|
||||
});
|
||||
|
||||
await testPage.goto(`/t/${task.id}`);
|
||||
const session = new SessionPage(testPage);
|
||||
await session.waitForLoad();
|
||||
await waitForActiveSessionForegroundActivity(testPage, "generating");
|
||||
await prCapture.screenshot("before-full-archive-dialog", {
|
||||
caption: "In-flight task before opening full archive confirmation",
|
||||
});
|
||||
|
||||
const taskRow = session.sidebarTaskItem(title);
|
||||
await expect(taskRow).toBeVisible({ timeout: 15_000 });
|
||||
const taskActions = taskRow.getByRole("button", { name: "Task actions" });
|
||||
await expect(taskActions).toBeVisible();
|
||||
await taskActions.click({ force: true });
|
||||
const archiveMenuItem = testPage.getByRole("menuitem", { name: "Archive", exact: true });
|
||||
await expect(archiveMenuItem).toBeVisible();
|
||||
await archiveMenuItem.click();
|
||||
const dialog = testPage.getByRole("alertdialog");
|
||||
await expect(dialog).toBeVisible();
|
||||
const warning = dialog.getByTestId("still-working-warning");
|
||||
await expect(warning).toBeVisible();
|
||||
await prCapture.screenshot("full-archive-dialog-warning", {
|
||||
caption: "Compact still-working warning in full archive dialog",
|
||||
});
|
||||
|
||||
const hierarchy = await dialog.evaluate((dialogElement) => {
|
||||
const warning = dialogElement.querySelector<HTMLElement>(
|
||||
'[data-testid="still-working-warning"]',
|
||||
);
|
||||
const description = dialogElement.querySelector<HTMLElement>(
|
||||
'[data-slot="alert-dialog-description"]',
|
||||
);
|
||||
if (!warning || !description) throw new Error("dialog hierarchy targets missing");
|
||||
const warningStyle = getComputedStyle(warning);
|
||||
const descriptionStyle = getComputedStyle(description);
|
||||
const warningBox = warning.getBoundingClientRect();
|
||||
const dialogBox = dialogElement.getBoundingClientRect();
|
||||
return {
|
||||
warningFontSize: warningStyle.fontSize,
|
||||
warningLineHeight: warningStyle.lineHeight,
|
||||
descriptionFontSize: descriptionStyle.fontSize,
|
||||
warning: { left: warningBox.left, right: warningBox.right },
|
||||
dialog: { left: dialogBox.left, right: dialogBox.right },
|
||||
documentWidth: document.documentElement.scrollWidth,
|
||||
viewportWidth: document.documentElement.clientWidth,
|
||||
};
|
||||
});
|
||||
expect(Number.parseFloat(hierarchy.warningFontSize)).toBeLessThan(
|
||||
Number.parseFloat(hierarchy.descriptionFontSize),
|
||||
);
|
||||
expect(hierarchy.warningLineHeight).toBe("20px");
|
||||
expect(hierarchy.warning.left).toBeGreaterThanOrEqual(hierarchy.dialog.left);
|
||||
expect(hierarchy.warning.right).toBeLessThanOrEqual(hierarchy.dialog.right);
|
||||
expect(hierarchy.documentWidth).toBeLessThanOrEqual(hierarchy.viewportWidth);
|
||||
|
||||
await dialog.getByRole("button", { name: "Cancel", exact: true }).click();
|
||||
await expect(dialog).toBeHidden();
|
||||
});
|
||||
|
||||
test("disabling confirmation archives immediately from the desktop sidebar", async ({
|
||||
testPage,
|
||||
apiClient,
|
||||
|
||||
@@ -4,6 +4,7 @@ import path from "node:path";
|
||||
import { test, expect } from "../../fixtures/test-base";
|
||||
import { activeSessionId, seedSecondaryClarificationTask } from "../../helpers/clarification";
|
||||
import { makeGitEnv } from "../../helpers/git-helper";
|
||||
import { waitForActiveSessionForegroundActivity } from "../../helpers/session-store";
|
||||
import { assertNoDocumentHorizontalOverflow } from "../../helpers/layout-assertions";
|
||||
import { SessionPage } from "../../pages/session-page";
|
||||
|
||||
@@ -265,6 +266,80 @@ test.describe("Mobile sidebar task actions", () => {
|
||||
expect(pageWidth.scroll).toBeLessThanOrEqual(pageWidth.client);
|
||||
});
|
||||
|
||||
test("keeps the in-flight warning compact and reachable on a phone", async ({
|
||||
testPage,
|
||||
apiClient,
|
||||
seedData,
|
||||
prCapture,
|
||||
}) => {
|
||||
test.setTimeout(120_000);
|
||||
const title = "Mobile in-flight archive warning";
|
||||
const task = await apiClient.createTaskWithAgent(
|
||||
seedData.workspaceId,
|
||||
title,
|
||||
seedData.agentProfileId,
|
||||
{
|
||||
description: "/background 30s",
|
||||
workflow_id: seedData.workflowId,
|
||||
workflow_step_id: seedData.startStepId,
|
||||
repository_ids: [seedData.repositoryId],
|
||||
},
|
||||
);
|
||||
|
||||
await testPage.goto(`/t/${task.id}`);
|
||||
const session = new SessionPage(testPage);
|
||||
await session.waitForLoad();
|
||||
await waitForActiveSessionForegroundActivity(testPage, "generating");
|
||||
await testPage.getByTestId("mobile-session-menu").tap();
|
||||
|
||||
const drawer = testPage.getByRole("dialog", { name: "Tasks" });
|
||||
const taskRow = drawer.getByTestId("sidebar-task-item").filter({ hasText: title });
|
||||
const heightBefore = await taskRow.evaluate(
|
||||
(element) => element.getBoundingClientRect().height,
|
||||
);
|
||||
await prCapture.screenshot("before-mobile-in-flight-archive", {
|
||||
caption: "Phone task row before in-flight archive confirmation",
|
||||
});
|
||||
await taskRow.getByRole("button", { name: "Task actions" }).tap();
|
||||
await testPage.getByRole("menuitem", { name: "Archive", exact: true }).tap();
|
||||
|
||||
const confirmation = taskRow.getByTestId("task-archive-inline-confirmation");
|
||||
const warning = confirmation.getByTestId("still-working-warning");
|
||||
await expect(confirmation).toBeVisible();
|
||||
await expect(warning).toBeVisible();
|
||||
await prCapture.screenshot("mobile-in-flight-archive-warning", {
|
||||
caption: "Compact in-flight warning in phone archive confirmation",
|
||||
});
|
||||
|
||||
const warningMetrics = await warning.evaluate((element) => {
|
||||
const style = getComputedStyle(element);
|
||||
return { fontSize: style.fontSize, lineHeight: style.lineHeight };
|
||||
});
|
||||
expect(warningMetrics).toEqual({ fontSize: "12px", lineHeight: "20px" });
|
||||
await expect(warning).toHaveClass(/text-pretty/);
|
||||
const heightOpen = await taskRow.evaluate((element) => element.getBoundingClientRect().height);
|
||||
expect(heightOpen).toBeGreaterThan(heightBefore);
|
||||
|
||||
for (const button of [
|
||||
confirmation.getByRole("button", { name: "Cancel" }),
|
||||
confirmation.getByTestId("archive-task-confirm"),
|
||||
]) {
|
||||
const box = await button.boundingBox();
|
||||
if (!box) throw new Error("in-flight inline archive action has no layout box");
|
||||
expect(box.width).toBeGreaterThanOrEqual(44);
|
||||
expect(box.height).toBeGreaterThanOrEqual(44);
|
||||
await expect(button).toBeInViewport();
|
||||
}
|
||||
|
||||
const pageWidth = await testPage.evaluate(() => ({
|
||||
scroll: document.documentElement.scrollWidth,
|
||||
client: document.documentElement.clientWidth,
|
||||
}));
|
||||
expect(pageWidth.scroll).toBeLessThanOrEqual(pageWidth.client);
|
||||
await confirmation.getByRole("button", { name: "Cancel" }).tap();
|
||||
await expect(confirmation).toBeHidden();
|
||||
});
|
||||
|
||||
test("keeps the tablet task switcher as a left-side sheet", async ({
|
||||
testPage,
|
||||
apiClient,
|
||||
|
||||
@@ -0,0 +1,131 @@
|
||||
---
|
||||
created: 2026-08-24
|
||||
status: complete
|
||||
requirements:
|
||||
- ../../specs/ui/requirements/confirmation-warning-hierarchy.md
|
||||
system_design:
|
||||
- ../../specs/ui/system-design/confirmation-warning-hierarchy.md
|
||||
legacy_specs: []
|
||||
---
|
||||
|
||||
# Implementation Plan: Task Confirmation Warning Polish
|
||||
|
||||
## Overview
|
||||
|
||||
Reduce the visual weight of the shared still-working warning so archive and
|
||||
delete confirmations keep title and primary cleanup copy as the dominant
|
||||
hierarchy. Give the fine-pointer archive popover modest extra width and remove
|
||||
its source sidebar-row layout jitter. Deliver shared style/placement changes,
|
||||
regression coverage, and focused desktop and phone rendered evidence without
|
||||
changing text or behavior.
|
||||
|
||||
## Scope
|
||||
|
||||
### In scope
|
||||
|
||||
- Compact shared warning typography, wrapping, spacing, and icon scale.
|
||||
- Archive-only fine-pointer popover width opt-in with viewport containment.
|
||||
- Fine-pointer archive mounting that keeps the originating sidebar row height
|
||||
stable while preserving coarse-pointer inline expansion.
|
||||
- Regression assertions for the shared warning contract in archive and delete
|
||||
dialog tests.
|
||||
- Regression assertions for archive width and fine-pointer sidebar row-height
|
||||
stability.
|
||||
- Focused production-build desktop and phone checks, screenshots, computed
|
||||
hierarchy, containment, action reachability, and zero document horizontal
|
||||
overflow.
|
||||
|
||||
### Out of scope
|
||||
|
||||
- Warning copy, translations, in-flight detection, archive/delete callbacks,
|
||||
global confirmation-popover width, dialog geometry, focus, Escape, safe-area,
|
||||
action sizes, or animation.
|
||||
|
||||
## Technical approach
|
||||
|
||||
Change `apps/web/components/task/task-still-working-warning.tsx` for the shared
|
||||
visual contract. Keep the existing semantic classes and markup identity; use
|
||||
compact body typography with `text-pretty`, a readable explicit line height,
|
||||
and proportionally smaller padding, gap, and icon.
|
||||
|
||||
Add a width/size contract to
|
||||
`apps/web/components/confirmation/action-confirm-popover.tsx` with `w-64` as
|
||||
the default. Pass the wider archive-only variant from
|
||||
`TaskArchiveConfirmation`, with a viewport-aware max width. Keep title/body
|
||||
pretty-wrapped and action geometry unchanged.
|
||||
|
||||
At `TaskItemWithContextMenu`, use `useResponsiveBreakpoint` to keep the
|
||||
fine-pointer archive confirmation outside the `TaskItem` action slot while
|
||||
retaining the same anchor/focus refs and shared confirmation node. Continue
|
||||
injecting the node into `TaskItem` for coarse-pointer inline confirmation.
|
||||
This targets the observed `flex-wrap` plus `basis-full` extra-line cause, not
|
||||
the row's dimensions.
|
||||
|
||||
Add a style-hierarchy regression to the existing archive and delete warning
|
||||
tests. Run it against current main first so the old `text-sm`, `p-3`, and 16px
|
||||
icon contract fails, then make the shared style change and rerun the focused
|
||||
tests green. No catalog changes are expected, but the affected-file i18n
|
||||
ratchet/check remains part of verification.
|
||||
|
||||
Use the real desktop sidebar archive flow and the phone
|
||||
sidebar-task-actions inline confirmation coverage. Rebuild the production web
|
||||
assets before Playwright. Capture before and after screenshots of the full
|
||||
desktop dialog/popover and phone inline confirmation with the warning visible,
|
||||
and inspect computed font size/line height, warning-to-description hierarchy,
|
||||
popover width and viewport bounds, source-row height before/open/cancel,
|
||||
button reachability, and document `scrollWidth`.
|
||||
|
||||
## Tests
|
||||
|
||||
| Acceptance criterion | Evidence |
|
||||
| --- | --- |
|
||||
| `AC-TASKS-CONFIRMATION-WARNING-001.1` | Archive and delete component warning tests assert compact class contract and existing warning text. |
|
||||
| `AC-TASKS-CONFIRMATION-WARNING-001.2` | Existing archive/delete activity tests assert localized warning presence and `role=alert`; shared component keeps test ID and semantics. |
|
||||
| `AC-TASKS-CONFIRMATION-WARNING-001.3` | Desktop card-action confirmation E2E plus phone inline confirmation E2E and rendered geometry checks. |
|
||||
| `AC-TASKS-CONFIRMATION-SURFACE-002.1` | Action-confirm-popover width-contract test plus desktop sidebar computed width/bounds assertion; non-archive default remains `w-64`. |
|
||||
| `AC-TASKS-CONFIRMATION-SURFACE-002.2` | Desktop sidebar E2E records exact source-row height before/open/cancel and fails on the current extra flex-line behavior. |
|
||||
| `AC-TASKS-CONFIRMATION-SURFACE-002.3` | Desktop anchor/focus/callback checks plus phone inline 44px and zero-overflow checks. |
|
||||
|
||||
## E2E tests
|
||||
|
||||
- Desktop: `tests/task/archive-confirmation-preference.spec.ts` with a real
|
||||
sidebar task row, fine-pointer archive popover width/bounds, and
|
||||
before/open/cancel row-height measurements. Retain the existing
|
||||
`tests/kanban/card-menu-delete-archive.spec.ts` archive/delete behavior
|
||||
coverage and add full-dialog warning hierarchy checks where the seeded
|
||||
in-flight task can open it.
|
||||
- Phone: `tests/task/mobile-sidebar-task-actions.spec.ts` existing inline
|
||||
confirmation scenario, extended only if needed to expose the warning state;
|
||||
preserve 44px actions, in-viewport checks, and zero document overflow.
|
||||
- Manual rendered evidence: capture `.pr-assets` before/after screenshots for
|
||||
desktop full dialog and phone inline confirmation. Do not commit screenshot
|
||||
binaries to the feature branch.
|
||||
|
||||
## Work orders
|
||||
|
||||
- [x] [Task 01: Compact shared confirmation warning and stable archive surface](task-01-compact-warning.md)
|
||||
|
||||
## Verification results
|
||||
|
||||
- RED: the new archive/delete compactness assertions and wide-popover contract
|
||||
failed against the unchanged implementation (3 failed, 38 passed); the
|
||||
desktop row-height regression measured 54.203125px before vs 62.203125px
|
||||
while the old fine-pointer popover was open.
|
||||
- GREEN: focused Vitest passed 5 files and 59 tests; affected ESLint passed;
|
||||
web typecheck passed; `i18n:ratchet` passed with 0 new violations and 644
|
||||
guard entries; `make build-web` passed.
|
||||
- GREEN rendered checks against rebuilt assets: desktop archive geometry/full
|
||||
dialog 2/2, phone inline archive flows 2/2, and card archive/delete behavior
|
||||
5/5. Desktop assertions verified stable row height before/open/cancel,
|
||||
popover width above 256px, viewport containment, and zero document overflow.
|
||||
Phone assertions verified compact 12px/20px warning type, intentional inline
|
||||
row expansion, 44px-or-larger in-viewport actions, and zero overflow.
|
||||
- Fresh compressed media is captured under `apps/web/.pr-assets/`; screenshot
|
||||
binaries remain uncommitted and are published through the PR media ref.
|
||||
|
||||
## Risks
|
||||
|
||||
- Shared styling affects delete as well as archive. Preserve the semantic
|
||||
yellow treatment and run both dialog suites.
|
||||
- Longer locale strings may wrap differently. `text-pretty`, viewport checks,
|
||||
and the pseudo-locale build must remain clean.
|
||||
@@ -0,0 +1,143 @@
|
||||
---
|
||||
id: "01-compact-warning"
|
||||
title: "Compact warning and stable archive surface"
|
||||
status: done
|
||||
wave: 1
|
||||
depends_on: []
|
||||
plan: "plan.md"
|
||||
requirements:
|
||||
- ../../specs/ui/requirements/confirmation-warning-hierarchy.md
|
||||
acceptance_criteria:
|
||||
- AC-TASKS-CONFIRMATION-WARNING-001.1
|
||||
- AC-TASKS-CONFIRMATION-WARNING-001.2
|
||||
- AC-TASKS-CONFIRMATION-WARNING-001.3
|
||||
- AC-TASKS-CONFIRMATION-SURFACE-002.1
|
||||
- AC-TASKS-CONFIRMATION-SURFACE-002.2
|
||||
- AC-TASKS-CONFIRMATION-SURFACE-002.3
|
||||
system_design:
|
||||
- ../../specs/ui/system-design/confirmation-warning-hierarchy.md
|
||||
---
|
||||
|
||||
# Task 01: Compact warning and stable archive surface
|
||||
|
||||
## Summary
|
||||
|
||||
Reduce shared still-working warning density in archive and delete confirmation
|
||||
surfaces while preserving text, localization, alert semantics, in-flight
|
||||
visibility, and action behavior. Widen only the fine-pointer archive popover
|
||||
and remove its originating sidebar row's extra layout line while preserving
|
||||
coarse-pointer inline expansion. Prove each contract with failing assertions
|
||||
before production changes, then verify rendered desktop and phone states
|
||||
against a rebuilt frontend.
|
||||
|
||||
## In scope
|
||||
|
||||
- Update the shared warning container and icon classes.
|
||||
- Add an archive-only wider `ActionConfirmPopover` width contract, retaining
|
||||
the `w-64` default for unrelated consumers and constraining it to the
|
||||
viewport.
|
||||
- Mount the fine-pointer archive confirmation outside `TaskItem`'s inline
|
||||
action slot at the context-menu adapter boundary; keep coarse-pointer
|
||||
confirmation row-owned.
|
||||
- Add or update compactness assertions in archive and delete warning tests.
|
||||
- Add a desktop sidebar RED/GREEN assertion for archive popover width,
|
||||
viewport containment, and exact source-row height before/open/cancel.
|
||||
- Run focused unit, lint/typecheck/i18n, production-build, desktop, and phone
|
||||
rendered checks.
|
||||
- Capture and inspect before/after PR screenshots and computed geometry evidence.
|
||||
|
||||
## Out of scope
|
||||
|
||||
- Warning copy or locale values.
|
||||
- Global confirmation-popover width changes.
|
||||
- Archive/delete state, callbacks, detection, dialog composition, action
|
||||
dimensions, focus, Escape, safe-area, or animation.
|
||||
|
||||
## Acceptance
|
||||
|
||||
- The warning uses compact body typography with intentional line height and
|
||||
pretty wrapping; spacing and icon scale are optically reduced in proportion.
|
||||
- Archive and delete tests retain generating/background/idle behavior coverage,
|
||||
`role=alert`, localized text, and shared warning identity.
|
||||
- The archive popover is wider than 256px only for the archive variant, stays
|
||||
inside the viewport at compact widths, and keeps title/body hierarchy.
|
||||
- Fine-pointer sidebar row height is stable before opening, while open, and
|
||||
after cancel; coarse-pointer inline confirmation remains intentionally
|
||||
expanding.
|
||||
- Desktop full-dialog and archive popover/inline paths remain contained; phone
|
||||
actions stay reachable at 44px or larger and document horizontal overflow is
|
||||
zero.
|
||||
|
||||
## Verification
|
||||
|
||||
```bash
|
||||
(cd apps && pnpm install --frozen-lockfile)
|
||||
(cd apps && pnpm --filter @kandev/web test -- --run components/task/task-archive-confirm-dialog.test.tsx components/task/task-delete-confirm-dialog.test.tsx components/task/task-archive-confirmation.test.tsx)
|
||||
(cd apps && pnpm --filter @kandev/web test -- --run components/confirmation/action-confirm-popover.test.tsx components/task/task-switcher-context-menu.test.tsx)
|
||||
(cd apps && pnpm --filter @kandev/web exec eslint components/task/task-still-working-warning.tsx components/task/task-archive-confirm-dialog.test.tsx components/task/task-delete-confirm-dialog.test.tsx components/task/task-archive-confirmation.test.tsx)
|
||||
(cd apps && pnpm --filter @kandev/web exec eslint components/confirmation/action-confirm-popover.tsx components/task/task-archive-confirmation.tsx components/task/task-switcher-context-menu.tsx)
|
||||
(cd apps/web && pnpm run typecheck)
|
||||
(cd apps/web && pnpm run i18n:ratchet)
|
||||
(make build-web)
|
||||
(cd apps/web && pnpm e2e:run --project chromium tests/task/archive-confirmation-preference.spec.ts tests/kanban/card-menu-delete-archive.spec.ts)
|
||||
(cd apps/web && pnpm e2e:run --project mobile-chrome tests/task/mobile-sidebar-task-actions.spec.ts)
|
||||
```
|
||||
|
||||
The compactness regression must fail before the shared class change and pass
|
||||
after it. The final rendered checks must use the production web build and
|
||||
record screenshot paths plus computed hierarchy, viewport containment, action
|
||||
reachability, and document overflow results.
|
||||
|
||||
## Files likely touched
|
||||
|
||||
- `apps/web/components/task/task-still-working-warning.tsx`
|
||||
- `apps/web/components/confirmation/action-confirm-popover.tsx`
|
||||
- `apps/web/components/task/task-archive-confirmation.tsx`
|
||||
- `apps/web/components/task/task-switcher-context-menu.tsx`
|
||||
- `apps/web/components/task/task-archive-confirm-dialog.test.tsx`
|
||||
- `apps/web/components/task/task-delete-confirm-dialog.test.tsx`
|
||||
- `apps/web/components/confirmation/action-confirm-popover.test.tsx`
|
||||
- `apps/web/components/task/task-switcher-context-menu.test.tsx`
|
||||
- `apps/web/e2e/tests/task/archive-confirmation-preference.spec.ts`
|
||||
- `apps/web/e2e/tests/kanban/card-menu-delete-archive.spec.ts` if a focused
|
||||
desktop warning assertion is needed
|
||||
- `apps/web/e2e/tests/task/mobile-sidebar-task-actions.spec.ts` if the existing
|
||||
phone scenario needs a warning-state assertion
|
||||
- `apps/web/.pr-assets/` only as uncommitted capture output
|
||||
|
||||
## Dependencies
|
||||
|
||||
None.
|
||||
|
||||
## Risks
|
||||
|
||||
- Shared change intentionally affects delete warning density too; check both
|
||||
dialog variants and all supported locale wrapping.
|
||||
- Do not use stale Vite assets for E2E. Rebuild after every frontend edit.
|
||||
|
||||
## Parallelism
|
||||
|
||||
`sequential`
|
||||
|
||||
## Inputs
|
||||
|
||||
- `docs/specs/ui/requirements/confirmation-warning-hierarchy.md`
|
||||
- `docs/specs/ui/system-design/confirmation-warning-hierarchy.md`
|
||||
- `apps/web/components/task/task-still-working-warning.tsx`
|
||||
- Existing archive/delete warning tests and phone exemplar in
|
||||
`apps/web/e2e/tests/task/mobile-sidebar-task-actions.spec.ts`
|
||||
|
||||
## Results
|
||||
|
||||
- RED compactness/width assertions failed against current main before the
|
||||
production style and width changes. The old fine-pointer flow also grew the
|
||||
source row from 54.203125px to 62.203125px while open.
|
||||
- GREEN focused unit coverage: 5 files, 59 tests. Affected ESLint, web
|
||||
typecheck, i18n ratchet, and `make build-web` passed.
|
||||
- GREEN rendered coverage: desktop sidebar geometry and full dialog 2/2,
|
||||
phone idle/in-flight inline archive 2/2, and card archive/delete behavior
|
||||
5/5. Width, containment, computed hierarchy, row stability, touch targets,
|
||||
and zero-overflow assertions passed.
|
||||
- Captured and inspected fresh compressed desktop and phone before/after media
|
||||
in `apps/web/.pr-assets/`; media is intentionally not committed to the PR
|
||||
branch.
|
||||
@@ -125,6 +125,7 @@ contract that other capabilities can reuse.
|
||||
- [Slash Command Composer Selection](requirements/slash-command-composer.md)
|
||||
- [Subagent Observability](requirements/subagent-observability.md)
|
||||
- [Nested Submodule Review](requirements/submodule-review.md)
|
||||
- [Task Confirmation Warning Hierarchy](requirements/confirmation-warning-hierarchy.md)
|
||||
- [Task Layout Profiles](requirements/task-layout-profiles.md)
|
||||
- [Task Listing Display Preferences](requirements/task-listing-display-preferences.md)
|
||||
- [Task transcript history visibility](requirements/task-prompt-transcript-visibility.md)
|
||||
@@ -161,6 +162,7 @@ contract that other capabilities can reuse.
|
||||
- [Persistent status motion](system-design/persistent-status-motion.md)
|
||||
- [Prompt History Panel](system-design/prompt-history-panel.md)
|
||||
- [Quick Chat and Terminal Tabs](system-design/quick-terminal.md)
|
||||
- [Task Confirmation Warning Hierarchy](system-design/confirmation-warning-hierarchy.md)
|
||||
- [Resizable Markdown Table Columns](system-design/resizable-markdown-tables.md)
|
||||
- [Task Layout Profiles](system-design/task-layout-profiles.md)
|
||||
- [Terminal Rendering](system-design/terminal-rendering.md)
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
---
|
||||
status: active
|
||||
system: ui
|
||||
created: 2026-08-24
|
||||
owners:
|
||||
- kandev
|
||||
---
|
||||
|
||||
# Task Confirmation Warning Hierarchy Requirements
|
||||
|
||||
## Overview
|
||||
|
||||
Task archive and delete confirmations reuse one in-flight caution. Its visual
|
||||
treatment must read as supporting warning copy, not compete with the
|
||||
confirmation title or primary cleanup description. The fine-pointer archive
|
||||
popover must also give its copy enough room without changing unrelated
|
||||
confirmation surfaces or changing the originating sidebar row's geometry.
|
||||
|
||||
## Terminology
|
||||
|
||||
- **Still-working warning:** The localized caution shown when the task or one
|
||||
of the selected tasks has generating or background activity.
|
||||
|
||||
## Requirements
|
||||
|
||||
### REQ-TASKS-CONFIRMATION-WARNING-001: Compact still-working warning
|
||||
|
||||
**Intent:** Make in-flight caution easy to scan while preserving its semantic
|
||||
meaning and the existing archive and delete workflows.
|
||||
|
||||
#### Acceptance criteria
|
||||
|
||||
- **AC-TASKS-CONFIRMATION-WARNING-001.1:** When an archive or delete
|
||||
confirmation renders for an in-flight task, the still-working warning shall
|
||||
use compact secondary typography with readable line height and deliberate
|
||||
short-text wrapping, and its icon, spacing, and padding shall remain optically
|
||||
balanced with that typography.
|
||||
- **AC-TASKS-CONFIRMATION-WARNING-001.2:** When the warning renders in a full
|
||||
dialog, desktop archive popover, archive inline confirmation, or shared delete
|
||||
dialog, it shall preserve its localized text, `role=alert`, restrained yellow
|
||||
semantic treatment, and existing in-flight visibility conditions.
|
||||
- **AC-TASKS-CONFIRMATION-WARNING-001.3:** At desktop and phone confirmation
|
||||
widths, the warning shall remain contained without document-level horizontal
|
||||
overflow, and existing confirmation actions shall remain reachable with their
|
||||
current touch-target geometry.
|
||||
|
||||
### REQ-TASKS-CONFIRMATION-SURFACE-002: Stable fine-pointer archive surface
|
||||
|
||||
**Intent:** Give the fine-pointer archive confirmation readable width while
|
||||
keeping the source sidebar row stable and preserving the intentional coarse
|
||||
pointer inline confirmation behavior.
|
||||
|
||||
#### Acceptance criteria
|
||||
|
||||
- **AC-TASKS-CONFIRMATION-SURFACE-002.1:** The fine-pointer archive popover
|
||||
shall use an archive-only width opt-in wider than the existing 256px default
|
||||
(targeting a modest `w-72` contract after rendered inspection), with a
|
||||
viewport-aware maximum width. Unrelated `ActionConfirmPopover` consumers
|
||||
shall retain the existing `w-64` default.
|
||||
- **AC-TASKS-CONFIRMATION-SURFACE-002.2:** Opening and cancelling the
|
||||
fine-pointer archive popover shall leave the originating sidebar task row's
|
||||
`getBoundingClientRect().height` stable within normal subpixel precision.
|
||||
The implementation shall remove the source of any extra flex line rather
|
||||
than mask it with fixed or minimum row height, negative margins, or a
|
||||
tolerance-only assertion.
|
||||
- **AC-TASKS-CONFIRMATION-SURFACE-002.3:** The fine-pointer popover shall
|
||||
preserve its existing anchor, focus-return boundary, confirmation callbacks,
|
||||
and compact desktop/tablet viewport containment. Coarse-pointer inline
|
||||
confirmation may continue to expand its row and shall retain its existing
|
||||
44px action geometry and zero-overflow behavior.
|
||||
|
||||
## Out of scope
|
||||
|
||||
- Rewriting or re-translating warning text.
|
||||
- Widening all confirmation popovers globally.
|
||||
- Changing in-flight detection, archive behavior, delete behavior, API
|
||||
contracts, focus handling, Escape handling, safe-area handling, or action
|
||||
dimensions.
|
||||
- Masking sidebar row layout changes with fixed/minimum heights or negative
|
||||
offsets.
|
||||
- Adding animation or redesigning any confirmation surface beyond the shared
|
||||
warning density.
|
||||
@@ -0,0 +1,125 @@
|
||||
---
|
||||
status: current
|
||||
system: ui
|
||||
requirements:
|
||||
- REQ-TASKS-CONFIRMATION-WARNING-001
|
||||
- REQ-TASKS-CONFIRMATION-SURFACE-002
|
||||
---
|
||||
|
||||
# Task Confirmation Warning Hierarchy System Design
|
||||
|
||||
## Purpose and boundaries
|
||||
|
||||
This design owns the presentation contract for the shared still-working warning
|
||||
and the fine-pointer archive confirmation surface used by task archive and
|
||||
delete workflows. It changes density, archive-only width, and mounting location
|
||||
only; the task in-flight signal and destructive-action behavior remain owned by
|
||||
their existing components and runtime contracts.
|
||||
|
||||
## Requirement mapping
|
||||
|
||||
| Requirement | Design section |
|
||||
| --- | --- |
|
||||
| `REQ-TASKS-CONFIRMATION-WARNING-001` | [Components and responsibilities](#components-and-responsibilities) and [Mobile and desktop containment](#mobile-and-desktop-containment) |
|
||||
| `REQ-TASKS-CONFIRMATION-SURFACE-002` | [Popover width contract](#popover-width-contract), [Fine-pointer mounting](#fine-pointer-mounting), and [Mobile and desktop containment](#mobile-and-desktop-containment) |
|
||||
|
||||
## Components and responsibilities
|
||||
|
||||
- `apps/web/components/task/task-still-working-warning.tsx` remains the single
|
||||
markup and style owner.
|
||||
- `TaskArchiveConfirmDialog` and `TaskDeleteConfirmDialog` render it inside the
|
||||
existing full confirmation dialog.
|
||||
- `TaskArchiveConfirmation` renders the same component through
|
||||
`ArchiveDescription` for the desktop popover and phone inline branch.
|
||||
- Consumers continue to decide whether a warning mounts. The shared component
|
||||
does not inspect task state or alter callbacks.
|
||||
|
||||
### Popover width contract
|
||||
|
||||
`ActionConfirmPopover` gains a small width/size contract whose default remains
|
||||
the current `w-64` surface. The archive confirmation opts into a modest wider
|
||||
variant, targeting `w-72`, with a viewport-aware maximum width such as
|
||||
`max-w-[calc(100vw-1rem)]`. The class contract stays local to the shared
|
||||
popover primitive, so watcher and other confirmation consumers do not widen
|
||||
implicitly. The archive body keeps `text-pretty` wrapping and the existing
|
||||
title/action hierarchy.
|
||||
|
||||
### Fine-pointer mounting
|
||||
|
||||
At the `TaskItemWithContextMenu` adapter boundary, `useResponsiveBreakpoint`
|
||||
determines where the already-created archive confirmation node mounts:
|
||||
|
||||
- Fine-pointer confirmation mounts as a sibling of the cloned task row inside
|
||||
the existing anchor wrapper. It does not pass `archiveConfirmation` into
|
||||
`TaskItem`, so `TaskItem` does not add its `flex-wrap` row branch or the
|
||||
`basis-full` action slot for a portaled popover.
|
||||
- Coarse-pointer confirmation continues to pass through `TaskItem`'s existing
|
||||
inline action slot. Its intentional row expansion and mobile action geometry
|
||||
remain unchanged.
|
||||
|
||||
Both branches reuse the same `useTaskSwitcherArchiveConfirmation` node,
|
||||
callbacks, anchor ref, and focus-return ref. No business logic or duplicate
|
||||
confirmation markup is introduced. The change removes the layout cause of
|
||||
fine-pointer row growth instead of compensating for it with row dimensions.
|
||||
|
||||
## Data and contracts
|
||||
|
||||
The component preserves `data-testid="still-working-warning"`, `role="alert"`,
|
||||
the translated `task:stillWorkingWarning` and subject keys, and the existing
|
||||
yellow border/background/text classes. The compact style contract is:
|
||||
|
||||
- warning container: `gap-1.5`, `p-2.5`, `text-xs`, `leading-5`, and
|
||||
`text-pretty`;
|
||||
- warning icon: `h-3.5 w-3.5`, `mt-0.5`, and `shrink-0`;
|
||||
- existing rounded border, yellow semantic colors, and dark-mode contrast stay
|
||||
unchanged.
|
||||
|
||||
No API, WebSocket, state, localization catalog, or persisted-data contract
|
||||
changes are required.
|
||||
|
||||
## Control flow
|
||||
|
||||
The existing task-level `foregroundActivity` projection and explicit
|
||||
`isInFlight` props continue to determine whether the warning is rendered. Once
|
||||
mounted, the shared component formats the same localized subject and warning
|
||||
text, then returns the compact alert markup. Archive/delete callbacks and
|
||||
dialog state remain untouched. The context-menu adapter only chooses the
|
||||
mounting branch described above based on the existing responsive pointer
|
||||
classification.
|
||||
|
||||
## Failure and recovery
|
||||
|
||||
No new failure path exists. If localized text is longer than the available
|
||||
width, `text-pretty` and the explicit line height allow natural wrapping inside
|
||||
the existing flex container. If no task activity is present, no warning mounts,
|
||||
as before.
|
||||
|
||||
## Persistence
|
||||
|
||||
None. This is a client-side presentation-only change.
|
||||
|
||||
## Observability
|
||||
|
||||
Existing component tests continue to assert warning presence and absence for
|
||||
generating, background, and idle activity. A compactness regression asserts the
|
||||
shared class contract. A focused archive-popover regression asserts the wider
|
||||
archive-only class contract. Rendered desktop and phone checks inspect
|
||||
computed type hierarchy, popover width and viewport bounds, source-row height
|
||||
before/open/cancel, action reachability, and document overflow.
|
||||
|
||||
## Mobile and desktop containment
|
||||
|
||||
The desktop check uses the real sidebar task row with a fine pointer. It
|
||||
records the row `getBoundingClientRect().height` before opening Archive, while
|
||||
the archive popover is visible, and after Cancel; all three values must remain
|
||||
stable within subpixel precision. It also verifies the archive popover is
|
||||
strictly wider than 256px and remains inside the viewport at compact widths.
|
||||
|
||||
The phone check keeps the existing coarse-pointer sidebar inline flow. It
|
||||
expects the inline confirmation to remain intentionally row-owned, keeps
|
||||
actions at or above 44px, and asserts zero document horizontal overflow.
|
||||
|
||||
## Related decisions
|
||||
|
||||
- [ADR 0049: Fine-grained foreground-idle busy signal](../../../decisions/0049-fine-grained-foreground-idle-busy-signal.md)
|
||||
- [Mobile task navigation](../requirements/mobile-task-navigation.md)
|
||||
Reference in New Issue
Block a user