From d8f2dc69a2c64651e0fdb83ee50020f314263ace Mon Sep 17 00:00:00 2001 From: iamnbutler Date: Mon, 19 Oct 2026 16:53:56 -0400 Subject: [PATCH 1/3] fix(ui): put faces after the channel name and pin only in the context menu Sidebar rows reserved space at the right edge for the facepile and a hover pin button, so short names truncated with room to spare. The facepile now follows the name inside the row, the name alone shrinks when both don't fit, and pin and unpin live only in the row's context menu. Fixes #505 Why: packages/ui/src/components/session-item/session-item.tsx#function Presence | the facepile sits inside the row's button, so its trigger keeps an empty title or the row's summary tooltip covers the presence tooltip --- .../ui/src/components/session-item/actions.ts | 4 - .../session-item/session-item-actions.tsx | 96 +------------------ .../components/session-item/session-item.tsx | 54 +++-------- 3 files changed, 12 insertions(+), 142 deletions(-) diff --git a/packages/ui/src/components/session-item/actions.ts b/packages/ui/src/components/session-item/actions.ts index 009fc01e..a49528b5 100644 --- a/packages/ui/src/components/session-item/actions.ts +++ b/packages/ui/src/components/session-item/actions.ts @@ -10,7 +10,3 @@ export type ActionProps = { export function canTogglePin({ pinned, onPin, onUnpin }: ActionProps) { return pinned ? Boolean(onUnpin) : Boolean(onPin); } - -export function hasVisibleActions(actions: ActionProps) { - return canTogglePin(actions) || Boolean(actions.onArchive); -} diff --git a/packages/ui/src/components/session-item/session-item-actions.tsx b/packages/ui/src/components/session-item/session-item-actions.tsx index df220b15..6e3002d6 100644 --- a/packages/ui/src/components/session-item/session-item-actions.tsx +++ b/packages/ui/src/components/session-item/session-item-actions.tsx @@ -1,12 +1,9 @@ -import { type ActionProps, canTogglePin, hasVisibleActions } from "./actions"; +import { type ActionProps, canTogglePin } from "./actions"; import type { MouseEvent, ReactNode } from "react"; import { useRef, useState } from "react"; import { ContextMenu } from "@base-ui/react/context-menu"; -import { hitArea } from "../../lib/hit-area"; -import { cn } from "../../lib/utils"; import { motion } from "../../ui/popup"; -import { Tooltip, TooltipContent, TooltipTrigger } from "../../ui/tooltip"; import type { MoveTarget, SidebarRow } from "./session-item.types"; import { @@ -35,20 +32,6 @@ const MENU_ITEM_DESTRUCTIVE = "text-destructive focus:bg-destructive/10 focus:text-destructive dark:focus:bg-destructive/20"; const MENU_ITEM_DESTRUCTIVE_FULL = `${MENU_ITEM} ${MENU_ITEM_DESTRUCTIVE}`; const MENU_SEPARATOR = "-mx-1 my-1 h-px bg-border/50"; -// The enlarged hit area spills into neighbouring rows, so only the hovered row's may catch presses. -const ACTION_BUTTON = cn( - hitArea, - "before:pointer-events-none group-hover/row:before:pointer-events-auto", - "grid size-6 place-items-center rounded-md squircle text-muted-foreground outline-2 outline-offset-[-1px] outline-transparent transition-[background-color,color,transform] duration-150 ease-out hover:bg-background/70 hover:text-foreground group-data-[primary=true]/menu:active:translate-y-px focus-visible:outline-ring/50", -); -// Hit-testing must not wait for :hover. A press that arrives before hover styles apply lands on the -// row, its release lands on the button, and the click then reaches neither. -const SESSION_ACTIONS = - "inline-flex items-center gap-0 -space-x-0.5 opacity-0 transition-opacity duration-150 ease-out motion-reduce:transition-none group-hover/row:opacity-100 group-focus-within/row:opacity-100 group-data-[menu-open=true]/menu:opacity-100"; - -type SessionActionsProps = ActionProps & { - data: SidebarRow; -}; type RowMenuProps = ActionProps & { data: SidebarRow; @@ -82,83 +65,6 @@ function stop(event: MouseEvent) { event.stopPropagation(); } -function ActionButton( - { - label, - children, - onClick, - }: { - label: string; - children: ReactNode; - onClick?: (event: MouseEvent) => void; - }, -) { - return ( - - - } - > - {children} - - - {label} - - - ); -} - -export function SessionActions( - { - data, - pinned, - onPin, - onUnpin, - onArchive, - }: SessionActionsProps, -) { - let canPin = canTogglePin({ pinned, onPin, onUnpin, onArchive }); - if (!hasVisibleActions({ pinned, onPin, onUnpin, onArchive })) return null; - - function pin(event: MouseEvent) { - stop(event); - if (pinned) onUnpin?.(data); - else onPin?.(data); - } - - function archive(event: MouseEvent) { - stop(event); - onArchive?.(data); - } - - return ( - - {canPin && ( - - {pinned ? : } - - )} - {onArchive && (data.lifecycle === "archived" - ? ( - - - - ) - : ( - - - - ))} - - ); -} - function MoveSection( { destinations, moving, onMove }: { destinations: Destinations; diff --git a/packages/ui/src/components/session-item/session-item.tsx b/packages/ui/src/components/session-item/session-item.tsx index f2012095..884654bb 100644 --- a/packages/ui/src/components/session-item/session-item.tsx +++ b/packages/ui/src/components/session-item/session-item.tsx @@ -5,8 +5,7 @@ import { Facepile } from "../facepile/facepile"; import { cn } from "../../lib/utils"; import { Tooltip, TooltipContent, TooltipTrigger } from "../../ui/tooltip"; -import { RowMenu, SessionActions } from "./session-item-actions"; -import { hasVisibleActions } from "./actions"; +import { RowMenu } from "./session-item-actions"; import { RowGlyph } from "./session-item-glyph"; import type { MoveTarget, SidebarRow } from "./session-item.types"; import { @@ -23,11 +22,6 @@ const EXPANDED_CONTENT_PADDING_BOTTOM = "data-[open=true]:pb-(--session-item-expanded-padding-bottom)"; const TITLE_TO_EXPANDED_CONTENT_GAP = "data-[open=true]:mt-(--session-item-title-to-content-gap)"; const EXPANDED_CONTENT_ROW_GAP = "gap-y-1"; -// The facepile and the actions sit side by side so pointing at a face never reaches pin, and -// neither moves on hover. The 8px gap equals the spill of the pin's 40px hit area around its 24px -// button, so that hit area ends at the facepile's edge; the facepile also stacks above it. -const TRAILING = - "pointer-events-none absolute top-1/2 right-1 z-20 inline-flex -translate-y-1/2 items-center gap-2 *:pointer-events-auto"; export type SessionItemProps = ComponentPropsWithRef<"button"> & { data: SidebarRow; @@ -72,6 +66,9 @@ export function Presence({ members }: { members: SidebarRow["online"] }) { render={ member.alt).join(", ")}`} + // The facepile sits inside the row's button; an empty title keeps the row's summary + // tooltip from covering this one. + title="" className="shrink-0 rounded-full" /> } @@ -232,12 +229,7 @@ function expandedBody(data: SidebarRow, content: SessionItemProps["expandedConte return typeof content === "function" ? content(data) : content ?? defaultExpandedContent(data); } -function buttonClass( - hoverExpand: boolean | undefined, - hasActions: boolean, - online: number, - className?: string, -) { +function buttonClass(hoverExpand: boolean | undefined, className?: string) { return cn( "relative flex h-[calc(var(--spacing)*8-2px)] w-full px-1 text-left", hoverExpand ? "flex-col items-stretch justify-center gap-0" : "items-center gap-1.5", @@ -247,14 +239,9 @@ function buttonClass( "data-[status=active]:selected-surface", "data-[status=active]:text-accent-text data-[status=active]:font-medium", hoverExpand - ? "transition-[background-color,filter,padding-right] duration-150 ease-out motion-reduce:transition-none data-[expanded=true]:rounded-b-none data-[expanded=true]:bg-popover data-[expanded=true]:hover:bg-popover" - : "transition-[padding-right] duration-150 ease-out motion-reduce:transition-none", + && "transition-[background-color,filter] duration-150 ease-out motion-reduce:transition-none data-[expanded=true]:rounded-b-none data-[expanded=true]:bg-popover data-[expanded=true]:hover:bg-popover", "group-data-[primary=true]/menu:active:translate-y-px group-data-[primary=true]/menu:active:brightness-95", "outline-2 outline-offset-0 outline-transparent focus-visible:outline-ring/50", - hasActions - && online === 0 - && "group-hover/row:pr-13 group-focus-within/row:pr-13 group-data-[menu-open=true]/menu:pr-13", - online > 0 && (hasActions ? "pr-24" : "pr-17"), className, ); } @@ -290,7 +277,7 @@ export function SessionItem( ...props }: SessionItemProps, ) { - let { onPin: actionPin, onUnpin: actionUnpin, ...menu } = actions(data, { + let menu = actions(data, { onPin, onUnpin, onFork, @@ -304,7 +291,6 @@ export function SessionItem( onMove, onCompute, }); - let hasActions = hasVisibleActions({ pinned, onPin: actionPin, onUnpin: actionUnpin }); let [hovered, setHovered] = useState(false); let timer = useRef | undefined>(undefined); let content = hoverExpand ? expandedBody(data, expandedContent) : null; @@ -320,7 +306,8 @@ export function SessionItem( - {data.name} + {data.name} + ); @@ -342,13 +329,7 @@ export function SessionItem( }, []); return ( - +
{hoverExpand @@ -411,19 +392,6 @@ export function SessionItem( ) : line} - - {data.online.length > 0 && ( - - - - )} - -
); From 7d2fc43239b90812b856588548c0fd5fe490ae03 Mon Sep 17 00:00:00 2001 From: iamnbutler Date: Mon, 19 Oct 2026 16:54:13 -0400 Subject: [PATCH 2/3] chore: retire lane notes for the removed row pin button --- ...Z5DE0F497YRARJ7QZ1R811-row-actions-must-be-hit-test.md | 0 ...Z5DE0YMYWG1H4KR7RVJQ3N-the-enlarged-hit-area-only-t.md | 0 ...ZM8GB6RD86274YM563E355-the-gap-between-facepile-and.md | 0 ...AZ2FXC4FD834XGK86MA2NC-the-facepile-sits-inside-the.md | 8 ++++++++ 4 files changed, 8 insertions(+) rename .lane/{memory => attic}/packages/ui/src/components/session-item/session-item-actions.tsx/01M4Z5DE0F497YRARJ7QZ1R811-row-actions-must-be-hit-test.md (100%) rename .lane/{memory => attic}/packages/ui/src/components/session-item/session-item-actions.tsx/01M4Z5DE0YMYWG1H4KR7RVJQ3N-the-enlarged-hit-area-only-t.md (100%) rename .lane/{memory => attic}/packages/ui/src/components/session-item/session-item.tsx/01M4ZM8GB6RD86274YM563E355-the-gap-between-facepile-and.md (100%) create mode 100644 .lane/memory/packages/ui/src/components/session-item/session-item.tsx/01M5AZ2FXC4FD834XGK86MA2NC-the-facepile-sits-inside-the.md diff --git a/.lane/memory/packages/ui/src/components/session-item/session-item-actions.tsx/01M4Z5DE0F497YRARJ7QZ1R811-row-actions-must-be-hit-test.md b/.lane/attic/packages/ui/src/components/session-item/session-item-actions.tsx/01M4Z5DE0F497YRARJ7QZ1R811-row-actions-must-be-hit-test.md similarity index 100% rename from .lane/memory/packages/ui/src/components/session-item/session-item-actions.tsx/01M4Z5DE0F497YRARJ7QZ1R811-row-actions-must-be-hit-test.md rename to .lane/attic/packages/ui/src/components/session-item/session-item-actions.tsx/01M4Z5DE0F497YRARJ7QZ1R811-row-actions-must-be-hit-test.md diff --git a/.lane/memory/packages/ui/src/components/session-item/session-item-actions.tsx/01M4Z5DE0YMYWG1H4KR7RVJQ3N-the-enlarged-hit-area-only-t.md b/.lane/attic/packages/ui/src/components/session-item/session-item-actions.tsx/01M4Z5DE0YMYWG1H4KR7RVJQ3N-the-enlarged-hit-area-only-t.md similarity index 100% rename from .lane/memory/packages/ui/src/components/session-item/session-item-actions.tsx/01M4Z5DE0YMYWG1H4KR7RVJQ3N-the-enlarged-hit-area-only-t.md rename to .lane/attic/packages/ui/src/components/session-item/session-item-actions.tsx/01M4Z5DE0YMYWG1H4KR7RVJQ3N-the-enlarged-hit-area-only-t.md diff --git a/.lane/memory/packages/ui/src/components/session-item/session-item.tsx/01M4ZM8GB6RD86274YM563E355-the-gap-between-facepile-and.md b/.lane/attic/packages/ui/src/components/session-item/session-item.tsx/01M4ZM8GB6RD86274YM563E355-the-gap-between-facepile-and.md similarity index 100% rename from .lane/memory/packages/ui/src/components/session-item/session-item.tsx/01M4ZM8GB6RD86274YM563E355-the-gap-between-facepile-and.md rename to .lane/attic/packages/ui/src/components/session-item/session-item.tsx/01M4ZM8GB6RD86274YM563E355-the-gap-between-facepile-and.md diff --git a/.lane/memory/packages/ui/src/components/session-item/session-item.tsx/01M5AZ2FXC4FD834XGK86MA2NC-the-facepile-sits-inside-the.md b/.lane/memory/packages/ui/src/components/session-item/session-item.tsx/01M5AZ2FXC4FD834XGK86MA2NC-the-facepile-sits-inside-the.md new file mode 100644 index 00000000..10401441 --- /dev/null +++ b/.lane/memory/packages/ui/src/components/session-item/session-item.tsx/01M5AZ2FXC4FD834XGK86MA2NC-the-facepile-sits-inside-the.md @@ -0,0 +1,8 @@ +--- +id: 01M5AZ2FXC4FD834XGK86MA2NC +anchor: function Presence +created: 2026-10-19T20:53:56Z +norm: '1' +--- + +the facepile sits inside the row's button, so its trigger keeps an empty title or the row's summary tooltip covers the presence tooltip From eec56fca6d8b9b18776277e934194dc4598d3108 Mon Sep 17 00:00:00 2001 From: iamnbutler Date: Mon, 19 Oct 2026 16:54:16 -0400 Subject: [PATCH 3/3] lane: sync sidebar-faces memory --- ...01M5AZ2FXC4FD834XGK86MA2NC-the-facepile-sits-inside-the.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.lane/memory/packages/ui/src/components/session-item/session-item.tsx/01M5AZ2FXC4FD834XGK86MA2NC-the-facepile-sits-inside-the.md b/.lane/memory/packages/ui/src/components/session-item/session-item.tsx/01M5AZ2FXC4FD834XGK86MA2NC-the-facepile-sits-inside-the.md index 10401441..cd86d5ca 100644 --- a/.lane/memory/packages/ui/src/components/session-item/session-item.tsx/01M5AZ2FXC4FD834XGK86MA2NC-the-facepile-sits-inside-the.md +++ b/.lane/memory/packages/ui/src/components/session-item/session-item.tsx/01M5AZ2FXC4FD834XGK86MA2NC-the-facepile-sits-inside-the.md @@ -3,6 +3,10 @@ id: 01M5AZ2FXC4FD834XGK86MA2NC anchor: function Presence created: 2026-10-19T20:53:56Z norm: '1' +sig: 5b691c6498a6be05 +body_hash: 6f560726b2f43907 +raw_hash: 533cb5f940611bea +lines: 58-90 --- the facepile sits inside the row's button, so its trigger keeps an empty title or the row's summary tooltip covers the presence tooltip