mirror of
https://github.com/danny-avila/LibreChat.git
synced 2025-12-18 09:20:15 +01:00
🎨 style: Refine SidePanel and Textarea Styling (#2209)
* experimental: use TextareaAutosize wrapper with useLayoutEffect to hopefully fix random textarea jankiness * fix(Textarea): force a resize when placeholder text changes * style(ScrollToBottom): update styling for scroll button * style: memoize values and improve side panel toggle states * refactor(SidePanel): more control for toggle states, new hide panel button, and improve toggle state logic * chore: hide resizable panel handle on smaller screens
This commit is contained in:
parent
cb62847838
commit
718572b7c8
9 changed files with 87 additions and 33 deletions
|
|
@ -1,6 +1,5 @@
|
|||
import { useRecoilState } from 'recoil';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import TextareaAutosize from 'react-textarea-autosize';
|
||||
import { memo, useCallback, useRef, useMemo } from 'react';
|
||||
import {
|
||||
supportsFiles,
|
||||
|
|
@ -10,6 +9,7 @@ import {
|
|||
} from 'librechat-data-provider';
|
||||
import { useChatContext, useAssistantsMapContext } from '~/Providers';
|
||||
import { useRequiresKey, useTextarea } from '~/hooks';
|
||||
import { TextareaAutosize } from '~/components/ui';
|
||||
import { useGetFileConfig } from '~/data-provider';
|
||||
import { cn, removeFocusOutlines } from '~/utils';
|
||||
import AttachFile from './Files/AttachFile';
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ export default function ScrollToBottom({ scrollHandler }: Props) {
|
|||
return (
|
||||
<button
|
||||
onClick={scrollHandler}
|
||||
className="absolute bottom-5 right-1/2 cursor-pointer rounded-full border border-white bg-gray-50 text-gray-600 dark:border-white/10 dark:bg-white/10 dark:text-gray-200"
|
||||
className="absolute bottom-5 right-1/2 cursor-pointer rounded-full border border-gray-200 bg-white bg-clip-padding text-gray-600 dark:border-white/10 dark:bg-gray-750/90 dark:text-gray-200"
|
||||
>
|
||||
<svg
|
||||
width="24"
|
||||
|
|
|
|||
|
|
@ -42,6 +42,11 @@ export default function Nav({ links, isCollapsed, resize, defaultActive }: NavPr
|
|||
: '',
|
||||
)}
|
||||
onClick={() => {
|
||||
if (link.onClick) {
|
||||
link.onClick();
|
||||
setActive('');
|
||||
return;
|
||||
}
|
||||
setActive(link.id);
|
||||
resize && resize(25);
|
||||
}}
|
||||
|
|
@ -50,7 +55,11 @@ export default function Nav({ links, isCollapsed, resize, defaultActive }: NavPr
|
|||
<span className="sr-only">{link.title}</span>
|
||||
</button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="right" className="flex items-center gap-4">
|
||||
<TooltipContent
|
||||
side="left"
|
||||
sideOffset={10}
|
||||
className="flex items-center gap-4"
|
||||
>
|
||||
{localize(link.title)}
|
||||
{link.label && (
|
||||
<span className="text-muted-foreground ml-auto">{link.label}</span>
|
||||
|
|
@ -78,6 +87,12 @@ export default function Nav({ links, isCollapsed, resize, defaultActive }: NavPr
|
|||
'hover:bg-gray-50 data-[state=open]:bg-gray-50 data-[state=open]:text-black dark:hover:bg-gray-700 dark:data-[state=open]:bg-gray-700 dark:data-[state=open]:text-white',
|
||||
'w-full justify-start rounded-md border dark:border-gray-700',
|
||||
)}
|
||||
onClick={() => {
|
||||
if (link.onClick) {
|
||||
link.onClick();
|
||||
setActive('');
|
||||
}
|
||||
}}
|
||||
>
|
||||
<link.icon className="mr-2 h-4 w-4" />
|
||||
{localize(link.title)}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import throttle from 'lodash/throttle';
|
||||
import { useState, useRef, useCallback, useEffect, useMemo } from 'react';
|
||||
import { ArrowRightToLine } from 'lucide-react';
|
||||
import { useState, useRef, useCallback, useEffect, useMemo, memo } from 'react';
|
||||
import { useGetEndpointsQuery, useUserKeyQuery } from 'librechat-data-provider/react-query';
|
||||
import type { ImperativePanelHandle } from 'react-resizable-panels';
|
||||
import { EModelEndpoint, type TEndpointsConfig } from 'librechat-data-provider';
|
||||
|
|
@ -25,12 +26,12 @@ interface SidePanelProps {
|
|||
|
||||
const defaultMinSize = 20;
|
||||
|
||||
export default function SidePanel({
|
||||
const SidePanel = ({
|
||||
defaultLayout = [97, 3],
|
||||
defaultCollapsed = false,
|
||||
navCollapsedSize = 3,
|
||||
children,
|
||||
}: SidePanelProps) {
|
||||
}: SidePanelProps) => {
|
||||
const [minSize, setMinSize] = useState(defaultMinSize);
|
||||
const [isHovering, setIsHovering] = useState(false);
|
||||
const [newUser, setNewUser] = useLocalStorage('newUser', true);
|
||||
|
|
@ -42,14 +43,20 @@ export default function SidePanel({
|
|||
|
||||
const panelRef = useRef<ImperativePanelHandle>(null);
|
||||
|
||||
const activePanel = localStorage.getItem('side:active-panel');
|
||||
const defaultActive = activePanel ? activePanel : undefined;
|
||||
const defaultActive = useMemo(() => {
|
||||
const activePanel = localStorage.getItem('side:active-panel');
|
||||
return activePanel ? activePanel : undefined;
|
||||
}, []);
|
||||
|
||||
const assistants = useMemo(() => endpointsConfig?.[EModelEndpoint.assistants], [endpointsConfig]);
|
||||
const userProvidesKey = useMemo(() => !!assistants?.userProvide, [assistants]);
|
||||
const keyProvided = useMemo(
|
||||
() => (userProvidesKey ? !!keyExpiry?.expiresAt : true),
|
||||
[keyExpiry?.expiresAt, userProvidesKey],
|
||||
);
|
||||
|
||||
const Links = useMemo(() => {
|
||||
const links: NavLink[] = [];
|
||||
const assistants = endpointsConfig?.[EModelEndpoint.assistants];
|
||||
const userProvidesKey = !!assistants?.userProvide;
|
||||
const keyProvided = userProvidesKey ? !!keyExpiry?.expiresAt : true;
|
||||
if (assistants && assistants.disableBuilder !== true && keyProvided) {
|
||||
links.push({
|
||||
title: 'com_sidepanel_assistant_builder',
|
||||
|
|
@ -68,8 +75,22 @@ export default function SidePanel({
|
|||
Component: FilesPanel,
|
||||
});
|
||||
|
||||
links.push({
|
||||
title: 'com_sidepanel_hide_panel',
|
||||
label: '',
|
||||
icon: ArrowRightToLine,
|
||||
onClick: () => {
|
||||
console.log('hide-panel');
|
||||
setIsCollapsed(true);
|
||||
setCollapsedSize(0);
|
||||
setMinSize(defaultMinSize - 1);
|
||||
panelRef.current?.collapse();
|
||||
},
|
||||
id: 'hide-panel',
|
||||
});
|
||||
|
||||
return links;
|
||||
}, [endpointsConfig, keyExpiry?.expiresAt]);
|
||||
}, [assistants, keyProvided]);
|
||||
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
const throttledSaveLayout = useCallback(
|
||||
|
|
@ -82,24 +103,26 @@ export default function SidePanel({
|
|||
useEffect(() => {
|
||||
if (isSmallScreen) {
|
||||
setIsCollapsed(true);
|
||||
setMinSize(0);
|
||||
setCollapsedSize(0);
|
||||
setMinSize(defaultMinSize);
|
||||
panelRef.current?.collapse();
|
||||
return;
|
||||
} else {
|
||||
setIsCollapsed(defaultCollapsed);
|
||||
setCollapsedSize(navCollapsedSize);
|
||||
setMinSize(defaultMinSize);
|
||||
panelRef.current?.collapse();
|
||||
}
|
||||
}, [isSmallScreen]);
|
||||
}, [isSmallScreen, defaultCollapsed, navCollapsedSize]);
|
||||
|
||||
const toggleNavVisible = () => {
|
||||
const toggleNavVisible = useCallback(() => {
|
||||
if (newUser) {
|
||||
setNewUser(false);
|
||||
}
|
||||
setIsCollapsed((prev: boolean) => {
|
||||
if (!prev) {
|
||||
setMinSize(0);
|
||||
setCollapsedSize(0);
|
||||
} else {
|
||||
if (prev) {
|
||||
setMinSize(defaultMinSize);
|
||||
setCollapsedSize(3);
|
||||
setCollapsedSize(navCollapsedSize);
|
||||
}
|
||||
return !prev;
|
||||
});
|
||||
|
|
@ -108,11 +131,7 @@ export default function SidePanel({
|
|||
} else {
|
||||
panelRef.current?.expand();
|
||||
}
|
||||
};
|
||||
|
||||
const assistants = endpointsConfig?.[EModelEndpoint.assistants];
|
||||
const userProvidesKey = !!assistants?.userProvide;
|
||||
const keyProvided = userProvidesKey ? !!keyExpiry?.expiresAt : true;
|
||||
}, [isCollapsed, newUser, setNewUser, navCollapsedSize]);
|
||||
|
||||
return (
|
||||
<>
|
||||
|
|
@ -139,7 +158,10 @@ export default function SidePanel({
|
|||
setIsHovering={setIsHovering}
|
||||
className={cn(
|
||||
'fixed top-1/2',
|
||||
isCollapsed && (minSize === 0 || collapsedSize === 0) ? 'mr-9' : 'mr-16',
|
||||
(isCollapsed && (minSize === 0 || collapsedSize === 0)) ||
|
||||
minSize === defaultMinSize - 1
|
||||
? 'mr-9'
|
||||
: 'mr-16',
|
||||
)}
|
||||
translateX={false}
|
||||
side="right"
|
||||
|
|
@ -147,7 +169,7 @@ export default function SidePanel({
|
|||
</div>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
{(!isCollapsed || minSize > 0) && (
|
||||
{(!isCollapsed || (minSize > 0 && minSize !== defaultMinSize - 1)) && (
|
||||
<ResizableHandleAlt withHandle className="bg-transparent dark:text-white" />
|
||||
)}
|
||||
<ResizablePanel
|
||||
|
|
@ -159,9 +181,7 @@ export default function SidePanel({
|
|||
ref={panelRef}
|
||||
style={{
|
||||
overflowY: 'auto',
|
||||
visibility:
|
||||
isCollapsed && (minSize === 0 || collapsedSize === 0) ? 'hidden' : 'visible',
|
||||
transition: 'width 0.2s ease',
|
||||
transition: 'width 0.2s ease, visibility 0s linear 0.2s',
|
||||
}}
|
||||
onExpand={() => {
|
||||
setIsCollapsed(false);
|
||||
|
|
@ -172,9 +192,12 @@ export default function SidePanel({
|
|||
localStorage.setItem('react-resizable-panels:collapsed', 'true');
|
||||
}}
|
||||
className={cn(
|
||||
'sidenav hide-scrollbar border-l border-gray-200 bg-white dark:border-gray-800/50 dark:bg-gray-850',
|
||||
'sidenav hide-scrollbar border-l border-gray-200 bg-white transition-opacity dark:border-gray-800/50 dark:bg-gray-850',
|
||||
isCollapsed ? 'min-w-[50px]' : 'min-w-[340px] sm:min-w-[352px]',
|
||||
minSize === 0 ? 'min-w-0' : '',
|
||||
(isSmallScreen && isCollapsed && (minSize === 0 || collapsedSize === 0)) ||
|
||||
minSize === defaultMinSize - 1
|
||||
? 'hidden min-w-0'
|
||||
: 'opacity-100',
|
||||
)}
|
||||
>
|
||||
{keyProvided && (
|
||||
|
|
@ -211,4 +234,6 @@ export default function SidePanel({
|
|||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export default memo(SidePanel);
|
||||
|
|
|
|||
10
client/src/components/ui/TextareaAutosize.tsx
Normal file
10
client/src/components/ui/TextareaAutosize.tsx
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
import { forwardRef, useLayoutEffect, useState } from 'react';
|
||||
import ReactTextareaAutosize from 'react-textarea-autosize';
|
||||
import type { TextareaAutosizeProps } from 'react-textarea-autosize';
|
||||
export const TextareaAutosize = forwardRef<HTMLTextAreaElement, TextareaAutosizeProps>(
|
||||
(props, ref) => {
|
||||
const [, setIsRerendered] = useState(false);
|
||||
useLayoutEffect(() => setIsRerendered(true), []);
|
||||
return <ReactTextareaAutosize {...props} ref={ref} />;
|
||||
},
|
||||
);
|
||||
|
|
@ -18,6 +18,7 @@ export * from './Table';
|
|||
export * from './Tabs';
|
||||
export * from './Templates';
|
||||
export * from './Textarea';
|
||||
export * from './TextareaAutosize';
|
||||
export * from './Tooltip';
|
||||
export { default as Dropdown } from './Dropdown';
|
||||
export { default as FileUpload } from './FileUpload';
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue