diff --git a/ui/src/components/BlockedInboxView.tsx b/ui/src/components/BlockedInboxView.tsx index c858655e1d..f8f18a6cbe 100644 --- a/ui/src/components/BlockedInboxView.tsx +++ b/ui/src/components/BlockedInboxView.tsx @@ -349,10 +349,10 @@ function BlockedInboxRow({ const mobileMeta = ( - {stoppedAge} + {presentation === "legacy" && {stoppedAge}} {ownerName ? ( <> - + {presentation === "legacy" && } } mobileMeta={mobileMeta} + mobileTitleMeta={presentation === "task" ? {stoppedAge} : undefined} desktopTrailing={desktopTrailing} trailingMeta={presentation === "task" && showUpdatedColumn ? stoppedAge : null} /> diff --git a/ui/src/components/BreadcrumbBar.test.tsx b/ui/src/components/BreadcrumbBar.test.tsx index e002a40539..25ec2f0c30 100644 --- a/ui/src/components/BreadcrumbBar.test.tsx +++ b/ui/src/components/BreadcrumbBar.test.tsx @@ -6,6 +6,8 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { BreadcrumbProvider, useBreadcrumbs } from "../context/BreadcrumbContext"; import { BreadcrumbBar } from "./BreadcrumbBar"; +const viewport = vi.hoisted(() => ({ isMobile: false })); + vi.mock("@/lib/router", () => ({ Link: ({ children, className, to }: { children: ReactNode; className?: string; to: string }) => ( {children} @@ -15,7 +17,7 @@ vi.mock("@/lib/router", () => ({ vi.mock("../context/SidebarContext", () => ({ useSidebar: () => ({ collapsed: false, - isMobile: false, + isMobile: viewport.isMobile, toggleCollapsed: vi.fn(), toggleSidebar: vi.fn(), }), @@ -44,11 +46,13 @@ function TaskBreadcrumbs({ panelControl, taskDetailLayout = false, identifier = "PAP-16679", + sourceHref = "/issues", }: { onOpen?: () => void; panelControl?: { open: boolean; onToggle: () => void }; taskDetailLayout?: boolean; identifier?: string; + sourceHref?: string; }) { const { setBreadcrumbs, @@ -58,7 +62,7 @@ function TaskBreadcrumbs({ useEffect(() => { setBreadcrumbs([ - { label: "Tasks", href: "/issues" }, + { label: "Tasks", href: sourceHref }, { label: "Hire your first engineer and create a hiring plan", identifier, @@ -84,6 +88,7 @@ function TaskBreadcrumbs({ setBreadcrumbPanelControl, setBreadcrumbToolbar, setBreadcrumbs, + sourceHref, ]); return ; @@ -97,6 +102,7 @@ describe("BreadcrumbBar", () => { let root: Root; beforeEach(() => { + viewport.isMobile = false; container = document.createElement("div"); document.body.appendChild(container); root = createRoot(container); @@ -107,6 +113,23 @@ describe("BreadcrumbBar", () => { container.remove(); }); + it("shows only the title followed by its identifier for a company-scoped mobile task header", async () => { + viewport.isMobile = true; + await act(async () => { + root.render( + + + , + ); + }); + expect(container.querySelector('a[href="/TES/issues"]')).toBeNull(); + const identifier = container.querySelector('[data-slot="task-title-identifier"]'); + expect(identifier?.textContent).toBe("TES-3"); + expect(identifier?.previousElementSibling?.textContent).toBe("Hire your first engineer and create a hiring plan"); + expect(identifier?.previousElementSibling?.className).toContain("truncate"); + expect(container.querySelector('button[aria-label="Open sidebar"]')).not.toBeNull(); + }); + it("renders a page toolbar in the same persistent row as the task breadcrumb", async () => { const onOpen = vi.fn(); await act(async () => { diff --git a/ui/src/components/BreadcrumbBar.tsx b/ui/src/components/BreadcrumbBar.tsx index 751bdc7fdd..e51f942b65 100644 --- a/ui/src/components/BreadcrumbBar.tsx +++ b/ui/src/components/BreadcrumbBar.tsx @@ -108,6 +108,23 @@ export function BreadcrumbBar({ taskDetailLayout = false }: { taskDetailLayout?: ); + const currentCrumb = breadcrumbs[breadcrumbs.length - 1]; + if (isMobile && breadcrumbs[0]?.label === "Tasks" && currentCrumb.identifier) { + return ( +
+ {menuButton} +

+ {currentCrumb.leading ? ( + {currentCrumb.leading} + ) : null} + {currentCrumb.label} + +

+ {globalToolbarSlots} +
+ ); + } + const breadcrumbTrail = (
diff --git a/ui/src/components/InlineEntitySelector.tsx b/ui/src/components/InlineEntitySelector.tsx index 53853c73b4..c251b7ac89 100644 --- a/ui/src/components/InlineEntitySelector.tsx +++ b/ui/src/components/InlineEntitySelector.tsx @@ -31,6 +31,8 @@ interface InlineEntitySelectorProps { disabled?: boolean; /** Optional test id forwarded to the trigger button. */ triggerTestId?: string; + /** Optional slot name used by consuming surfaces for scoped presentation rules. */ + triggerDataSlot?: string; } const EMPTY_RECENT_OPTION_IDS: string[] = []; @@ -54,6 +56,7 @@ export const InlineEntitySelector = forwardRef { act(() => root.unmount()); }); - it("keeps canonical leading geometry independent of unread state", () => { + it("keeps read and unread rows aligned while allowing a smaller plain-row gutter", () => { const root = createRoot(container); act(() => { root.render( <> + , ); }); const rows = Array.from(container.querySelectorAll('[data-slot="task-row"]')); const unreadSlot = rows[0]?.querySelector('[data-testid="issue-row-unread-slot"]'); - expect(rows).toHaveLength(2); - expect(rows[0]?.className).toBe(rows[1]?.className); + expect(rows).toHaveLength(3); + expect(rows[0]?.className).toBe(rows[2]?.className); + expect(rows[1]?.className).toContain("pl-2 sm:pl-4"); expect(unreadSlot).not.toBeNull(); expect(unreadSlot?.className).toContain("absolute"); expect(unreadSlot?.querySelector('button[aria-label="Mark as read"]')).toBeNull(); diff --git a/ui/src/components/IssueRow.tsx b/ui/src/components/IssueRow.tsx index 1a29621e6e..722cbe3f55 100644 --- a/ui/src/components/IssueRow.tsx +++ b/ui/src/components/IssueRow.tsx @@ -48,6 +48,8 @@ export interface IssueRowProps { desktopMetaLeading?: ReactNode; desktopLeadingSpacer?: boolean; mobileMeta?: ReactNode; + /** Compact mobile timestamp beside the title in canonical task lists. */ + mobileTitleMeta?: ReactNode; desktopTrailing?: ReactNode; /** * Optional pre-fetched external-object summary. Renders a compact severity @@ -130,6 +132,7 @@ export function IssueRow({ desktopMetaLeading, desktopLeadingSpacer = false, mobileMeta, + mobileTitleMeta, desktopTrailing, externalObjectSummary, trailingMeta, @@ -233,7 +236,8 @@ export function IssueRow({ data-slot="task-row" data-unread={isUnread ? "true" : undefined} className={cn( - "group relative flex min-w-0 items-start gap-2 rounded-lg py-2.5 pl-4 pr-2 text-sm no-underline text-inherit sm:items-center sm:py-2", + "group relative flex min-w-0 items-start gap-2 rounded-lg py-2.5 pr-2 text-sm no-underline text-inherit sm:items-center sm:py-2", + showUnreadSlot ? "pl-4" : "pl-2 sm:pl-4", "[&_button]:relative [&_button]:z-10", selected ? "bg-accent/50 hover:bg-accent/50" : "hover:bg-accent/50", checklistCurrentStep && "bg-primary/5", @@ -263,7 +267,7 @@ export function IssueRow({ ) : null} - + {treeGuides > 0 ? Array.from({ length: treeGuides }, (_, level) => { const gapForChevron = chevronInGuide && level === treeGuides - 1; @@ -272,7 +276,7 @@ export function IssueRow({ key={`task-guide-${level}`} data-slot="task-row-tree-guide" aria-hidden="true" - className="relative hidden w-4 shrink-0 self-stretch sm:block" + className="relative block w-4 shrink-0 self-stretch" > - + {recoveryIndicator} + {mobileTitleMeta ? ( + + {mobileTitleMeta} + + ) : null} {checklistDependencyChips ? ( {checklistDependencyChips} diff --git a/ui/src/components/IssuesList.tsx b/ui/src/components/IssuesList.tsx index 378fd79948..5f1569d316 100644 --- a/ui/src/components/IssuesList.tsx +++ b/ui/src/components/IssuesList.tsx @@ -541,7 +541,7 @@ function IssueSearchInput({ }, [draftValue, onDebouncedChange]); return ( -
+
openCreateIssueDialog()}> + @@ -2006,7 +2007,7 @@ function StreamlinedIssuesList({ onUpdateIssue={onUpdateIssue} /> ) : ( - <> +
{groupedContent.map((group) => { if (remainingRowsToRender <= 0) return null; return ( @@ -2154,10 +2155,8 @@ function StreamlinedIssuesList({
0 ? MOBILE_TREE_INDENT[Math.min(depth, MOBILE_TREE_INDENT.length - 1)] : undefined} + // Canonical rows use the same tree-guide slots at every width. + className={rowPresentation === "legacy" && depth > 0 ? MOBILE_TREE_INDENT[Math.min(depth, MOBILE_TREE_INDENT.length - 1)] : undefined} style={useDeferredRowRendering ? { contentVisibility: "auto", @@ -2231,8 +2230,11 @@ function StreamlinedIssuesList({ ) ) : undefined} statusSlot={rowPresentation === "task" ? ( - { e.preventDefault(); e.stopPropagation(); }}> + { e.preventDefault(); e.stopPropagation(); }}> onUpdateIssue(issue.id, { status: s })} /> + {hasChildren && isExpanded ? ( + ) : undefined} metadata={rowPresentation === "task" ? ( @@ -2286,7 +2288,8 @@ function StreamlinedIssuesList({ /> ) : undefined} - mobileMeta={issueActivityText(issue).toLowerCase()} + mobileTitleMeta={rowPresentation === "task" ? issueActivityTimestamp(issue) : undefined} + mobileMeta={rowPresentation === "legacy" ? issueActivityText(issue).toLowerCase() : undefined} trailingMeta={rowPresentation === "task" && visibleIssueColumnSet.has("updated") && availableIssueColumnSet.has("updated") @@ -2489,7 +2492,7 @@ function StreamlinedIssuesList({

)} - +
)}
); diff --git a/ui/src/components/Layout.tsx b/ui/src/components/Layout.tsx index 653b23d5bc..a39e370ffe 100644 --- a/ui/src/components/Layout.tsx +++ b/ui/src/components/Layout.tsx @@ -738,7 +738,7 @@ export function Layout() { ? ({ "--tc-composer-bottom": mobileNavVisible ? "var(--sz-calc-14)" - : "var(--sz-calc-8)", + : "var(--tc-composer-hidden-nav-offset)", } as CSSProperties) : undefined } @@ -752,7 +752,9 @@ export function Layout() { // changes (e.g. switching skill-detail tabs) don't widen/shift // when the vertical scrollbar appears or disappears (PAP-10907). isMobile - ? "overflow-visible pb-(--sz-calc-14)" + ? isTaskDetailRoute && !mobileNavVisible + ? "overflow-visible pb-(--tc-composer-hidden-nav-offset)" + : "overflow-visible pb-(--sz-calc-14)" : "overflow-auto [scrollbar-gutter:stable]", )} > diff --git a/ui/src/components/MobileBottomNav.tsx b/ui/src/components/MobileBottomNav.tsx index 0f8c47c70f..66578e0fc7 100644 --- a/ui/src/components/MobileBottomNav.tsx +++ b/ui/src/components/MobileBottomNav.tsx @@ -2,7 +2,7 @@ import { useMemo } from "react"; import { NavLink, useLocation } from "@/lib/router"; import { House, - CircleDot, + CircleCheck, SquarePen, Users, Inbox, @@ -44,8 +44,8 @@ export function MobileBottomNav({ visible }: MobileBottomNavProps) { const items = useMemo( () => [ { type: "link", to: "/dashboard", label: "Home", icon: House }, - { type: "link", to: "/issues", label: "Tasks", icon: CircleDot }, - { type: "action", label: "Create", icon: SquarePen, onClick: () => openNewIssue() }, + { type: "link", to: "/issues", label: "Tasks", icon: CircleCheck }, + { type: "action", label: "New Task", icon: SquarePen, onClick: () => openNewIssue() }, { type: "link", to: "/agents/all", label: "Agents", icon: Users }, { type: "link", @@ -61,7 +61,7 @@ export function MobileBottomNav({ visible }: MobileBottomNavProps) { return (