diff --git a/apps/web/src/components/Sidebar.tsx b/apps/web/src/components/Sidebar.tsx index c4ba300a030c3..62934576e365b 100644 --- a/apps/web/src/components/Sidebar.tsx +++ b/apps/web/src/components/Sidebar.tsx @@ -3371,7 +3371,6 @@ export default function Sidebar({ routeReveal }: { routeReveal: SidebarRouteReve className="mt-auto" label="Working" count={workingThreads.length} - hostsUndoNotice toggle={{ expanded: workingShelfExpanded, onToggle: toggleWorkingShelf, @@ -3387,7 +3386,6 @@ export default function Sidebar({ routeReveal }: { routeReveal: SidebarRouteReve className={cn(workingThreads.length === 0 && "mt-auto")} label="Snoozed" count={snoozedThreads.length} - hostsUndoNotice={workingThreads.length === 0} toggle={{ expanded: snoozedShelfExpanded, onToggle: toggleSnoozedShelf, @@ -3405,9 +3403,6 @@ export default function Sidebar({ routeReveal }: { routeReveal: SidebarRouteReve )} label="Settled" count={settledCount} - hostsUndoNotice={ - workingThreads.length + snoozedThreads.length === 0 - } dragging={from !== null} isDropTarget={dragTargetSection === "settled"} toggle={{ @@ -3502,9 +3497,7 @@ export default function Sidebar({ routeReveal }: { routeReveal: SidebarRouteReve ) : null} - + ); } diff --git a/apps/web/src/components/sidebar/SidebarChrome.tsx b/apps/web/src/components/sidebar/SidebarChrome.tsx index ec161e9998548..e233a4144d435 100644 --- a/apps/web/src/components/sidebar/SidebarChrome.tsx +++ b/apps/web/src/components/sidebar/SidebarChrome.tsx @@ -223,19 +223,15 @@ const SidebarUtilityMenu = memo(function SidebarUtilityMenu() { /> )} + ); }); -export const SidebarChromeFooter = memo(function SidebarChromeFooter({ - showUndoNotice = false, -}: { - showUndoNotice?: boolean; -}) { +export const SidebarChromeFooter = memo(function SidebarChromeFooter() { return ( - {showUndoNotice ? : null} diff --git a/apps/web/src/components/sidebar/SidebarListMarkers.tsx b/apps/web/src/components/sidebar/SidebarListMarkers.tsx index 3a78876fa5832..104bda5582b4d 100644 --- a/apps/web/src/components/sidebar/SidebarListMarkers.tsx +++ b/apps/web/src/components/sidebar/SidebarListMarkers.tsx @@ -9,7 +9,6 @@ import { type SidebarListMarker, } from "../Sidebar.logic"; import { SIDEBAR_SORTABLE_DRAG_DISABLED } from "./SidebarThreadRow"; -import { SidebarThreadUndoNotice } from "./SidebarThreadUndoNotice"; function SortableSidebarMarker(props: { marker: SidebarListMarker; @@ -117,8 +116,6 @@ export function SidebarSectionHeader(props: { dragging?: boolean; isDropTarget?: boolean; toggle: { expanded: boolean; onToggle: () => void }; - /** The first shelf at the bottom of the list hosts the undo notice just above itself. */ - hostsUndoNotice?: boolean; }) { const shelf = props.marker === "working-header" @@ -132,9 +129,6 @@ export function SidebarSectionHeader(props: { data-testid={`sidebar-${props.marker}`} className={cn("relative mx-0.5 h-8", props.className)} > - {props.hostsUndoNotice ? ( - - ) : null} state.notice); const keybindings = useAtomValue(primaryServerKeybindingsAtom); - const summary = notice ? threadUndoNoticeSummary(notice) : null; + // Short enough to fit beside the utility icons; screen readers hear the full summary. + const summary = notice + ? notice.count === 1 + ? notice.action + : `${notice.action} ${notice.count}` + : null; // Keeps the last text on screen while it fades out, then unmounts on transitionend. const [shownSummary, setShownSummary] = useState(summary); @@ -23,21 +29,23 @@ export function SidebarThreadUndoNotice({ className }: { className?: string }) { if (!text) return null; return ( -
{ - if (!summary && event.target === event.currentTarget) setShownSummary(null); - }} - className={cn( - "flex gap-1 px-2 text-2xs leading-6 text-sidebar-muted-foreground transition-opacity duration-200 ease-out starting:opacity-0", - !summary && "opacity-0", - className, - )} - > - {text}, - {threadUndoLabel(keybindings)} -
+ // No min-w-0: the undo button sets this item's minimum, so a shared row squeezes its neighbour. + +
{ + if (!summary && event.target === event.currentTarget) setShownSummary(null); + }} + // Reversed wrap: a summary that does not fit beside the undo button drops to a + // second line that overflow hides, instead of truncating mid-word. Browse mode + // reads the button first; the live announcement carries the full sentence. + className={cn( + "flex h-6 min-w-0 flex-1 flex-row-reverse flex-wrap gap-x-1 overflow-hidden text-2xs leading-6 whitespace-nowrap text-sidebar-muted-foreground transition-opacity duration-200 ease-out starting:opacity-0", + !summary && "opacity-0", + )} + > + {threadUndoLabel(keybindings)} + {text}, +
+
); } diff --git a/apps/web/src/hooks/showThreadUndoNotice.ts b/apps/web/src/hooks/showThreadUndoNotice.ts index 5b6b68826fdea..977dcd68d1ab7 100644 --- a/apps/web/src/hooks/showThreadUndoNotice.ts +++ b/apps/web/src/hooks/showThreadUndoNotice.ts @@ -28,7 +28,7 @@ type UndoNotice = { export const useThreadUndoNotice = create<{ notice: UndoNotice | null }>(() => ({ notice: null })); -export function threadUndoNoticeSummary(notice: Pick): string { +function threadUndoNoticeSummary(notice: Pick): string { const noun = notice.action === "Discarded" ? "draft" : "thread"; return `${notice.action} ${notice.count} ${noun}${notice.count === 1 ? "" : "s"}`; }