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