feat: collapsible side menu

issue #110
This commit is contained in:
Jing Hua 2023-03-26 16:56:38 +08:00
parent 470aa40a84
commit b813df5343
8 changed files with 101 additions and 39 deletions

View file

@ -1,6 +1,6 @@
import React from 'react'; import React from 'react';
const DownArrow = () => { const DownArrow = (props: React.SVGProps<SVGSVGElement>) => {
return ( return (
<svg <svg
stroke='currentColor' stroke='currentColor'
@ -13,6 +13,7 @@ const DownArrow = () => {
height='1em' height='1em'
width='1em' width='1em'
xmlns='http://www.w3.org/2000/svg' xmlns='http://www.w3.org/2000/svg'
{...props}
> >
<line x1='12' y1='5' x2='12' y2='19'></line> <line x1='12' y1='5' x2='12' y2='19'></line>
<polyline points='19 12 12 19 5 12'></polyline> <polyline points='19 12 12 19 5 12'></polyline>

View file

@ -0,0 +1,25 @@
import React from 'react';
const MenuIcon = (props: React.SVGProps<SVGSVGElement>) => {
return (
<svg
stroke='currentColor'
fill='none'
strokeWidth='1.5'
viewBox='0 0 24 24'
strokeLinecap='round'
strokeLinejoin='round'
className='h-6 w-6'
height='1em'
width='1em'
xmlns='http://www.w3.org/2000/svg'
{...props}
>
<line x1='3' y1='12' x2='21' y2='12'></line>
<line x1='3' y1='6' x2='21' y2='6'></line>
<line x1='3' y1='18' x2='21' y2='18'></line>
</svg>
);
};
export default MenuIcon;

View file

@ -1,12 +1,19 @@
import React from 'react'; import React from 'react';
import useStore from '@store/store';
import ChatContent from './ChatContent'; import ChatContent from './ChatContent';
import MobileBar from '../MobileBar'; import MobileBar from '../MobileBar';
import StopGeneratingButton from '@components/StopGeneratingButton/StopGeneratingButton'; import StopGeneratingButton from '@components/StopGeneratingButton/StopGeneratingButton';
const Chat = () => { const Chat = () => {
const hideSideMenu = useStore((state) => state.hideSideMenu);
return ( return (
<div className='flex h-full flex-1 flex-col md:pl-[260px]'> <div
className={`flex h-full flex-1 flex-col ${
hideSideMenu ? 'md:pl-0' : 'md:pl-[260px]'
}`}
>
<MobileBar /> <MobileBar />
<main className='relative h-full w-full transition-width flex flex-col overflow-hidden items-stretch flex-1'> <main className='relative h-full w-full transition-width flex flex-col overflow-hidden items-stretch flex-1'>
<ChatContent /> <ChatContent />

View file

@ -1,4 +1,5 @@
import React from 'react'; import React from 'react';
import useStore from '@store/store';
import Avatar from './Avatar'; import Avatar from './Avatar';
import MessageContent from './MessageContent'; import MessageContent from './MessageContent';
@ -25,13 +26,21 @@ const Message = React.memo(
messageIndex: number; messageIndex: number;
sticky?: boolean; sticky?: boolean;
}) => { }) => {
const hideSideMenu = useStore((state) => state.hideSideMenu);
return ( return (
<div <div
className={`w-full border-b border-black/10 dark:border-gray-900/50 text-gray-800 dark:text-gray-100 group ${ className={`w-full border-b border-black/10 dark:border-gray-900/50 text-gray-800 dark:text-gray-100 group ${
backgroundStyle[messageIndex % 2] backgroundStyle[messageIndex % 2]
}`} }`}
> >
<div className='text-base gap-4 md:gap-6 m-auto md:max-w-2xl lg:max-w-2xl xl:max-w-3xl p-4 md:py-6 flex lg:px-0'> <div
className={`text-base gap-4 md:gap-6 m-auto p-4 md:py-6 flex lg:px-0 transition-all ease-in-out ${
hideSideMenu
? 'md:max-w-5xl lg:max-w-5xl xl:max-w-6xl'
: 'md:max-w-3xl lg:max-w-3xl xl:max-w-4xl'
}`}
>
<Avatar role={role} /> <Avatar role={role} />
<div className='w-[calc(100%-50px)] '> <div className='w-[calc(100%-50px)] '>
<RoleSelector <RoleSelector

View file

@ -1,16 +1,31 @@
import React from 'react'; import React, { useEffect } from 'react';
import useStore from '@store/store';
import NewChat from './NewChat'; import NewChat from './NewChat';
import ChatHistoryList from './ChatHistoryList'; import ChatHistoryList from './ChatHistoryList';
import MenuOptions from './MenuOptions'; import MenuOptions from './MenuOptions';
import CrossIcon2 from '@icon/CrossIcon2'; import CrossIcon2 from '@icon/CrossIcon2';
import DownArrow from '@icon/DownArrow';
import MenuIcon from '@icon/MenuIcon';
const Menu = () => { const Menu = () => {
const hideSideMenu = useStore((state) => state.hideSideMenu);
const setHideSideMenu = useStore((state) => state.setHideSideMenu);
useEffect(() => {
window.addEventListener('resize', () => {
if (window.innerWidth < 768) setHideSideMenu(true);
});
}, []);
return ( return (
<> <>
<div <div
id='menu' id='menu'
className='dark bg-gray-900 md:fixed md:inset-y-0 md:flex md:w-[260px] md:flex-col max-md:translate-x-[-100%] max-md:fixed max-md:transition-transform max-md:z-[999] max-md:top-0 max-md:left-0 max-md:h-full max-md:w-3/4' className={`group dark bg-gray-900 fixed md:inset-y-0 md:flex md:w-[260px] md:flex-col transition-transform z-[999] top-0 left-0 h-full max-md:w-3/4 ${
hideSideMenu ? 'translate-x-[-100%]' : 'translate-x-[0%]'
}`}
> >
<div className='flex h-full min-h-0 flex-col'> <div className='flex h-full min-h-0 flex-col'>
<div className='scrollbar-trigger flex h-full w-full flex-1 items-start border-white/20'> <div className='scrollbar-trigger flex h-full w-full flex-1 items-start border-white/20'>
@ -23,27 +38,39 @@ const Menu = () => {
</div> </div>
<div <div
id='menu-close' id='menu-close'
className='hidden md:hidden absolute z-[999] right-0 translate-x-full top-10 bg-gray-900 p-2 cursor-pointer hover:bg-black text-white' className={`${
hideSideMenu ? 'hidden' : ''
} md:hidden absolute z-[999] right-0 translate-x-full top-10 bg-gray-900 p-2 cursor-pointer hover:bg-black text-white`}
onClick={() => { onClick={() => {
document setHideSideMenu(true);
.getElementById('menu')
?.classList.remove('max-md:translate-x-[0%]');
document.getElementById('menu-close')?.classList.add('hidden');
document.getElementById('menu-backdrop')?.classList.add('hidden');
}} }}
> >
<CrossIcon2 /> <CrossIcon2 />
</div> </div>
<div
className={`${
hideSideMenu ? 'opacity-100' : 'opacity-0'
} group md:group-hover:opacity-100 max-md:hidden transition-opacity absolute z-[999] right-0 translate-x-full top-10 bg-gray-900 p-2 cursor-pointer hover:bg-black text-white ${
hideSideMenu ? '' : 'rotate-90'
}`}
onClick={() => {
setHideSideMenu(!hideSideMenu);
}}
>
{hideSideMenu ? (
<MenuIcon className='h-4 w-4' />
) : (
<DownArrow className='h-4 w-4' />
)}
</div>
</div> </div>
<div <div
id='menu-backdrop' id='menu-backdrop'
className='hidden md:hidden fixed top-0 left-0 h-full w-full z-[60] bg-gray-900/70' className={`${
hideSideMenu ? 'hidden' : ''
} md:hidden fixed top-0 left-0 h-full w-full z-[60] bg-gray-900/70`}
onClick={() => { onClick={() => {
document setHideSideMenu(true);
.getElementById('menu')
?.classList.remove('max-md:translate-x-[0%]');
document.getElementById('menu-close')?.classList.add('hidden');
document.getElementById('menu-backdrop')?.classList.add('hidden');
}} }}
/> />
</> </>

View file

@ -2,10 +2,12 @@ import React from 'react';
import useStore from '@store/store'; import useStore from '@store/store';
import PlusIcon from '@icon/PlusIcon'; import PlusIcon from '@icon/PlusIcon';
import MenuIcon from '@icon/MenuIcon';
import useAddChat from '@hooks/useAddChat'; import useAddChat from '@hooks/useAddChat';
const MobileBar = () => { const MobileBar = () => {
const generating = useStore((state) => state.generating); const generating = useStore((state) => state.generating);
const setHideSideMenu = useStore((state) => state.setHideSideMenu);
const chatTitle = useStore((state) => const chatTitle = useStore((state) =>
state.chats && state.chats &&
state.chats.length > 0 && state.chats.length > 0 &&
@ -23,30 +25,11 @@ const MobileBar = () => {
type='button' type='button'
className='-ml-0.5 -mt-0.5 inline-flex h-10 w-10 items-center justify-center rounded-md hover:text-gray-900 focus:outline-none focus:ring-2 focus:ring-inset focus:ring-white dark:hover:text-white' className='-ml-0.5 -mt-0.5 inline-flex h-10 w-10 items-center justify-center rounded-md hover:text-gray-900 focus:outline-none focus:ring-2 focus:ring-inset focus:ring-white dark:hover:text-white'
onClick={() => { onClick={() => {
document setHideSideMenu(false);
.getElementById('menu')
?.classList.add('max-md:translate-x-[0%]');
document.getElementById('menu-close')?.classList.remove('hidden');
document.getElementById('menu-backdrop')?.classList.remove('hidden');
}} }}
> >
<span className='sr-only'>Open sidebar</span> <span className='sr-only'>Open sidebar</span>
<svg <MenuIcon />
stroke='currentColor'
fill='none'
strokeWidth='1.5'
viewBox='0 0 24 24'
strokeLinecap='round'
strokeLinejoin='round'
className='h-6 w-6'
height='1em'
width='1em'
xmlns='http://www.w3.org/2000/svg'
>
<line x1='3' y1='12' x2='21' y2='12'></line>
<line x1='3' y1='6' x2='21' y2='6'></line>
<line x1='3' y1='18' x2='21' y2='18'></line>
</svg>
</button> </button>
<h1 className='flex-1 text-center text-base font-normal'>{chatTitle}</h1> <h1 className='flex-1 text-center text-base font-normal'>{chatTitle}</h1>
<button <button

View file

@ -10,18 +10,21 @@ export interface ConfigSlice {
hideMenuOptions: boolean; hideMenuOptions: boolean;
defaultChatConfig: ConfigInterface; defaultChatConfig: ConfigInterface;
defaultSystemMessage: string; defaultSystemMessage: string;
hideSideMenu: boolean;
setOpenConfig: (openConfig: boolean) => void; setOpenConfig: (openConfig: boolean) => void;
setTheme: (theme: Theme) => void; setTheme: (theme: Theme) => void;
setAutoTitle: (autoTitle: boolean) => void; setAutoTitle: (autoTitle: boolean) => void;
setDefaultChatConfig: (defaultChatConfig: ConfigInterface) => void; setDefaultChatConfig: (defaultChatConfig: ConfigInterface) => void;
setDefaultSystemMessage: (defaultSystemMessage: string) => void; setDefaultSystemMessage: (defaultSystemMessage: string) => void;
setHideMenuOptions: (hideMenuOptions: boolean) => void; setHideMenuOptions: (hideMenuOptions: boolean) => void;
setHideSideMenu: (hideSideMenu: boolean) => void;
} }
export const createConfigSlice: StoreSlice<ConfigSlice> = (set, get) => ({ export const createConfigSlice: StoreSlice<ConfigSlice> = (set, get) => ({
openConfig: false, openConfig: false,
theme: 'dark', theme: 'dark',
hideMenuOptions: false, hideMenuOptions: false,
hideSideMenu: false,
autoTitle: false, autoTitle: false,
defaultChatConfig: _defaultChatConfig, defaultChatConfig: _defaultChatConfig,
defaultSystemMessage: _defaultSystemMessage, defaultSystemMessage: _defaultSystemMessage,
@ -61,4 +64,10 @@ export const createConfigSlice: StoreSlice<ConfigSlice> = (set, get) => ({
hideMenuOptions: hideMenuOptions, hideMenuOptions: hideMenuOptions,
})); }));
}, },
setHideSideMenu: (hideSideMenu: boolean) => {
set((prev: ConfigSlice) => ({
...prev,
hideSideMenu: hideSideMenu,
}));
},
}); });

View file

@ -57,6 +57,7 @@ const useStore = create<StoreState>()(
defaultSystemMessage: state.defaultSystemMessage, defaultSystemMessage: state.defaultSystemMessage,
hideMenuOptions: state.hideMenuOptions, hideMenuOptions: state.hideMenuOptions,
firstVisit: state.firstVisit, firstVisit: state.firstVisit,
hideSideMenu: state.hideSideMenu,
}), }),
version: 6, version: 6,
migrate: (persistedState, version) => { migrate: (persistedState, version) => {