From 8ae0353c0047629f82e5b95b1e2968791c2bf8fd Mon Sep 17 00:00:00 2001 From: Marco Beretta <81851188+berry-13@users.noreply.github.com> Date: Wed, 23 Sep 2026 17:00:26 +0200 Subject: [PATCH 1/4] feat(client): Give the mobile drawer one place for each control The drawer put its two global destinations in opposite corners and repeated one of them under every panel. The marketplace was a bare icon in the top strip, the hardest thing there to notice; new chat was a large footer button sitting under prompts, memories and MCP settings alike, none of which it has anything to do with; and the close toggle wore the chat header's bordered plate beside two flat icons, so the row read as one control and two afterthoughts. They swap. New chat takes the icon slot beside the panel switcher, which is where the drawer keeps what means the same thing whichever panel is showing, and its click behaviour moves with it into a component of its own rather than staying behind in the footer. The marketplace becomes a full-width entry above the panel content, labelled, in the list where someone already looks for somewhere to go, and outside the scroller so it stays put while the panels change under it. The toggle drops to `ghost` like its neighbours. The footer is left with search alone and stands down entirely on a panel that has nothing to search, rather than taking its padding out of the list above it. The drawer now carries a text colour beside its background. Only the panel nav set one, so the header strip, the new marketplace row and the footer all inherited black from the document and rendered near-invisible in dark mode, except where a control happened to name a token itself. That was hidden while the toggle wore `header-action`, which brought its own. --- .../components/Nav/AgentMarketplaceButton.tsx | 79 +++++++----- .../__tests__/AgentMarketplaceButton.spec.tsx | 67 ++++++++++ .../UnifiedSidebar/UnifiedSidebar.tsx | 13 +- .../__tests__/drawerPaint.spec.tsx | 1 + .../UnifiedSidebar/mobile/BottomBar.tsx | 87 +++---------- .../UnifiedSidebar/mobile/Header.tsx | 13 +- .../UnifiedSidebar/mobile/NewChat.tsx | 74 ++++++++++++ .../mobile/__tests__/BottomBar.spec.tsx | 114 ++++++------------ .../mobile/__tests__/Header.spec.tsx | 75 ++++++------ .../mobile/__tests__/NewChat.spec.tsx | 103 ++++++++++++++++ eslint-suppressions.json | 8 +- 11 files changed, 412 insertions(+), 222 deletions(-) create mode 100644 client/src/components/Nav/__tests__/AgentMarketplaceButton.spec.tsx create mode 100644 client/src/components/UnifiedSidebar/mobile/NewChat.tsx create mode 100644 client/src/components/UnifiedSidebar/mobile/__tests__/NewChat.spec.tsx diff --git a/client/src/components/Nav/AgentMarketplaceButton.tsx b/client/src/components/Nav/AgentMarketplaceButton.tsx index a9416d7cba2..df33d69d408 100644 --- a/client/src/components/Nav/AgentMarketplaceButton.tsx +++ b/client/src/components/Nav/AgentMarketplaceButton.tsx @@ -2,13 +2,22 @@ import { Link } from 'react-router-dom'; import { LayoutGrid } from 'lucide-react'; import { Button, TooltipAnchor } from '@librechat/client'; import { useLocalize, useShowMarketplace } from '~/hooks'; +import { cn } from '~/utils'; interface AgentMarketplaceButtonProps { /** Which way the tooltip opens: the desktop rail is a left edge, the mobile - * drawer header is a top edge. */ + * drawer header is a top edge. Ignored by the `row` layout, which has no + * tooltip to place. */ side?: 'right' | 'bottom'; /** Mobile dismisses the drawer on navigation; the desktop rail stays put. */ onNavigate?: () => void; + /** + * `icon` is the rail's square, labelled by a tooltip. `row` is a full-width + * entry carrying its own label, for the mobile drawer: an icon in a header + * strip is the easiest thing there to miss, and the list it now heads is + * where someone already looks for somewhere to go. + */ + layout?: 'icon' | 'row'; } /** Agent Marketplace entry in the sidebar. Self-gated on marketplace @@ -16,6 +25,7 @@ interface AgentMarketplaceButtonProps { export default function AgentMarketplaceButton({ side = 'right', onNavigate, + layout = 'icon', }: AgentMarketplaceButtonProps) { const localize = useLocalize(); const showAgentMarketplace = useShowMarketplace(); @@ -24,33 +34,46 @@ export default function AgentMarketplaceButton({ return null; } + const isRow = layout === 'row'; + + /** Composed through the shared button so the focus ring, hover fill and theme + * timing come from the primitive rather than being restated: a hand-rolled + * version of this had no focus ring at all. `asChild` keeps it a router Link, + * so modifier- and middle-clicks still open the marketplace in a new tab, and + * those stay on the current page and so must not dismiss. */ + const button = ( + + ); + + if (isRow) { + return button; + } + return ( - - { - if (event.button !== 0 || event.ctrlKey || event.metaKey || event.shiftKey) { - return; - } - onNavigate?.(); - }} - > -