Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
197 changes: 108 additions & 89 deletions apps/web/src/components/Rail.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -240,57 +240,64 @@ function SessionRowItem({
<div
className={cn(
"group/session flex min-w-0 items-stretch rounded-md transition-colors duration-(--motion-duration-fast)",
active ? "bg-secondary" : "hover:bg-accent",
active ? "bg-secondary shadow-[inset_2px_0_0_0_var(--color-brand)]" : "hover:bg-accent",
session.freshness === "offline" && "opacity-72",
)}
>
<button
aria-current={active ? "true" : undefined}
aria-label={`${session.title}, ${session.model}, ${archived ? "archived, " : ""}${ariaState}${row.unread ? ", unread" : ""}`}
className="flex min-w-0 flex-1 flex-col gap-0.5 rounded-md px-2 py-1.5 text-left outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1 focus-visible:ring-offset-background"
data-session-row={session.id}
onClick={() => {
workspaceStore.getState().setRailOverlayOpen(false);
void navigate({ params: { sessionId: session.id }, to: "/sessions/$sessionId" });
}}
tabIndex={index === 0 ? 0 : -1}
title={session.title}
type="button"
>
<span className="flex w-full items-center gap-1.5">
<span
className={cn(
"min-w-0 flex-1 truncate text-sm",
active ? "font-medium text-foreground" : "text-foreground",
)}
>
{session.title}
</span>
{session.pendingApprovals > 0 && (
<Badge
aria-label={`${session.pendingApprovals} waiting for approval`}
className="shrink-0"
variant="warning"
<Tooltip>
<TooltipTrigger
render={
<button
aria-current={active ? "true" : undefined}
aria-label={`${session.title}, ${session.model}, ${archived ? "archived, " : ""}${ariaState}${row.unread ? ", unread" : ""}`}
className="flex min-w-0 flex-1 flex-col gap-0.5 rounded-md px-2 py-1.5 text-left outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1 focus-visible:ring-offset-background"
data-session-row={session.id}
onClick={() => {
workspaceStore.getState().setRailOverlayOpen(false);
void navigate({
params: { sessionId: session.id },
to: "/sessions/$sessionId",
});
}}
tabIndex={index === 0 ? 0 : -1}
type="button"
>
{session.pendingApprovals}
</Badge>
)}
{row.unread && (
<span aria-hidden="true" className="size-1.5 shrink-0 rounded-full bg-brand" />
)}
</span>
<span className="flex w-full items-center gap-1.5 text-muted-foreground text-xs">
<span className="truncate font-mono text-[11px]">{session.model}</span>
<span aria-hidden="true">·</span>
<span className="shrink-0">{formatRelativeTime(session.updatedAt, nowMs)}</span>
<span className="min-w-0 flex-1" />
{session.status !== null ? (
<StatusPill className="shrink-0" status={session.status} />
) : (
stateLabel !== "" && <span className="shrink-0">{stateLabel}</span>
)}
</span>
</button>
<span className="flex w-full items-center gap-1.5">
<span className={cn("min-w-0 flex-1 line-clamp-2 break-words text-foreground text-sm leading-5", active ? "font-semibold" : "font-medium")}>
{session.title}
</span>
{session.pendingApprovals > 0 && (
<Badge
aria-label={`${session.pendingApprovals} waiting for approval`}
className="shrink-0"
variant="warning"
>
{session.pendingApprovals}
</Badge>
)}
{row.unread && (
<span aria-hidden="true" className="size-1.5 shrink-0 rounded-full bg-brand" />
)}
</span>
<span className="flex w-full items-center gap-1 text-xs text-muted-foreground leading-4">
<span className="shrink-0">{formatRelativeTime(session.updatedAt, nowMs)}</span>
<span className="min-w-0 flex-1" />
{session.status !== null ? (
<StatusPill className="shrink-0 gap-1" status={session.status} />
) : (
stateLabel !== "" && <span className="shrink-0">{stateLabel}</span>
)}
</span>
</button>
}
/>
<TooltipPopup className="max-w-72" collisionPadding={8} side="right">
<span className="flex min-w-0 flex-col gap-0.5">
<span className="font-medium">{session.title}</span>
<span className="break-words font-mono text-muted-foreground">{session.model}</span>
</span>
</TooltipPopup>
</Tooltip>
{controller !== null && address !== null && (
<Popover.Root onOpenChange={setMenuOpen} open={menuOpen}>
<Popover.Trigger
Expand Down Expand Up @@ -651,50 +658,62 @@ function ProjectHeaderRow({
return (
<div className="flex flex-col">
<div className="flex items-center gap-0.5">
<button
aria-expanded={group.expanded}
className="flex min-h-11 min-w-0 flex-1 items-center gap-1 rounded-md px-1.5 py-1 text-left outline-none transition-colors duration-(--motion-duration-fast) hover:bg-accent focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1 focus-visible:ring-offset-background sm:min-h-0"
data-project-disclosure={group.project.id}
onClick={() =>
workspaceStore.getState().setProjectExpanded(group.project.id, !group.expanded)
}
ref={disclosureRef}
type="button"
>
<ChevronRight
aria-hidden="true"
className={cn(
"size-3.5 shrink-0 text-muted-foreground transition-transform duration-(--motion-duration-fast)",
group.expanded && "rotate-90",
)}
<Tooltip>
<TooltipTrigger
render={
<button
aria-expanded={group.expanded}
aria-label={`${group.project.name}, ${group.sessions.length} ${group.sessions.length === 1 ? "session" : "sessions"}${group.unreadCount > 0 ? `, ${group.unreadCount} unread` : ""}`}
className="flex min-h-11 min-w-0 flex-1 items-center gap-1 rounded-md px-1.5 py-1 text-left outline-none transition-colors duration-(--motion-duration-fast) hover:bg-accent focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1 focus-visible:ring-offset-background sm:min-h-0"
data-project-disclosure={group.project.id}
onClick={() =>
workspaceStore.getState().setProjectExpanded(group.project.id, !group.expanded)
}
ref={disclosureRef}
type="button"
>
<ChevronRight
aria-hidden="true"
className={cn(
"size-3.5 shrink-0 text-muted-foreground transition-transform duration-(--motion-duration-fast)",
group.expanded && "rotate-90",
)}
/>
<span className="min-w-0 flex-1 line-clamp-2 break-words font-medium text-foreground text-xs leading-4">
{group.project.name}
</span>
{group.host.kind === "remote" && (
<Cable aria-hidden="true" className="size-3 shrink-0 text-muted-foreground" />
)}
{group.host.kind === "local" &&
group.host.profileId !== undefined &&
group.host.profileId !== "default" && (
<UsersRound aria-hidden="true" className="size-3 shrink-0 text-muted-foreground" />
)}
{!group.expanded && group.unreadCount > 0 && (
<span
aria-hidden="true"
className="size-1.5 shrink-0 rounded-full bg-brand"
/>
)}
{!group.expanded && group.groupStatus !== null && (
<StatusPill className="shrink-0" labelHidden status={group.groupStatus} />
)}
<span className="shrink-0 text-xs text-muted-foreground leading-4">
{group.sessions.length}
</span>
</button>
}
/>
<span className="truncate font-medium text-foreground text-xs">{group.project.name}</span>
{group.host.kind === "remote" && (
<span className="flex min-w-0 items-center gap-1 text-muted-foreground text-xs">
<Cable aria-hidden="true" className="size-3 shrink-0" />
<span className="truncate">{group.host.name}</span>
</span>
)}
{group.host.kind === "local" &&
group.host.profileId !== undefined &&
group.host.profileId !== "default" && (
<span className="flex min-w-0 items-center gap-1 text-muted-foreground text-xs">
<UsersRound aria-hidden="true" className="size-3 shrink-0" />
<span className="truncate">{group.host.name}</span>
<TooltipPopup className="max-w-72" collisionPadding={8} side="right">
<span className="flex min-w-0 flex-col gap-0.5">
<span className="font-medium">{group.project.name}</span>
<span className="break-words text-muted-foreground">
{group.host.kind === "remote" ? "Remote host" : "Host profile"}: {group.host.name}
</span>
)}
<span className="flex-1" />
{!group.expanded && group.unreadCount > 0 && (
<span
aria-label={`${group.unreadCount} unread`}
className="size-1.5 shrink-0 rounded-full bg-brand"
/>
)}
{!group.expanded && group.groupStatus !== null && (
<StatusPill labelHidden status={group.groupStatus} />
)}
<span className="text-muted-foreground text-xs">{group.sessions.length}</span>
</button>
</span>
</TooltipPopup>
</Tooltip>
{allowCreate && chooseCreateProfile ? (
<Popover.Root onOpenChange={setCreateMenuOpen} open={createMenuOpen}>
<Tooltip>
Expand Down
15 changes: 15 additions & 0 deletions apps/web/test/mobile-touch-targets.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,21 @@ describe("phone touch targets", () => {
expect(session).toContain("Enter focus mode");
expect(session).toContain("⌘⇧F");
expect(rail).toContain("flex min-h-11 min-w-0 flex-1 items-center");
// Folder names and session titles are the rail's visible hierarchy and
// wrap up to two lines. Host and model identifiers stay available in hover
// tooltips instead of competing for the narrow row width; a non-default
// host icon stays inline so touch surfaces (where tooltips never fire)
// keep the host differentiator. The model lives tooltip-only.
expect(rail).toContain("min-w-0 flex-1 line-clamp-2 break-words font-medium text-foreground text-xs");
expect(rail).toContain("line-clamp-2 break-words text-foreground text-sm leading-5");
expect(rail).not.toContain("modelMonogram");
expect(rail).toContain('group.host.kind === "remote" && (');
expect(rail).toContain('Cable aria-hidden="true" className="size-3 shrink-0 text-muted-foreground"');
expect(rail).toContain("collisionPadding={8}");
expect(rail).toContain('aria-label={`${group.project.name}, ${group.sessions.length}');
expect(rail).toContain('Remote host" : "Host profile"');
expect(rail).toContain("break-words font-mono text-muted-foreground");
expect(rail).not.toContain('truncate font-mono text-[11px]">{session.model}');
// Project-row create actions are labeled `New`, stay 44px on touch, and
// announce that the chosen OMP profile owns the session.
expect(rail.match(/flex h-11 shrink-0 [^"]*sm:h-6/g)).toHaveLength(2);
Expand Down
6 changes: 6 additions & 0 deletions packages/ui/src/primitives/tooltip.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,20 +20,26 @@ function TooltipPopup({
sideOffset = 4,
side = "top",
anchor,
collisionPadding,
collisionAvoidance,
children,
...props
}: TooltipPrimitive.Popup.Props & {
align?: TooltipPrimitive.Positioner.Props["align"];
side?: TooltipPrimitive.Positioner.Props["side"];
sideOffset?: TooltipPrimitive.Positioner.Props["sideOffset"];
anchor?: TooltipPrimitive.Positioner.Props["anchor"];
collisionPadding?: TooltipPrimitive.Positioner.Props["collisionPadding"];
collisionAvoidance?: TooltipPrimitive.Positioner.Props["collisionAvoidance"];
}) {
return (
<TooltipPrimitive.Portal>
<TooltipPrimitive.Positioner
align={align}
anchor={anchor}
className="pointer-events-none z-50 h-(--positioner-height) w-(--positioner-width) max-w-(--available-width) transition-[top,left,right,bottom,transform] duration-(--motion-duration-base) ease-(--motion-ease-out) data-instant:transition-none"
collisionAvoidance={collisionAvoidance}
collisionPadding={collisionPadding}
data-slot="tooltip-positioner"
side={side}
sideOffset={sideOffset}
Expand Down
2 changes: 1 addition & 1 deletion provenance/t3code/imports/f0-20260711.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
"sourceBlobSha": "442c394ec220c204ab82aff6b76b3a2e4701e8a8",
"targetPath": "packages/ui/src/primitives/tooltip.tsx",
"classification": "adapted",
"checksum": "sha256:a1b46b18a02855a3fdf1953086c6e39823c44b41292bafd40f4fb14386615c9e"
"checksum": "sha256:53c76a9d6fc7a7ebedac1e32be5962eb782ea30a464a6a1cb2c52802dade419d"
},
{
"sourcePath": "apps/web/src/components/ui/dialog.tsx",
Expand Down
Loading