fix(ui): refine mobile task surfaces (#13122)
This commit is contained in:
parent
5488a79eb5
commit
2d45f42e47
|
|
@ -349,10 +349,10 @@ function BlockedInboxRow({
|
|||
|
||||
const mobileMeta = (
|
||||
<span className="flex flex-wrap items-center gap-x-1.5 gap-y-1 text-xs text-muted-foreground">
|
||||
<span data-testid="blocked-row-age-mobile">{stoppedAge}</span>
|
||||
{presentation === "legacy" && <span data-testid="blocked-row-age-mobile">{stoppedAge}</span>}
|
||||
{ownerName ? (
|
||||
<>
|
||||
<span aria-hidden="true">·</span>
|
||||
{presentation === "legacy" && <span aria-hidden="true">·</span>}
|
||||
<span
|
||||
className={cn(isAgent ? "font-medium text-foreground/90" : null)}
|
||||
data-testid="blocked-row-owner-mobile"
|
||||
|
|
@ -397,6 +397,7 @@ function BlockedInboxRow({
|
|||
/>
|
||||
}
|
||||
mobileMeta={mobileMeta}
|
||||
mobileTitleMeta={presentation === "task" ? <span data-testid="blocked-row-age-mobile">{stoppedAge}</span> : undefined}
|
||||
desktopTrailing={desktopTrailing}
|
||||
trailingMeta={presentation === "task" && showUpdatedColumn ? stoppedAge : null}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -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 }) => (
|
||||
<a className={className} href={to}>{children}</a>
|
||||
|
|
@ -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 <BreadcrumbBar taskDetailLayout={taskDetailLayout} />;
|
||||
|
|
@ -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(
|
||||
<BreadcrumbProvider>
|
||||
<TaskBreadcrumbs identifier="TES-3" sourceHref="/TES/issues" />
|
||||
</BreadcrumbProvider>,
|
||||
);
|
||||
});
|
||||
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 () => {
|
||||
|
|
|
|||
|
|
@ -108,6 +108,23 @@ export function BreadcrumbBar({ taskDetailLayout = false }: { taskDetailLayout?:
|
|||
</Button>
|
||||
);
|
||||
|
||||
const currentCrumb = breadcrumbs[breadcrumbs.length - 1];
|
||||
if (isMobile && breadcrumbs[0]?.label === "Tasks" && currentCrumb.identifier) {
|
||||
return (
|
||||
<div className="h-(--sz-60px) shrink-0 flex items-center border-b border-border px-4">
|
||||
{menuButton}
|
||||
<h1 className="flex min-w-0 flex-1 items-center gap-1.5 text-sm">
|
||||
{currentCrumb.leading ? (
|
||||
<span className="flex shrink-0 items-center">{currentCrumb.leading}</span>
|
||||
) : null}
|
||||
<span className="min-w-0 truncate" title={currentCrumb.label}>{currentCrumb.label}</span>
|
||||
<CrumbIdentifier identifier={currentCrumb.identifier} />
|
||||
</h1>
|
||||
{globalToolbarSlots}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const breadcrumbTrail = (
|
||||
<div className="min-w-0 overflow-hidden flex-1">
|
||||
<Breadcrumb className="min-w-0 overflow-hidden">
|
||||
|
|
|
|||
|
|
@ -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<HTMLButtonElement, InlineEntitySe
|
|||
openOnFocus = true,
|
||||
disabled = false,
|
||||
triggerTestId,
|
||||
triggerDataSlot,
|
||||
},
|
||||
ref,
|
||||
) {
|
||||
|
|
@ -121,6 +124,7 @@ export const InlineEntitySelector = forwardRef<HTMLButtonElement, InlineEntitySe
|
|||
type="button"
|
||||
disabled={disabled}
|
||||
data-testid={triggerTestId}
|
||||
data-slot={triggerDataSlot}
|
||||
className={cn(
|
||||
"inline-flex min-w-0 items-center gap-1 rounded-md border border-border bg-muted/40 px-2 py-1 text-sm font-medium text-foreground transition-colors hover:bg-accent/50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:opacity-50 disabled:pointer-events-none",
|
||||
className,
|
||||
|
|
|
|||
|
|
@ -230,21 +230,23 @@ describe("IssueRow", () => {
|
|||
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(
|
||||
<>
|
||||
<IssueRow issue={createIssue({ id: "read" })} presentation="task" unreadState="hidden" />
|
||||
<IssueRow issue={createIssue({ id: "plain" })} presentation="task" />
|
||||
<IssueRow issue={createIssue({ id: "unread" })} presentation="task" unreadState="visible" />
|
||||
</>,
|
||||
);
|
||||
});
|
||||
|
||||
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();
|
||||
|
|
|
|||
|
|
@ -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({
|
|||
</span>
|
||||
) : null}
|
||||
|
||||
<span data-slot="task-row-leading" className="flex shrink-0 items-center gap-1 pt-px sm:pt-0">
|
||||
<span data-slot="task-row-leading" className="flex shrink-0 items-start self-stretch gap-1 pt-px sm:items-center sm:pt-0">
|
||||
{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"
|
||||
>
|
||||
<span
|
||||
data-slot="task-row-tree-connector"
|
||||
|
|
@ -306,7 +310,7 @@ export function IssueRow({
|
|||
</span>
|
||||
|
||||
<span className="flex min-w-0 flex-1 flex-col gap-1 sm:flex-row sm:items-center sm:gap-2">
|
||||
<span data-slot="task-row-title-cluster" className="flex min-w-0 flex-1 items-start gap-1.5 sm:items-center">
|
||||
<span data-slot="task-row-title-cluster" className="flex min-w-0 flex-1 items-baseline gap-1.5 sm:items-center">
|
||||
<span
|
||||
data-slot="task-row-title"
|
||||
className={cn(
|
||||
|
|
@ -318,6 +322,11 @@ export function IssueRow({
|
|||
{issue.title}{titleSuffix}
|
||||
</span>
|
||||
{recoveryIndicator}
|
||||
{mobileTitleMeta ? (
|
||||
<span className="ml-auto shrink-0 whitespace-nowrap text-right text-xs text-muted-foreground sm:hidden">
|
||||
{mobileTitleMeta}
|
||||
</span>
|
||||
) : null}
|
||||
</span>
|
||||
{checklistDependencyChips ? (
|
||||
<span className="flex flex-wrap gap-1">{checklistDependencyChips}</span>
|
||||
|
|
|
|||
|
|
@ -541,7 +541,7 @@ function IssueSearchInput({
|
|||
}, [draftValue, onDebouncedChange]);
|
||||
|
||||
return (
|
||||
<div className="relative w-48 sm:w-64 md:w-80">
|
||||
<div className="relative w-full sm:w-64 md:w-80">
|
||||
<Search className="pointer-events-none absolute left-2 top-1/2 h-3.5 w-3.5 -translate-y-1/2 text-muted-foreground" />
|
||||
<Input
|
||||
value={draftValue}
|
||||
|
|
@ -1726,9 +1726,10 @@ function StreamlinedIssuesList({
|
|||
|
||||
{/* Toolbar */}
|
||||
<IssuesToolbar
|
||||
className="paperclip-task-list-toolbar"
|
||||
ariaLabel={toolbarPresentation === "collection" ? "Task controls" : undefined}
|
||||
context={(
|
||||
<Button size="sm" variant="outline" onClick={() => openCreateIssueDialog()}>
|
||||
<Button size="sm" variant="outline" aria-label={createButtonLabel} onClick={() => openCreateIssueDialog()}>
|
||||
<Plus className="h-4 w-4 sm:mr-1" />
|
||||
<span className="hidden sm:inline">{createButtonLabel}</span>
|
||||
</Button>
|
||||
|
|
@ -2006,7 +2007,7 @@ function StreamlinedIssuesList({
|
|||
onUpdateIssue={onUpdateIssue}
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
<div className="-mx-2 sm:mx-0">
|
||||
{groupedContent.map((group) => {
|
||||
if (remainingRowsToRender <= 0) return null;
|
||||
return (
|
||||
|
|
@ -2154,10 +2155,8 @@ function StreamlinedIssuesList({
|
|||
<div
|
||||
key={issue.id}
|
||||
data-issue-row-id={issue.id}
|
||||
// Desktop indentation comes from IssueRow's treeGuides
|
||||
// (vertical connector slots); mobile keeps a plain
|
||||
// padding indent (guides are sm-only).
|
||||
className={depth > 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" ? (
|
||||
<span className="inline-flex items-center" onClick={(e) => { e.preventDefault(); e.stopPropagation(); }}>
|
||||
<span className="relative inline-flex items-start self-stretch sm:items-center" onClick={(e) => { e.preventDefault(); e.stopPropagation(); }}>
|
||||
<StatusIcon status={issue.status} size="md" blockerAttention={issue.blockerAttention} onChange={(s) => onUpdateIssue(issue.id, { status: s })} />
|
||||
{hasChildren && isExpanded ? (
|
||||
<span aria-hidden="true" className="pointer-events-none absolute top-5 -bottom-2.5 left-1/2 w-px bg-border sm:hidden" />
|
||||
) : null}
|
||||
</span>
|
||||
) : 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({
|
|||
</p>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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]",
|
||||
)}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -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<MobileNavItem[]>(
|
||||
() => [
|
||||
{ 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 (
|
||||
<nav
|
||||
className={cn(
|
||||
"fixed bottom-0 left-0 right-0 z-30 border-t border-border bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/85 transition-transform duration-200 ease-out md:hidden pb-(--sz-safe-bottom)",
|
||||
"fixed bottom-0 left-0 right-0 z-30 bg-border/50 transition-transform duration-200 ease-out dark:bg-muted md:hidden pb-(--sz-safe-bottom)",
|
||||
visible ? "translate-y-0" : "translate-y-full",
|
||||
)}
|
||||
aria-label="Mobile navigation"
|
||||
|
|
|
|||
|
|
@ -186,11 +186,13 @@ vi.mock("./InlineEntitySelector", async () => {
|
|||
{
|
||||
value: string;
|
||||
placeholder?: string;
|
||||
className?: string;
|
||||
triggerDataSlot?: string;
|
||||
renderTriggerValue?: (option: { id: string; label: string } | null) => ReactNode;
|
||||
}
|
||||
>(function InlineEntitySelectorMock({ value, placeholder, renderTriggerValue }, ref) {
|
||||
>(function InlineEntitySelectorMock({ value, placeholder, className, triggerDataSlot, renderTriggerValue }, ref) {
|
||||
return (
|
||||
<button ref={ref} type="button">
|
||||
<button ref={ref} type="button" className={className} data-slot={triggerDataSlot}>
|
||||
{(renderTriggerValue?.(value ? { id: value, label: value } : null) ?? value) || placeholder}
|
||||
</button>
|
||||
);
|
||||
|
|
@ -412,6 +414,31 @@ describe("NewIssueDialog", () => {
|
|||
act(() => rerendered.root.unmount());
|
||||
});
|
||||
|
||||
it("uses the compact composer control proportions for mobile task fields", async () => {
|
||||
const { root } = renderDialog(container);
|
||||
await flush();
|
||||
|
||||
const compactControls = Array.from(
|
||||
container.querySelectorAll<HTMLElement>('[data-slot="new-issue-compact-control"]'),
|
||||
);
|
||||
const prefix = compactControls.find((control) => control.textContent === "PAP");
|
||||
const assignee = compactControls.find((control) => control.textContent === "Assignee");
|
||||
const project = compactControls.find((control) => control.textContent === "Project");
|
||||
const status = compactControls.find((control) => control.textContent?.trim() === "Todo");
|
||||
const upload = compactControls.find((control) => control.textContent?.trim() === "Upload");
|
||||
const mode = compactControls.find((control) => control.hasAttribute("data-issue-work-mode-chip"));
|
||||
const more = container.querySelector<HTMLElement>('[data-testid="new-issue-more-menu-trigger"]');
|
||||
|
||||
expect(prefix?.className).toContain("p-1.5");
|
||||
for (const control of [assignee, project, status, upload, mode]) {
|
||||
expect(control?.className).toContain("h-8");
|
||||
expect(control?.className).toContain("px-2.5");
|
||||
}
|
||||
expect(more?.className).toContain("size-8");
|
||||
|
||||
act(() => root.unmount());
|
||||
});
|
||||
|
||||
it("submits parent and goal context for sub-issues", async () => {
|
||||
mockProjectsApi.list.mockResolvedValue([
|
||||
{
|
||||
|
|
|
|||
|
|
@ -1390,7 +1390,8 @@ export function NewIssueDialog() {
|
|||
<Popover open={companyOpen} onOpenChange={setCompanyOpen}>
|
||||
<PopoverTrigger asChild>
|
||||
<button
|
||||
className="px-1.5 py-0.5 rounded bg-muted text-xs font-semibold cursor-pointer hover:opacity-80 transition-opacity"
|
||||
data-slot="new-issue-compact-control"
|
||||
className="rounded bg-muted p-1.5 text-xs font-semibold cursor-pointer hover:opacity-80 transition-opacity sm:px-1.5 sm:py-0.5"
|
||||
disabled={isSubIssueMode}
|
||||
>
|
||||
{dialogCompany?.issuePrefix ?? ""}
|
||||
|
|
@ -1478,6 +1479,8 @@ export function NewIssueDialog() {
|
|||
options={assigneeOptions}
|
||||
recentOptionIds={recentAssigneeOptionIds}
|
||||
placeholder="Assignee"
|
||||
className="h-8 px-2.5 py-0 sm:h-auto sm:px-2 sm:py-1"
|
||||
triggerDataSlot="new-issue-compact-control"
|
||||
disablePortal
|
||||
noneLabel="No assignee"
|
||||
searchPlaceholder="Search assignees..."
|
||||
|
|
@ -1537,6 +1540,8 @@ export function NewIssueDialog() {
|
|||
options={projectOptions}
|
||||
recentOptionIds={recentProjectIds}
|
||||
placeholder="Project"
|
||||
className="h-8 px-2.5 py-0 sm:h-auto sm:px-2 sm:py-1"
|
||||
triggerDataSlot="new-issue-compact-control"
|
||||
disablePortal
|
||||
noneLabel="No project"
|
||||
searchPlaceholder="Search projects..."
|
||||
|
|
@ -2079,7 +2084,10 @@ export function NewIssueDialog() {
|
|||
{/* Status chip */}
|
||||
<Popover open={statusOpen} onOpenChange={setStatusOpen}>
|
||||
<PopoverTrigger asChild>
|
||||
<button className="inline-flex items-center gap-1.5 rounded-md border border-border px-2 py-1 text-xs hover:bg-accent/50 transition-colors">
|
||||
<button
|
||||
data-slot="new-issue-compact-control"
|
||||
className="inline-flex h-8 items-center gap-1.5 rounded-md border border-border px-2.5 py-0 text-xs hover:bg-accent/50 transition-colors sm:h-auto sm:px-2 sm:py-1"
|
||||
>
|
||||
<CircleDot className={cn("h-3 w-3", currentStatus.color)} />
|
||||
{currentStatus.label}
|
||||
</button>
|
||||
|
|
@ -2161,7 +2169,8 @@ export function NewIssueDialog() {
|
|||
multiple
|
||||
/>
|
||||
<button
|
||||
className="inline-flex items-center gap-1.5 rounded-md border border-border px-2 py-1 text-xs hover:bg-accent/50 transition-colors text-muted-foreground"
|
||||
data-slot="new-issue-compact-control"
|
||||
className="inline-flex h-8 items-center gap-1.5 rounded-md border border-border px-2.5 py-0 text-xs hover:bg-accent/50 transition-colors text-muted-foreground sm:h-auto sm:px-2 sm:py-1"
|
||||
onClick={() => stageFileInputRef.current?.click()}
|
||||
disabled={createIssue.isPending}
|
||||
>
|
||||
|
|
@ -2175,9 +2184,10 @@ export function NewIssueDialog() {
|
|||
<button
|
||||
type="button"
|
||||
data-issue-work-mode-chip={workMode}
|
||||
data-slot="new-issue-compact-control"
|
||||
aria-keyshortcuts="Meta+Period Control+Period"
|
||||
className={cn(
|
||||
"inline-flex items-center gap-1.5 rounded-md border px-2 py-1 text-xs transition-colors",
|
||||
"inline-flex h-8 items-center gap-1.5 rounded-md border px-2.5 py-0 text-xs transition-colors sm:h-auto sm:px-2 sm:py-1",
|
||||
currentWorkMode.classes.chip,
|
||||
)}
|
||||
>
|
||||
|
|
@ -2217,7 +2227,8 @@ export function NewIssueDialog() {
|
|||
<button
|
||||
type="button"
|
||||
data-testid="new-issue-more-menu-trigger"
|
||||
className="inline-flex items-center justify-center rounded-md border border-border p-1 text-xs text-muted-foreground transition-colors hover:bg-accent/50"
|
||||
data-slot="new-issue-compact-control"
|
||||
className="inline-flex size-8 items-center justify-center rounded-md border border-border p-0 text-xs text-muted-foreground transition-colors hover:bg-accent/50 sm:size-auto sm:p-1"
|
||||
>
|
||||
<MoreHorizontal className="h-3 w-3" />
|
||||
</button>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import type { ReactNode } from "react";
|
||||
import { ChevronDown } from "lucide-react";
|
||||
import { TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||
import { useSidebar } from "../context/SidebarContext";
|
||||
|
||||
|
|
@ -19,18 +20,21 @@ export function PageTabBar({ items, value, onValueChange, align = "center" }: Pa
|
|||
|
||||
if (isMobile && value !== undefined && onValueChange) {
|
||||
return (
|
||||
<select
|
||||
value={value}
|
||||
onChange={(e) => onValueChange(e.target.value)}
|
||||
className="h-9 rounded-md border border-border bg-background px-2 py-1 text-base focus:outline-none focus:ring-1 focus:ring-ring"
|
||||
aria-label="Page section"
|
||||
>
|
||||
{items.map((item) => (
|
||||
<option key={item.value} value={item.value}>
|
||||
{typeof item.label === "string" ? item.label : item.value}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<div className="relative inline-flex">
|
||||
<select
|
||||
value={value}
|
||||
onChange={(e) => onValueChange(e.target.value)}
|
||||
className="h-9 appearance-none rounded-md border border-border bg-background pl-3 pr-9 py-1 text-base focus:outline-none focus:ring-1 focus:ring-ring"
|
||||
aria-label="Page section"
|
||||
>
|
||||
{items.map((item) => (
|
||||
<option key={item.value} value={item.value}>
|
||||
{typeof item.label === "string" ? item.label : item.value}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<ChevronDown aria-hidden="true" className="pointer-events-none absolute right-3 top-1/2 h-3.5 w-3.5 -translate-y-1/2 text-muted-foreground" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,94 @@
|
|||
// @vitest-environment jsdom
|
||||
|
||||
import { act } from "react";
|
||||
import { createRoot, type Root } from "react-dom/client";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { ScrollToBottom } from "./ScrollToBottom";
|
||||
|
||||
vi.mock("../context/SidebarContext", () => ({
|
||||
useSidebar: () => ({ isMobile: true }),
|
||||
}));
|
||||
|
||||
vi.mock("../context/PanelContext", () => ({
|
||||
usePanel: () => ({ panelVisible: false, panelContent: null }),
|
||||
}));
|
||||
|
||||
(globalThis as typeof globalThis & { IS_REACT_ACT_ENVIRONMENT: boolean })
|
||||
.IS_REACT_ACT_ENVIRONMENT = true;
|
||||
|
||||
describe("ScrollToBottom mobile composer docking", () => {
|
||||
let host: HTMLDivElement;
|
||||
let main: HTMLElement;
|
||||
let dock: HTMLDivElement;
|
||||
let root: Root;
|
||||
|
||||
beforeEach(() => {
|
||||
Object.defineProperty(document.documentElement, "scrollHeight", {
|
||||
configurable: true,
|
||||
value: 1000,
|
||||
});
|
||||
Object.defineProperty(window, "innerHeight", {
|
||||
configurable: true,
|
||||
value: 500,
|
||||
});
|
||||
Object.defineProperty(window, "scrollY", {
|
||||
configurable: true,
|
||||
value: 0,
|
||||
});
|
||||
|
||||
main = document.createElement("main");
|
||||
main.id = "main-content";
|
||||
dock = document.createElement("div");
|
||||
dock.dataset.testid = "task-chat-composer-dock";
|
||||
main.appendChild(dock);
|
||||
document.body.appendChild(main);
|
||||
|
||||
host = document.createElement("div");
|
||||
document.body.appendChild(host);
|
||||
root = createRoot(host);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
act(() => root.unmount());
|
||||
document.body.replaceChildren();
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
it("moves with the active mobile composer dock", async () => {
|
||||
await act(async () => {
|
||||
root.render(<ScrollToBottom />);
|
||||
});
|
||||
|
||||
const initialButton = dock.querySelector<HTMLButtonElement>(
|
||||
'button[aria-label="Scroll to bottom"]',
|
||||
);
|
||||
expect(initialButton).not.toBeNull();
|
||||
expect(initialButton?.classList).toContain("absolute");
|
||||
expect(host.querySelector('button[aria-label="Scroll to bottom"]')).toBeNull();
|
||||
|
||||
await act(async () => {
|
||||
dock.remove();
|
||||
await Promise.resolve();
|
||||
});
|
||||
|
||||
const fallbackButton = host.querySelector<HTMLButtonElement>(
|
||||
'button[aria-label="Scroll to bottom"]',
|
||||
);
|
||||
expect(fallbackButton).not.toBeNull();
|
||||
expect(fallbackButton?.classList).toContain("fixed");
|
||||
|
||||
const replacementDock = document.createElement("div");
|
||||
replacementDock.dataset.testid = "task-chat-composer-dock";
|
||||
await act(async () => {
|
||||
main.appendChild(replacementDock);
|
||||
await Promise.resolve();
|
||||
});
|
||||
|
||||
const replacementButton = replacementDock.querySelector<HTMLButtonElement>(
|
||||
'button[aria-label="Scroll to bottom"]',
|
||||
);
|
||||
expect(replacementButton).not.toBeNull();
|
||||
expect(replacementButton?.classList).toContain("absolute");
|
||||
expect(host.querySelector('button[aria-label="Scroll to bottom"]')).toBeNull();
|
||||
});
|
||||
});
|
||||
|
|
@ -1,5 +1,7 @@
|
|||
import { useCallback, useEffect, useState } from "react";
|
||||
import { ArrowDown } from "lucide-react";
|
||||
import { createPortal } from "react-dom";
|
||||
import { useSidebar } from "../context/SidebarContext";
|
||||
import { usePanel } from "../context/PanelContext";
|
||||
import { cn } from "../lib/utils";
|
||||
|
||||
|
|
@ -36,6 +38,24 @@ function distanceFromBottom(target: ReturnType<typeof resolveScrollTarget>) {
|
|||
export function ScrollToBottom() {
|
||||
const [visible, setVisible] = useState(false);
|
||||
const { panelVisible, panelContent } = usePanel();
|
||||
const { isMobile } = useSidebar();
|
||||
const [composerDock, setComposerDock] = useState<HTMLElement | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isMobile) {
|
||||
setComposerDock(null);
|
||||
return;
|
||||
}
|
||||
const main = document.getElementById("main-content");
|
||||
if (!main) return;
|
||||
const findDock = () => setComposerDock(
|
||||
main.querySelector<HTMLElement>('[data-testid="task-chat-composer-dock"]'),
|
||||
);
|
||||
findDock();
|
||||
const observer = new MutationObserver(findDock);
|
||||
observer.observe(main, { childList: true, subtree: true });
|
||||
return () => observer.disconnect();
|
||||
}, [isMobile]);
|
||||
|
||||
useEffect(() => {
|
||||
const check = () => {
|
||||
|
|
@ -70,16 +90,22 @@ export function ScrollToBottom() {
|
|||
|
||||
if (!visible) return null;
|
||||
|
||||
return (
|
||||
const button = (
|
||||
<button
|
||||
data-slot="icon-button"
|
||||
onClick={scroll}
|
||||
className={cn(
|
||||
"fixed bottom-(--sz-calc-21) right-6 z-40 flex h-9 w-9 items-center justify-center rounded-full border border-border bg-background shadow-md hover:bg-accent transition-(--tp-background-color-right) duration-200 md:bottom-6",
|
||||
panelVisible && panelContent && "md:right-(--sz-calc-22)",
|
||||
"z-40 flex h-9 w-9 items-center justify-center rounded-full border border-border bg-background shadow-md hover:bg-accent transition-(--tp-background-color-right) duration-200",
|
||||
isMobile && composerDock
|
||||
? "absolute bottom-full left-1/2 -translate-x-1/2 mb-3"
|
||||
: "fixed bottom-(--sz-calc-21) right-6 md:bottom-6",
|
||||
!isMobile && panelVisible && panelContent && "md:right-(--sz-calc-22)",
|
||||
)}
|
||||
aria-label="Scroll to bottom"
|
||||
>
|
||||
<ArrowDown className="h-4 w-4" />
|
||||
</button>
|
||||
);
|
||||
// Anchoring to the dock follows editor growth and the nav's sticky offset.
|
||||
return isMobile && composerDock ? createPortal(button, composerDock) : button;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -363,7 +363,7 @@ describe("TaskChatThread draft pass-through", () => {
|
|||
expect(scroller?.firstElementChild?.classList).toContain("pt-3");
|
||||
});
|
||||
|
||||
it("keeps the composer dock aligned with the thread's horizontal padding", () => {
|
||||
it("lets the mobile composer dock use the full thread width", () => {
|
||||
render(
|
||||
<TaskChatThread
|
||||
comments={[
|
||||
|
|
@ -393,7 +393,8 @@ describe("TaskChatThread draft pass-through", () => {
|
|||
);
|
||||
expect(thread?.classList).not.toContain("h-(--tc-thread-max-h)");
|
||||
expect(thread?.classList).toContain("flex-1");
|
||||
expect(dock?.classList).toContain("px-4");
|
||||
expect(dock?.classList).toContain("px-2");
|
||||
expect(dock?.classList).toContain("md:px-0");
|
||||
expect(dock?.classList).not.toContain("px-1");
|
||||
expect(dock?.classList).not.toContain("-mt-(--radius-task-composer)");
|
||||
expect(dock?.classList).not.toContain("pt-1");
|
||||
|
|
@ -1834,8 +1835,8 @@ describe("TaskChatThread composer alignment", () => {
|
|||
expect(dock?.classList).not.toContain("-mt-(--radius-task-composer)");
|
||||
expect(composer?.classList).not.toContain("border");
|
||||
expect(composer?.classList).toContain("bg-card");
|
||||
expect(send?.classList).toContain("rounded-md");
|
||||
expect(send?.classList).not.toContain("rounded-full");
|
||||
expect(send?.classList).toContain("rounded-full");
|
||||
expect(send?.classList).not.toContain("rounded-md");
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -2684,10 +2684,10 @@ export function TaskChatThread(props: TaskChatThreadProps) {
|
|||
isMobile
|
||||
? "bottom-(--tc-composer-bottom) z-20 transition-[bottom] duration-200 ease-out"
|
||||
: "bottom-0 z-10",
|
||||
"mx-auto flex w-full max-w-(--tc-shell-max-w) flex-col gap-2 px-4 pb-2",
|
||||
"mx-auto flex w-full max-w-(--tc-shell-max-w) flex-col gap-2 px-2 pb-2 md:px-4",
|
||||
streamlinedUiEnabled && "md:px-0 md:pb-0",
|
||||
(!streamlinedUiEnabled || isMobile) &&
|
||||
"bg-background/80 pt-1 backdrop-blur supports-[backdrop-filter]:bg-background/60",
|
||||
"bg-background/80 pt-1 backdrop-blur supports-[backdrop-filter]:bg-background/60 dark:bg-transparent dark:backdrop-blur-none dark:supports-[backdrop-filter]:bg-transparent",
|
||||
)}
|
||||
>
|
||||
{composerAccessory}
|
||||
|
|
|
|||
|
|
@ -353,10 +353,11 @@ describe("TaskChatComposer", () => {
|
|||
);
|
||||
});
|
||||
|
||||
it("reserves enough mobile editor height for a wrapped two-line placeholder", () => {
|
||||
it("uses a compact mobile editor that can grow with the message", () => {
|
||||
render(<TaskChatComposer onAdd={vi.fn()} workMode="standard" mobile />);
|
||||
|
||||
expect(editable().dataset.contentClassName).toContain("min-h-(--sz-72px)");
|
||||
expect(editable().dataset.contentClassName).toContain("min-h-(--sz-48px)");
|
||||
expect(editable().dataset.contentClassName).toContain("max-h-(--sz-28dvh)");
|
||||
});
|
||||
|
||||
it("submits the trimmed body on Cmd+Enter and clears the draft", async () => {
|
||||
|
|
|
|||
|
|
@ -280,7 +280,12 @@ const MODE_DESCRIPTION: Partial<Record<IssueWorkMode, string>> = {
|
|||
};
|
||||
|
||||
/** v7 per-mode placeholder copy; `{agent}` is the pending assignee's name. */
|
||||
function modePlaceholder(mode: IssueWorkMode, agentName: string): string {
|
||||
function modePlaceholder(mode: IssueWorkMode, agentName: string, mobile: boolean): string {
|
||||
if (mobile) {
|
||||
if (mode === "planning") return `Plan with ${agentName}…`;
|
||||
if (mode === "ask") return `Ask ${agentName}…`;
|
||||
return `Message ${agentName}…`;
|
||||
}
|
||||
switch (mode) {
|
||||
case "planning":
|
||||
return `Plan with ${agentName} — shapes the plan doc, no code changes…`;
|
||||
|
|
@ -502,7 +507,7 @@ export function TaskChatComposer({
|
|||
assigneeLabel === "Unassigned" ? "the agent" : assigneeLabel;
|
||||
const effectivePlaceholder = queuedEdit
|
||||
? "Edit queued message…"
|
||||
: (placeholder ?? modePlaceholder(pendingMode, assigneeName));
|
||||
: (placeholder ?? modePlaceholder(pendingMode, assigneeName, mobile));
|
||||
const goalUnavailable = runnerGoalCapability?.availability !== "available";
|
||||
const goalCommandOption: ActionCommandOption = {
|
||||
id: "action:goal",
|
||||
|
|
@ -837,6 +842,7 @@ export function TaskChatComposer({
|
|||
streamlined
|
||||
? "paperclip-task-chat-composer rounded-(--radius-task-composer) border border-border bg-card p-(--sz-18px) shadow-(--shadow-task-composer) dark:border-0 dark:bg-muted dark:shadow-none"
|
||||
: "paperclip-task-chat-composer rounded-xl bg-card p-(--sz-18px)",
|
||||
mobile && "p-3",
|
||||
)}
|
||||
onKeyDownCapture={(e) => {
|
||||
// Capture mode shortcuts on the wrapper so they work while the rich
|
||||
|
|
@ -977,7 +983,7 @@ export function TaskChatComposer({
|
|||
className={cn(disabled && "opacity-60")}
|
||||
contentClassName={
|
||||
mobile
|
||||
? "max-h-(--sz-28dvh) min-h-(--sz-72px) overflow-y-auto px-1 py-1 text-base scrollbar-auto-hide"
|
||||
? "max-h-(--sz-28dvh) min-h-(--sz-48px) overflow-y-auto px-1 py-1 text-base scrollbar-auto-hide"
|
||||
: "max-h-(--sz-28dvh) min-h-(--sz-48px) overflow-y-auto px-1 py-1 text-sm scrollbar-auto-hide"
|
||||
}
|
||||
/>
|
||||
|
|
@ -1100,6 +1106,7 @@ export function TaskChatComposer({
|
|||
)}
|
||||
style={{ "--sc": modeHue(pendingMode) } as CSSProperties}
|
||||
data-testid="task-chat-composer-mode"
|
||||
data-slot="task-chat-mode-trigger"
|
||||
data-pending-work-mode={pendingMode}
|
||||
>
|
||||
{modeMeta.label}
|
||||
|
|
@ -1248,14 +1255,15 @@ export function TaskChatComposer({
|
|||
: "Send"
|
||||
}
|
||||
className={cn(
|
||||
"flex h-8 w-8 shrink-0 items-center justify-center transition-transform hover:scale-105 disabled:scale-100",
|
||||
"flex h-8 w-8 shrink-0 items-center justify-center rounded-full transition-transform hover:scale-105 disabled:scale-100",
|
||||
streamlined
|
||||
? "rounded-full bg-foreground text-background disabled:bg-foreground disabled:text-background disabled:opacity-100"
|
||||
: "rounded-md bg-primary text-primary-foreground disabled:bg-muted disabled:text-muted-foreground",
|
||||
? "bg-foreground text-background disabled:bg-foreground disabled:text-background disabled:opacity-100"
|
||||
: "bg-primary text-primary-foreground disabled:bg-muted disabled:text-muted-foreground",
|
||||
)}
|
||||
data-testid={
|
||||
showStop ? "task-chat-composer-stop" : "task-chat-composer-send"
|
||||
}
|
||||
data-slot="icon-button"
|
||||
>
|
||||
{submitting || (showStop && stopControl.stopping) ? (
|
||||
<Loader2 className="h-4 w-4 animate-spin" aria-hidden />
|
||||
|
|
|
|||
|
|
@ -354,7 +354,7 @@ export function TaskChatThreadView({
|
|||
const body = (
|
||||
<div
|
||||
className={cn(
|
||||
"mx-auto flex w-full max-w-(--tc-shell-max-w) flex-col px-4 py-4",
|
||||
"paperclip-mobile-thread mx-auto flex w-full max-w-(--tc-shell-max-w) flex-col px-2 py-4 md:px-4",
|
||||
streamlined ? "md:px-0" : "gap-5",
|
||||
className,
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -348,6 +348,8 @@
|
|||
composer's action row never hides behind the nav. Desktop and the classic
|
||||
thread never override it and fall through to the safe-area default. */
|
||||
--tc-composer-bottom: var(--sz-calc-8);
|
||||
/* Together with the dock's pb-2, match the page's p-4 side gutters. */
|
||||
--tc-composer-hidden-nav-offset: calc(var(--sz-safe-bottom) + var(--spacing) * 2);
|
||||
}
|
||||
|
||||
.dark {
|
||||
|
|
@ -542,6 +544,8 @@
|
|||
[data-slot="toggle"],
|
||||
[data-slot="checkbox"],
|
||||
[data-slot="icon-button"],
|
||||
[data-slot="task-chat-mode-trigger"],
|
||||
[data-slot="new-issue-compact-control"],
|
||||
[data-size="xs"],
|
||||
[data-size="icon-xs"],
|
||||
[data-size="icon-sm"],
|
||||
|
|
@ -555,6 +559,55 @@
|
|||
}
|
||||
}
|
||||
|
||||
/* Mobile collection toolbars and square agent actions. */
|
||||
@media (width < 40rem) {
|
||||
[role="toolbar"][aria-label="Inbox controls"] [data-slot="tabs-list"][data-variant="line"] [data-slot="tabs-trigger"]::after {
|
||||
bottom: var(--spacing);
|
||||
}
|
||||
|
||||
[role="toolbar"][aria-label="Inbox controls"] button[data-size="icon"],
|
||||
[role="group"][aria-label="Agent view"] > button {
|
||||
width: var(--sz-44px);
|
||||
height: var(--sz-44px);
|
||||
min-height: var(--sz-44px);
|
||||
padding: 0;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.paperclip-task-list-toolbar > :first-child {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) auto;
|
||||
}
|
||||
|
||||
.paperclip-task-list-toolbar [data-slot="collection-toolbar-search"] {
|
||||
grid-column: 1;
|
||||
grid-row: 1;
|
||||
}
|
||||
|
||||
.paperclip-task-list-toolbar [data-slot="collection-toolbar-search"] input {
|
||||
height: var(--sz-44px);
|
||||
}
|
||||
|
||||
.paperclip-task-list-toolbar [data-slot="collection-toolbar-context"] {
|
||||
grid-column: 2;
|
||||
grid-row: 1;
|
||||
}
|
||||
|
||||
.paperclip-task-list-toolbar [data-slot="collection-toolbar-context"] button,
|
||||
.paperclip-task-list-toolbar [data-slot="collection-toolbar-controls"] button {
|
||||
width: var(--sz-44px);
|
||||
height: var(--sz-44px);
|
||||
min-height: var(--sz-44px);
|
||||
padding: 0;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.paperclip-task-list-toolbar > :first-child > :last-child {
|
||||
grid-column: 1 / -1;
|
||||
grid-row: 2;
|
||||
}
|
||||
}
|
||||
|
||||
/* Dark mode scrollbars */
|
||||
.dark {
|
||||
color-scheme: dark;
|
||||
|
|
@ -1950,6 +2003,12 @@ a.paperclip-mention-chip[data-mention-kind="agent"]::before {
|
|||
padding-left: 0.2rem;
|
||||
}
|
||||
|
||||
@media (width < 48rem) {
|
||||
.paperclip-mobile-thread .paperclip-markdown :where(ul, ol) {
|
||||
padding-left: calc(var(--spacing) * 5);
|
||||
}
|
||||
}
|
||||
|
||||
.paperclip-markdown li > :where(p, ul, ol) {
|
||||
margin-top: 0.3rem;
|
||||
margin-bottom: 0.3rem;
|
||||
|
|
|
|||
|
|
@ -2689,6 +2689,7 @@ function StreamlinedInbox() {
|
|||
{actionError && <p className="text-sm text-destructive">{actionError}</p>}
|
||||
|
||||
{tab === "blocked" ? (
|
||||
<div className="-mx-2 sm:mx-0">
|
||||
<BlockedInboxView
|
||||
companyId={selectedCompanyId!}
|
||||
searchQuery={searchQuery}
|
||||
|
|
@ -2707,6 +2708,7 @@ function StreamlinedInbox() {
|
|||
showUpdatedColumn={visibleIssueColumnSet.has("updated") && availableIssueColumnSet.has("updated")}
|
||||
presentation={streamlinedUiEnabled ? "task" : "legacy"}
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{tab !== "blocked" && !allLoaded && visibleSections.length === 0 && (
|
||||
|
|
@ -2736,7 +2738,7 @@ function StreamlinedInbox() {
|
|||
<div>
|
||||
<div
|
||||
ref={listRef}
|
||||
className="overflow-hidden"
|
||||
className="-mx-2 overflow-hidden sm:mx-0"
|
||||
onPointerDownCapture={noteInboxSortInteraction}
|
||||
onWheelCapture={noteInboxSortInteraction}
|
||||
>
|
||||
|
|
@ -2816,7 +2818,7 @@ function StreamlinedInbox() {
|
|||
<ChevronRight className={cn("h-3.5 w-3.5 transition-transform", isExpanded && "rotate-90")} />
|
||||
</button>
|
||||
) : streamlinedUiEnabled ? (
|
||||
<span data-slot="task-row-disclosure-spacer" className="h-4 w-4 shrink-0" aria-hidden="true" />
|
||||
<span data-slot="task-row-disclosure-spacer" className={cn("h-4 w-4 shrink-0", !nestingEnabled && "hidden sm:block")} aria-hidden="true" />
|
||||
) : undefined}
|
||||
statusSlot={streamlinedUiEnabled ? rowStatusIcon : undefined}
|
||||
metadata={streamlinedUiEnabled ? (
|
||||
|
|
@ -2868,7 +2870,8 @@ function StreamlinedInbox() {
|
|||
({childCount} sub-task{childCount !== 1 ? "s" : ""})
|
||||
</span>
|
||||
) : undefined}
|
||||
mobileMeta={issueActivityText(issue).toLowerCase()}
|
||||
mobileTitleMeta={streamlinedUiEnabled ? issueActivityTimestamp(issue) : undefined}
|
||||
mobileMeta={streamlinedUiEnabled ? undefined : issueActivityText(issue).toLowerCase()}
|
||||
mobileLeading={!streamlinedUiEnabled ? (
|
||||
depth === 0 && hasChildren && collapseParentId ? (
|
||||
<button
|
||||
|
|
|
|||
Loading…
Reference in New Issue