From e2ccf4d376200a6ea1542a2d99492a0be553d4f0 Mon Sep 17 00:00:00 2001 From: Souravrajvi0 <144546710+Souravrajvi0@users.noreply.github.com> Date: Fri, 24 Jul 2026 07:41:05 +0530 Subject: [PATCH] fix(studio): show chat sidebar menu on touch devices (#7297) * fix(studio): show chat sidebar menu on touch devices Recents/Pinned chat row actions were hidden until hover, so iPad users could not open the kebab menu to delete chats. Reveal actions on coarse pointers using the same pattern as hub model rows. Fixes #7276 * Fix coarse-pointer sidebar row action visibility (#7276) Move the touch-device override into index.css after .sidebar-row-action so it wins the cascade. Arbitrary Tailwind media utilities on the element had equal specificity and were overridden by the base rule. * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Scope coarse-pointer sidebar actions to chat rows (#7276) Only chat kebabs/unpin buttons that reserve touch padding get sidebar-touch-reveal, so project/run/nav rows stay hover-revealed. * Tighten comments * Reserve full kebab hit area on coarse-pointer unpinned rows --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Daniel Han --- .../frontend/src/components/app-sidebar.tsx | 15 ++++++------ studio/frontend/src/index.css | 6 +++++ ...t_desktop_reliability_frontend_contract.py | 24 +++++++++++++++++++ 3 files changed, 38 insertions(+), 7 deletions(-) diff --git a/studio/frontend/src/components/app-sidebar.tsx b/studio/frontend/src/components/app-sidebar.tsx index a6f64243ad..c95112c748 100644 --- a/studio/frontend/src/components/app-sidebar.tsx +++ b/studio/frontend/src/components/app-sidebar.tsx @@ -901,8 +901,8 @@ export function AppSidebar() { : "group/recent-item relative"; const actionClass = variant === "project" - ? "sidebar-row-action group-hover/project-chat-item:opacity-100 group-hover/project-chat-item:pointer-events-auto focus-visible:opacity-100 focus-visible:pointer-events-auto" - : "sidebar-row-action group-hover/recent-item:opacity-100 group-hover/recent-item:pointer-events-auto focus-visible:opacity-100 focus-visible:pointer-events-auto"; + ? "sidebar-row-action sidebar-touch-reveal group-hover/project-chat-item:opacity-100 group-hover/project-chat-item:pointer-events-auto focus-visible:opacity-100 focus-visible:pointer-events-auto" + : "sidebar-row-action sidebar-touch-reveal group-hover/recent-item:opacity-100 group-hover/recent-item:pointer-events-auto focus-visible:opacity-100 focus-visible:pointer-events-auto"; const buttonClass = cn( "sidebar-nav-btn h-[33px] cursor-pointer rounded-full pr-4 text-ui-14p5 leading-ui-19 tracking-nav font-medium", // pl-3 (12px) over the content's pl-1.5 (6px) = 18px, aligning the @@ -912,13 +912,14 @@ export function AppSidebar() { isPinned && variant !== "project" && "gap-[8.5px]", variant === "project" ? // Room for the hover pin quick-action plus the kebab. - "group-hover/project-chat-item:pr-14 group-has-[.sidebar-row-action[data-state=open]]/project-chat-item:pr-8" + "group-hover/project-chat-item:pr-14 group-has-[.sidebar-row-action[data-state=open]]/project-chat-item:pr-8 [@media(pointer:coarse)]:pr-14" : isPinned ? // Pinned rows show an extra unpin button on hover, so reserve more room // (pr-8 when the menu is open keeps the unpin button clear of the title). - "group-hover/recent-item:pr-16 group-has-[.sidebar-row-action[data-state=open]]/recent-item:pr-8" + "group-hover/recent-item:pr-16 group-has-[.sidebar-row-action[data-state=open]]/recent-item:pr-8 [@media(pointer:coarse)]:pr-16" : // Hover room for the kebab only; title keeps one more character. - "group-hover/recent-item:pr-6 group-has-[.sidebar-row-action[data-state=open]]/recent-item:pr-6", + // Touch rows clear the full always-visible kebab hit area (pr-10). + "group-hover/recent-item:pr-6 group-has-[.sidebar-row-action[data-state=open]]/recent-item:pr-6 [@media(pointer:coarse)]:pr-10", ); const isRenamingThis = @@ -987,7 +988,7 @@ export function AppSidebar() { togglePinnedChat(item.id); }} aria-label={isPinned ? "Unpin chat" : "Pin chat"} - className="sidebar-row-action is-unpin-action group-hover/project-chat-item:opacity-100 group-hover/project-chat-item:pointer-events-auto focus-visible:opacity-100 focus-visible:pointer-events-auto" + className="sidebar-row-action sidebar-touch-reveal is-unpin-action group-hover/project-chat-item:opacity-100 group-hover/project-chat-item:pointer-events-auto focus-visible:opacity-100 focus-visible:pointer-events-auto" > @@ -1002,7 +1003,7 @@ export function AppSidebar() { togglePinnedChat(item.id); }} aria-label="Unpin chat" - className="sidebar-row-action is-unpin-action group-hover/recent-item:opacity-100 group-hover/recent-item:pointer-events-auto focus-visible:opacity-100 focus-visible:pointer-events-auto" + className="sidebar-row-action sidebar-touch-reveal is-unpin-action group-hover/recent-item:opacity-100 group-hover/recent-item:pointer-events-auto focus-visible:opacity-100 focus-visible:pointer-events-auto" > diff --git a/studio/frontend/src/index.css b/studio/frontend/src/index.css index 0cba3407fe..ba112d5f53 100644 --- a/studio/frontend/src/index.css +++ b/studio/frontend/src/index.css @@ -1018,6 +1018,12 @@ html[data-chat-font] .aui-root { .sidebar-row-action { @apply absolute top-0 bottom-0 right-0 inline-flex cursor-pointer items-center justify-end pl-2 pr-1.5 opacity-0 pointer-events-none outline-none; } + @media (pointer: coarse) { + /* Only chat rows reserve touch padding (#7276); other rows stay hover-revealed to avoid clipped labels. */ + .sidebar-row-action.sidebar-touch-reveal { + @apply opacity-100 pointer-events-auto; + } + } .sidebar-row-action[data-state="open"] { @apply opacity-100 pointer-events-auto; } diff --git a/tests/studio/test_desktop_reliability_frontend_contract.py b/tests/studio/test_desktop_reliability_frontend_contract.py index 868895b8f0..2552b6c1d8 100644 --- a/tests/studio/test_desktop_reliability_frontend_contract.py +++ b/tests/studio/test_desktop_reliability_frontend_contract.py @@ -14,6 +14,7 @@ DATA_TAB = FRONTEND / "features/settings/tabs/data-tab.tsx" PROMPT_STORAGE = FRONTEND / "features/chat/prompt-storage/prompt-storage-dialog.tsx" APP_SIDEBAR = FRONTEND / "components/app-sidebar.tsx" +INDEX_CSS = FRONTEND / "index.css" THREAD = FRONTEND / "components/assistant-ui/thread.tsx" THREAD_SIDEBAR = FRONTEND / "features/chat/thread-sidebar.tsx" SHARED_COMPOSER = FRONTEND / "features/chat/shared-composer.tsx" @@ -129,3 +130,26 @@ def test_expanded_titlebar_button_and_corner_match_sidebar_edge(): 'className="pointer-events-none absolute top-full size-3 -translate-x-px rounded-tl-[12px] border-l border-t border-sidebar-border bg-background"' in source ) + + +def test_chat_sidebar_row_actions_visible_on_coarse_pointers(): + """unslothai/unsloth#7276: Recents chat kebab must be tappable on iPad.""" + sidebar_source = APP_SIDEBAR.read_text(encoding = "utf-8") + css_source = INDEX_CSS.read_text(encoding = "utf-8") + assert "renderChatSidebarItem" in sidebar_source + block = sidebar_source.split("function renderChatSidebarItem", 1)[1].split("\n function ", 1)[ + 0 + ] + assert "[@media(pointer:coarse)]:pr-10" in block + assert "sidebar-touch-reveal" in block + # Coarse-pointer visibility must come after .sidebar-row-action { opacity-0 }. + coarse_idx = css_source.index("@media (pointer: coarse)") + base_idx = css_source.index(".sidebar-row-action {") + assert coarse_idx > base_idx + coarse_block = css_source[coarse_idx : coarse_idx + 280] + assert "sidebar-touch-reveal" in coarse_block + assert "opacity-100" in coarse_block + assert "pointer-events-auto" in coarse_block + # Must not reveal every sidebar-row-action (project/run/nav rows lack padding). + assert ".sidebar-row-action {\n\t\t\t@apply opacity-100" not in coarse_block + assert ".sidebar-row-action.sidebar-touch-reveal" in coarse_block