mirror of
https://github.com/coder/coder.git
synced 2026-10-16 14:48:56 +08:00
fix(site/src/pages/AgentsPage): preserve chat row width with menu open (#29422)
Opening a chat row's actions menu could leave the row inset with rounded corners after the pointer moved into the portaled menu. Apply the same full-width layout used on hover while the menu is open, keeping the row edge-to-edge until the menu closes. Linear: https://linear.app/codercom/issue/CODAGT-1040/new-chat-sidebar-row-changes-from-full-width-to-pill-shape-on-hover <details> <summary>Coder Agents implementation notes</summary> - Reproduce the visual state using the existing sidebar menu Storybook story. - Preserve the chat row's hover geometry whenever its dropdown trigger reports `data-state="open"`. - Verify with the sidebar Storybook suite, frontend typecheck, repository hooks, and visual inspection. </details> _Generated by Coder Agents on behalf of @tracyjohnsonux._
This commit is contained in:
@@ -1752,6 +1752,33 @@ export const ActiveChatKebabPersistent: Story = {
|
||||
},
|
||||
};
|
||||
|
||||
export const ActiveChatContextMenuOpen: Story = {
|
||||
args: {
|
||||
chats: [
|
||||
buildChat({
|
||||
id: "active-context-menu",
|
||||
title: "Active context menu chat",
|
||||
updated_at: recentTimestamp,
|
||||
}),
|
||||
],
|
||||
},
|
||||
parameters: {
|
||||
reactRouter: reactRouterParameters({
|
||||
location: {
|
||||
path: "/agents/active-context-menu",
|
||||
pathParams: { agentId: "active-context-menu" },
|
||||
},
|
||||
routing: agentsRouting,
|
||||
}),
|
||||
},
|
||||
play: async ({ canvasElement }) => {
|
||||
fireEvent.contextMenu(
|
||||
within(canvasElement).getByTestId("agents-tree-node-active-context-menu"),
|
||||
);
|
||||
await within(document.body).findByText("Pin agent");
|
||||
},
|
||||
};
|
||||
|
||||
export const WithUnreadChats: Story = {
|
||||
args: {
|
||||
chats: [
|
||||
@@ -1848,6 +1875,33 @@ export const AgentWithWorkspaceMenuFull: Story = {
|
||||
},
|
||||
};
|
||||
|
||||
export const AgentWithWorkspaceContextMenuFull: Story = {
|
||||
args: {
|
||||
chats: [
|
||||
buildChat({
|
||||
id: "chat-with-context-menu",
|
||||
title: "Agent with context menu",
|
||||
workspace_id: "workspace-1",
|
||||
updated_at: recentTimestamp,
|
||||
}),
|
||||
],
|
||||
},
|
||||
parameters: {
|
||||
reactRouter: reactRouterParameters({
|
||||
location: { path: "/agents" },
|
||||
routing: agentsRouting,
|
||||
}),
|
||||
},
|
||||
play: async ({ canvasElement }) => {
|
||||
fireEvent.contextMenu(
|
||||
within(canvasElement).getByTestId(
|
||||
"agents-tree-node-chat-with-context-menu",
|
||||
),
|
||||
);
|
||||
await within(document.body).findByText("Pin agent");
|
||||
},
|
||||
};
|
||||
|
||||
export const ArchiveActionsFollowChatStatus: Story = {
|
||||
args: {
|
||||
chats: [
|
||||
|
||||
@@ -155,10 +155,6 @@ export const ChatTreeNode: FC<ChatTreeNodeProps> = ({ chat, depth = 0 }) => {
|
||||
hasSubagentsToggle: hasChildren,
|
||||
});
|
||||
|
||||
const hoverLayout =
|
||||
"[@media(hover:hover)]:hover:-mx-2 [@media(hover:hover)]:hover:pl-3 [@media(hover:hover)]:hover:pr-3.5 [@media(hover:hover)]:hover:rounded-none";
|
||||
const activeLayout =
|
||||
"has-[[aria-current=page]]:-mx-2 has-[[aria-current=page]]:pl-[11px] has-[[aria-current=page]]:pr-3.5 has-[[aria-current=page]]:rounded-none has-[[aria-current=page]]:border-l has-[[aria-current=page]]:border-content-primary [@media(hover:hover)]:has-[[aria-current=page]]:hover:pl-[11px]";
|
||||
const sharedMenuItemProps = {
|
||||
chat,
|
||||
canManage,
|
||||
@@ -189,11 +185,10 @@ export const ChatTreeNode: FC<ChatTreeNodeProps> = ({ chat, depth = 0 }) => {
|
||||
<div
|
||||
data-testid={`agents-tree-node-${chat.id}`}
|
||||
className={cn(
|
||||
"group relative flex min-w-0 select-none pointer-coarse:[-webkit-touch-callout:none] items-start gap-1.5 rounded-md pl-1 pr-1.5 text-content-secondary",
|
||||
"transition-none [@media(hover:hover)]:hover:bg-surface-tertiary/50 [@media(hover:hover)]:hover:text-content-primary has-data-[state=open]:bg-surface-tertiary",
|
||||
"has-[[aria-current=page]]:bg-surface-quaternary/50 has-[[aria-current=page]]:text-content-primary [@media(hover:hover)]:has-[[aria-current=page]]:hover:bg-surface-quaternary/50",
|
||||
hoverLayout,
|
||||
activeLayout,
|
||||
"group relative -mx-2 flex min-w-0 select-none pointer-coarse:[-webkit-touch-callout:none] items-start gap-1.5 rounded-none pl-3 pr-3.5 text-content-secondary",
|
||||
"transition-none [@media(hover:hover)]:hover:bg-surface-tertiary/50 [@media(hover:hover)]:hover:text-content-primary data-[state=open]:bg-surface-tertiary/50 data-[state=open]:text-content-primary has-data-[state=open]:bg-surface-tertiary/50 has-data-[state=open]:text-content-primary",
|
||||
// pl-[11px] is pl-3 minus the active border, so content does not shift.
|
||||
"has-[[aria-current=page]]:border-l has-[[aria-current=page]]:border-content-primary has-[[aria-current=page]]:bg-surface-quaternary/50 has-[[aria-current=page]]:pl-[11px] has-[[aria-current=page]]:text-content-primary data-[state=open]:has-[[aria-current=page]]:bg-surface-quaternary/50 has-data-[state=open]:has-[[aria-current=page]]:bg-surface-quaternary/50 [@media(hover:hover)]:has-[[aria-current=page]]:hover:bg-surface-quaternary/50",
|
||||
)}
|
||||
>
|
||||
<div
|
||||
@@ -254,7 +249,7 @@ export const ChatTreeNode: FC<ChatTreeNodeProps> = ({ chat, depth = 0 }) => {
|
||||
className={cn(
|
||||
"block flex-1 truncate text-[13px] text-content-primary",
|
||||
!isActive &&
|
||||
"opacity-85 [@media(hover:hover)]:group-hover:opacity-100",
|
||||
"opacity-85 [@media(hover:hover)]:group-hover:opacity-100 group-data-[state=open]:opacity-100 group-has-data-[state=open]:opacity-100",
|
||||
)}
|
||||
>
|
||||
{chat.title}
|
||||
@@ -311,10 +306,10 @@ export const ChatTreeNode: FC<ChatTreeNodeProps> = ({ chat, depth = 0 }) => {
|
||||
className={cn(
|
||||
"flex items-center justify-end text-xs text-content-secondary/50 tabular-nums",
|
||||
// The timestamp swaps out for the actions trigger on
|
||||
// hover; without menu actions there is no trigger, so
|
||||
// keep the timestamp visible.
|
||||
// hover or while a menu is open. Without menu actions,
|
||||
// there is no trigger, so keep the timestamp visible.
|
||||
hasMenuActions &&
|
||||
"[@media(hover:hover)]:group-hover:hidden group-has-data-[state=open]:hidden",
|
||||
"[@media(hover:hover)]:group-hover:hidden group-data-[state=open]:hidden group-has-data-[state=open]:hidden",
|
||||
hasMenuActions && isActiveChat && "hidden",
|
||||
)}
|
||||
>
|
||||
@@ -353,7 +348,7 @@ export const ChatTreeNode: FC<ChatTreeNodeProps> = ({ chat, depth = 0 }) => {
|
||||
size="icon"
|
||||
variant="subtle"
|
||||
className={cn(
|
||||
"absolute inset-0 flex h-6 w-7 min-w-0 justify-end rounded-none px-0 opacity-0 text-content-secondary hover:text-content-primary [@media(hover:hover)]:group-hover:opacity-100 data-[state=open]:opacity-100",
|
||||
"absolute inset-0 flex h-6 w-7 min-w-0 justify-end rounded-none px-0 opacity-0 text-content-secondary hover:text-content-primary [@media(hover:hover)]:group-hover:opacity-100 data-[state=open]:opacity-100 group-data-[state=open]:opacity-100",
|
||||
isActiveChat && "opacity-100",
|
||||
)}
|
||||
aria-label={`Open actions for ${chat.title}`}
|
||||
|
||||
Reference in New Issue
Block a user