mirror of
https://github.com/danny-avila/LibreChat.git
synced 2025-12-19 09:50:15 +01:00
🖼️ style: Conversation Menu and Dialogs update (#3601)
* feat: new dropdown * fix: maintain popover active when open * fix: update DeleteButton and ShareButton component to use useState for managing dialog state * BREAKING: style improvement of base Button component * style: update export button * a11y: ExportAndShareButton * add border * quick style fix * fix: flick issue on convo * fix: DropDown opens when renaming * chore: update radix-ui/react-dropdown-menu to latest * small fix * style: bookmarks update * reorder export modal * feat: imporved dropdowns * style: a lot of changes; header, bookmarks, export, nav, convo, convoOptions * fix: small style issues * fix: button * fix: bookmarks header menu * fix: dropdown close glitch * feat: Improve accessibility and keyboard navigation in ModelSpec component * fix: Nav related type issues * style: ConvoOptions theming and focus ring --------- Co-authored-by: Danny Avila <danny@librechat.ai>
This commit is contained in:
parent
7f50d2f7c0
commit
96581d56df
62 changed files with 2627 additions and 1821 deletions
|
|
@ -3,26 +3,50 @@ import { VariantProps, cva } from 'class-variance-authority';
|
|||
import { cn } from '~/utils';
|
||||
|
||||
const buttonVariants = cva(
|
||||
'rounded-md inline-flex items-center justify-center text-sm font-medium transition-colors dark:hover:bg-gray-700 dark:hover:text-gray-100 disabled:opacity-50 disabled:pointer-events-none data-[state=open]:bg-gray-100 dark:data-[state=open]:bg-gray-700 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-gray-500',
|
||||
'rounded-md inline-flex items-center justify-center text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:opacity-50 disabled:pointer-events-none',
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: 'bg-gray-850 text-white hover:bg-gray-800 dark:bg-gray-50 dark:text-gray-900',
|
||||
destructive: 'bg-red-500 text-white hover:bg-red-600 dark:hover:bg-red-600',
|
||||
default:
|
||||
'bg-gray-600 text-white hover:bg-gray-800 dark:bg-gray-200 dark:text-gray-900 dark:hover:bg-gray-300',
|
||||
destructive: 'bg-red-600 text-white hover:bg-red-700 dark:bg-red-600 dark:hover:bg-red-700',
|
||||
outline:
|
||||
'bg-transparent border border-gray-200 hover:bg-gray-100 dark:border-gray-700 dark:text-gray-100',
|
||||
subtle: 'bg-gray-100 text-gray-900 hover:bg-gray-200 dark:bg-gray-800 dark:text-gray-100',
|
||||
'bg-transparent border border-gray-200 text-gray-700 hover:bg-gray-200 dark:border-gray-700 dark:text-gray-100 dark:hover:bg-gray-700',
|
||||
subtle:
|
||||
'bg-gray-100 text-gray-900 hover:bg-gray-200 dark:bg-gray-700 dark:text-gray-100 dark:hover:bg-gray-600',
|
||||
ghost:
|
||||
'bg-transparent hover:bg-gray-100 dark:hover:bg-gray-800 dark:text-gray-100 dark:hover:text-gray-100 data-[state=open]:bg-transparent dark:data-[state=open]:bg-transparent',
|
||||
link: 'bg-transparent underline-offset-4 hover:underline text-gray-900 dark:text-gray-100 hover:bg-transparent dark:hover:bg-transparent',
|
||||
'bg-transparent text-gray-900 hover:bg-gray-100 dark:text-gray-100 dark:hover:bg-gray-800 data-[state=open]:bg-transparent',
|
||||
link: 'bg-transparent underline-offset-4 hover:underline text-gray-600 dark:text-gray-400 hover:bg-transparent dark:hover:bg-transparent',
|
||||
success:
|
||||
'bg-green-500 text-white hover:bg-green-700 dark:bg-green-500 dark:hover:bg-green-700',
|
||||
warning:
|
||||
'bg-yellow-500 text-white hover:bg-yellow-600 dark:bg-yellow-600 dark:hover:bg-yellow-700',
|
||||
info: 'bg-blue-500 text-white hover:bg-blue-600 dark:bg-blue-600 dark:hover:bg-blue-700',
|
||||
},
|
||||
size: {
|
||||
default: 'h-10 py-2 px-4',
|
||||
sm: 'h-9 px-2 rounded-md',
|
||||
lg: 'h-11 px-8 rounded-md',
|
||||
sm: 'h-8 px-3 rounded',
|
||||
lg: 'h-12 px-6 rounded-md',
|
||||
xl: 'h-14 px-8 rounded-lg text-base',
|
||||
icon: 'h-10 w-10',
|
||||
},
|
||||
fullWidth: {
|
||||
true: 'w-full',
|
||||
},
|
||||
loading: {
|
||||
true: 'opacity-80 pointer-events-none',
|
||||
},
|
||||
},
|
||||
compoundVariants: [
|
||||
{
|
||||
variant: ['default', 'destructive', 'success', 'warning', 'info'],
|
||||
className: 'focus-visible:ring-white focus-visible:ring-offset-2',
|
||||
},
|
||||
{
|
||||
variant: 'outline',
|
||||
className: 'focus-visible:ring-gray-400 dark:focus-visible:ring-gray-500',
|
||||
},
|
||||
],
|
||||
defaultVariants: {
|
||||
variant: 'default',
|
||||
size: 'default',
|
||||
|
|
@ -32,17 +56,63 @@ const buttonVariants = cva(
|
|||
|
||||
export interface ButtonProps
|
||||
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
|
||||
VariantProps<typeof buttonVariants> {}
|
||||
VariantProps<typeof buttonVariants> {
|
||||
loading?: boolean;
|
||||
leftIcon?: React.ReactNode;
|
||||
rightIcon?: React.ReactNode;
|
||||
}
|
||||
|
||||
const Button = React.forwardRef<HTMLButtonElement, ButtonProps & { customId?: string }>(
|
||||
({ className, variant, size, customId, ...props }, ref) => {
|
||||
(
|
||||
{
|
||||
className,
|
||||
variant,
|
||||
size,
|
||||
fullWidth,
|
||||
loading,
|
||||
leftIcon,
|
||||
rightIcon,
|
||||
children,
|
||||
customId,
|
||||
...props
|
||||
},
|
||||
ref,
|
||||
) => {
|
||||
return (
|
||||
<button
|
||||
className={cn(buttonVariants({ variant, size, className }))}
|
||||
className={cn(buttonVariants({ variant, size, fullWidth, loading, className }))}
|
||||
ref={ref}
|
||||
{...props}
|
||||
id={customId ?? props.id ?? 'shadcn-button'}
|
||||
/>
|
||||
disabled={props.disabled || loading}
|
||||
aria-busy={loading}
|
||||
>
|
||||
{loading && (
|
||||
<svg
|
||||
className="-ml-1 mr-3 h-5 w-5 animate-spin text-current"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<circle
|
||||
className="opacity-25"
|
||||
cx="12"
|
||||
cy="12"
|
||||
r="10"
|
||||
stroke="currentColor"
|
||||
strokeWidth="4"
|
||||
></circle>
|
||||
<path
|
||||
className="opacity-75"
|
||||
fill="currentColor"
|
||||
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
|
||||
></path>
|
||||
</svg>
|
||||
)}
|
||||
{leftIcon && <span className="mr-2">{leftIcon}</span>}
|
||||
{children}
|
||||
{rightIcon && <span className="ml-2">{rightIcon}</span>}
|
||||
</button>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
|
|
|||
100
client/src/components/ui/DropdownPopup.tsx
Normal file
100
client/src/components/ui/DropdownPopup.tsx
Normal file
|
|
@ -0,0 +1,100 @@
|
|||
import React from 'react';
|
||||
import { Menu, MenuButton, MenuItem, MenuItems, Transition } from '@headlessui/react';
|
||||
|
||||
interface DropdownProps {
|
||||
trigger: React.ReactNode;
|
||||
items: {
|
||||
label?: string;
|
||||
onClick?: () => void;
|
||||
icon?: React.ReactNode;
|
||||
kbd?: string;
|
||||
show?: boolean;
|
||||
disabled?: boolean;
|
||||
separate?: boolean;
|
||||
}[];
|
||||
isOpen: boolean;
|
||||
setIsOpen: (isOpen: boolean) => void;
|
||||
className?: string;
|
||||
anchor?: string;
|
||||
}
|
||||
|
||||
const DropdownPopup: React.FC<DropdownProps> = ({
|
||||
trigger,
|
||||
items,
|
||||
isOpen,
|
||||
setIsOpen,
|
||||
className,
|
||||
anchor = { x: 'bottom', y: 'start' },
|
||||
}) => {
|
||||
const handleButtonClick = () => {
|
||||
setIsOpen(!isOpen);
|
||||
};
|
||||
|
||||
return (
|
||||
<Menu>
|
||||
{({ open }) => (
|
||||
<>
|
||||
<MenuButton
|
||||
onClick={handleButtonClick}
|
||||
className={`inline-flex items-center gap-2 rounded-md ${className}`}
|
||||
>
|
||||
{trigger}
|
||||
</MenuButton>
|
||||
<Transition
|
||||
show={open}
|
||||
enter="transition-opacity duration-150"
|
||||
enterFrom="opacity-0"
|
||||
enterTo="opacity-100"
|
||||
leave="transition-opacity duration-150"
|
||||
leaveFrom="opacity-100"
|
||||
leaveTo="opacity-0"
|
||||
afterLeave={() => setIsOpen(false)}
|
||||
>
|
||||
<div className={`${isOpen ? 'visible' : 'invisible'}`}>
|
||||
{open && (
|
||||
<MenuItems
|
||||
static
|
||||
// @ts-ignore
|
||||
anchor={anchor}
|
||||
className="mt-2 overflow-hidden rounded-lg bg-header-primary p-1.5 shadow-lg outline-none focus-visible:ring-2 focus-visible:ring-ring-primary"
|
||||
>
|
||||
<div>
|
||||
{items
|
||||
.filter((item) => item.show !== false)
|
||||
.map((item, index) =>
|
||||
item.separate ? (
|
||||
<div key={index} className="my-1 h-px bg-white/10" />
|
||||
) : (
|
||||
<MenuItem key={index}>
|
||||
<button
|
||||
onClick={item.onClick}
|
||||
className="group flex w-full gap-2 rounded-lg p-2.5 text-sm text-text-primary transition-colors duration-200 data-[focus]:bg-surface-hover"
|
||||
disabled={item.disabled}
|
||||
>
|
||||
{item.icon && (
|
||||
<span className="mr-2 h-5 w-5" aria-hidden="true">
|
||||
{item.icon}
|
||||
</span>
|
||||
)}
|
||||
{item.label}
|
||||
{item.kbd && (
|
||||
<kbd className="ml-auto hidden font-sans text-xs text-black/50 group-data-[focus]:inline dark:text-white/50">
|
||||
⌘{item.kbd}
|
||||
</kbd>
|
||||
)}
|
||||
</button>
|
||||
</MenuItem>
|
||||
),
|
||||
)}
|
||||
</div>
|
||||
</MenuItems>
|
||||
)}
|
||||
</div>
|
||||
</Transition>
|
||||
</>
|
||||
)}
|
||||
</Menu>
|
||||
);
|
||||
};
|
||||
|
||||
export default DropdownPopup;
|
||||
|
|
@ -15,8 +15,8 @@ const Slider = React.forwardRef<React.ElementRef<typeof SliderPrimitive.Root>, S
|
|||
className={cn('relative flex w-full touch-none select-none items-center', className ?? '')}
|
||||
{...props}
|
||||
>
|
||||
<SliderPrimitive.Track className="relative h-1 w-full grow overflow-hidden rounded-full bg-gray-200 dark:bg-gray-800">
|
||||
<SliderPrimitive.Range className="absolute h-full bg-gray-400 dark:bg-gray-400" />
|
||||
<SliderPrimitive.Track className="relative h-1 w-full grow overflow-hidden rounded-full bg-gray-200 dark:bg-gray-850">
|
||||
<SliderPrimitive.Range className="absolute h-full bg-gray-850 dark:bg-white" />
|
||||
</SliderPrimitive.Track>
|
||||
<SliderPrimitive.Thumb
|
||||
onClick={
|
||||
|
|
@ -25,7 +25,7 @@ const Slider = React.forwardRef<React.ElementRef<typeof SliderPrimitive.Root>, S
|
|||
return;
|
||||
})
|
||||
}
|
||||
className="block h-4 w-4 cursor-pointer rounded-full border-2 border-gray-400 bg-white transition-colors focus:outline-none focus:ring-2 focus:ring-gray-400 focus:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 dark:border-gray-200 dark:bg-gray-400 dark:focus:ring-gray-400 dark:focus:ring-offset-gray-800"
|
||||
className="block h-4 w-4 cursor-pointer rounded-full border border-border-medium-alt bg-white shadow ring-ring-primary transition-colors focus-visible:ring-1 focus-visible:ring-offset-1 disabled:pointer-events-none disabled:opacity-50 dark:border-none"
|
||||
/>
|
||||
</SliderPrimitive.Root>
|
||||
),
|
||||
|
|
|
|||
|
|
@ -31,3 +31,4 @@ export { default as SelectDropDown } from './SelectDropDown';
|
|||
export { default as MultiSelectPop } from './MultiSelectPop';
|
||||
export { default as SelectDropDownPop } from './SelectDropDownPop';
|
||||
export { default as MultiSelectDropDown } from './MultiSelectDropDown';
|
||||
export { default as DropdownPopup } from './DropdownPopup';
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue