authorgravatar for jappyjan@users.noreply.github.comJan Jaap <jappyjan@users.noreply.github.com> 2026-04-30 20:46:05+02:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-04-30 18:46:05+00:00
log532a1d891db03276764d2528b30a0e6f6c396b6a
treee202d61837b3432a169cd0d06599e7cc2bb9445e
parent9e2f7129e3bce9058294051eb6f99d2efc6a8e86
signature Signed by PGP key B5690EEEBB952194

fix(web): make thread archive button always visible on mobile (#2423)

Co-authored-by: Claude Sonnet 4.7 <noreply@anthropic.com> Co-authored-by: Julius Marminge <julius0216@outlook.com>

3 files changed, 65 insertions(+), 19 deletions(-)

apps/web/src/components/Sidebar.tsx+19-11
...@@ -373,7 +373,7 @@ const SidebarThreadRow = memo(function SidebarThreadRow(props: SidebarThreadRowP...@@ -373,7 +373,7 @@ const SidebarThreadRow = memo(function SidebarThreadRow(props: SidebarThreadRowP
373 const threadMetaClassName = isConfirmingArchive373 const threadMetaClassName = isConfirmingArchive
374 ? "pointer-events-none opacity-0"374 ? "pointer-events-none opacity-0"
375 : !isThreadRunning375 : !isThreadRunning
376 ? "pointer-events-none transition-opacity duration-150 group-hover/menu-sub-item:opacity-0 group-focus-within/menu-sub-item:opacity-0"376 ? "pointer-events-none transition-opacity duration-150 max-sm:pr-6 group-hover/menu-sub-item:opacity-0 group-focus-within/menu-sub-item:opacity-0"
377 : "pointer-events-none";377 : "pointer-events-none";
378 const clearConfirmingArchive = useCallback(() => {378 const clearConfirmingArchive = useCallback(() => {
379 setConfirmingArchiveThreadKey((current) => (current === threadKey ? null : current));379 setConfirmingArchiveThreadKey((current) => (current === threadKey ? null : current));
...@@ -605,7 +605,11 @@ const SidebarThreadRow = memo(function SidebarThreadRow(props: SidebarThreadRowP...@@ -605,7 +605,11 @@ const SidebarThreadRow = memo(function SidebarThreadRow(props: SidebarThreadRowP
605 <TerminalIcon className={`size-3 ${terminalStatus.pulse ? "animate-pulse" : ""}`} />605 <TerminalIcon className={`size-3 ${terminalStatus.pulse ? "animate-pulse" : ""}`} />
606 </span>606 </span>
607 )}607 )}
608 <div className="flex min-w-12 justify-end">608 <div
609 className={`flex min-w-12 justify-end ${
610 isRemoteThread ? "max-sm:min-w-24" : "max-sm:min-w-20"
611 }`}
612 >
609 {isConfirmingArchive ? (613 {isConfirmingArchive ? (
610 <button614 <button
611 ref={handleConfirmArchiveRef}615 ref={handleConfirmArchiveRef}
...@@ -621,13 +625,13 @@ const SidebarThreadRow = memo(function SidebarThreadRow(props: SidebarThreadRowP...@@ -621,13 +625,13 @@ const SidebarThreadRow = memo(function SidebarThreadRow(props: SidebarThreadRowP
621 </button>625 </button>
622 ) : !isThreadRunning ? (626 ) : !isThreadRunning ? (
623 appSettingsConfirmThreadArchive ? (627 appSettingsConfirmThreadArchive ? (
624 <div className="pointer-events-none absolute top-1/2 right-1 -translate-y-1/2 opacity-0 transition-opacity duration-150 group-hover/menu-sub-item:pointer-events-auto group-hover/menu-sub-item:opacity-100 group-focus-within/menu-sub-item:pointer-events-auto group-focus-within/menu-sub-item:opacity-100">628 <div className="pointer-events-none absolute top-1/2 right-1 -translate-y-1/2 opacity-0 transition-opacity duration-150 max-sm:pointer-events-auto max-sm:opacity-100 group-hover/menu-sub-item:pointer-events-auto group-hover/menu-sub-item:opacity-100 group-focus-within/menu-sub-item:pointer-events-auto group-focus-within/menu-sub-item:opacity-100">
625 <button629 <button
626 type="button"630 type="button"
627 data-thread-selection-safe631 data-thread-selection-safe
628 data-testid={`thread-archive-${thread.id}`}632 data-testid={`thread-archive-${thread.id}`}
629 aria-label={`Archive ${thread.title}`}633 aria-label={`Archive ${thread.title}`}
630 className="inline-flex size-5 cursor-pointer items-center justify-center text-muted-foreground transition-colors hover:text-foreground focus-visible:outline-hidden focus-visible:ring-1 focus-visible:ring-ring"634 className="inline-flex size-5 cursor-pointer items-center justify-center text-muted-foreground/60 transition-colors hover:text-foreground focus-visible:outline-hidden focus-visible:ring-1 focus-visible:ring-ring"
631 onPointerDown={stopPropagationOnPointerDown}635 onPointerDown={stopPropagationOnPointerDown}
632 onClick={handleStartArchiveConfirmation}636 onClick={handleStartArchiveConfirmation}
633 >637 >
...@@ -638,13 +642,13 @@ const SidebarThreadRow = memo(function SidebarThreadRow(props: SidebarThreadRowP...@@ -638,13 +642,13 @@ const SidebarThreadRow = memo(function SidebarThreadRow(props: SidebarThreadRowP
638 <Tooltip>642 <Tooltip>
639 <TooltipTrigger643 <TooltipTrigger
640 render={644 render={
641 <div className="pointer-events-none absolute top-1/2 right-1 -translate-y-1/2 opacity-0 transition-opacity duration-150 group-hover/menu-sub-item:pointer-events-auto group-hover/menu-sub-item:opacity-100 group-focus-within/menu-sub-item:pointer-events-auto group-focus-within/menu-sub-item:opacity-100">645 <div className="pointer-events-none absolute top-1/2 right-1 -translate-y-1/2 opacity-0 transition-opacity duration-150 max-sm:pointer-events-auto max-sm:opacity-100 group-hover/menu-sub-item:pointer-events-auto group-hover/menu-sub-item:opacity-100 group-focus-within/menu-sub-item:pointer-events-auto group-focus-within/menu-sub-item:opacity-100">
642 <button646 <button
643 type="button"647 type="button"
644 data-thread-selection-safe648 data-thread-selection-safe
645 data-testid={`thread-archive-${thread.id}`}649 data-testid={`thread-archive-${thread.id}`}
646 aria-label={`Archive ${thread.title}`}650 aria-label={`Archive ${thread.title}`}
647 className="inline-flex size-5 cursor-pointer items-center justify-center text-muted-foreground transition-colors hover:text-foreground focus-visible:outline-hidden focus-visible:ring-1 focus-visible:ring-ring"651 className="inline-flex size-5 cursor-pointer items-center justify-center text-muted-foreground/60 transition-colors hover:text-foreground focus-visible:outline-hidden focus-visible:ring-1 focus-visible:ring-ring"
648 onPointerDown={stopPropagationOnPointerDown}652 onPointerDown={stopPropagationOnPointerDown}
649 onClick={handleArchiveImmediateClick}653 onClick={handleArchiveImmediateClick}
650 >654 >
...@@ -665,11 +669,11 @@ const SidebarThreadRow = memo(function SidebarThreadRow(props: SidebarThreadRowP...@@ -665,11 +669,11 @@ const SidebarThreadRow = memo(function SidebarThreadRow(props: SidebarThreadRowP
665 render={669 render={
666 <span670 <span
667 aria-label={threadEnvironmentLabel ?? "Remote"}671 aria-label={threadEnvironmentLabel ?? "Remote"}
668 className="inline-flex items-center justify-center"672 className="inline-flex h-5 items-center justify-center"
669 />673 />
670 }674 }
671 >675 >
672 <CloudIcon className="size-3 text-muted-foreground/40" />676 <CloudIcon className="block size-3 text-muted-foreground/60" />
673 </TooltipTrigger>677 </TooltipTrigger>
674 <TooltipPopup side="top">{threadEnvironmentLabel}</TooltipPopup>678 <TooltipPopup side="top">{threadEnvironmentLabel}</TooltipPopup>
675 </Tooltip>679 </Tooltip>
...@@ -2025,7 +2029,7 @@ const SidebarProjectItem = memo(function SidebarProjectItem(props: SidebarProjec...@@ -2025,7 +2029,7 @@ const SidebarProjectItem = memo(function SidebarProjectItem(props: SidebarProjec
2025 ? "Remote project"2029 ? "Remote project"
2026 : "Available in multiple environments"2030 : "Available in multiple environments"
2027 }2031 }
2028 className="pointer-events-none absolute top-1 right-1.5 inline-flex size-5 items-center justify-center rounded-md text-muted-foreground/50 transition-opacity duration-150 max-sm:right-7 group-hover/project-header:opacity-0 group-focus-within/project-header:opacity-0 max-sm:group-hover/project-header:opacity-100 max-sm:group-focus-within/project-header:opacity-100"2032 className="pointer-events-none absolute top-1 right-1.5 inline-flex size-5 items-center justify-center rounded-md text-muted-foreground/60 transition-opacity duration-150 max-sm:right-7 group-hover/project-header:opacity-0 group-focus-within/project-header:opacity-0 max-sm:group-hover/project-header:opacity-100 max-sm:group-focus-within/project-header:opacity-100"
2029 />2033 />
2030 }2034 }
2031 >2035 >
...@@ -2044,7 +2048,7 @@ const SidebarProjectItem = memo(function SidebarProjectItem(props: SidebarProjec...@@ -2044,7 +2048,7 @@ const SidebarProjectItem = memo(function SidebarProjectItem(props: SidebarProjec
2044 type="button"2048 type="button"
2045 aria-label={`Create new thread in ${project.displayName}`}2049 aria-label={`Create new thread in ${project.displayName}`}
2046 data-testid="new-thread-button"2050 data-testid="new-thread-button"
2047 className="inline-flex size-5 cursor-pointer items-center justify-center rounded-md text-muted-foreground/70 hover:bg-secondary hover:text-foreground focus-visible:outline-hidden focus-visible:ring-1 focus-visible:ring-ring"2051 className="inline-flex size-5 cursor-pointer items-center justify-center rounded-md text-muted-foreground/60 hover:bg-secondary hover:text-foreground focus-visible:outline-hidden focus-visible:ring-1 focus-visible:ring-ring"
2048 onClick={handleCreateThreadClick}2052 onClick={handleCreateThreadClick}
2049 >2053 >
2050 <SquarePenIcon className="size-3.5" />2054 <SquarePenIcon className="size-3.5" />
...@@ -2420,9 +2424,13 @@ const SidebarChromeHeader = memo(function SidebarChromeHeader({...@@ -2420,9 +2424,13 @@ const SidebarChromeHeader = memo(function SidebarChromeHeader({
24202424
2421const SidebarChromeFooter = memo(function SidebarChromeFooter() {2425const SidebarChromeFooter = memo(function SidebarChromeFooter() {
2422 const navigate = useNavigate();2426 const navigate = useNavigate();
2427 const { isMobile, setOpenMobile } = useSidebar();
2423 const handleSettingsClick = useCallback(() => {2428 const handleSettingsClick = useCallback(() => {
2429 if (isMobile) {
2430 setOpenMobile(false);
2431 }
2424 void navigate({ to: "/settings" });2432 void navigate({ to: "/settings" });
2425 }, [navigate]);2433 }, [isMobile, navigate, setOpenMobile]);
24262434
2427 return (2435 return (
2428 <SidebarFooter className="p-2">2436 <SidebarFooter className="p-2">
apps/web/src/components/settings/SettingsSidebarNav.tsx+26-4
...@@ -1,6 +1,6 @@...@@ -1,6 +1,6 @@
1import type { ComponentType } from "react";1import { useCallback, type ComponentType } from "react";
2import { ArchiveIcon, ArrowLeftIcon, Link2Icon, Settings2Icon } from "lucide-react";2import { ArchiveIcon, ArrowLeftIcon, Link2Icon, Settings2Icon } from "lucide-react";
3import { useNavigate } from "@tanstack/react-router";3import { useCanGoBack, useNavigate } from "@tanstack/react-router";
44
5import {5import {
6 SidebarContent,6 SidebarContent,
...@@ -10,6 +10,7 @@ import {...@@ -10,6 +10,7 @@ import {
10 SidebarMenuButton,10 SidebarMenuButton,
11 SidebarMenuItem,11 SidebarMenuItem,
12 SidebarSeparator,12 SidebarSeparator,
13 useSidebar,
13} from "../ui/sidebar";14} from "../ui/sidebar";
1415
15export type SettingsSectionPath =16export type SettingsSectionPath =
...@@ -29,6 +30,27 @@ export const SETTINGS_NAV_ITEMS: ReadonlyArray<{...@@ -29,6 +30,27 @@ export const SETTINGS_NAV_ITEMS: ReadonlyArray<{
2930
30export function SettingsSidebarNav({ pathname }: { pathname: string }) {31export function SettingsSidebarNav({ pathname }: { pathname: string }) {
31 const navigate = useNavigate();32 const navigate = useNavigate();
33 const canGoBack = useCanGoBack();
34 const { isMobile, setOpenMobile } = useSidebar();
35 const handleSectionClick = useCallback(
36 (to: SettingsSectionPath) => {
37 if (isMobile) {
38 setOpenMobile(false);
39 }
40 void navigate({ to, replace: true });
41 },
42 [isMobile, navigate, setOpenMobile],
43 );
44 const handleBackClick = useCallback(() => {
45 if (isMobile) {
46 setOpenMobile(false);
47 }
48 if (canGoBack) {
49 window.history.back();
50 return;
51 }
52 void navigate({ to: "/" });
53 }, [canGoBack, isMobile, navigate, setOpenMobile]);
3254
33 return (55 return (
34 <>56 <>
...@@ -48,7 +70,7 @@ export function SettingsSidebarNav({ pathname }: { pathname: string }) {...@@ -48,7 +70,7 @@ export function SettingsSidebarNav({ pathname }: { pathname: string }) {
48 ? "gap-2.5 px-2.5 py-2 text-left text-[13px] font-medium text-foreground"70 ? "gap-2.5 px-2.5 py-2 text-left text-[13px] font-medium text-foreground"
49 : "gap-2.5 px-2.5 py-2 text-left text-[13px] text-muted-foreground/70 hover:text-foreground/80"71 : "gap-2.5 px-2.5 py-2 text-left text-[13px] text-muted-foreground/70 hover:text-foreground/80"
50 }72 }
51 onClick={() => void navigate({ to: item.to, replace: true })}73 onClick={() => handleSectionClick(item.to)}
52 >74 >
53 <Icon75 <Icon
54 className={76 className={
...@@ -73,7 +95,7 @@ export function SettingsSidebarNav({ pathname }: { pathname: string }) {...@@ -73,7 +95,7 @@ export function SettingsSidebarNav({ pathname }: { pathname: string }) {
73 <SidebarMenuButton95 <SidebarMenuButton
74 size="sm"96 size="sm"
75 className="gap-2 px-2 py-2 text-xs text-muted-foreground hover:bg-accent hover:text-foreground"97 className="gap-2 px-2 py-2 text-xs text-muted-foreground hover:bg-accent hover:text-foreground"
76 onClick={() => window.history.back()}98 onClick={handleBackClick}
77 >99 >
78 <ArrowLeftIcon className="size-4" />100 <ArrowLeftIcon className="size-4" />
79 <span>Back</span>101 <span>Back</span>
apps/web/src/routes/settings.tsx+20-4
...@@ -1,6 +1,13 @@...@@ -1,6 +1,13 @@
1import { RotateCcwIcon } from "lucide-react";1import { RotateCcwIcon } from "lucide-react";
2import { Outlet, createFileRoute, redirect, useLocation } from "@tanstack/react-router";2import {
3import { useEffect, useState } from "react";3 Outlet,
4 createFileRoute,
5 redirect,
6 useCanGoBack,
7 useLocation,
8 useNavigate,
9} from "@tanstack/react-router";
10import { useCallback, useEffect, useState } from "react";
411
5import { useSettingsRestore } from "../components/settings/SettingsPanels";12import { useSettingsRestore } from "../components/settings/SettingsPanels";
6import { Button } from "../components/ui/button";13import { Button } from "../components/ui/button";
...@@ -25,16 +32,25 @@ function RestoreDefaultsButton({ onRestored }: { onRestored: () => void }) {...@@ -25,16 +32,25 @@ function RestoreDefaultsButton({ onRestored }: { onRestored: () => void }) {
2532
26function SettingsContentLayout() {33function SettingsContentLayout() {
27 const location = useLocation();34 const location = useLocation();
35 const navigate = useNavigate();
36 const canGoBack = useCanGoBack();
28 const [restoreSignal, setRestoreSignal] = useState(0);37 const [restoreSignal, setRestoreSignal] = useState(0);
29 const showRestoreDefaults = location.pathname === "/settings/general";38 const showRestoreDefaults = location.pathname === "/settings/general";
30 const handleRestored = () => setRestoreSignal((value) => value + 1);39 const handleRestored = () => setRestoreSignal((value) => value + 1);
40 const navigateBackWithinApp = useCallback(() => {
41 if (canGoBack) {
42 window.history.back();
43 return;
44 }
45 void navigate({ to: "/" });
46 }, [canGoBack, navigate]);
3147
32 useEffect(() => {48 useEffect(() => {
33 const onKeyDown = (event: KeyboardEvent) => {49 const onKeyDown = (event: KeyboardEvent) => {
34 if (event.defaultPrevented) return;50 if (event.defaultPrevented) return;
35 if (event.key === "Escape") {51 if (event.key === "Escape") {
36 event.preventDefault();52 event.preventDefault();
37 window.history.back();53 navigateBackWithinApp();
38 }54 }
39 };55 };
4056
...@@ -42,7 +58,7 @@ function SettingsContentLayout() {...@@ -42,7 +58,7 @@ function SettingsContentLayout() {
42 return () => {58 return () => {
43 window.removeEventListener("keydown", onKeyDown);59 window.removeEventListener("keydown", onKeyDown);
44 };60 };
45 }, []);61 }, [navigateBackWithinApp]);
4662
47 return (63 return (
48 <SidebarInset className="h-dvh min-h-0 overflow-hidden overscroll-y-none bg-background text-foreground isolate">64 <SidebarInset className="h-dvh min-h-0 overflow-hidden overscroll-y-none bg-background text-foreground isolate">