mirror of
https://github.com/danny-avila/LibreChat.git
synced 2026-01-05 01:58:50 +01:00
refactor: Enhance navigation and chat components with new sidebar and button functionalities that free up space in the nav
This commit is contained in:
parent
cd4a3bd061
commit
84a1a16c3a
12 changed files with 205 additions and 211 deletions
|
|
@ -508,7 +508,10 @@ export interface ModelItemProps {
|
|||
className?: string;
|
||||
}
|
||||
|
||||
export type ContextType = { navVisible: boolean; setNavVisible: (visible: boolean) => void };
|
||||
export type ContextType = {
|
||||
navVisible: boolean;
|
||||
setNavVisible: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
};
|
||||
|
||||
export interface SwitcherProps {
|
||||
endpoint?: t.EModelEndpoint | null;
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import { useOutletContext } from 'react-router-dom';
|
|||
import { getConfigDefaults, PermissionTypes, Permissions } from 'librechat-data-provider';
|
||||
import type { ContextType } from '~/common';
|
||||
import ModelSelector from './Menus/Endpoints/ModelSelector';
|
||||
import { PresetsMenu, HeaderNewChat } from './Menus';
|
||||
import { PresetsMenu, HeaderNewChat, OpenSidebar } from './Menus';
|
||||
import { useGetStartupConfig } from '~/data-provider';
|
||||
import ExportAndShareMenu from './ExportAndShareMenu';
|
||||
import { useMediaQuery, useHasAccess } from '~/hooks';
|
||||
|
|
@ -15,7 +15,7 @@ const defaultInterface = getConfigDefaults().interface;
|
|||
|
||||
export default function Header() {
|
||||
const { data: startupConfig } = useGetStartupConfig();
|
||||
const { navVisible } = useOutletContext<ContextType>();
|
||||
const { navVisible, setNavVisible } = useOutletContext<ContextType>();
|
||||
const interfaceConfig = useMemo(
|
||||
() => startupConfig?.interface ?? defaultInterface,
|
||||
[startupConfig],
|
||||
|
|
@ -37,6 +37,7 @@ export default function Header() {
|
|||
<div className="sticky top-0 z-10 flex h-14 w-full items-center justify-between bg-white p-2 font-semibold text-text-primary dark:bg-gray-800">
|
||||
<div className="hide-scrollbar flex w-full items-center justify-between gap-2 overflow-x-auto">
|
||||
<div className="mx-2 flex items-center gap-2">
|
||||
{!navVisible && <OpenSidebar setNavVisible={setNavVisible} />}
|
||||
{!navVisible && <HeaderNewChat />}
|
||||
{<ModelSelector startupConfig={startupConfig} />}
|
||||
{interfaceConfig.presets === true && interfaceConfig.modelSelect && <PresetsMenu />}
|
||||
|
|
|
|||
|
|
@ -1,36 +1,37 @@
|
|||
import { useQueryClient } from '@tanstack/react-query';
|
||||
import { QueryKeys, Constants } from 'librechat-data-provider';
|
||||
import type { TMessage } from 'librechat-data-provider';
|
||||
import { useMediaQuery, useLocalize } from '~/hooks';
|
||||
import { Button, NewChatIcon } from '~/components';
|
||||
import { TooltipAnchor, Button } from '~/components/ui';
|
||||
import { NewChatIcon } from '~/components/svg';
|
||||
import { useChatContext } from '~/Providers';
|
||||
import { useLocalize } from '~/hooks';
|
||||
|
||||
export default function HeaderNewChat() {
|
||||
const localize = useLocalize();
|
||||
const queryClient = useQueryClient();
|
||||
const { conversation, newConversation } = useChatContext();
|
||||
const isSmallScreen = useMediaQuery('(max-width: 768px)');
|
||||
const localize = useLocalize();
|
||||
|
||||
if (isSmallScreen) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Button
|
||||
size="icon"
|
||||
variant="outline"
|
||||
data-testid="wide-header-new-chat-button"
|
||||
aria-label={localize('com_ui_new_chat')}
|
||||
className="rounded-xl border border-border-light bg-surface-secondary p-2 hover:bg-surface-hover"
|
||||
onClick={() => {
|
||||
queryClient.setQueryData<TMessage[]>(
|
||||
[QueryKeys.messages, conversation?.conversationId ?? Constants.NEW_CONVO],
|
||||
[],
|
||||
);
|
||||
newConversation();
|
||||
}}
|
||||
>
|
||||
<NewChatIcon />
|
||||
</Button>
|
||||
<TooltipAnchor
|
||||
description={localize('com_ui_new_chat')}
|
||||
render={
|
||||
<Button
|
||||
size="icon"
|
||||
variant="outline"
|
||||
data-testid="wide-header-new-chat-button"
|
||||
aria-label={localize('com_ui_new_chat')}
|
||||
className="rounded-xl border border-border-light bg-surface-secondary p-2 hover:bg-surface-hover max-md:hidden"
|
||||
onClick={() => {
|
||||
queryClient.setQueryData<TMessage[]>(
|
||||
[QueryKeys.messages, conversation?.conversationId ?? Constants.NEW_CONVO],
|
||||
[],
|
||||
);
|
||||
newConversation();
|
||||
}}
|
||||
>
|
||||
<NewChatIcon />
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
33
client/src/components/Chat/Menus/OpenSidebar.tsx
Normal file
33
client/src/components/Chat/Menus/OpenSidebar.tsx
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
import { TooltipAnchor, Button } from '~/components/ui';
|
||||
import { Sidebar } from '~/components/svg';
|
||||
import { useLocalize } from '~/hooks';
|
||||
|
||||
export default function OpenSidebar({
|
||||
setNavVisible,
|
||||
}: {
|
||||
setNavVisible: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
}) {
|
||||
const localize = useLocalize();
|
||||
return (
|
||||
<TooltipAnchor
|
||||
description={localize('com_nav_open_sidebar')}
|
||||
render={
|
||||
<Button
|
||||
size="icon"
|
||||
variant="outline"
|
||||
data-testid="open-sidebar-button"
|
||||
aria-label={localize('com_nav_open_sidebar')}
|
||||
className="rounded-xl border border-border-light bg-surface-secondary p-2 hover:bg-surface-hover max-md:hidden"
|
||||
onClick={() =>
|
||||
setNavVisible((prev) => {
|
||||
localStorage.setItem('navVisible', JSON.stringify(!prev));
|
||||
return !prev;
|
||||
})
|
||||
}
|
||||
>
|
||||
<Sidebar />
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,2 +1,3 @@
|
|||
export { default as PresetsMenu } from './PresetsMenu';
|
||||
export { default as OpenSidebar } from './OpenSidebar';
|
||||
export { default as HeaderNewChat } from './HeaderNewChat';
|
||||
|
|
|
|||
|
|
@ -1,10 +1,12 @@
|
|||
import { type FC } from 'react';
|
||||
import { useMemo } from 'react';
|
||||
import type { FC } from 'react';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import { Menu, MenuButton, MenuItems } from '@headlessui/react';
|
||||
import { BookmarkFilledIcon, BookmarkIcon } from '@radix-ui/react-icons';
|
||||
import { BookmarkContext } from '~/Providers/BookmarkContext';
|
||||
import { useGetConversationTags } from '~/data-provider';
|
||||
import BookmarkNavItems from './BookmarkNavItems';
|
||||
import { TooltipAnchor } from '~/components/ui';
|
||||
import { useLocalize } from '~/hooks';
|
||||
import { cn } from '~/utils';
|
||||
import store from '~/store';
|
||||
|
|
@ -19,33 +21,41 @@ const BookmarkNav: FC<BookmarkNavProps> = ({ tags, setTags, isSmallScreen }: Boo
|
|||
const localize = useLocalize();
|
||||
const { data } = useGetConversationTags();
|
||||
const conversation = useRecoilValue(store.conversationByIndex(0));
|
||||
const label = useMemo(
|
||||
() => (tags.length > 0 ? tags.join(', ') : localize('com_ui_bookmarks')),
|
||||
[tags, localize],
|
||||
);
|
||||
|
||||
return (
|
||||
<Menu as="div" className="group relative">
|
||||
{({ open }) => (
|
||||
<>
|
||||
<MenuButton
|
||||
className={cn(
|
||||
'mt-text-sm flex h-10 w-full items-center gap-2 rounded-lg p-2 text-sm transition-colors duration-200 hover:bg-surface-active-alt',
|
||||
open ? 'bg-surface-active-alt' : '',
|
||||
isSmallScreen ? 'h-12' : '',
|
||||
)}
|
||||
data-testid="bookmark-menu"
|
||||
>
|
||||
<div className="h-7 w-7 flex-shrink-0">
|
||||
<div className="relative flex h-full items-center justify-center text-text-primary">
|
||||
{tags.length > 0 ? (
|
||||
<BookmarkFilledIcon className="h-4 w-4" aria-hidden="true" />
|
||||
) : (
|
||||
<BookmarkIcon className="h-4 w-4" aria-hidden="true" />
|
||||
<TooltipAnchor
|
||||
description={label}
|
||||
render={
|
||||
<MenuButton
|
||||
id="bookmark-menu-button"
|
||||
aria-label={localize('com_ui_bookmarks')}
|
||||
className={cn(
|
||||
'flex items-center justify-center',
|
||||
'size-10 border-none text-text-primary hover:bg-accent hover:text-accent-foreground',
|
||||
'rounded-xl border-none p-2 hover:bg-surface-hover',
|
||||
open ? 'bg-surface-hover' : '',
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="grow overflow-hidden whitespace-nowrap text-left text-sm font-medium text-text-primary">
|
||||
{tags.length > 0 ? tags.join(', ') : localize('com_ui_bookmarks')}
|
||||
</div>
|
||||
</MenuButton>
|
||||
<MenuItems className="absolute left-0 top-full z-[100] mt-1 w-full translate-y-0 overflow-hidden rounded-lg bg-surface-secondary p-1.5 shadow-lg outline-none">
|
||||
data-testid="bookmark-menu"
|
||||
>
|
||||
{tags.length > 0 ? (
|
||||
<BookmarkFilledIcon className="icon-lg text-text-primary" aria-hidden="true" />
|
||||
) : (
|
||||
<BookmarkIcon className="icon-lg text-text-primary" aria-hidden="true" />
|
||||
)}
|
||||
</MenuButton>
|
||||
}
|
||||
/>
|
||||
<MenuItems
|
||||
anchor="bottom"
|
||||
className="absolute left-0 top-full z-[100] mt-1 w-60 translate-y-0 overflow-hidden rounded-lg bg-surface-secondary p-1.5 shadow-lg outline-none"
|
||||
>
|
||||
{data && conversation && (
|
||||
<BookmarkContext.Provider value={{ bookmarks: data.filter((tag) => tag.count > 0) }}>
|
||||
<BookmarkNavItems
|
||||
|
|
|
|||
|
|
@ -152,20 +152,21 @@ const Nav = memo(
|
|||
}, [isFetchingNextPage, computedHasNextPage, fetchNextPage]);
|
||||
|
||||
const subHeaders = useMemo(
|
||||
() => (
|
||||
<>
|
||||
{search.enabled === true && <SearchBar isSmallScreen={isSmallScreen} />}
|
||||
{hasAccessToBookmarks && (
|
||||
<>
|
||||
<div className="mt-1.5" />
|
||||
<Suspense fallback={null}>
|
||||
<BookmarkNav tags={tags} setTags={setTags} isSmallScreen={isSmallScreen} />
|
||||
</Suspense>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
),
|
||||
[search.enabled, hasAccessToBookmarks, isSmallScreen, tags, setTags],
|
||||
() => search.enabled === true && <SearchBar isSmallScreen={isSmallScreen} />,
|
||||
[search.enabled, isSmallScreen],
|
||||
);
|
||||
|
||||
const headerButtons = useMemo(
|
||||
() =>
|
||||
hasAccessToBookmarks && (
|
||||
<>
|
||||
<div className="mt-1.5" />
|
||||
<Suspense fallback={null}>
|
||||
<BookmarkNav tags={tags} setTags={setTags} isSmallScreen={isSmallScreen} />
|
||||
</Suspense>
|
||||
</>
|
||||
),
|
||||
[hasAccessToBookmarks, tags, isSmallScreen],
|
||||
);
|
||||
|
||||
const [isSearchLoading, setIsSearchLoading] = useState(
|
||||
|
|
@ -212,9 +213,10 @@ const Nav = memo(
|
|||
>
|
||||
<div className="flex flex-1 flex-col" ref={outerContainerRef}>
|
||||
<MemoNewChat
|
||||
toggleNav={itemToggleNav}
|
||||
isSmallScreen={isSmallScreen}
|
||||
subHeaders={subHeaders}
|
||||
toggleNav={toggleNavVisible}
|
||||
headerButtons={headerButtons}
|
||||
isSmallScreen={isSmallScreen}
|
||||
/>
|
||||
<Conversations
|
||||
conversations={conversations}
|
||||
|
|
@ -235,15 +237,6 @@ const Nav = memo(
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<NavToggle
|
||||
isHovering={isToggleHovering}
|
||||
setIsHovering={setIsToggleHovering}
|
||||
onToggle={toggleNavVisible}
|
||||
navVisible={navVisible}
|
||||
className="fixed left-0 top-1/2 z-40 hidden md:flex"
|
||||
/>
|
||||
|
||||
{isSmallScreen && <NavMask navVisible={navVisible} toggleNavVisible={toggleNavVisible} />}
|
||||
</>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,88 +1,27 @@
|
|||
import React, { useMemo, useCallback } from 'react';
|
||||
import { Search } from 'lucide-react';
|
||||
import React, { useCallback } from 'react';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useQueryClient } from '@tanstack/react-query';
|
||||
import { QueryKeys, Constants } from 'librechat-data-provider';
|
||||
import type { TConversation, TMessage } from 'librechat-data-provider';
|
||||
import { createChatSearchParams, getEndpointField, getIconEndpoint, getIconKey } from '~/utils';
|
||||
import ConvoIconURL from '~/components/Endpoints/ConvoIconURL';
|
||||
import { useGetEndpointsQuery } from '~/data-provider';
|
||||
import type { TMessage } from 'librechat-data-provider';
|
||||
import { NewChatIcon, MobileSidebar, Sidebar } from '~/components/svg';
|
||||
import { TooltipAnchor, Button } from '~/components/ui';
|
||||
import { useLocalize, useNewConvo } from '~/hooks';
|
||||
import { icons } from '~/hooks/Endpoint/Icons';
|
||||
import { NewChatIcon } from '~/components/svg';
|
||||
import { cn } from '~/utils';
|
||||
import { createChatSearchParams } from '~/utils';
|
||||
import store from '~/store';
|
||||
|
||||
const NewChatButtonIcon = React.memo(({ conversation }: { conversation: TConversation | null }) => {
|
||||
const { data: endpointsConfig } = useGetEndpointsQuery();
|
||||
const search = useRecoilValue(store.search);
|
||||
const searchQuery = search.debouncedQuery;
|
||||
|
||||
const computedIcon = useMemo(() => {
|
||||
if (searchQuery) {
|
||||
return null;
|
||||
}
|
||||
let { endpoint = '' } = conversation ?? {};
|
||||
const iconURL = conversation?.iconURL ?? '';
|
||||
endpoint = getIconEndpoint({ endpointsConfig, iconURL, endpoint });
|
||||
const endpointType = getEndpointField(endpointsConfig, endpoint, 'type');
|
||||
const endpointIconURL = getEndpointField(endpointsConfig, endpoint, 'iconURL');
|
||||
const iconKey = getIconKey({ endpoint, endpointsConfig, endpointType, endpointIconURL });
|
||||
const Icon = icons[iconKey];
|
||||
return { iconURL, endpoint, endpointType, endpointIconURL, Icon };
|
||||
}, [searchQuery, conversation, endpointsConfig]);
|
||||
|
||||
if (searchQuery) {
|
||||
return (
|
||||
<div className="shadow-stroke relative flex h-7 w-7 items-center justify-center rounded-full bg-white text-black dark:bg-white">
|
||||
<Search className="h-5 w-5" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!computedIcon) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { iconURL, endpoint, endpointIconURL, Icon } = computedIcon;
|
||||
|
||||
return (
|
||||
<div className="h-7 w-7 flex-shrink-0">
|
||||
{iconURL && iconURL.includes('http') ? (
|
||||
<ConvoIconURL
|
||||
iconURL={iconURL}
|
||||
modelLabel={conversation?.chatGptLabel ?? conversation?.modelLabel ?? ''}
|
||||
endpointIconURL={iconURL}
|
||||
context="nav"
|
||||
/>
|
||||
) : (
|
||||
<div className="shadow-stroke relative flex h-full items-center justify-center rounded-full bg-white text-black dark:bg-surface-primary-alt dark:text-white dark:after:shadow-none">
|
||||
{endpoint && Icon && (
|
||||
<Icon
|
||||
size={41}
|
||||
context="nav"
|
||||
className="h-2/3 w-2/3"
|
||||
endpoint={endpoint}
|
||||
iconURL={endpointIconURL}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
export default function NewChat({
|
||||
index = 0,
|
||||
toggleNav,
|
||||
subHeaders,
|
||||
isSmallScreen,
|
||||
headerButtons,
|
||||
}: {
|
||||
index?: number;
|
||||
toggleNav: () => void;
|
||||
isSmallScreen?: boolean;
|
||||
subHeaders?: React.ReactNode;
|
||||
isSmallScreen: boolean;
|
||||
headerButtons?: React.ReactNode;
|
||||
}) {
|
||||
const queryClient = useQueryClient();
|
||||
/** Note: this component needs an explicit index passed if using more than one */
|
||||
|
|
@ -102,33 +41,50 @@ export default function NewChat({
|
|||
|
||||
newConvo();
|
||||
navigate(newRoute);
|
||||
toggleNav();
|
||||
}, [queryClient, conversation, newConvo, navigate, toggleNav, defaultPreset]);
|
||||
if (isSmallScreen) {
|
||||
toggleNav();
|
||||
}
|
||||
}, [queryClient, conversation, newConvo, navigate, toggleNav, defaultPreset, isSmallScreen]);
|
||||
|
||||
return (
|
||||
<div className="sticky left-0 right-0 top-0 z-50 bg-surface-primary-alt pt-3.5">
|
||||
<div className="pb-0.5 last:pb-0" style={{ transform: 'none' }}>
|
||||
<button
|
||||
data-testid="nav-new-chat-button"
|
||||
onClick={clickHandler}
|
||||
className={cn(
|
||||
'group flex h-10 w-full items-center gap-2 rounded-lg px-2 font-medium transition-colors duration-200 hover:bg-surface-hover',
|
||||
isSmallScreen ? 'h-14' : '',
|
||||
)}
|
||||
aria-label={localize('com_ui_new_chat')}
|
||||
>
|
||||
<NewChatButtonIcon conversation={conversation} />
|
||||
<div className="grow overflow-hidden text-ellipsis whitespace-nowrap text-sm text-text-primary">
|
||||
{localize('com_ui_new_chat')}
|
||||
</div>
|
||||
<div className="flex gap-3">
|
||||
<span className="flex items-center" data-state="closed">
|
||||
<NewChatIcon className="size-5" />
|
||||
</span>
|
||||
</div>
|
||||
</button>
|
||||
<>
|
||||
<div className="h-header-height xs:pe-3 flex items-center justify-between py-2">
|
||||
<TooltipAnchor
|
||||
description={localize('com_nav_close_sidebar')}
|
||||
render={
|
||||
<Button
|
||||
size="icon"
|
||||
variant="outline"
|
||||
data-testid="close-sidebar-button"
|
||||
aria-label={localize('com_nav_close_sidebar')}
|
||||
className="rounded-xl border-none bg-transparent p-2 hover:bg-surface-hover"
|
||||
onClick={toggleNav}
|
||||
>
|
||||
<Sidebar className="max-md:hidden" />
|
||||
<MobileSidebar className="md:hidden" />
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
<div className="flex">
|
||||
{headerButtons}
|
||||
<TooltipAnchor
|
||||
description={localize('com_ui_new_chat')}
|
||||
render={
|
||||
<Button
|
||||
size="icon"
|
||||
variant="outline"
|
||||
data-testid="nav-new-chat-button"
|
||||
aria-label={localize('com_ui_new_chat')}
|
||||
className="rounded-xl border-none bg-transparent p-2 hover:bg-surface-hover"
|
||||
onClick={clickHandler}
|
||||
>
|
||||
<NewChatIcon />
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{subHeaders != null ? subHeaders : null}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
19
client/src/components/svg/MobileSidebar.tsx
Normal file
19
client/src/components/svg/MobileSidebar.tsx
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
export default function MobileSidebar({ className }: { className?: string }) {
|
||||
return (
|
||||
<svg
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className={className}
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M3 8C3 7.44772 3.44772 7 4 7H20C20.5523 7 21 7.44772 21 8C21 8.55228 20.5523 9 20 9H4C3.44772 9 3 8.55228 3 8ZM3 16C3 15.4477 3.44772 15 4 15H14C14.5523 15 15 15.4477 15 16C15 16.5523 14.5523 17 14 17H4C3.44772 17 3 16.5523 3 16Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,43 +0,0 @@
|
|||
export default function Panel({ open = false }) {
|
||||
const openPanel = (
|
||||
<svg
|
||||
stroke="currentColor"
|
||||
fill="none"
|
||||
strokeWidth="2"
|
||||
viewBox="0 0 24 24"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
className="h-4 w-4"
|
||||
height="1em"
|
||||
width="1em"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect>
|
||||
<line x1="9" y1="3" x2="9" y2="21"></line>
|
||||
</svg>
|
||||
);
|
||||
|
||||
const closePanel = (
|
||||
<svg
|
||||
stroke="currentColor"
|
||||
fill="none"
|
||||
strokeWidth="2"
|
||||
viewBox="0 0 24 24"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
className="h-4 w-4"
|
||||
height="1em"
|
||||
width="1em"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect>
|
||||
<line x1="9" y1="3" x2="9" y2="21"></line>
|
||||
</svg>
|
||||
);
|
||||
|
||||
if (open) {
|
||||
return openPanel;
|
||||
} else {
|
||||
return closePanel;
|
||||
}
|
||||
}
|
||||
19
client/src/components/svg/Sidebar.tsx
Normal file
19
client/src/components/svg/Sidebar.tsx
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
export default function Sidebar({ className }: { className?: string }) {
|
||||
return (
|
||||
<svg
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className={className}
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M8.85719 3H15.1428C16.2266 2.99999 17.1007 2.99998 17.8086 3.05782C18.5375 3.11737 19.1777 3.24318 19.77 3.54497C20.7108 4.02433 21.4757 4.78924 21.955 5.73005C22.2568 6.32234 22.3826 6.96253 22.4422 7.69138C22.5 8.39925 22.5 9.27339 22.5 10.3572V13.6428C22.5 14.7266 22.5 15.6008 22.4422 16.3086C22.3826 17.0375 22.2568 17.6777 21.955 18.27C21.4757 19.2108 20.7108 19.9757 19.77 20.455C19.1777 20.7568 18.5375 20.8826 17.8086 20.9422C17.1008 21 16.2266 21 15.1428 21H8.85717C7.77339 21 6.89925 21 6.19138 20.9422C5.46253 20.8826 4.82234 20.7568 4.23005 20.455C3.28924 19.9757 2.52433 19.2108 2.04497 18.27C1.74318 17.6777 1.61737 17.0375 1.55782 16.3086C1.49998 15.6007 1.49999 14.7266 1.5 13.6428V10.3572C1.49999 9.27341 1.49998 8.39926 1.55782 7.69138C1.61737 6.96253 1.74318 6.32234 2.04497 5.73005C2.52433 4.78924 3.28924 4.02433 4.23005 3.54497C4.82234 3.24318 5.46253 3.11737 6.19138 3.05782C6.89926 2.99998 7.77341 2.99999 8.85719 3ZM6.35424 5.05118C5.74907 5.10062 5.40138 5.19279 5.13803 5.32698C4.57354 5.6146 4.1146 6.07354 3.82698 6.63803C3.69279 6.90138 3.60062 7.24907 3.55118 7.85424C3.50078 8.47108 3.5 9.26339 3.5 10.4V13.6C3.5 14.7366 3.50078 15.5289 3.55118 16.1458C3.60062 16.7509 3.69279 17.0986 3.82698 17.362C4.1146 17.9265 4.57354 18.3854 5.13803 18.673C5.40138 18.8072 5.74907 18.8994 6.35424 18.9488C6.97108 18.9992 7.76339 19 8.9 19H9.5V5H8.9C7.76339 5 6.97108 5.00078 6.35424 5.05118ZM11.5 5V19H15.1C16.2366 19 17.0289 18.9992 17.6458 18.9488C18.2509 18.8994 18.5986 18.8072 18.862 18.673C19.4265 18.3854 19.8854 17.9265 20.173 17.362C20.3072 17.0986 20.3994 16.7509 20.4488 16.1458C20.4992 15.5289 20.5 14.7366 20.5 13.6V10.4C20.5 9.26339 20.4992 8.47108 20.4488 7.85424C20.3994 7.24907 20.3072 6.90138 20.173 6.63803C19.8854 6.07354 19.4265 5.6146 18.862 5.32698C18.5986 5.19279 18.2509 5.10062 17.6458 5.05118C17.0289 5.00078 16.2366 5 15.1 5H11.5ZM5 8.5C5 7.94772 5.44772 7.5 6 7.5H7C7.55229 7.5 8 7.94772 8 8.5C8 9.05229 7.55229 9.5 7 9.5H6C5.44772 9.5 5 9.05229 5 8.5ZM5 12C5 11.4477 5.44772 11 6 11H7C7.55229 11 8 11.4477 8 12C8 12.5523 7.55229 13 7 13H6C5.44772 13 5 12.5523 5 12Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
|
@ -4,7 +4,8 @@ export { default as Plugin } from './Plugin';
|
|||
export { default as GPTIcon } from './GPTIcon';
|
||||
export { default as EditIcon } from './EditIcon';
|
||||
export { default as DataIcon } from './DataIcon';
|
||||
export { default as Panel } from './Panel';
|
||||
export { default as Sidebar } from './Sidebar';
|
||||
export { default as MobileSidebar } from './MobileSidebar';
|
||||
export { default as Spinner } from './Spinner';
|
||||
export { default as Clipboard } from './Clipboard';
|
||||
export { default as CheckMark } from './CheckMark';
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue