From 0d24cbd496451eeb57fecaed491e7296dffe1685 Mon Sep 17 00:00:00 2001 From: Danny Avila Date: Wed, 10 Jun 2026 15:31:56 -0400 Subject: [PATCH] =?UTF-8?q?=F0=9F=9A=AA=20fix:=20Align=20Mobile=20Sidebar?= =?UTF-8?q?=20Toggle=20Gating=20with=20JS=20Breakpoint=20Across=20Views=20?= =?UTF-8?q?(#13654)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 🚪 fix: Align Mobile Sidebar Toggle Gating with JS Breakpoint Across Views * 🚪 fix: Sidebar Toggle on Read-Only Prompt Details View --- client/src/components/Agents/Marketplace.tsx | 14 +++---- client/src/components/Chat/Header.tsx | 2 +- .../Prompts/forms/CreatePromptForm.tsx | 27 +++++++----- .../components/Prompts/forms/PromptForm.tsx | 42 ++++++++++++------- .../components/Skills/layouts/SkillsView.tsx | 42 ++++++++++++++----- .../layouts/__tests__/SkillsView.spec.tsx | 30 +++++++++++++ 6 files changed, 113 insertions(+), 44 deletions(-) diff --git a/client/src/components/Agents/Marketplace.tsx b/client/src/components/Agents/Marketplace.tsx index adf406f7b0..1e534dbf85 100644 --- a/client/src/components/Agents/Marketplace.tsx +++ b/client/src/components/Agents/Marketplace.tsx @@ -218,17 +218,17 @@ const AgentMarketplace: React.FC = ({ className = '' }) = {/* Sticky wrapper for search bar and categories */}
-
- - -
+ {isSmallScreen ? ( +
+ + +
+ ) : null} {/* Search bar */}
{/* TODO: Remove this once we have a better way to handle admin settings */} -
- -
+ {!isSmallScreen && }
{/* Category tabs */} diff --git a/client/src/components/Chat/Header.tsx b/client/src/components/Chat/Header.tsx index 31ff30ffce..fc3e4e750e 100644 --- a/client/src/components/Chat/Header.tsx +++ b/client/src/components/Chat/Header.tsx @@ -45,7 +45,7 @@ function Header() {
- + {isSmallScreen ? : null} {!(navVisible && isSmallScreen) && (
{ const localize = useLocalize(); const navigate = useNavigate(); + const isSmallScreen = useMediaQuery('(max-width: 768px)'); const { hasAccess: hasUseAccess } = usePromptGroupsContext() ?? {}; const hasCreateAccess = useHasAccess({ permissionType: PermissionTypes.PROMPTS, @@ -114,10 +115,12 @@ const CreatePromptForm = ({

{localize('com_ui_create_prompt_page')}

-
- - -
+ {isSmallScreen ? ( +
+ + +
+ ) : null}
)} /> -
- -
+ {!isSmallScreen && ( +
+ +
+ )}
diff --git a/client/src/components/Prompts/forms/PromptForm.tsx b/client/src/components/Prompts/forms/PromptForm.tsx index 9407db4333..722498564c 100644 --- a/client/src/components/Prompts/forms/PromptForm.tsx +++ b/client/src/components/Prompts/forms/PromptForm.tsx @@ -1,10 +1,10 @@ import React, { useEffect, useState, useMemo, useCallback, useRef } from 'react'; import debounce from 'lodash/debounce'; import { useRecoilValue } from 'recoil'; -import { Menu, Rocket, X } from 'lucide-react'; import { useParams } from 'react-router-dom'; +import { Menu, Rocket, X } from 'lucide-react'; import { useForm, FormProvider } from 'react-hook-form'; -import { Button, Skeleton, useToastContext } from '@librechat/client'; +import { Button, Skeleton, useToastContext, useMediaQuery } from '@librechat/client'; import { Permissions, ResourceType, @@ -30,8 +30,8 @@ import DeletePrompt from '../dialogs/DeletePrompt'; import NoPromptGroup from '../lists/NoPromptGroup'; import PromptEditor from '../editor/PromptEditor'; import SkeletonForm from '../utils/SkeletonForm'; -import Description from '../fields/Description'; import SharePrompt from '../dialogs/SharePrompt'; +import Description from '../fields/Description'; import PromptName from '../fields/PromptName'; import { cn, findPromptGroup } from '~/utils'; import { PromptsEditorMode } from '~/common'; @@ -187,6 +187,7 @@ const PromptForm = ({ promptId: promptIdProp }: { promptId?: string }) => { const promptId = promptIdProp || params.promptId || ''; const editorMode = useRecoilValue(store.promptsEditorMode); + const isSmallScreen = useMediaQuery('(max-width: 768px)'); const [selectionIndex, setSelectionIndex] = useState(0); const prevIsEditingRef = useRef(false); @@ -437,7 +438,16 @@ const PromptForm = ({ promptId: promptIdProp }: { promptId?: string }) => { } if (fetchedPrompt || group) { - return ; + return ( +
+ {isSmallScreen && ( +
+ +
+ )} + +
+ ); } } @@ -462,8 +472,8 @@ const PromptForm = ({ promptId: promptIdProp }: { promptId?: string }) => {
{/* Mobile Actions Row */} - {!isLoadingGroup && group && ( -
+ {!isLoadingGroup && group && isSmallScreen && ( +
{ )}
-
- -
+ {!isSmallScreen && ( +
+ +
+ )} )}
diff --git a/client/src/components/Skills/layouts/SkillsView.tsx b/client/src/components/Skills/layouts/SkillsView.tsx index b19ba588fb..405953eac8 100644 --- a/client/src/components/Skills/layouts/SkillsView.tsx +++ b/client/src/components/Skills/layouts/SkillsView.tsx @@ -1,12 +1,13 @@ -import { Navigate, useNavigate, useParams, useLocation, useSearchParams } from 'react-router-dom'; -import { Spinner } from '@librechat/client'; +import { Spinner, useMediaQuery } from '@librechat/client'; import { PermissionTypes, Permissions } from 'librechat-data-provider'; -import { useGetSkillByIdQuery } from '~/data-provider'; -import { useHasAccess, useAuthContext, useLocalize } from '~/hooks'; +import { Navigate, useNavigate, useParams, useLocation, useSearchParams } from 'react-router-dom'; import SkillFileViewer from '~/components/Skills/display/SkillFileViewer'; +import { CreateSkillForm, SkillForm } from '~/components/Skills/forms'; +import { useHasAccess, useAuthContext, useLocalize } from '~/hooks'; import SkillDetail from '~/components/Skills/display/SkillDetail'; import SkillState from '~/components/Skills/display/SkillState'; -import { CreateSkillForm, SkillForm } from '~/components/Skills/forms'; +import OpenSidebar from '~/components/Chat/Menus/OpenSidebar'; +import { useGetSkillByIdQuery } from '~/data-provider'; /** * Skill detail / edit / create route content. @@ -58,11 +59,14 @@ export default function SkillsView() { // No skill selected — empty state if (!skillId) { return ( -
- +
+ +
+ +
); } @@ -73,6 +77,7 @@ export default function SkillsView() { function CreateView() { return (
+
); @@ -90,6 +95,7 @@ function DetailView({ skillId }: { skillId: string }) { if (activeFile) { return (
+
); @@ -106,6 +112,7 @@ function DetailView({ skillId }: { skillId: string }) { if (skillQuery.isError || !skillQuery.data) { return (
+ + navigate(`/skills/${skillId}/edit`)} @@ -130,7 +138,21 @@ function DetailView({ skillId }: { skillId: string }) { function EditView({ skillId }: { skillId: string }) { return (
+
); } + +/** Sidebar reopen affordance for small screens, where the drawer is the only navigation. */ +function MobileSidebarToggle() { + const isSmallScreen = useMediaQuery('(max-width: 768px)'); + if (!isSmallScreen) { + return null; + } + return ( +
+ +
+ ); +} diff --git a/client/src/components/Skills/layouts/__tests__/SkillsView.spec.tsx b/client/src/components/Skills/layouts/__tests__/SkillsView.spec.tsx index 84c7d97b4f..c5dc2c56b9 100644 --- a/client/src/components/Skills/layouts/__tests__/SkillsView.spec.tsx +++ b/client/src/components/Skills/layouts/__tests__/SkillsView.spec.tsx @@ -4,6 +4,7 @@ import { createMemoryRouter, RouterProvider } from 'react-router-dom'; import SkillsView from '../SkillsView'; const mockUseHasAccess = jest.fn((..._args: unknown[]) => true); +const mockUseMediaQuery = jest.fn((_query: string) => false); jest.mock( 'librechat-data-provider', @@ -18,10 +19,16 @@ jest.mock( '@librechat/client', () => ({ Spinner: () =>
, + useMediaQuery: (query: string) => mockUseMediaQuery(query), }), { virtual: true }, ); +jest.mock('~/components/Chat/Menus/OpenSidebar', () => ({ + __esModule: true, + default: () =>
, +})); + jest.mock('~/hooks', () => ({ useLocalize: () => (key: string) => key, useHasAccess: (...args: unknown[]) => mockUseHasAccess(...args), @@ -58,6 +65,8 @@ describe('SkillsView', () => { beforeEach(() => { mockUseHasAccess.mockReset(); mockUseHasAccess.mockReturnValue(true); + mockUseMediaQuery.mockReset(); + mockUseMediaQuery.mockReturnValue(false); }); it('renders the create skill form for /skills/new', () => { @@ -69,4 +78,25 @@ describe('SkillsView', () => { expect(screen.getByTestId('create-skill-form')).toBeInTheDocument(); }); + + it('renders the sidebar toggle on small screens', () => { + mockUseMediaQuery.mockReturnValue(true); + const router = createMemoryRouter([{ path: '/skills', element: }], { + initialEntries: ['/skills'], + }); + + render(); + + expect(screen.getByTestId('open-sidebar')).toBeInTheDocument(); + }); + + it('does not render the sidebar toggle on large screens', () => { + const router = createMemoryRouter([{ path: '/skills', element: }], { + initialEntries: ['/skills'], + }); + + render(); + + expect(screen.queryByTestId('open-sidebar')).not.toBeInTheDocument(); + }); });