📦 feat: Move Shared Components to @librechat/client (#8685)

* feat: init @librechat/client

* feat: Add common types and interfaces for accessibility, agents, artifacts, assistants, and tools

* feat: Add jotai as a peer dependency

* fix build client package

* feat: cleanup unused types from common/index.ts

- Remove 104 unused type exports from packages/client/src/common/index.ts
- Keep only 7 actually used exports (93% reduction)
- Add cleanup script with enhanced import pattern detection
- Support both named imports and namespace imports (* as t)
- Create automatic backups and comprehensive documentation
- Maintain type safety with build verification
- No breaking changes to existing code

Kept exports:
- TShowToast, Option, OptionWithIcon, DropdownValueSetter
- MentionOption, NotificationSeverity, MenuItemProps

Scripts: cleanup-common-types-safe.js, README-CLEANUP.md

* fix: cleanup

* fix: package; refactor: tsconfig

* feat: add back `recoil`

* fix: move dependencies to peerDependencies in client package

* feat: add @librechat/client as a dependency in package.json and package-lock.json

* feat: update client package configuration and dependencies

- Added new dependencies for Rollup plugins and updated existing ones in package.json and package-lock.json.
- Introduced a new Rollup configuration file for building the client package.
- Refactored build scripts to include a dedicated build command for the client.
- Updated TypeScript configuration for improved module resolution and type declaration output.
- Integrated a Toast component from the client package into the main App component.

* feat: enhance Rollup configuration for client package

- Updated terser plugin settings to preserve directives like 'use client'.
- Added custom warning handler to ignore "use client" directive warnings during the build process.

* chore: rename package/client build script command

* feat: update client package dependencies and Rollup configuration

- Added rollup-plugin-postcss to package.json and updated package-lock.json.
- Enhanced Rollup configuration to include postcss plugin for CSS handling.
- Updated index.ts to export all components from the components directory for better modularity.

* feat: add client package directory to update configuration

- Included the 'client' package directory in the update.js configuration to ensure it is recognized during updates.

* feat: export Toast component in client package

- Added export for the Toast component in index.ts to enhance modularity and accessibility of components.

* feat: /client transition to @librechat/client

* chore: fixed formatting issues

* fix: update peer dependencies in @librechat/client to prevent bundling them

* fix: correct useSprings implementation in SplitText component

* fix: circular dependencies in DataTable

* fix: add remaining peer dependencies and match actual versions previously used in `client/package.json`

* fix: correct frontend:ci script to include client package build

* chore: enhance unused package detection for @librechat/client and improve dependency extraction

* fix: add missing peer dependency for @radix-ui/react-collapsible

* chore: include "packages/client" in unused i18next keys detection

* test: update AgentFooter tests to use document.querySelector for spinner checks
test: mock window.matchMedia in setupTests.js for consistent test environment

* feat: add react-hook-form dependency and update FormInput component to use its types

* chore: linting

* refactor: remove unused defaultSelectedValues prop from MCPSelect and MultiSelect components

* chore: linting

* feat: update GitHub Actions workflow to publish @librechat/client

* chore: update GitHub Actions workflow to install and build data-provider and client dependencies

* chore: add missing @testing-library/react dependency to client package

* chore: update tsconfig.json to exclude additional test files

* chore: fix build issues, resolve latest LC changes

* chore: move MCP components outside of `~/components/ui`

* feat: implement dynamic theme system with environment variable support and Tailwind CSS integration

* chore: remove unnecessary logging of sttExternal and ttsExternal in Speech component

* chore: squashed cleanup commits

chore: move @tanstack/react-virtual to dependencies and remove recoil from package.json

chore: move dependencies to peerDependencies in package.json

feat: update package.json and rollup.config.js to include jotai and enhance bundling configuration

feat: update package.json and rollup.config.js to include jotai and enhance bundling configuration

refactor: reorganize exports in index.ts for improved clarity

refactor: remove unused types and interfaces from common files

refactor: update peer dependencies and improve component typings

- Removed duplicate peer dependencies from package.json and organized them.
- Updated rollup.config.js to disable TypeScript checking during the build process.
- Modified AnimatedTabs component to use React.ReactNode for label and content types, and added TypeScript workarounds for compatibility.
- Enhanced Label and Separator components to accept an optional className prop and improved prop spreading.
- Updated Slider component to include an optional className prop and refined prop handling for better type safety.

refactor: clean up client workflow and update package dependencies

refactor: update package dependencies and improve PostCSS and Rollup configurations

chore: bump version to 0.1.2 in package.json

chore: bump client version to 0.1.2 in package-lock.json

chore: bump client version to 0.1.3 and update dependencies

chore: bump client version to 0.1.4 and update @react-spring dependencies

chore: update package version to 0.1.5 and adjust peer dependencies

- Bump version in package.json from 0.1.4 to 0.1.5.
- Update peer dependency for @tanstack/react-query to allow version 5.0.0.
- Add @tanstack/react-table and @tanstack/react-virtual as dependencies.
- Update various dependencies to their latest compatible versions.
- Simplify postcss.config.js by removing unnecessary options.
- Clean up rollup.config.js by removing ignored PostCSS warnings.
- Update CheckboxButton component to cast icon as React JSX element.
- Adjust Combobox component's class names for better styling.
- Change DropdownPopup component to use React's namespace import.
- Modify InputOTP component to use 'any' type for OTPInputContext.
- Ensure displayLabel and value in ModelParameters are converted to strings.
- Update MultiSearch component's placeholder to ensure it's a string.
- Cast selectIcon in MultiSelect as React JSX element for consistency.
- Update OGDialogTemplate to cast selectText as React JSX element.
- Initialize animationRef in PixelCard with undefined for clarity.
- Add TypeScript ignore comments in Select and SelectDropDown components for Radix UI type conflicts.
- Ensure title in SelectDropDown is a string and adjust rendering of options.
- Update useLocalize hook to cast options as any for compatibility.

refactor: code structure; chore: translations cleanup

chore: remove unused imports and clean up code in NewChat component

refactor: enhance Menu component to support custom render functions for menu items

style: update itemClassName in ToolsDropdown for improved UI consistency

fix: merge conflicts

chore: update @radix-ui/react-accordion to version 1.2.11

* refactor: remove unnecessary TypeScript type assertions in AnimatedTabs, Label, Separator, and Slider components

* feat: enhance theme system with localStorage persistence and new theme atoms

* chore: bump version of @librechat/client to 0.1.7

* chore: fix ci/cd warnings/errors related to linting and unused localization keys

* chore: update dependencies for class-variance-authority, clsx, and match-sorter

* chore: bump @librechat/client to v0.1.8

* feat: add utility colors for theme customization and remove unused tailwindConfig

* v0.1.9

---------

Co-authored-by: Marco Beretta <81851188+berry-13@users.noreply.github.com>
This commit is contained in:
Danny Avila 2025-07-27 12:19:01 -04:00 committed by GitHub
parent 97e1cdd224
commit 79197454f8
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
569 changed files with 7010 additions and 1848 deletions

View file

@ -1,51 +0,0 @@
import * as React from 'react';
import * as AccordionPrimitive from '@radix-ui/react-accordion';
import { ChevronDownIcon } from '@radix-ui/react-icons';
import { cn } from '~/utils';
const Accordion = AccordionPrimitive.Root;
const AccordionItem = React.forwardRef<
React.ElementRef<typeof AccordionPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Item>
>(({ className = '', ...props }, ref) => (
<AccordionPrimitive.Item ref={ref} className={cn('border-b', className)} {...props} />
));
AccordionItem.displayName = 'AccordionItem';
const AccordionTrigger = React.forwardRef<
React.ElementRef<typeof AccordionPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Trigger>
>(({ className = '', children, ...props }, ref) => (
<AccordionPrimitive.Header className="flex">
<AccordionPrimitive.Trigger
ref={ref}
className={cn(
'flex flex-1 items-center justify-between py-4 text-sm font-medium transition-all hover:underline [&[data-state=open]>svg]:rotate-180',
className,
)}
{...props}
>
{children}
<ChevronDownIcon className="text-muted-foreground h-4 w-4 shrink-0 transition-transform duration-200" />
</AccordionPrimitive.Trigger>
</AccordionPrimitive.Header>
));
AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName;
const AccordionContent = React.forwardRef<
React.ElementRef<typeof AccordionPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Content>
>(({ className = '', children, ...props }, ref) => (
<AccordionPrimitive.Content
ref={ref}
className="overflow-hidden text-sm data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down"
{...props}
>
<div className={cn('pb-4 pt-0', className)}>{children}</div>
</AccordionPrimitive.Content>
));
AccordionContent.displayName = AccordionPrimitive.Content.displayName;
export { Accordion, AccordionItem, AccordionTrigger, AccordionContent };

View file

@ -1,132 +0,0 @@
import * as React from 'react';
import * as AlertDialogPrimitive from '@radix-ui/react-alert-dialog';
import { cn } from '../../utils';
const AlertDialog = AlertDialogPrimitive.Root;
const AlertDialogTrigger = AlertDialogPrimitive.Trigger;
type AlertPortalProps = AlertDialogPrimitive.AlertDialogPortalProps & { className?: string };
const AlertDialogPortal = ({ className = '', children, ...props }: AlertPortalProps) => (
<AlertDialogPrimitive.Portal className={cn(className)} {...(props as AlertPortalProps)}>
<div className="fixed inset-0 z-50 flex items-end justify-center sm:items-center">
{children}
</div>
</AlertDialogPrimitive.Portal>
);
AlertDialogPortal.displayName = AlertDialogPrimitive.Portal.displayName;
const AlertDialogOverlay = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Overlay>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Overlay>
>(({ className = '', ...props }, ref) => (
<AlertDialogPrimitive.Overlay
className={cn(
'fixed inset-0 z-50 bg-gray-500/90 transition-opacity animate-in fade-in dark:bg-gray-800/90',
className,
)}
{...props}
ref={ref}
/>
));
AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName;
const AlertDialogContent = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Content>
>(({ className = '', ...props }, ref) => (
<AlertDialogPortal>
<AlertDialogOverlay />
<AlertDialogPrimitive.Content
ref={ref}
className={cn(
'fixed z-50 grid w-full max-w-lg scale-100 gap-4 bg-white p-6 opacity-100 animate-in fade-in-90 slide-in-from-bottom-10 sm:rounded-lg sm:zoom-in-90 sm:slide-in-from-bottom-0 md:w-full',
'dark:bg-gray-900',
className,
)}
{...props}
/>
</AlertDialogPortal>
));
AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName;
const AlertDialogHeader = ({ className = '', ...props }: React.HTMLAttributes<HTMLDivElement>) => (
<div className={cn('flex flex-col space-y-2 text-center sm:text-left', className)} {...props} />
);
AlertDialogHeader.displayName = 'AlertDialogHeader';
const AlertDialogFooter = ({ className = '', ...props }: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn('flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2', className)}
{...props}
/>
);
AlertDialogFooter.displayName = 'AlertDialogFooter';
const AlertDialogTitle = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Title>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Title>
>(({ className = '', ...props }, ref) => (
<AlertDialogPrimitive.Title
ref={ref}
className={cn('text-lg font-semibold text-gray-900', 'dark:text-gray-50', className)}
{...props}
/>
));
AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName;
const AlertDialogDescription = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Description>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Description>
>(({ className = '', ...props }, ref) => (
<AlertDialogPrimitive.Description
ref={ref}
className={cn('text-sm text-gray-500', 'dark:text-gray-400', className)}
{...props}
/>
));
AlertDialogDescription.displayName = AlertDialogPrimitive.Description.displayName;
const AlertDialogAction = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Action>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Action>
>(({ className = '', ...props }, ref) => (
<AlertDialogPrimitive.Action
ref={ref}
className={cn(
'inline-flex h-10 items-center justify-center rounded-md bg-gray-900 px-4 py-2 text-sm font-semibold text-white transition-colors hover:bg-gray-800 focus:outline-none focus:ring-2 focus:ring-gray-400 focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 dark:bg-gray-100 dark:text-gray-900 dark:hover:bg-gray-100 dark:focus:ring-gray-400 dark:focus:ring-offset-gray-900',
className,
)}
{...props}
/>
));
AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName;
const AlertDialogCancel = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Cancel>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Cancel>
>(({ className = '', ...props }, ref) => (
<AlertDialogPrimitive.Cancel
ref={ref}
className={cn(
'mt-2 inline-flex h-10 items-center justify-center rounded-md border border-gray-200 bg-transparent px-4 py-2 text-sm font-semibold text-gray-900 transition-colors hover:bg-gray-100 focus:outline-none focus:ring-2 focus:ring-gray-400 focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 dark:border-gray-700 dark:text-gray-100 dark:hover:bg-gray-800 dark:focus:ring-gray-400 dark:focus:ring-offset-gray-900 sm:mt-0',
className,
)}
{...props}
/>
));
AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName;
export {
AlertDialog,
AlertDialogTrigger,
AlertDialogContent,
AlertDialogHeader,
AlertDialogFooter,
AlertDialogTitle,
AlertDialogDescription,
AlertDialogAction,
AlertDialogCancel,
};

View file

@ -1,111 +0,0 @@
import React from 'react';
import { Search } from 'lucide-react';
import { cn } from '~/utils';
const AnimatedSearchInput = ({
value,
onChange,
isSearching: searching,
placeholder,
}: {
value?: string;
onChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
isSearching?: boolean;
placeholder: string;
}) => {
const isSearching = searching === true;
const hasValue = value != null && value.length > 0;
return (
<div className="relative w-full">
<div className="relative rounded-lg transition-all duration-500 ease-in-out">
<div className="relative">
{/* Icon on the left */}
<div className="absolute left-3 top-1/2 z-50 -translate-y-1/2">
<Search
className={cn(
`
h-4 w-4 transition-all duration-500 ease-in-out`,
isSearching && hasValue ? 'text-blue-400' : 'text-gray-400',
)}
/>
</div>
{/* Input field */}
<input
type="text"
value={value}
onChange={onChange}
placeholder={placeholder}
className={`
peer relative z-20 w-full rounded-lg bg-surface-secondary px-10
py-2 outline-none ring-0 backdrop-blur-sm transition-all
duration-500 ease-in-out placeholder:text-gray-400
focus:outline-none focus:ring-0
`}
/>
{/* Gradient overlay */}
<div
className={`
pointer-events-none absolute inset-0 z-20 rounded-lg
bg-gradient-to-r from-blue-500/20 via-purple-500/20 to-blue-500/20
transition-all duration-500 ease-in-out
${isSearching && hasValue ? 'opacity-100 blur-sm' : 'opacity-0 blur-none'}
`}
/>
{/* Animated loading indicator */}
<div
className={`
absolute right-3 top-1/2 z-20 -translate-y-1/2
transition-all duration-500 ease-in-out
${isSearching && hasValue ? 'scale-100 opacity-100' : 'scale-0 opacity-0'}
`}
>
<div className="relative h-2 w-2">
<div className="absolute inset-0 animate-ping rounded-full bg-blue-500/60" />
<div className="absolute inset-0 rounded-full bg-blue-500" />
</div>
</div>
</div>
</div>
{/* Outer glow effect */}
<div
className={`
absolute -inset-8 -z-10
transition-all duration-700 ease-in-out
${isSearching && hasValue ? 'scale-105 opacity-100' : 'scale-100 opacity-0'}
`}
>
<div className="absolute inset-0">
<div
className={`
bg-gradient-radial absolute inset-0 from-blue-500/10 to-transparent
transition-opacity duration-700 ease-in-out
${isSearching && hasValue ? 'animate-pulse-slow opacity-100' : 'opacity-0'}
`}
/>
<div
className={`
absolute inset-0 bg-gradient-to-r from-purple-500/5 via-blue-500/5 to-purple-500/5
blur-xl transition-all duration-700 ease-in-out
${isSearching && hasValue ? 'animate-gradient-x opacity-100' : 'opacity-0'}
`}
/>
</div>
</div>
<div
className={`
absolute inset-0 -z-20 scale-100 bg-gradient-to-r from-blue-500/10
via-purple-500/10 to-blue-500/10 opacity-0 blur-xl
transition-all duration-500 ease-in-out
peer-focus:scale-105 peer-focus:opacity-100
`}
/>
</div>
);
};
export default AnimatedSearchInput;

View file

@ -1,57 +0,0 @@
/* AnimatedTabs.css */
.animated-tab-panel {
transition-property: opacity, translate;
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
transition-duration: 300ms;
animation-duration: 300ms;
}
/* Sliding underline animation for tabs */
.animated-tab-list {
position: relative;
}
.animated-tab-list::after {
content: '';
position: absolute;
bottom: 0;
height: 2px;
background-color: currentColor; /* Inherit color from active tab */
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
left: var(--tab-left, 0);
width: var(--tab-width, 0);
}
.animated-tab {
position: relative;
}
.animated-tab[data-state="active"] {
border-bottom-color: transparent !important;
}
.animated-tab-panel[data-enter] {
opacity: 1 !important;
translate: 0 !important;
}
@media (prefers-reduced-motion: reduce) {
.animated-tab-panel {
transition: none;
}
}
.animated-tab-panel:not([data-open]) {
position: absolute;
top: 0px;
}
.animated-panels:has(> [data-was-open]) > .animated-tab-panel {
opacity: 0;
translate: -100%;
}
.animated-panels [data-was-open] ~ .animated-tab-panel,
.animated-panels [data-open] ~ .animated-tab-panel {
translate: 100%;
}

View file

@ -1,160 +0,0 @@
import * as Ariakit from '@ariakit/react';
import { ReactNode, forwardRef, useEffect, useRef } from 'react';
import type { ElementRef } from 'react';
import { cn } from '~/utils';
import './AnimatedTabs.css';
export interface TabItem {
id?: string;
label: ReactNode;
content: ReactNode;
disabled?: boolean;
}
export interface AnimatedTabsProps {
tabs: TabItem[];
className?: string;
tabListClassName?: string;
tabClassName?: string;
tabPanelClassName?: string;
tabListProps?: Ariakit.TabListProps;
containerClassName?: string;
defaultSelectedId?: string;
}
function usePrevious<T>(value: T) {
const ref = useRef<T>();
useEffect(() => {
ref.current = value;
}, [value]);
return ref.current;
}
const Tab = forwardRef<ElementRef<typeof Ariakit.Tab>, Ariakit.TabProps>(function Tab(props, ref) {
const tabRef = useRef<HTMLButtonElement | null>(null);
useEffect(() => {
const tabElement = tabRef.current;
if (!tabElement) return;
const updateState = () => {
const isSelected = tabElement.getAttribute('aria-selected') === 'true';
tabElement.setAttribute('data-state', isSelected ? 'active' : 'inactive');
};
updateState();
const observer = new MutationObserver(updateState);
observer.observe(tabElement, { attributes: true, attributeFilter: ['aria-selected'] });
return () => observer.disconnect();
}, []);
return (
<Ariakit.Tab
ref={(node) => {
// Forward the ref to both our local ref and the provided ref
tabRef.current = node;
if (typeof ref === 'function') ref(node);
else if (ref) ref.current = node;
}}
{...props}
className={`animated-tab aria-selected:text-token-text-primary flex select-none items-center justify-center gap-2 whitespace-nowrap border-none text-sm font-medium outline-none transition-colors aria-disabled:opacity-50 ${props.className || ''}`}
/>
);
});
const TabPanel = forwardRef<ElementRef<typeof Ariakit.TabPanel>, Ariakit.TabPanelProps>(
function TabPanel(props, ref) {
const tab = Ariakit.useTabContext();
const previousTabId = usePrevious(Ariakit.useStoreState(tab, 'selectedId'));
const wasOpen = props.tabId && previousTabId === props.tabId;
return (
<Ariakit.TabPanel
ref={ref}
{...props}
data-was-open={wasOpen || undefined}
className={`animated-tab-panel max-w-full ${props.className || ''}`}
/>
);
},
);
export function AnimatedTabs({
tabs,
className = '',
tabListClassName = '',
tabClassName = '',
tabPanelClassName = '',
containerClassName = '',
tabListProps = {},
defaultSelectedId,
}: AnimatedTabsProps) {
const tabIds = tabs.map((tab, index) => tab.id || `tab-${index}`);
const firstTabId = defaultSelectedId || tabIds[0];
const tabListRef = useRef<HTMLDivElement | null>(null);
useEffect(() => {
const tabList = tabListRef.current;
if (!tabList) return;
// Function to update the underline position
const updateUnderline = () => {
const activeTab = tabList.querySelector('[data-state="active"]') as HTMLElement;
if (!activeTab) return;
tabList.style.setProperty('--tab-left', `${activeTab.offsetLeft}px`);
tabList.style.setProperty('--tab-width', `${activeTab.offsetWidth}px`);
};
updateUnderline();
const observer = new MutationObserver(updateUnderline);
observer.observe(tabList, { attributes: true, subtree: true, attributeFilter: ['data-state'] });
return () => observer.disconnect();
}, [tabs]);
return (
<div className={`w-full ${className}`}>
<Ariakit.TabProvider defaultSelectedId={firstTabId}>
<Ariakit.TabList
ref={tabListRef}
aria-label="Tabs"
className={`animated-tab-list flex py-1 ${tabListClassName}`}
{...tabListProps}
>
{tabs.map((tab, index) => (
<Tab
key={tabIds[index]}
id={tabIds[index]}
disabled={tab.disabled}
className={tabClassName}
data-state={tabIds[index] === firstTabId ? 'active' : 'inactive'}
>
{tab.label}
</Tab>
))}
</Ariakit.TabList>
<div
className={cn(
'animated-panels relative flex w-full flex-col items-center overflow-hidden p-0',
containerClassName,
)}
>
{tabs.map((tab, index) => (
<TabPanel
key={`panel-${tabIds[index]}`}
id={`panel-${tabIds[index]}`}
tabId={tabIds[index]}
className={tabPanelClassName}
>
{tab.content}
</TabPanel>
))}
</div>
</Ariakit.TabProvider>
</div>
);
}

View file

@ -1,97 +0,0 @@
import type React from 'react';
import { X, Plus } from 'lucide-react';
import { motion } from 'framer-motion';
import type { ButtonHTMLAttributes } from 'react';
import type { LucideIcon } from 'lucide-react';
import { cn } from '~/utils';
interface BadgeProps extends ButtonHTMLAttributes<HTMLButtonElement> {
icon?: LucideIcon;
label: string;
id?: string;
isActive?: boolean;
isEditing?: boolean;
isDragging?: boolean;
isAvailable: boolean;
isInChat?: boolean;
onBadgeAction?: () => void;
onToggle?: () => void;
}
export default function Badge({
icon: Icon,
label,
id,
isActive = false,
isEditing = false,
isDragging = false,
isAvailable = true,
isInChat = false,
onBadgeAction,
onToggle,
className,
...props
}: BadgeProps) {
const isMoveable = isEditing && isAvailable;
const isDisabled = id === '1' && isInChat;
const handleClick: React.MouseEventHandler<HTMLButtonElement> = (e) => {
if (isDisabled) {
e.preventDefault();
e.stopPropagation();
return;
}
if (!isEditing && onToggle) {
e.preventDefault();
e.stopPropagation();
onToggle();
}
};
return (
<motion.button
onClick={handleClick}
className={cn(
'group relative inline-flex items-center gap-1.5 rounded-full px-4 py-1.5',
'border border-border-medium text-sm font-medium transition-shadow md:w-full',
'size-9 p-2 md:p-3',
isActive
? 'bg-surface-active shadow-md'
: 'bg-surface-chat shadow-sm hover:bg-surface-hover hover:shadow-md',
'active:scale-95 active:shadow-inner',
isMoveable && 'cursor-move',
isDisabled && 'cursor-not-allowed opacity-50 hover:shadow-sm',
className,
)}
animate={{
scale: isDragging ? 1.1 : 1,
boxShadow: isDragging ? '0 10px 25px rgba(0,0,0,0.1)' : undefined,
}}
whileTap={{ scale: isDragging ? 1.1 : isDisabled ? 1 : 0.97 }}
transition={{ type: 'tween', duration: 0.1, ease: 'easeOut' }}
{...props}
>
{Icon && <Icon className={cn('relative h-5 w-5 md:h-4 md:w-4', !label && 'mx-auto')} />}
<span className="relative hidden md:inline">{label}</span>
{isEditing && !isDragging && (
<motion.button
className="absolute -right-1 -top-1 flex h-6 w-6 items-center justify-center rounded-full bg-surface-secondary-alt text-text-primary shadow-sm md:h-5 md:w-5"
initial={{ opacity: 0, scale: 0.8 }}
animate={{ opacity: 1, scale: 1 }}
exit={{ opacity: 0, scale: 0.8 }}
whileTap={{ scale: 0.9 }}
onMouseDown={(e) => e.stopPropagation()}
onClick={(e) => {
e.stopPropagation();
onBadgeAction?.();
}}
>
{isAvailable ? <X className="h-3 w-3" /> : <Plus className="h-3 w-3" />}
</motion.button>
)}
</motion.button>
);
}

View file

@ -1,101 +0,0 @@
import * as React from 'react';
import { Slot } from '@radix-ui/react-slot';
import { ChevronRight, MoreHorizontal } from 'lucide-react';
import { cn } from '~/utils';
const Breadcrumb = React.forwardRef<
HTMLElement,
React.ComponentPropsWithoutRef<'nav'> & {
separator?: React.ReactNode;
}
>(({ ...props }, ref) => <nav ref={ref} aria-label="breadcrumb" {...props} />);
Breadcrumb.displayName = 'Breadcrumb';
const BreadcrumbList = React.forwardRef<HTMLOListElement, React.ComponentPropsWithoutRef<'ol'>>(
({ className, ...props }, ref) => (
<ol
ref={ref}
className={cn(
'text-muted-foreground flex flex-wrap items-center gap-1.5 break-words text-sm sm:gap-2.5',
className,
)}
{...props}
/>
),
);
BreadcrumbList.displayName = 'BreadcrumbList';
const BreadcrumbItem = React.forwardRef<HTMLLIElement, React.ComponentPropsWithoutRef<'li'>>(
({ className, ...props }, ref) => (
<li ref={ref} className={cn('inline-flex items-center gap-1.5', className)} {...props} />
),
);
BreadcrumbItem.displayName = 'BreadcrumbItem';
const BreadcrumbLink = React.forwardRef<
HTMLAnchorElement,
React.ComponentPropsWithoutRef<'a'> & {
asChild?: boolean;
}
>(({ asChild, className, ...props }, ref) => {
const Comp = asChild ? Slot : 'a';
return (
<Comp
ref={ref}
className={cn('hover:text-foreground transition-colors', className)}
{...props}
/>
);
});
BreadcrumbLink.displayName = 'BreadcrumbLink';
const BreadcrumbPage = React.forwardRef<HTMLSpanElement, React.ComponentPropsWithoutRef<'span'>>(
({ className, ...props }, ref) => (
<span
ref={ref}
role="link"
aria-disabled="true"
aria-current="page"
className={cn('text-foreground font-normal', className)}
{...props}
/>
),
);
BreadcrumbPage.displayName = 'BreadcrumbPage';
const BreadcrumbSeparator = ({ children, className, ...props }: React.ComponentProps<'li'>) => (
<li
role="presentation"
aria-hidden="true"
className={cn('[&>svg]:size-3.5', className)}
{...props}
>
{children ?? <ChevronRight />}
</li>
);
BreadcrumbSeparator.displayName = 'BreadcrumbSeparator';
const BreadcrumbEllipsis = ({ className, ...props }: React.ComponentProps<'span'>) => (
<span
role="presentation"
aria-hidden="true"
className={cn('flex h-9 w-9 items-center justify-center', className)}
{...props}
>
<MoreHorizontal className="h-4 w-4" />
<span className="sr-only">More</span>
</span>
);
BreadcrumbEllipsis.displayName = 'BreadcrumbElipssis';
export {
Breadcrumb,
BreadcrumbList,
BreadcrumbItem,
BreadcrumbLink,
BreadcrumbPage,
BreadcrumbSeparator,
BreadcrumbEllipsis,
};

View file

@ -1,52 +0,0 @@
import * as React from 'react';
import { Slot } from '@radix-ui/react-slot';
import { cva, type VariantProps } from 'class-variance-authority';
import { cn } from '~/utils';
const buttonVariants = cva(
'inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-lg text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50',
{
variants: {
variant: {
default: 'bg-primary text-primary-foreground hover:bg-primary/90',
destructive:
'bg-surface-destructive text-destructive-foreground hover:bg-surface-destructive-hover',
outline:
'text-text-primary border border-border-light bg-background hover:bg-accent hover:text-accent-foreground',
secondary: 'bg-secondary text-secondary-foreground hover:bg-secondary/80',
ghost: 'hover:bg-surface-hover hover:text-accent-foreground',
link: 'text-primary underline-offset-4 hover:underline',
// hardcoded text color because of WCAG contrast issues (text-white)
submit: 'bg-surface-submit text-white hover:bg-surface-submit-hover',
},
size: {
default: 'h-10 px-4 py-2',
sm: 'h-9 rounded-lg px-3',
lg: 'h-11 rounded-lg px-8',
icon: 'size-10',
},
},
defaultVariants: {
variant: 'default',
size: 'default',
},
},
);
export interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {
asChild?: boolean;
}
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant, size, asChild = false, ...props }, ref) => {
const Comp = asChild ? Slot : 'button';
return (
<Comp className={cn(buttonVariants({ variant, size, className }))} ref={ref} {...props} />
);
},
);
Button.displayName = 'Button';
export { Button, buttonVariants };

View file

@ -1,25 +0,0 @@
import * as React from 'react';
import * as CheckboxPrimitive from '@radix-ui/react-checkbox';
import { Check } from 'lucide-react';
import { cn } from '../../utils';
const Checkbox = React.forwardRef<
React.ElementRef<typeof CheckboxPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof CheckboxPrimitive.Root>
>(({ className = '', ...props }, ref) => (
<CheckboxPrimitive.Root
ref={ref}
className={cn(
'peer h-4 w-4 shrink-0 rounded-sm border border-primary ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground',
className,
)}
{...props}
>
<CheckboxPrimitive.Indicator className={cn('flex items-center justify-center')}>
<Check className="h-4 w-4" />
</CheckboxPrimitive.Indicator>
</CheckboxPrimitive.Root>
));
Checkbox.displayName = CheckboxPrimitive.Root.displayName;
export { Checkbox };

View file

@ -1,76 +0,0 @@
import { useEffect } from 'react';
import { Checkbox, useStoreState, useCheckboxStore } from '@ariakit/react';
import { cn } from '~/utils';
import * as React from 'react';
const CheckboxButton = React.forwardRef<
HTMLInputElement,
{
icon?: React.ReactNode;
label: string;
className?: string;
checked?: boolean;
defaultChecked?: boolean;
isCheckedClassName?: string;
setValue?: (values: {
e?: React.ChangeEvent<HTMLInputElement>;
value: boolean | string;
}) => void;
}
>(({ icon, label, setValue, className, checked, defaultChecked, isCheckedClassName }, ref) => {
const checkbox = useCheckboxStore();
const isChecked = useStoreState(checkbox, (state) => state?.value);
const onChange = (e: React.ChangeEvent<HTMLInputElement>) => {
e.stopPropagation();
if (typeof isChecked !== 'boolean') {
return;
}
setValue?.({ e, value: !isChecked });
};
// Sync with controlled checked prop
useEffect(() => {
if (checked !== undefined) {
checkbox.setValue(checked);
}
}, [checked, checkbox]);
// Set initial value from defaultChecked
useEffect(() => {
if (defaultChecked !== undefined && checked === undefined) {
checkbox.setValue(defaultChecked);
}
}, [defaultChecked, checked, checkbox]);
return (
<Checkbox
ref={ref}
store={checkbox}
onChange={onChange}
className={cn(
// Base styling from MultiSelect's selectClassName
'group relative inline-flex items-center justify-center gap-1.5',
'rounded-full border border-border-medium text-sm font-medium',
'size-9 p-2 transition-all md:w-full md:p-3',
'bg-transparent shadow-sm hover:bg-surface-hover hover:shadow-md active:shadow-inner',
// Checked state styling
isChecked && isCheckedClassName && isCheckedClassName,
// Additional custom classes
className,
)}
render={<button type="button" aria-label={label} />}
>
{/* Icon if provided */}
{icon && <span className="icon-md text-text-primary">{icon}</span>}
{/* Show the label on larger screens */}
<span className="hidden truncate md:block">{label}</span>
</Checkbox>
);
});
CheckboxButton.displayName = 'CheckboxButton';
export default CheckboxButton;

View file

@ -1,9 +0,0 @@
import * as CollapsiblePrimitive from '@radix-ui/react-collapsible';
const Collapsible = CollapsiblePrimitive.Root;
const CollapsibleTrigger = CollapsiblePrimitive.CollapsibleTrigger;
const CollapsibleContent = CollapsiblePrimitive.CollapsibleContent;
export { Collapsible, CollapsibleTrigger, CollapsibleContent };

View file

@ -1,176 +0,0 @@
import { startTransition } from 'react';
import { Search as SearchIcon } from 'lucide-react';
import * as RadixSelect from '@radix-ui/react-select';
import { CheckIcon, ChevronDownIcon } from '@radix-ui/react-icons';
import {
Combobox,
ComboboxItem,
ComboboxList,
ComboboxProvider,
ComboboxCancel,
} from '@ariakit/react';
import type { OptionWithIcon } from '~/common';
import { SelectTrigger, SelectValue, SelectScrollDownButton } from './Select';
import useCombobox from '~/hooks/Input/useCombobox';
import { cn } from '~/utils';
export default function ComboboxComponent({
selectedValue,
displayValue,
items,
setValue,
ariaLabel,
searchPlaceholder,
selectPlaceholder,
isCollapsed,
SelectIcon,
}: {
ariaLabel: string;
displayValue?: string;
selectedValue: string;
searchPlaceholder?: string;
selectPlaceholder?: string;
items: OptionWithIcon[] | string[];
setValue: (value: string) => void;
isCollapsed: boolean;
SelectIcon?: React.ReactNode;
}) {
const options: OptionWithIcon[] = (items ?? []).map((option: string | OptionWithIcon) => {
if (typeof option === 'string') {
return { label: option, value: option };
}
return option;
});
const { open, setOpen, setSearchValue, matches } = useCombobox({
value: selectedValue,
options,
});
return (
<RadixSelect.Root
value={selectedValue}
onValueChange={setValue}
open={open}
/** Hacky fix for radix-ui Android issue: https://github.com/radix-ui/primitives/issues/1658 */
onOpenChange={() => {
if (open === true) {
setOpen(false);
return;
}
setTimeout(() => {
setOpen(!open);
}, 75);
}}
>
<ComboboxProvider
open={open}
setOpen={setOpen}
resetValueOnHide
includesBaseElement={false}
setValue={(value) => {
startTransition(() => {
setSearchValue(value);
});
}}
>
<SelectTrigger
aria-label={ariaLabel}
className={cn(
'flex items-center gap-2 [&>span]:line-clamp-1 [&>span]:flex [&>span]:w-full [&>span]:items-center [&>span]:gap-1 [&>span]:truncate [&_svg]:h-4 [&_svg]:w-4 [&_svg]:shrink-0',
isCollapsed
? 'flex h-9 w-9 shrink-0 items-center justify-center p-0 [&>span]:w-auto [&>svg]:hidden'
: '',
'bg-white text-black hover:bg-gray-50 focus-visible:ring-2 focus-visible:ring-gray-500 dark:bg-gray-850 dark:text-white ',
)}
>
<SelectValue placeholder={selectPlaceholder}>
<div className="assistant-item flex items-center justify-center overflow-hidden rounded-full">
{SelectIcon ? SelectIcon : <ChevronDownIcon />}
</div>
<span
className={cn('ml-2', isCollapsed ? 'hidden' : '')}
style={{ userSelect: 'none' }}
>
{selectedValue
? displayValue ?? selectedValue
: selectPlaceholder && selectPlaceholder}
</span>
</SelectValue>
</SelectTrigger>
<RadixSelect.Portal>
<RadixSelect.Content
role="dialog"
aria-label={ariaLabel + 's'}
position="popper"
className={cn(
'bg-popover text-popover-foreground relative z-50 max-h-[52vh] min-w-[8rem] overflow-hidden rounded-md border border-gray-200 shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 dark:border-gray-600',
'data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1',
'bg-white dark:bg-gray-700',
)}
>
<RadixSelect.Viewport className="mb-5 h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]">
<div className="group sticky left-0 top-0 z-10 flex h-12 items-center gap-2 bg-white px-2 px-3 py-2 text-black duration-300 dark:bg-gray-700 dark:text-white">
<SearchIcon className="h-4 w-4 text-gray-500 transition-colors duration-300 dark:group-focus-within:text-gray-300 dark:group-hover:text-gray-300" />
<Combobox
autoSelect
placeholder={searchPlaceholder}
className="flex-1 rounded-md border-none bg-transparent px-2.5 py-2 text-sm focus:outline-none focus:ring-1 focus:ring-gray-700/10 dark:focus:ring-gray-200/10"
// Ariakit's Combobox manually triggers a blur event on virtually
// blurred items, making them work as if they had actual DOM
// focus. These blur events might happen after the corresponding
// focus events in the capture phase, leading Radix Select to
// close the popover. This happens because Radix Select relies on
// the order of these captured events to discern if the focus was
// outside the element. Since we don't have access to the
// onInteractOutside prop in the Radix SelectContent component to
// stop this behavior, we can turn off Ariakit's behavior here.
onBlurCapture={(event) => {
event.preventDefault();
event.stopPropagation();
}}
/>
<ComboboxCancel
hideWhenEmpty={true}
className="relative flex h-5 w-5 items-center justify-end text-gray-500 transition-colors duration-300 dark:group-focus-within:text-gray-300 dark:group-hover:text-gray-300"
/>
</div>
<ComboboxList className="overflow-y-auto p-1 py-2">
{matches.map(({ label, value, icon }) => (
<RadixSelect.Item key={value} value={`${value ?? ''}`} asChild>
<ComboboxItem
className={cn(
'focus:bg-accent focus:text-accent-foreground relative flex w-full cursor-pointer select-none items-center rounded-sm py-1.5 pl-2 pr-8 text-sm outline-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
'rounded-lg hover:bg-gray-100/50 hover:bg-gray-50 dark:text-white dark:hover:bg-gray-600',
)}
/** Hacky fix for radix-ui Android issue: https://github.com/radix-ui/primitives/issues/1658 */
onTouchEnd={() => {
setValue(`${value ?? ''}`);
setOpen(false);
}}
>
<span className="absolute right-2 flex h-3.5 w-3.5 items-center justify-center">
<RadixSelect.ItemIndicator>
<CheckIcon className="h-4 w-4" />
</RadixSelect.ItemIndicator>
</span>
<RadixSelect.ItemText>
<div className="[&_svg]:text-foreground flex items-center justify-center gap-3 dark:text-white [&_svg]:h-4 [&_svg]:w-4 [&_svg]:shrink-0">
<div className="assistant-item overflow-hidden rounded-full ">
{icon && icon}
</div>
{label}
</div>
</RadixSelect.ItemText>
</ComboboxItem>
</RadixSelect.Item>
))}
</ComboboxList>
</RadixSelect.Viewport>
<SelectScrollDownButton className="absolute bottom-0 left-0 right-0" />
</RadixSelect.Content>
</RadixSelect.Portal>
</ComboboxProvider>
</RadixSelect.Root>
);
}

View file

@ -1,182 +0,0 @@
import * as Ariakit from '@ariakit/react';
import { matchSorter } from 'match-sorter';
import { Search, ChevronDown } from 'lucide-react';
import { useMemo, useState, useRef, memo, useEffect } from 'react';
import { SelectRenderer } from '@ariakit/react-core/select/select-renderer';
import type { OptionWithIcon } from '~/common';
import { cn } from '~/utils';
interface ControlComboboxProps {
selectedValue: string;
displayValue?: string;
items: OptionWithIcon[];
setValue: (value: string) => void;
ariaLabel: string;
searchPlaceholder?: string;
selectPlaceholder?: string;
isCollapsed: boolean;
SelectIcon?: React.ReactNode;
containerClassName?: string;
iconClassName?: string;
showCarat?: boolean;
className?: string;
disabled?: boolean;
iconSide?: 'left' | 'right';
selectId?: string;
}
const ROW_HEIGHT = 36;
function ControlCombobox({
selectedValue,
displayValue,
items,
setValue,
ariaLabel,
searchPlaceholder,
selectPlaceholder,
containerClassName,
isCollapsed,
SelectIcon,
showCarat,
className,
disabled,
iconClassName,
iconSide = 'left',
selectId,
}: ControlComboboxProps) {
const [searchValue, setSearchValue] = useState('');
const buttonRef = useRef<HTMLButtonElement>(null);
const [buttonWidth, setButtonWidth] = useState<number | null>(null);
const getItem = (option: OptionWithIcon) => ({
id: `item-${option.value}`,
value: option.value as string | undefined,
label: option.label,
icon: option.icon,
});
const combobox = Ariakit.useComboboxStore({
defaultItems: items.map(getItem),
resetValueOnHide: true,
value: searchValue,
setValue: setSearchValue,
});
const select = Ariakit.useSelectStore({
combobox,
defaultItems: items.map(getItem),
value: selectedValue,
setValue,
});
const matches = useMemo(() => {
const filteredItems = matchSorter(items, searchValue, {
keys: ['value', 'label'],
baseSort: (a, b) => (a.index < b.index ? -1 : 1),
});
return filteredItems.map(getItem);
}, [searchValue, items]);
useEffect(() => {
if (buttonRef.current && !isCollapsed) {
setButtonWidth(buttonRef.current.offsetWidth);
}
}, [isCollapsed]);
const selectIconClassName = cn(
'flex h-5 w-5 items-center justify-center overflow-hidden rounded-full',
iconClassName,
);
const optionIconClassName = cn(
'mr-2 flex h-5 w-5 items-center justify-center overflow-hidden rounded-full',
iconClassName,
);
return (
<div className={cn('flex w-full items-center justify-center px-1', containerClassName)}>
<Ariakit.SelectLabel store={select} className="sr-only">
{ariaLabel}
</Ariakit.SelectLabel>
<Ariakit.Select
ref={buttonRef}
store={select}
id={selectId}
disabled={disabled}
className={cn(
'flex items-center justify-center gap-2 rounded-full bg-surface-secondary',
'text-text-primary hover:bg-surface-tertiary',
'border border-border-light',
isCollapsed ? 'h-10 w-10' : 'h-10 w-full rounded-xl px-3 py-2 text-sm',
className,
)}
>
{SelectIcon != null && iconSide === 'left' && (
<div className={selectIconClassName}>{SelectIcon}</div>
)}
{!isCollapsed && (
<>
<span className="flex-grow truncate text-left">
{displayValue != null
? displayValue || selectPlaceholder
: selectedValue || selectPlaceholder}
</span>
{SelectIcon != null && iconSide === 'right' && (
<div className={selectIconClassName}>{SelectIcon}</div>
)}
{showCarat && <ChevronDown className="h-4 w-4 text-text-secondary" />}
</>
)}
</Ariakit.Select>
<Ariakit.SelectPopover
store={select}
gutter={4}
portal
className={cn(
'animate-popover z-50 overflow-hidden rounded-xl border border-border-light bg-surface-secondary shadow-lg',
)}
style={{ width: isCollapsed ? '300px' : (buttonWidth ?? '300px') }}
>
<div className="py-1.5">
<div className="relative">
<Search className="absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-text-primary" />
<Ariakit.Combobox
store={combobox}
autoSelect
placeholder={searchPlaceholder}
className="w-full rounded-md bg-surface-secondary py-2 pl-9 pr-3 text-sm text-text-primary focus:outline-none"
/>
</div>
</div>
<div className="max-h-[300px] overflow-auto">
<Ariakit.ComboboxList store={combobox}>
<SelectRenderer store={select} items={matches} itemSize={ROW_HEIGHT} overscan={5}>
{({ value, icon, label, ...item }) => (
<Ariakit.ComboboxItem
key={item.id}
{...item}
className={cn(
'flex w-full cursor-pointer items-center px-3 text-sm',
'text-text-primary hover:bg-surface-tertiary',
'data-[active-item]:bg-surface-tertiary',
)}
render={<Ariakit.SelectItem value={value} />}
>
{icon != null && iconSide === 'left' && (
<div className={optionIconClassName}>{icon}</div>
)}
<span className="flex-grow truncate text-left">{label}</span>
{icon != null && iconSide === 'right' && (
<div className={optionIconClassName}>{icon}</div>
)}
</Ariakit.ComboboxItem>
)}
</SelectRenderer>
</Ariakit.ComboboxList>
</div>
</Ariakit.SelectPopover>
</div>
);
}
export default memo(ControlCombobox);

View file

@ -1,481 +0,0 @@
import React, { useCallback, useEffect, useRef, useState, memo, useMemo } from 'react';
import { useRecoilValue } from 'recoil';
import { useVirtualizer } from '@tanstack/react-virtual';
import {
Row,
ColumnDef,
flexRender,
SortingState,
useReactTable,
getCoreRowModel,
VisibilityState,
getSortedRowModel,
ColumnFiltersState,
getFilteredRowModel,
} from '@tanstack/react-table';
import type { Table as TTable } from '@tanstack/react-table';
import {
Button,
Table,
Checkbox,
TableRow,
TableBody,
TableCell,
TableHead,
TableHeader,
AnimatedSearchInput,
Skeleton,
} from './';
import { TrashIcon, Spinner } from '~/components/svg';
import { useLocalize, useMediaQuery } from '~/hooks';
import { LocalizeFunction } from '~/common';
import { cn } from '~/utils';
import store from '~/store';
type TableColumn<TData, TValue> = ColumnDef<TData, TValue> & {
meta?: {
size?: string | number;
mobileSize?: string | number;
minWidth?: string | number;
};
};
const SelectionCheckbox = memo(
({
checked,
onChange,
ariaLabel,
}: {
checked: boolean;
onChange: (value: boolean) => void;
ariaLabel: string;
}) => (
<div
role="button"
tabIndex={0}
onKeyDown={(e) => e.stopPropagation()}
className="flex h-full w-[30px] items-center justify-center"
onClick={(e) => e.stopPropagation()}
>
<Checkbox checked={checked} onCheckedChange={onChange} aria-label={ariaLabel} />
</div>
),
);
SelectionCheckbox.displayName = 'SelectionCheckbox';
interface DataTableProps<TData, TValue> {
columns: TableColumn<TData, TValue>[];
data: TData[];
onDelete?: (selectedRows: TData[]) => Promise<void>;
filterColumn?: string;
defaultSort?: SortingState;
columnVisibilityMap?: Record<string, string>;
className?: string;
pageSize?: number;
isFetchingNextPage?: boolean;
hasNextPage?: boolean;
fetchNextPage?: (options?: unknown) => Promise<unknown>;
enableRowSelection?: boolean;
showCheckboxes?: boolean;
onFilterChange?: (value: string) => void;
filterValue?: string;
isLoading?: boolean;
}
const TableRowComponent = <TData, TValue>({
row,
isSmallScreen,
onSelectionChange,
index,
isSearching,
}: {
row: Row<TData>;
isSmallScreen: boolean;
onSelectionChange?: (rowId: string, selected: boolean) => void;
index: number;
isSearching: boolean;
}) => {
const handleSelection = useCallback(
(value: boolean) => {
row.toggleSelected(value);
onSelectionChange?.(row.id, value);
},
[row, onSelectionChange],
);
return (
<TableRow
data-state={row.getIsSelected() ? 'selected' : undefined}
className="motion-safe:animate-fadeIn border-b border-border-light transition-all duration-300 ease-out hover:bg-surface-secondary"
style={{
animationDelay: `${index * 20}ms`,
transform: `translateY(${isSearching ? '4px' : '0'})`,
opacity: isSearching ? 0.5 : 1,
}}
>
{row.getVisibleCells().map((cell) => {
if (cell.column.id === 'select') {
return (
<TableCell key={cell.id} className="px-2 py-1 transition-all duration-300">
<SelectionCheckbox
checked={row.getIsSelected()}
onChange={handleSelection}
ariaLabel="Select row"
/>
</TableCell>
);
}
return (
<TableCell
key={cell.id}
className="w-0 max-w-0 px-2 py-1 align-middle text-xs transition-all duration-300 sm:px-4 sm:py-2 sm:text-sm"
style={getColumnStyle(
cell.column.columnDef as TableColumn<TData, TValue>,
isSmallScreen,
)}
>
<div className="overflow-hidden text-ellipsis">
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</div>
</TableCell>
);
})}
</TableRow>
);
};
const MemoizedTableRow = memo(TableRowComponent) as typeof TableRowComponent;
function getColumnStyle<TData, TValue>(
column: TableColumn<TData, TValue>,
isSmallScreen: boolean,
): React.CSSProperties {
return {
width: isSmallScreen ? column.meta?.mobileSize : column.meta?.size,
minWidth: column.meta?.minWidth,
maxWidth: column.meta?.size,
};
}
const DeleteButton = memo(
({
onDelete,
isDeleting,
disabled,
isSmallScreen,
localize,
}: {
onDelete?: () => Promise<void>;
isDeleting: boolean;
disabled: boolean;
isSmallScreen: boolean;
localize: LocalizeFunction;
}) => {
if (!onDelete) {
return null;
}
return (
<Button
variant="outline"
onClick={onDelete}
disabled={disabled}
className={cn('min-w-[40px] transition-all duration-200', isSmallScreen && 'px-2 py-1')}
>
{isDeleting ? (
<Spinner className="size-4" />
) : (
<>
<TrashIcon className="size-3.5 text-red-400 sm:size-4" />
{!isSmallScreen && <span className="ml-2">{localize('com_ui_delete')}</span>}
</>
)}
</Button>
);
},
);
export default function DataTable<TData, TValue>({
columns,
data,
onDelete,
filterColumn,
defaultSort = [],
className = '',
isFetchingNextPage = false,
hasNextPage = false,
fetchNextPage,
enableRowSelection = true,
showCheckboxes = true,
onFilterChange,
filterValue,
isLoading,
}: DataTableProps<TData, TValue>) {
const localize = useLocalize();
const isSmallScreen = useMediaQuery('(max-width: 768px)');
const tableContainerRef = useRef<HTMLDivElement>(null);
const search = useRecoilValue(store.search);
const [isDeleting, setIsDeleting] = useState(false);
const [rowSelection, setRowSelection] = useState<Record<string, boolean>>({});
const [sorting, setSorting] = useState<SortingState>(defaultSort);
const [columnFilters, setColumnFilters] = useState<ColumnFiltersState>([]);
const [columnVisibility, setColumnVisibility] = useState<VisibilityState>({});
const [searchTerm, setSearchTerm] = useState(filterValue ?? '');
const [isSearching, setIsSearching] = useState(false);
const tableColumns = useMemo(() => {
if (!enableRowSelection || !showCheckboxes) {
return columns;
}
const selectColumn = {
id: 'select',
header: ({ table }: { table: TTable<TData> }) => (
<div className="flex h-full w-[30px] items-center justify-center">
<Checkbox
checked={table.getIsAllPageRowsSelected()}
onCheckedChange={(value) => table.toggleAllPageRowsSelected(Boolean(value))}
aria-label="Select all"
/>
</div>
),
cell: ({ row }: { row: Row<TData> }) => (
<SelectionCheckbox
checked={row.getIsSelected()}
onChange={(value) => row.toggleSelected(value)}
ariaLabel="Select row"
/>
),
meta: { size: '50px' },
};
return [selectColumn, ...columns];
}, [columns, enableRowSelection, showCheckboxes]);
const table = useReactTable({
data,
columns: tableColumns,
getCoreRowModel: getCoreRowModel(),
getSortedRowModel: getSortedRowModel(),
getFilteredRowModel: getFilteredRowModel(),
enableRowSelection,
enableMultiRowSelection: true,
state: {
sorting,
columnFilters,
columnVisibility,
rowSelection,
},
onSortingChange: setSorting,
onColumnFiltersChange: setColumnFilters,
onColumnVisibilityChange: setColumnVisibility,
onRowSelectionChange: setRowSelection,
});
const { rows } = table.getRowModel();
const rowVirtualizer = useVirtualizer({
count: rows.length,
getScrollElement: () => tableContainerRef.current,
estimateSize: useCallback(() => 48, []),
overscan: 10,
});
const virtualRows = rowVirtualizer.getVirtualItems();
const totalSize = rowVirtualizer.getTotalSize();
const paddingTop = virtualRows.length > 0 ? virtualRows[0].start : 0;
const paddingBottom =
virtualRows.length > 0 ? totalSize - virtualRows[virtualRows.length - 1].end : 0;
useEffect(() => {
const scrollElement = tableContainerRef.current;
if (!scrollElement) {
return;
}
const handleScroll = async () => {
if (!hasNextPage || isFetchingNextPage) {
return;
}
const { scrollTop, scrollHeight, clientHeight } = scrollElement;
if (scrollHeight - scrollTop <= clientHeight * 1.5) {
try {
// Safely fetch next page without breaking if lastPage is undefined
await fetchNextPage?.();
} catch (error) {
console.error('Unable to fetch next page:', error);
}
}
};
scrollElement.addEventListener('scroll', handleScroll, { passive: true });
return () => scrollElement.removeEventListener('scroll', handleScroll);
}, [hasNextPage, isFetchingNextPage, fetchNextPage]);
useEffect(() => {
setIsSearching(true);
const timeout = setTimeout(() => {
onFilterChange?.(searchTerm);
setIsSearching(false);
}, 300);
return () => clearTimeout(timeout);
}, [searchTerm, onFilterChange]);
const handleDelete = useCallback(async () => {
if (!onDelete) {
return;
}
setIsDeleting(true);
try {
const itemsToDelete = table.getFilteredSelectedRowModel().rows.map((r) => r.original);
await onDelete(itemsToDelete);
setRowSelection({});
} finally {
setIsDeleting(false);
}
}, [onDelete, table]);
const getRandomWidth = () => Math.floor(Math.random() * (410 - 170 + 1)) + 170;
const skeletons = Array.from({ length: 13 }, (_, index) => {
const randomWidth = getRandomWidth();
const firstDataColumnIndex = tableColumns[0]?.id === 'select' ? 1 : 0;
return (
<TableRow key={index} className="motion-safe:animate-fadeIn border-b border-border-light">
{tableColumns.map((column, columnIndex) => {
const style = getColumnStyle(column as TableColumn<TData, TValue>, isSmallScreen);
const isFirstDataColumn = columnIndex === firstDataColumnIndex;
return (
<TableCell key={column.id} className="px-2 py-1 sm:px-4 sm:py-2" style={style}>
<Skeleton
className="h-6"
style={isFirstDataColumn ? { width: `${randomWidth}px` } : { width: '100%' }}
/>
</TableCell>
);
})}
</TableRow>
);
});
return (
<div className={cn('flex h-full flex-col gap-4', className)}>
{/* Table controls */}
<div className="flex flex-wrap items-center gap-2 sm:gap-4">
{enableRowSelection && showCheckboxes && (
<DeleteButton
onDelete={handleDelete}
isDeleting={isDeleting}
disabled={!table.getFilteredSelectedRowModel().rows.length || isDeleting}
isSmallScreen={isSmallScreen}
localize={localize}
/>
)}
{filterColumn !== undefined && table.getColumn(filterColumn) && search.enabled && (
<div className="relative flex-1">
<AnimatedSearchInput
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
isSearching={isSearching}
placeholder={`${localize('com_ui_search')}...`}
/>
</div>
)}
</div>
{/* Virtualized table */}
<div
ref={tableContainerRef}
className={cn(
'relative h-[calc(100vh-20rem)] max-w-full overflow-x-auto overflow-y-auto rounded-md border border-black/10 dark:border-white/10',
'transition-all duration-300 ease-out',
isSearching && 'bg-surface-secondary/50',
className,
)}
>
<Table className="w-full min-w-[300px] table-fixed border-separate border-spacing-0">
<TableHeader className="sticky top-0 z-50 bg-surface-secondary">
{table.getHeaderGroups().map((headerGroup) => (
<TableRow key={headerGroup.id} className="border-b border-border-light">
{headerGroup.headers.map((header) => (
<TableHead
key={header.id}
className="whitespace-nowrap bg-surface-secondary px-2 py-2 text-left text-sm font-medium text-text-secondary sm:px-4"
style={getColumnStyle(
header.column.columnDef as TableColumn<TData, TValue>,
isSmallScreen,
)}
onClick={
header.column.getCanSort()
? header.column.getToggleSortingHandler()
: undefined
}
>
{header.isPlaceholder
? null
: flexRender(header.column.columnDef.header, header.getContext())}
</TableHead>
))}
</TableRow>
))}
</TableHeader>
<TableBody>
{paddingTop > 0 && (
<tr>
<td style={{ height: `${paddingTop}px` }} />
</tr>
)}
{isLoading && skeletons}
{virtualRows.map((virtualRow) => {
const row = rows[virtualRow.index];
return (
<MemoizedTableRow
key={row.id}
row={row}
isSmallScreen={isSmallScreen}
index={virtualRow.index}
isSearching={isSearching}
/>
);
})}
{!virtualRows.length && (
<TableRow className="hover:bg-transparent">
<TableCell colSpan={columns.length} className="p-4 text-center">
{localize('com_ui_no_data')}
</TableCell>
</TableRow>
)}
{paddingBottom > 0 && (
<tr>
<td style={{ height: `${paddingBottom}px` }} />
</tr>
)}
{/* Loading indicator */}
{(isFetchingNextPage || hasNextPage) && (
<TableRow className="hover:bg-transparent">
<TableCell colSpan={columns.length} className="p-4">
<div className="flex h-full items-center justify-center">
{isFetchingNextPage ? (
<Spinner className="size-4" />
) : (
hasNextPage && <div className="h-6" />
)}
</div>
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
</div>
</div>
);
}

View file

@ -1,61 +0,0 @@
import { ArrowDownIcon, ArrowUpIcon, CaretSortIcon, EyeNoneIcon } from '@radix-ui/react-icons';
import { Column } from '@tanstack/react-table';
import { cn } from '~/utils';
import { Button } from './Button';
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from './DropdownMenu';
interface DataTableColumnHeaderProps<TData, TValue> extends React.HTMLAttributes<HTMLDivElement> {
column: Column<TData, TValue>;
title: string;
}
export function DataTableColumnHeader<TData, TValue>({
column,
title,
className = '',
}: DataTableColumnHeaderProps<TData, TValue>) {
if (!column.getCanSort()) {
return <div className={cn(className)}>{title}</div>;
}
return (
<div className={cn('flex items-center space-x-2', className)}>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" size="sm" className="data-[state=open]:bg-accent -ml-3 h-8">
<span>{title}</span>
{column.getIsSorted() === 'desc' ? (
<ArrowDownIcon className="ml-2 h-4 w-4" />
) : column.getIsSorted() === 'asc' ? (
<ArrowUpIcon className="ml-2 h-4 w-4" />
) : (
<CaretSortIcon className="ml-2 h-4 w-4" />
)}
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="start" className="z-[1001]">
<DropdownMenuItem onClick={() => column.toggleSorting(false)}>
<ArrowUpIcon className="text-muted-foreground/70 mr-2 h-3.5 w-3.5" />
Asc
</DropdownMenuItem>
<DropdownMenuItem onClick={() => column.toggleSorting(true)}>
<ArrowDownIcon className="text-muted-foreground/70 mr-2 h-3.5 w-3.5" />
Desc
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem onClick={() => column.toggleVisibility(false)}>
<EyeNoneIcon className="text-muted-foreground/70 mr-2 h-3.5 w-3.5" />
Hide
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
);
}

View file

@ -1,5 +0,0 @@
import { useDelayedRender } from '~/hooks';
const DelayedRender = ({ delay, children }) => useDelayedRender(delay)(() => children);
export default DelayedRender;

View file

@ -1,170 +0,0 @@
import * as React from 'react';
import * as DialogPrimitive from '@radix-ui/react-dialog';
import { Button } from '../ui/Button';
import { X } from 'lucide-react';
import { cn } from '~/utils';
import { useMediaQuery } from '~/hooks';
const Dialog = DialogPrimitive.Root;
const DialogTrigger = DialogPrimitive.Trigger;
type DialogPortalProps = DialogPrimitive.DialogPortalProps & { className?: string };
const DialogPortal = ({ className = '', children, ...props }: DialogPortalProps) => (
<DialogPrimitive.Portal className={cn(className)} {...(props as DialogPortalProps)}>
<div className="fixed inset-0 z-[999] flex items-start justify-center sm:items-center">
{children}
</div>
</DialogPrimitive.Portal>
);
DialogPortal.displayName = DialogPrimitive.Portal.displayName;
const DialogOverlay = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Overlay>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Overlay
className={cn(
'fixed inset-0 z-[999] bg-gray-600/65 transition-all duration-100 data-[state=closed]:animate-out data-[state=closed]:fade-out data-[state=open]:fade-in dark:bg-black/80',
className ?? '',
)}
{...props}
ref={ref}
/>
));
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName;
type DialogContentProps = React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content> & {
showCloseButton?: boolean;
disableScroll?: boolean;
};
const DialogContent = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Content>,
DialogContentProps
>(
(
{ className, children = true, showCloseButton = true, disableScroll = false, ...props },
ref,
) => {
const isSmallScreen = useMediaQuery('(max-width: 768px)');
return (
<DialogPortal>
<DialogOverlay />
<DialogPrimitive.Content
ref={ref}
className={cn(
'fixed z-[999] grid w-full gap-4 rounded-b-lg bg-white pb-6 animate-in data-[state=open]:fade-in-90 data-[state=open]:slide-in-from-bottom-10 sm:rounded-lg',
'dark:bg-gray-700',
isSmallScreen
? 'fixed left-1/2 top-1/2 z-[999] m-auto grid w-11/12 -translate-x-1/2 -translate-y-1/2 gap-4 rounded-xl bg-white pb-6'
: '',
disableScroll ? 'overflow-hidden' : '',
className ?? '',
)}
{...props}
>
{children}
{showCloseButton && (
<DialogPrimitive.Close className="absolute right-6 top-[1.6rem] rounded-sm opacity-70 transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-gray-400 focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-gray-100 dark:focus:ring-white dark:focus:ring-offset-gray-700 dark:data-[state=open]:bg-gray-800">
<X className="h-5 w-5 text-black dark:text-white" />
<span className="sr-only">Close</span>
</DialogPrimitive.Close>
)}
</DialogPrimitive.Content>
</DialogPortal>
);
},
);
DialogContent.displayName = DialogPrimitive.Content.displayName;
const DialogHeader = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
'flex flex-col space-y-2 border-b border-black/10 p-6 pb-4 text-left dark:border-white/10',
className ?? '',
)}
{...props}
/>
);
DialogHeader.displayName = 'DialogHeader';
const DialogFooter = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn('flex flex-row justify-between space-x-2 px-6 py-4', className ?? '')}
{...props}
/>
);
DialogFooter.displayName = 'DialogFooter';
const DialogTitle = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Title>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Title
ref={ref}
className={cn('text-lg font-semibold text-gray-900', 'dark:text-gray-50', className ?? '')}
{...props}
/>
));
DialogTitle.displayName = DialogPrimitive.Title.displayName;
const DialogDescription = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Description>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Description
ref={ref}
className={cn('text-sm text-gray-500', 'dark:text-gray-400', className ?? '')}
{...props}
/>
));
DialogDescription.displayName = DialogPrimitive.Description.displayName;
const DialogClose = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Close>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Close>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Close
ref={ref}
className={cn(
'mt-2 inline-flex h-10 items-center justify-center rounded-lg border border-gray-200 bg-transparent px-4 py-2 text-sm font-semibold text-gray-900 transition-colors hover:bg-gray-100 focus:outline-none disabled:cursor-not-allowed disabled:opacity-50 dark:border-gray-700 dark:text-gray-100 dark:hover:bg-gray-800 sm:mt-0',
className ?? '',
/* Important: for accessibility */
'focus:ring-2 focus:ring-gray-400 focus:ring-offset-2 dark:focus:ring-gray-400 dark:focus:ring-offset-gray-900',
)}
{...props}
/>
));
DialogClose.displayName = DialogPrimitive.Title.displayName;
const DialogButton = React.forwardRef<
React.ElementRef<typeof Button>,
React.ComponentPropsWithoutRef<typeof Button>
>(({ className, ...props }, ref) => (
<Button
ref={ref}
variant="outline"
className={cn(
'mt-2 inline-flex h-10 items-center justify-center rounded-lg border border-gray-200 bg-transparent px-4 py-2 text-sm font-semibold text-gray-900 transition-colors hover:bg-gray-100 focus:outline-none focus:ring-2 focus:ring-gray-400 focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 dark:border-gray-700 dark:text-gray-100 dark:hover:bg-gray-800 dark:focus:ring-gray-400 dark:focus:ring-offset-gray-900 sm:mt-0',
className ?? '',
/* Important: for accessibility */
'focus:ring-2 focus:ring-gray-400 focus:ring-offset-2 dark:focus:ring-gray-400 dark:focus:ring-offset-gray-900',
)}
{...props}
/>
));
DialogButton.displayName = DialogPrimitive.Title.displayName;
export {
Dialog,
DialogTrigger,
DialogContent,
DialogHeader,
DialogFooter,
DialogTitle,
DialogDescription,
DialogClose,
DialogButton,
};

View file

@ -1,89 +0,0 @@
import 'test/matchMedia.mock';
import React from 'react';
import { render, fireEvent } from '@testing-library/react';
import '@testing-library/jest-dom/extend-expect';
import DialogTemplate from './DialogTemplate';
import { Dialog } from '@radix-ui/react-dialog';
import { RecoilRoot } from 'recoil';
describe('DialogTemplate', () => {
let mockSelectHandler;
beforeEach(() => {
mockSelectHandler = jest.fn();
});
it('renders correctly with all props', () => {
const { getByText } = render(
<RecoilRoot>
<Dialog
open
data-testid="test-dialog"
onOpenChange={() => {
return;
}}
>
<DialogTemplate
title="Test Dialog"
description="Test Description"
main={<div>Main Content</div>}
buttons={<button>Button</button>}
leftButtons={<button>Left Button</button>}
selection={{ selectHandler: mockSelectHandler, selectText: 'Select' }}
/>
</Dialog>
</RecoilRoot>,
);
expect(getByText('Test Dialog')).toBeInTheDocument();
expect(getByText('Test Description')).toBeInTheDocument();
expect(getByText('Main Content')).toBeInTheDocument();
expect(getByText('Button')).toBeInTheDocument();
expect(getByText('Left Button')).toBeInTheDocument();
expect(getByText('Cancel')).toBeInTheDocument();
expect(getByText('Select')).toBeInTheDocument();
});
it('renders correctly without optional props', () => {
const { queryByText } = render(
<RecoilRoot>
<Dialog
open
onOpenChange={() => {
return;
}}
></Dialog>
</RecoilRoot>,
);
expect(queryByText('Test Dialog')).toBeNull();
expect(queryByText('Test Description')).not.toBeInTheDocument();
expect(queryByText('Main Content')).not.toBeInTheDocument();
expect(queryByText('Button')).not.toBeInTheDocument();
expect(queryByText('Left Button')).not.toBeInTheDocument();
expect(queryByText('Cancel')).not.toBeInTheDocument();
expect(queryByText('Select')).not.toBeInTheDocument();
});
it('calls selectHandler when the select button is clicked', () => {
const { getByText } = render(
<RecoilRoot>
<Dialog
open
onOpenChange={() => {
return;
}}
>
<DialogTemplate
title="Test Dialog"
selection={{ selectHandler: mockSelectHandler, selectText: 'Select' }}
/>
</Dialog>
</RecoilRoot>,
);
fireEvent.click(getByText('Select'));
expect(mockSelectHandler).toHaveBeenCalled();
});
});

View file

@ -1,96 +0,0 @@
import { forwardRef, ReactNode, Ref } from 'react';
import {
DialogClose,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from './';
import { cn } from '~/utils/';
import { useLocalize } from '~/hooks';
type SelectionProps = {
selectHandler?: () => void;
selectClasses?: string;
selectText?: string;
};
type DialogTemplateProps = {
title: string;
description?: string;
main?: ReactNode;
buttons?: ReactNode;
leftButtons?: ReactNode;
selection?: SelectionProps;
className?: string;
headerClassName?: string;
footerClassName?: string;
showCloseButton?: boolean;
showCancelButton?: boolean;
};
const DialogTemplate = forwardRef((props: DialogTemplateProps, ref: Ref<HTMLDivElement>) => {
const localize = useLocalize();
const {
title,
description,
main,
buttons,
leftButtons,
selection,
className,
headerClassName,
footerClassName,
showCloseButton,
showCancelButton = true,
} = props;
const { selectHandler, selectClasses, selectText } = selection || {};
const Cancel = localize('com_ui_cancel');
const defaultSelect =
'bg-gray-800 text-white transition-colors hover:bg-gray-700 disabled:cursor-not-allowed disabled:opacity-50 dark:bg-gray-200 dark:text-gray-800 dark:hover:bg-gray-200';
return (
<DialogContent
showCloseButton={showCloseButton}
ref={ref}
className={cn('shadow-2xl dark:bg-gray-700', className || '')}
onClick={(e) => e.stopPropagation()}
>
<DialogHeader className={cn(headerClassName ?? '')}>
<DialogTitle className="text-lg font-medium leading-6 text-gray-800 dark:text-gray-200">
{title}
</DialogTitle>
{description && (
<DialogDescription className="text-gray-600 dark:text-gray-300">
{description}
</DialogDescription>
)}
</DialogHeader>
<div className="px-6">{main ? main : null}</div>
<DialogFooter className={footerClassName}>
<div>{leftButtons ? leftButtons : null}</div>
<div className="flex h-auto gap-3">
{showCancelButton && (
<DialogClose className="border-gray-100 hover:bg-gray-100 dark:border-gray-600 dark:hover:bg-gray-600">
{Cancel}
</DialogClose>
)}
{buttons ? buttons : null}
{selection ? (
<DialogClose
onClick={selectHandler}
className={`${
selectClasses || defaultSelect
} inline-flex h-10 items-center justify-center rounded-lg border-none px-4 py-2 text-sm`}
>
{selectText}
</DialogClose>
) : null}
</div>
</DialogFooter>
</DialogContent>
);
});
export default DialogTemplate;

View file

@ -1,150 +0,0 @@
import React from 'react';
import * as Select from '@ariakit/react/select';
import type { Option } from '~/common';
import { cn } from '~/utils/';
interface DropdownProps {
value?: string;
label?: string;
onChange: (value: string) => void;
options: (string | Option | { divider: true })[];
className?: string;
sizeClasses?: string;
testId?: string;
icon?: React.ReactNode;
iconOnly?: boolean;
renderValue?: (option: Option) => React.ReactNode;
ariaLabel?: string;
portal?: boolean;
}
const isDivider = (item: string | Option | { divider: true }): item is { divider: true } =>
typeof item === 'object' && 'divider' in item;
const isOption = (item: string | Option | { divider: true }): item is Option =>
typeof item === 'object' && 'value' in item && 'label' in item;
const Dropdown: React.FC<DropdownProps> = ({
value: selectedValue,
label = '',
onChange,
options,
className = '',
sizeClasses,
testId = 'dropdown-menu',
icon,
iconOnly = false,
renderValue,
ariaLabel,
portal = true,
}) => {
const handleChange = (value: string) => {
onChange(value);
};
const selectProps = Select.useSelectStore({
value: selectedValue,
setValue: handleChange,
});
const getOptionObject = (val: string | undefined): Option | undefined => {
if (val == null || val === '') {
return undefined;
}
return options
.filter((o) => !isDivider(o))
.map((o) => (typeof o === 'string' ? { value: o, label: o } : o))
.find((o) => isOption(o) && o.value === val) as Option | undefined;
};
const getOptionLabel = (currentValue: string | undefined) => {
if (currentValue == null || currentValue === '') {
return '';
}
const option = getOptionObject(currentValue);
return option ? option.label : currentValue;
};
return (
<div className={cn('relative', className)}>
<Select.Select
store={selectProps}
className={cn(
'focus:ring-offset-ring-offset relative inline-flex items-center justify-between rounded-xl border border-input bg-background px-3 py-2 text-sm text-text-primary transition-all duration-200 ease-in-out hover:bg-accent hover:text-accent-foreground focus:ring-ring-primary',
iconOnly ? 'h-full w-10' : 'w-fit gap-2',
className,
)}
data-testid={testId}
aria-label={ariaLabel}
>
<div className="flex w-full items-center gap-2">
{icon}
{!iconOnly && (
<span className="block truncate">
{label}
{(() => {
const matchedOption = getOptionObject(selectedValue);
if (matchedOption && renderValue) {
return renderValue(matchedOption);
}
return getOptionLabel(selectedValue);
})()}
</span>
)}
</div>
{!iconOnly && <Select.SelectArrow />}
</Select.Select>
<Select.SelectPopover
portal={portal}
store={selectProps}
className={cn('popover-ui', sizeClasses, className, 'max-h-[80vh] overflow-y-auto')}
>
{options.map((item, index) => {
if (isDivider(item)) {
return <div key={`divider-${index}`} className="my-1 border-t border-border-heavy" />;
}
const option = typeof item === 'string' ? { value: item, label: item } : item;
if (!isOption(option)) {
return null;
}
return (
<Select.SelectItem
key={`option-${index}`}
value={String(option.value)}
className="select-item"
data-theme={option.value}
>
<div className="flex w-full items-center gap-2">
{option.icon != null && <span>{option.icon as React.ReactNode}</span>}
<span className="block truncate">{option.label}</span>
{selectedValue === option.value && (
<span className="ml-auto pl-2">
<svg
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
className="icon-md block group-hover:hidden"
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M2 12C2 6.47715 6.47715 2 12 2C17.5228 2 22 6.47715 22 12C22 17.5228 17.5228 22 12 22C6.47715 22 2 17.5228 2 12ZM16.0755 7.93219C16.5272 8.25003 16.6356 8.87383 16.3178 9.32549L11.5678 16.0755C11.3931 16.3237 11.1152 16.4792 10.8123 16.4981C10.5093 16.517 10.2142 16.3973 10.0101 16.1727L7.51006 13.4227C7.13855 13.014 7.16867 12.3816 7.57733 12.0101C7.98598 11.6386 8.61843 11.6687 8.98994 12.0773L10.6504 13.9039L14.6822 8.17451C15 7.72284 15.6238 7.61436 16.0755 7.93219Z"
fill="currentColor"
/>
</svg>
</span>
)}
</div>
</Select.SelectItem>
);
})}
</Select.SelectPopover>
</div>
);
};
export default Dropdown;

View file

@ -1,191 +0,0 @@
import * as React from 'react';
import * as DropdownMenuPrimitive from '@radix-ui/react-dropdown-menu';
import { Check, ChevronRight, Circle } from 'lucide-react';
import { cn } from '~/utils';
const DropdownMenu = DropdownMenuPrimitive.Root;
const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger;
const DropdownMenuGroup = DropdownMenuPrimitive.Group;
const DropdownMenuPortal = DropdownMenuPrimitive.Portal;
const DropdownMenuSub = DropdownMenuPrimitive.Sub;
const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup;
const DropdownMenuSubTrigger = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.SubTrigger>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubTrigger> & {
inset?: boolean;
}
>(({ className = '', inset, children, ...props }, ref) => (
<DropdownMenuPrimitive.SubTrigger
ref={ref}
className={cn(
'flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm font-medium outline-none focus:bg-gray-100 data-[state=open]:bg-gray-100 dark:focus:bg-gray-900 dark:data-[state=open]:bg-gray-900',
inset ? 'pl-8' : '',
className,
)}
{...props}
>
{children}
<ChevronRight className="ml-auto h-4 w-4" />
</DropdownMenuPrimitive.SubTrigger>
));
DropdownMenuSubTrigger.displayName = DropdownMenuPrimitive.SubTrigger.displayName;
const DropdownMenuSubContent = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.SubContent>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubContent>
>(({ className = '', ...props }, ref) => (
<DropdownMenuPrimitive.SubContent
ref={ref}
className={cn(
'z-50 min-w-[8rem] overflow-hidden rounded-md border border-gray-100 bg-white p-1 text-gray-700 shadow-md animate-in slide-in-from-left-1 dark:border-gray-800 dark:bg-gray-800 dark:text-gray-400',
className,
)}
{...props}
/>
));
DropdownMenuSubContent.displayName = DropdownMenuPrimitive.SubContent.displayName;
const DropdownMenuContent = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content>
>(({ className = '', sideOffset = 4, ...props }, ref) => (
<DropdownMenuPrimitive.Portal>
<DropdownMenuPrimitive.Content
ref={ref}
sideOffset={sideOffset}
className={cn(
'z-50 min-w-[8rem] overflow-hidden rounded-md border border-gray-100 bg-white p-1 text-gray-700 shadow-md animate-in data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 dark:border-gray-800 dark:bg-gray-800 dark:text-gray-400',
className,
)}
{...props}
/>
</DropdownMenuPrimitive.Portal>
));
DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName;
const DropdownMenuItem = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> & {
inset?: boolean;
}
>(({ className = '', inset, ...props }, ref) => (
<DropdownMenuPrimitive.Item
ref={ref}
className={cn(
'relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm font-medium outline-none focus:bg-gray-100 data-[disabled]:pointer-events-none data-[disabled]:opacity-50 dark:focus:bg-gray-900',
inset ? 'pl-8' : '',
className,
)}
{...props}
/>
));
DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName;
const DropdownMenuCheckboxItem = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.CheckboxItem>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.CheckboxItem>
>(({ className = '', children, checked, ...props }, ref) => (
<DropdownMenuPrimitive.CheckboxItem
ref={ref}
className={cn(
'relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm font-medium outline-none focus:bg-gray-100 data-[disabled]:pointer-events-none data-[disabled]:opacity-50 dark:focus:bg-gray-900',
className,
)}
checked={checked}
{...props}
>
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<DropdownMenuPrimitive.ItemIndicator>
<Check className="h-4 w-4" />
</DropdownMenuPrimitive.ItemIndicator>
</span>
{children}
</DropdownMenuPrimitive.CheckboxItem>
));
DropdownMenuCheckboxItem.displayName = DropdownMenuPrimitive.CheckboxItem.displayName;
const DropdownMenuRadioItem = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.RadioItem>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.RadioItem>
>(({ className = '', children, ...props }, ref) => (
<DropdownMenuPrimitive.RadioItem
ref={ref}
className={cn(
className,
'relative flex cursor-pointer select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm font-medium outline-none focus:bg-gray-100 data-[disabled]:pointer-events-none data-[disabled]:opacity-50 dark:focus:bg-gray-800',
)}
{...props}
>
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<DropdownMenuPrimitive.ItemIndicator>
<Circle className="h-2 w-2 fill-current" />
</DropdownMenuPrimitive.ItemIndicator>
</span>
{children}
</DropdownMenuPrimitive.RadioItem>
));
DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName;
const DropdownMenuLabel = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Label>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Label> & {
inset?: boolean;
}
>(({ className = '', inset, ...props }, ref) => (
<DropdownMenuPrimitive.Label
ref={ref}
className={cn(
'px-2 py-1.5 text-sm font-semibold text-gray-900 dark:text-gray-300',
inset ? 'pl-8' : '',
className,
)}
{...props}
/>
));
DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName;
const DropdownMenuSeparator = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Separator>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Separator>
>(({ className = '', ...props }, ref) => (
<DropdownMenuPrimitive.Separator
ref={ref}
className={cn('-mx-1 my-1 h-px bg-border-medium', className)}
{...props}
/>
));
DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName;
const DropdownMenuShortcut = ({
className = '',
...props
}: React.HTMLAttributes<HTMLSpanElement>) => {
return (
<span className={cn('ml-auto text-xs tracking-widest text-gray-500', className)} {...props} />
);
};
DropdownMenuShortcut.displayName = 'DropdownMenuShortcut';
export {
DropdownMenu,
DropdownMenuTrigger,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuCheckboxItem,
DropdownMenuRadioItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuShortcut,
DropdownMenuGroup,
DropdownMenuPortal,
DropdownMenuSub,
DropdownMenuSubContent,
DropdownMenuSubTrigger,
DropdownMenuRadioGroup,
};

View file

@ -1,140 +0,0 @@
import React, { FC } from 'react';
import {
Listbox,
ListboxButton,
ListboxOption,
ListboxOptions,
Transition,
} from '@headlessui/react';
import { AnchorPropsWithSelection } from '@headlessui/react/dist/internal/floating';
import type { Option } from '~/common';
import { cn } from '~/utils/';
interface DropdownProps {
value?: string | Option;
label?: string;
onChange: (value: string | Option) => void;
options: (string | Option)[];
className?: string;
anchor?: AnchorPropsWithSelection;
sizeClasses?: string;
testId?: string;
}
/*
* Mainly used for the Speech Voice Selection Dropdown
*/
const Dropdown: FC<DropdownProps> = ({
value,
label = '',
onChange,
options,
className = '',
anchor,
sizeClasses,
testId = 'dropdown-menu',
}) => {
const getValue = (option?: string | Option) =>
typeof option === 'string' ? option : option?.value;
const getDisplay = (option?: string | Option) =>
typeof option === 'string' ? option : option?.label ?? option?.value;
const isEqual = (a: string | Option, b: string | Option): boolean => getValue(a) === getValue(b);
const selectedOption = options.find((option) => isEqual(option, value ?? '')) ?? value;
const handleChange = (newValue: string | Option) => {
onChange(newValue);
};
return (
<div className={cn('relative', className)}>
<Listbox value={selectedOption} onChange={handleChange}>
<div className={cn('relative', className)}>
<ListboxButton
data-testid={testId}
className={cn(
'relative inline-flex items-center justify-between rounded-md border-gray-50 bg-white py-2 pl-3 pr-8 text-black transition-all duration-100 ease-in-out hover:bg-gray-100 dark:border-gray-600 dark:bg-gray-700 dark:text-white dark:hover:bg-gray-600 dark:focus:ring-white dark:focus:ring-offset-gray-700',
'w-auto',
className,
)}
aria-label="Select an option"
>
<span className="block truncate">
{label}
{getDisplay(selectedOption)}
</span>
<span className="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-2">
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
strokeWidth="2"
stroke="currentColor"
className="h-4 w-5 rotate-0 transform text-black transition-transform duration-300 ease-in-out dark:text-gray-50"
>
<polyline points="6 9 12 15 18 9"></polyline>
</svg>
</span>
</ListboxButton>
<Transition
leave="transition ease-in duration-50"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<ListboxOptions
className={cn(
'absolute z-50 mt-1 flex flex-col items-start gap-1 overflow-auto rounded-lg border border-gray-300 bg-white p-1.5 text-gray-700 shadow-lg transition-opacity focus:outline-none dark:border-gray-600 dark:bg-gray-700 dark:text-white',
sizeClasses,
className,
)}
anchor={anchor}
aria-label="List of options"
>
{options.map((item, index) => (
<ListboxOption
key={index}
value={item}
className={cn(
'relative cursor-pointer select-none rounded border-gray-300 bg-white py-2.5 pl-3 pr-3 text-sm text-gray-700 hover:bg-gray-100 dark:border-gray-300 dark:bg-gray-700 dark:text-white dark:hover:bg-gray-600',
)}
style={{ width: '100%' }}
data-theme={getValue(item)}
>
{({ selected }) => (
<div className="flex w-full items-center justify-between">
<span className="block truncate">{getDisplay(item)}</span>
{selected && (
<span className="ml-auto pl-2">
<svg
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
className="icon-md block group-hover:hidden"
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M2 12C2 6.47715 6.47715 2 12 2C17.5228 2 22 6.47715 22 12C22 17.5228 17.5228 22 12 22C6.47715 22 2 17.5228 2 12ZM16.0755 7.93219C16.5272 8.25003 16.6356 8.87383 16.3178 9.32549L11.5678 16.0755C11.3931 16.3237 11.1152 16.4792 10.8123 16.4981C10.5093 16.517 10.2142 16.3973 10.0101 16.1727L7.51006 13.4227C7.13855 13.014 7.16867 12.3816 7.57733 12.0101C7.98598 11.6386 8.61843 11.6687 8.98994 12.0773L10.6504 13.9039L14.6822 8.17451C15 7.72284 15.6238 7.61436 16.0755 7.93219Z"
fill="currentColor"
/>
</svg>
</span>
)}
</div>
)}
</ListboxOption>
))}
</ListboxOptions>
</Transition>
</div>
</Listbox>
</div>
);
};
export default Dropdown;

View file

@ -1,134 +0,0 @@
import React from 'react';
import * as Ariakit from '@ariakit/react';
import type * as t from '~/common';
import { cn } from '~/utils';
interface DropdownProps {
keyPrefix?: string;
trigger: React.ReactNode;
items: t.MenuItemProps[];
isOpen: boolean;
setIsOpen: (isOpen: boolean) => void;
className?: string;
iconClassName?: string;
itemClassName?: string;
sameWidth?: boolean;
anchor?: { x: string; y: string };
gutter?: number;
modal?: boolean;
portal?: boolean;
preserveTabOrder?: boolean;
focusLoop?: boolean;
menuId: string;
mountByState?: boolean;
unmountOnHide?: boolean;
finalFocus?: React.RefObject<HTMLElement>;
}
type MenuProps = Omit<
DropdownProps,
'trigger' | 'isOpen' | 'setIsOpen' | 'focusLoop' | 'mountByState'
>;
const DropdownPopup: React.FC<DropdownProps> = ({
trigger,
isOpen,
setIsOpen,
focusLoop,
mountByState,
...props
}) => {
const menu = Ariakit.useMenuStore({ open: isOpen, setOpen: setIsOpen, focusLoop });
if (mountByState) {
return (
<Ariakit.MenuProvider store={menu}>
{trigger}
{isOpen && <Menu {...props} />}
</Ariakit.MenuProvider>
);
}
return (
<Ariakit.MenuProvider store={menu}>
{trigger}
<Menu {...props} />
</Ariakit.MenuProvider>
);
};
const Menu: React.FC<MenuProps> = ({
items,
menuId,
keyPrefix,
className,
iconClassName,
itemClassName,
modal,
portal,
sameWidth,
gutter = 8,
finalFocus,
unmountOnHide,
preserveTabOrder,
}) => {
const menu = Ariakit.useMenuContext();
return (
<Ariakit.Menu
id={menuId}
modal={modal}
gutter={gutter}
portal={portal}
sameWidth={sameWidth}
finalFocus={finalFocus}
unmountOnHide={unmountOnHide}
preserveTabOrder={preserveTabOrder}
className={cn('popover-ui z-50', className)}
>
{items
.filter((item) => item.show !== false)
.map((item, index) => {
if (item.separate === true) {
return <Ariakit.MenuSeparator key={index} className="my-1 h-px bg-white/10" />;
}
return (
<Ariakit.MenuItem
key={`${keyPrefix ?? ''}${index}-${item.id ?? ''}`}
id={item.id}
className={cn(
'group flex w-full cursor-pointer items-center gap-2 rounded-lg px-3 py-3.5 text-sm text-text-primary outline-none transition-colors duration-200 hover:bg-surface-hover focus:bg-surface-hover md:px-2.5 md:py-2',
itemClassName,
)}
disabled={item.disabled}
render={item.render}
ref={item.ref}
hideOnClick={item.hideOnClick}
onClick={(event) => {
event.preventDefault();
if (item.onClick) {
item.onClick(event);
}
if (item.hideOnClick === false) {
return;
}
menu?.hide();
}}
>
{item.icon != null && (
<span className={cn('mr-2 size-4', iconClassName)} aria-hidden="true">
{item.icon}
</span>
)}
{item.label}
{item.kbd != null && (
// eslint-disable-next-line i18next/no-literal-string
<kbd className="ml-auto hidden font-sans text-xs text-black/50 group-hover:inline group-focus:inline dark:text-white/50">
{item.kbd}
</kbd>
)}
</Ariakit.MenuItem>
);
})}
</Ariakit.Menu>
);
};
export default DropdownPopup;

View file

@ -1,29 +0,0 @@
import React, { forwardRef } from 'react';
type FileUploadProps = {
className?: string;
onClick?: () => void;
children: React.ReactNode;
handleFileChange: (event: React.ChangeEvent<HTMLInputElement>) => void;
};
const FileUpload = forwardRef<HTMLInputElement, FileUploadProps>(
({ children, handleFileChange }, ref) => {
return (
<>
{children}
<input
ref={ref}
multiple
type="file"
style={{ display: 'none' }}
onChange={handleFileChange}
/>
</>
);
},
);
FileUpload.displayName = 'FileUpload';
export default FileUpload;

View file

@ -1,62 +0,0 @@
import React from 'react';
import { Label, Input } from '~/components/ui';
import { cn } from '~/utils';
export default function FormInput({
field,
label,
labelClass,
inputClass,
containerClass,
labelAdjacent,
placeholder = '',
type = 'string',
}: {
field: any;
label: string;
labelClass?: string;
inputClass?: string;
placeholder?: string;
containerClass?: string;
type?: 'string' | 'number';
labelAdjacent?: React.ReactNode;
}) {
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const value = e.target.value;
if (type !== 'number') {
field.onChange(value);
return;
}
if (value === '') {
field.onChange(value);
} else if (!isNaN(Number(value))) {
field.onChange(Number(value));
}
};
return (
<div className={cn('flex w-full flex-col items-center gap-2', containerClass)}>
<div className="flex w-full items-center justify-start gap-2">
<Label
htmlFor={`${field.name}-input`}
className={cn('text-left text-sm font-semibold text-text-primary', labelClass)}
>
{label}
</Label>
{labelAdjacent}
</div>
<Input
id={`${field.name}-input`}
value={field.value ?? ''}
onChange={handleChange}
placeholder={placeholder}
className={cn(
'flex h-10 max-h-10 w-full resize-none border-none bg-surface-secondary px-3 py-2',
inputClass,
)}
/>
</div>
);
}

View file

@ -1,35 +0,0 @@
import * as React from 'react';
import * as HoverCardPrimitive from '@radix-ui/react-hover-card';
import { cn } from '../../utils';
const HoverCard = HoverCardPrimitive.Root;
const HoverCardTrigger = HoverCardPrimitive.Trigger;
const HoverCardPortal = HoverCardPrimitive.Portal;
const HoverCardContent = React.forwardRef<
React.ElementRef<typeof HoverCardPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof HoverCardPrimitive.Content> & { disabled?: boolean }
>(({ className = '', align = 'center', sideOffset = 6, disabled = false, ...props }, ref) => {
if (disabled) {
return null;
}
return (
<HoverCardPrimitive.Content
ref={ref}
align={align}
sideOffset={sideOffset}
className={cn(
'z-50 w-64 origin-[--radix-hover-card-content-transform-origin] rounded-xl border border-border-light bg-surface-secondary p-4 text-text-primary shadow-md outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',
className,
)}
{...props}
/>
);
});
HoverCardContent.displayName = HoverCardPrimitive.Content.displayName;
export { HoverCard, HoverCardTrigger, HoverCardContent, HoverCardPortal };

View file

@ -1,22 +0,0 @@
import * as React from 'react';
import { cn } from '~/utils';
export type InputProps = React.InputHTMLAttributes<HTMLInputElement>;
const Input = React.forwardRef<HTMLInputElement, InputProps>(({ className, ...props }, ref) => {
return (
<input
className={cn(
'flex h-10 w-full rounded-lg border border-input bg-transparent px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50',
className ?? '',
)}
ref={ref}
{...props}
/>
);
});
Input.displayName = 'Input';
export { Input };

View file

@ -1,105 +0,0 @@
import React from 'react';
import * as Ariakit from '@ariakit/react';
import type { OptionWithIcon } from '~/common';
import { cn } from '~/utils';
type ComboboxProps = {
label?: string;
placeholder?: string;
options: OptionWithIcon[] | string[];
className?: string;
labelClassName?: string;
value: string;
onChange: (value: string) => void;
onBlur: () => void;
};
export const InputCombobox: React.FC<ComboboxProps> = ({
label,
labelClassName,
placeholder = 'Select an option',
options,
className,
value,
onChange,
onBlur,
}) => {
const isOptionObject = (option: unknown): option is OptionWithIcon => {
return option != null && typeof option === 'object' && 'value' in option;
};
const [isOpen, setIsOpen] = React.useState(false);
const [inputValue, setInputValue] = React.useState(value);
const [isKeyboardFocus, setIsKeyboardFocus] = React.useState(false);
React.useEffect(() => {
setInputValue(value);
}, [value]);
const handleChange = (newValue: string) => {
setInputValue(newValue);
onChange(newValue);
};
return (
<Ariakit.ComboboxProvider value={inputValue} setValue={handleChange}>
{label != null && (
<Ariakit.ComboboxLabel
className={cn('mb-2 block text-sm font-medium text-text-primary', labelClassName ?? '')}
>
{label}
</Ariakit.ComboboxLabel>
)}
<div className={cn('relative', isKeyboardFocus ? 'rounded-md ring-2 ring-ring-primary' : '')}>
<Ariakit.Combobox
placeholder={placeholder}
className={cn(
'h-10 w-full rounded-md border border-border-light bg-surface-primary px-3 py-2 text-sm',
'placeholder-text-secondary hover:bg-surface-hover',
'focus:outline-none',
className,
)}
onChange={(event) => handleChange(event.target.value)}
onBlur={() => {
setIsKeyboardFocus(false);
onBlur();
}}
onFocusVisible={() => {
setIsKeyboardFocus(true);
setIsOpen(true);
}}
onMouseDown={() => {
setIsKeyboardFocus(false);
}}
/>
</div>
<Ariakit.ComboboxPopover
gutter={4}
sameWidth
open={isOpen}
onClose={() => setIsOpen(false)}
className={cn(
'z-50 max-h-60 w-full overflow-auto rounded-md bg-surface-primary p-1 shadow-lg',
'animate-in fade-in-0 zoom-in-95',
)}
>
{options.map((option: string | OptionWithIcon, index: number) => (
<Ariakit.ComboboxItem
key={index}
className={cn(
'relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none',
'cursor-pointer hover:bg-surface-tertiary hover:text-text-primary',
'data-[active-item]:bg-surface-tertiary data-[active-item]:text-text-primary',
)}
value={isOptionObject(option) ? `${option.value ?? ''}` : option}
>
{isOptionObject(option) && option.icon != null && (
<span className="mr-2 flex-shrink-0">{option.icon}</span>
)}
{isOptionObject(option) ? option.label : option}
</Ariakit.ComboboxItem>
))}
</Ariakit.ComboboxPopover>
</Ariakit.ComboboxProvider>
);
};

View file

@ -1,45 +0,0 @@
import * as React from 'react';
// import { NumericFormat } from 'react-number-format';
import RCInputNumber from 'rc-input-number';
import * as InputNumberPrimitive from 'rc-input-number';
import { cn } from '~/utils';
// TODO help needed
// React.ElementRef<typeof LabelPrimitive.Root>,
// React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root>
const InputNumber = React.forwardRef<
React.ElementRef<typeof RCInputNumber>,
InputNumberPrimitive.InputNumberProps
>(({ className, ...props }, ref) => {
return (
<RCInputNumber
className={cn(
'flex max-h-5 w-full rounded-md border border-gray-300 bg-transparent px-3 py-2 text-sm placeholder:text-gray-400 focus:outline-none disabled:cursor-not-allowed disabled:opacity-50 dark:border-gray-700 dark:text-gray-50',
className ?? '',
)}
ref={ref}
{...props}
/>
);
});
InputNumber.displayName = 'Input';
// console.log(_InputNumber);
// const InputNumber = React.forwardRef(({ className, ...props }, ref) => {
// return (
// <NumericFormat
// className={cn(
// 'flex h-10 w-full rounded-md border border-gray-300 bg-transparent py-2 px-3 text-sm placeholder:text-gray-400 focus:outline-none focus:ring-2 focus:ring-gray-400 focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 dark:border-gray-700 dark:text-gray-50 dark:focus:ring-gray-400 dark:focus:ring-offset-gray-900',
// className
// )}
// ref={ref}
// {...props}
// />
// );
// });
export { InputNumber };

View file

@ -1,68 +0,0 @@
import * as React from 'react';
import { OTPInput, OTPInputContext } from 'input-otp';
import { Minus } from 'lucide-react';
import { cn } from '~/utils';
const InputOTP = React.forwardRef<
React.ElementRef<typeof OTPInput>,
React.ComponentPropsWithoutRef<typeof OTPInput>
>(({ className, containerClassName, ...props }, ref) => (
<OTPInput
ref={ref}
containerClassName={cn(
'flex items-center gap-2 has-[:disabled]:opacity-50',
containerClassName,
)}
className={cn('disabled:cursor-not-allowed', className)}
{...props}
/>
));
InputOTP.displayName = 'InputOTP';
const InputOTPGroup = React.forwardRef<
React.ElementRef<'div'>,
React.ComponentPropsWithoutRef<'div'>
>(({ className, ...props }, ref) => (
<div ref={ref} className={cn('flex items-center', className)} {...props} />
));
InputOTPGroup.displayName = 'InputOTPGroup';
const InputOTPSlot = React.forwardRef<
React.ElementRef<'div'>,
React.ComponentPropsWithoutRef<'div'> & { index: number }
>(({ index, className, ...props }, ref) => {
const inputOTPContext = React.useContext(OTPInputContext);
const { char, hasFakeCaret, isActive } = inputOTPContext.slots[index];
return (
<div
ref={ref}
className={cn(
'text-md relative flex h-11 w-11 items-center justify-center border-y border-r border-input shadow-sm transition-all first:rounded-l-xl first:border-l last:rounded-r-xl',
isActive && 'z-10 ring-1 ring-ring',
className,
)}
{...props}
>
{char}
{hasFakeCaret && (
<div className="pointer-events-none absolute inset-0 flex items-center justify-center">
<div className="animate-caret-blink h-4 w-px bg-foreground duration-1000" />
</div>
)}
</div>
);
});
InputOTPSlot.displayName = 'InputOTPSlot';
const InputOTPSeparator = React.forwardRef<
React.ElementRef<'div'>,
React.ComponentPropsWithoutRef<'div'>
>(({ ...props }, ref) => (
<div ref={ref} role="separator" {...props}>
<Minus />
</div>
));
InputOTPSeparator.displayName = 'InputOTPSeparator';
export { InputOTP, InputOTPGroup, InputOTPSlot, InputOTPSeparator };

View file

@ -1,154 +0,0 @@
import * as React from 'react';
import { Input } from '~/components/ui/Input';
import { cn } from '~/utils';
export type InputWithDropdownProps = React.InputHTMLAttributes<HTMLInputElement> & {
options: string[];
onSelect?: (value: string) => void;
};
const InputWithDropdown = React.forwardRef<HTMLInputElement, InputWithDropdownProps>(
({ className, options, onSelect, ...props }, ref) => {
const [isOpen, setIsOpen] = React.useState(false);
const [inputValue, setInputValue] = React.useState((props.value as string) || '');
const [highlightedIndex, setHighlightedIndex] = React.useState(-1);
const inputRef = React.useRef<HTMLInputElement>(null);
const handleSelect = (value: string) => {
setInputValue(value);
setIsOpen(false);
setHighlightedIndex(-1);
if (onSelect) {
onSelect(value);
}
if (props.onChange) {
props.onChange({ target: { value } } as React.ChangeEvent<HTMLInputElement>);
}
};
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setInputValue(e.target.value);
if (props.onChange) {
props.onChange(e);
}
};
const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
switch (e.key) {
case 'ArrowDown':
e.preventDefault();
if (!isOpen) {
setIsOpen(true);
} else {
setHighlightedIndex((prevIndex) =>
prevIndex < options.length - 1 ? prevIndex + 1 : prevIndex,
);
}
break;
case 'ArrowUp':
e.preventDefault();
setHighlightedIndex((prevIndex) => (prevIndex > 0 ? prevIndex - 1 : 0));
break;
case 'Enter':
e.preventDefault();
if (isOpen && highlightedIndex !== -1) {
handleSelect(options[highlightedIndex]);
}
setIsOpen(false);
break;
case 'Escape':
setIsOpen(false);
setHighlightedIndex(-1);
break;
}
};
React.useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
if (inputRef.current && !inputRef.current.contains(event.target as Node)) {
setIsOpen(false);
setHighlightedIndex(-1);
}
};
document.addEventListener('mousedown', handleClickOutside);
return () => {
document.removeEventListener('mousedown', handleClickOutside);
};
}, []);
return (
<div className="relative" ref={inputRef}>
<div className="relative">
<Input
{...props}
value={inputValue}
onChange={handleInputChange}
onKeyDown={handleKeyDown}
aria-haspopup="listbox"
aria-controls="dropdown-list"
className={cn('bg-surface-secondary', className ?? '')}
ref={ref}
/>
<button
type="button"
className="text-tertiary hover:text-secondary absolute inset-y-0 right-0 flex items-center rounded-md px-2 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring-primary"
onClick={() => setIsOpen(!isOpen)}
aria-label={isOpen ? 'Close dropdown' : 'Open dropdown'}
>
<svg
className="h-5 w-5"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M19 9l-7 7-7-7"
/>
</svg>
</button>
</div>
{isOpen && (
<ul
id="dropdown-list"
role="listbox"
className="absolute z-10 mt-1 max-h-60 w-full overflow-auto rounded-md border border-border-medium bg-surface-secondary shadow-lg focus:ring-1 focus:ring-inset focus:ring-ring-primary"
>
{options.map((option, index) => (
<li
key={index}
role="option"
aria-selected={index === highlightedIndex}
className={cn(
'cursor-pointer rounded-md px-3 py-2',
'focus:bg-surface-tertiary focus:outline-none focus:ring-1 focus:ring-inset focus:ring-ring-primary',
index === highlightedIndex
? 'text-primary bg-surface-active'
: 'text-secondary hover:bg-surface-tertiary',
)}
onClick={() => handleSelect(option)}
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
handleSelect(option);
}
}}
tabIndex={0}
>
{option}
</li>
))}
</ul>
)}
</div>
);
},
);
InputWithDropdown.displayName = 'InputWithDropdown';
export default InputWithDropdown;

View file

@ -1,21 +0,0 @@
import * as React from 'react';
import * as LabelPrimitive from '@radix-ui/react-label';
import { cn } from '../../utils';
const Label = React.forwardRef<
React.ElementRef<typeof LabelPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root>
>(({ className = '', ...props }, ref) => (
<LabelPrimitive.Root
ref={ref}
className={cn(
'block w-full break-all text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70 dark:text-gray-200',
className,
)}
{...props}
/>
));
Label.displayName = LabelPrimitive.Root.displayName;
export { Label };

View file

@ -1,161 +0,0 @@
import React, { useMemo } from 'react';
import { useForm, Controller } from 'react-hook-form';
import { Input, Label, Button } from '~/components/ui';
import { useLocalize } from '~/hooks';
import { useMCPAuthValuesQuery } from '~/data-provider/Tools/queries';
export interface CustomUserVarConfig {
title: string;
description?: string;
}
interface CustomUserVarsSectionProps {
serverName: string;
fields: Record<string, CustomUserVarConfig>;
onSave: (authData: Record<string, string>) => void;
onRevoke: () => void;
isSubmitting?: boolean;
}
interface AuthFieldProps {
name: string;
config: CustomUserVarConfig;
hasValue: boolean;
control: any;
errors: any;
}
function AuthField({ name, config, hasValue, control, errors }: AuthFieldProps) {
const localize = useLocalize();
return (
<div className="space-y-2">
<div className="flex items-center justify-between">
<Label htmlFor={name} className="text-sm font-medium">
{config.title}
</Label>
{hasValue ? (
<div className="flex min-w-fit items-center gap-2 whitespace-nowrap rounded-full border border-border-medium px-2 py-0.5 text-xs font-medium text-text-secondary">
<div className="h-1.5 w-1.5 rounded-full bg-green-500" />
<span>{localize('com_ui_set')}</span>
</div>
) : (
<div className="flex min-w-fit items-center gap-2 whitespace-nowrap rounded-full border border-border-medium px-2 py-0.5 text-xs font-medium text-text-secondary">
<div className="h-1.5 w-1.5 rounded-full border border-border-medium" />
<span>{localize('com_ui_unset')}</span>
</div>
)}
</div>
<Controller
name={name}
control={control}
defaultValue=""
render={({ field }) => (
<Input
id={name}
type="text"
{...field}
placeholder={
hasValue
? localize('com_ui_mcp_update_var', { 0: config.title })
: localize('com_ui_mcp_enter_var', { 0: config.title })
}
className="w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 dark:border-gray-600 dark:bg-gray-700 dark:text-white sm:text-sm"
/>
)}
/>
{config.description && (
<p
className="text-xs text-text-secondary [&_a]:text-blue-500 [&_a]:hover:text-blue-600 dark:[&_a]:text-blue-400 dark:[&_a]:hover:text-blue-300"
dangerouslySetInnerHTML={{ __html: config.description }}
/>
)}
{errors[name] && <p className="text-xs text-red-500">{errors[name]?.message}</p>}
</div>
);
}
export default function CustomUserVarsSection({
serverName,
fields,
onSave,
onRevoke,
isSubmitting = false,
}: CustomUserVarsSectionProps) {
const localize = useLocalize();
// Fetch auth value flags for the server
const { data: authValuesData } = useMCPAuthValuesQuery(serverName, {
enabled: !!serverName,
});
const {
control,
handleSubmit,
reset,
formState: { errors },
} = useForm<Record<string, string>>({
defaultValues: useMemo(() => {
const initial: Record<string, string> = {};
Object.keys(fields).forEach((key) => {
initial[key] = '';
});
return initial;
}, [fields]),
});
const onFormSubmit = (data: Record<string, string>) => {
onSave(data);
};
const handleRevokeClick = () => {
onRevoke();
// Reset form after revoke
reset();
};
// Don't render if no fields to configure
if (!fields || Object.keys(fields).length === 0) {
return null;
}
return (
<div className="space-y-4">
<form onSubmit={handleSubmit(onFormSubmit)} className="space-y-4">
{Object.entries(fields).map(([key, config]) => {
const hasValue = authValuesData?.authValueFlags?.[key] || false;
return (
<AuthField
key={key}
name={key}
config={config}
hasValue={hasValue}
control={control}
errors={errors}
/>
);
})}
</form>
<div className="flex justify-end gap-2 pt-2">
<Button
onClick={handleRevokeClick}
className="bg-red-600 text-white hover:bg-red-700 dark:hover:bg-red-800"
disabled={isSubmitting}
size="sm"
>
{localize('com_ui_revoke')}
</Button>
<Button
onClick={handleSubmit(onFormSubmit)}
className="bg-green-500 text-white hover:bg-green-600"
disabled={isSubmitting}
size="sm"
>
{isSubmitting ? localize('com_ui_saving') : localize('com_ui_save')}
</Button>
</div>
</div>
);
}

View file

@ -1,139 +0,0 @@
import React from 'react';
import { Loader2, KeyRound, PlugZap, AlertTriangle } from 'lucide-react';
import { MCPServerStatus } from 'librechat-data-provider/dist/types/types/queries';
import {
OGDialog,
OGDialogContent,
OGDialogHeader,
OGDialogTitle,
OGDialogDescription,
} from '~/components/ui/OriginalDialog';
import CustomUserVarsSection from './CustomUserVarsSection';
import ServerInitializationSection from './ServerInitializationSection';
import { useLocalize } from '~/hooks';
export interface ConfigFieldDetail {
title: string;
description: string;
}
interface MCPConfigDialogProps {
isOpen: boolean;
onOpenChange: (isOpen: boolean) => void;
fieldsSchema: Record<string, ConfigFieldDetail>;
initialValues: Record<string, string>;
onSave: (updatedValues: Record<string, string>) => void;
isSubmitting?: boolean;
onRevoke?: () => void;
serverName: string;
serverStatus?: MCPServerStatus;
}
export default function MCPConfigDialog({
isOpen,
onOpenChange,
fieldsSchema,
onSave,
isSubmitting = false,
onRevoke,
serverName,
serverStatus,
}: MCPConfigDialogProps) {
const localize = useLocalize();
const hasFields = Object.keys(fieldsSchema).length > 0;
const dialogTitle = hasFields
? localize('com_ui_configure_mcp_variables_for', { 0: serverName })
: `${serverName} MCP Server`;
const dialogDescription = hasFields
? localize('com_ui_mcp_dialog_desc')
: `Manage connection and settings for the ${serverName} MCP server.`;
// Helper function to render status badge based on connection state
const renderStatusBadge = () => {
if (!serverStatus) {
return null;
}
const { connectionState, requiresOAuth } = serverStatus;
if (connectionState === 'connecting') {
return (
<div className="flex items-center gap-2 rounded-full bg-blue-50 px-2 py-0.5 text-xs font-medium text-blue-600 dark:bg-blue-950 dark:text-blue-400">
<Loader2 className="h-3 w-3 animate-spin" />
<span>{localize('com_ui_connecting')}</span>
</div>
);
}
if (connectionState === 'disconnected') {
if (requiresOAuth) {
return (
<div className="flex items-center gap-2 rounded-full bg-amber-50 px-2 py-0.5 text-xs font-medium text-amber-600 dark:bg-amber-950 dark:text-amber-400">
<KeyRound className="h-3 w-3" />
<span>{localize('com_ui_oauth')}</span>
</div>
);
} else {
return (
<div className="flex items-center gap-2 rounded-full bg-orange-50 px-2 py-0.5 text-xs font-medium text-orange-600 dark:bg-orange-950 dark:text-orange-400">
<PlugZap className="h-3 w-3" />
<span>{localize('com_ui_offline')}</span>
</div>
);
}
}
if (connectionState === 'error') {
return (
<div className="flex items-center gap-2 rounded-full bg-red-50 px-2 py-0.5 text-xs font-medium text-red-600 dark:bg-red-950 dark:text-red-400">
<AlertTriangle className="h-3 w-3" />
<span>{localize('com_ui_error')}</span>
</div>
);
}
if (connectionState === 'connected') {
return (
<div className="flex items-center gap-2 rounded-full bg-green-100 px-2 py-0.5 text-xs font-medium text-green-700 dark:bg-green-900 dark:text-green-300">
<div className="h-1.5 w-1.5 rounded-full bg-green-500" />
<span>{localize('com_ui_active')}</span>
</div>
);
}
return null;
};
return (
<OGDialog open={isOpen} onOpenChange={onOpenChange}>
<OGDialogContent className="flex max-h-[90vh] w-full max-w-md flex-col">
<OGDialogHeader>
<div className="flex items-center gap-3">
<OGDialogTitle>{dialogTitle}</OGDialogTitle>
{renderStatusBadge()}
</div>
<OGDialogDescription>{dialogDescription}</OGDialogDescription>
</OGDialogHeader>
{/* Content */}
<div className="flex-1 overflow-y-auto p-6">
{/* Custom User Variables Section */}
<CustomUserVarsSection
serverName={serverName}
fields={fieldsSchema}
onSave={onSave}
onRevoke={onRevoke || (() => {})}
isSubmitting={isSubmitting}
/>
</div>
{/* Server Initialization Section */}
<ServerInitializationSection
serverName={serverName}
requiresOAuth={serverStatus?.requiresOAuth || false}
/>
</OGDialogContent>
</OGDialog>
);
}

View file

@ -1,190 +0,0 @@
import React from 'react';
import { SettingsIcon, AlertTriangle, Loader2, KeyRound, PlugZap, X } from 'lucide-react';
import type { MCPServerStatus, TPlugin } from 'librechat-data-provider';
import { useLocalize } from '~/hooks';
let localize: ReturnType<typeof useLocalize>;
interface StatusIconProps {
serverName: string;
onConfigClick: (e: React.MouseEvent) => void;
}
interface InitializingStatusProps extends StatusIconProps {
onCancel: (e: React.MouseEvent) => void;
canCancel: boolean;
}
interface MCPServerStatusIconProps {
serverName: string;
serverStatus?: MCPServerStatus;
tool?: TPlugin;
onConfigClick: (e: React.MouseEvent) => void;
isInitializing: boolean;
canCancel: boolean;
onCancel: (e: React.MouseEvent) => void;
hasCustomUserVars?: boolean;
}
/**
* Renders the appropriate status icon for an MCP server based on its state
*/
export default function MCPServerStatusIcon({
serverName,
serverStatus,
tool,
onConfigClick,
isInitializing,
canCancel,
onCancel,
hasCustomUserVars = false,
}: MCPServerStatusIconProps) {
localize = useLocalize();
if (isInitializing) {
return (
<InitializingStatusIcon
serverName={serverName}
onConfigClick={onConfigClick}
onCancel={onCancel}
canCancel={canCancel}
/>
);
}
if (!serverStatus) {
return null;
}
const { connectionState, requiresOAuth } = serverStatus;
if (connectionState === 'connecting') {
return <ConnectingStatusIcon serverName={serverName} onConfigClick={onConfigClick} />;
}
if (connectionState === 'disconnected') {
if (requiresOAuth) {
return <DisconnectedOAuthStatusIcon serverName={serverName} onConfigClick={onConfigClick} />;
}
return <DisconnectedStatusIcon serverName={serverName} onConfigClick={onConfigClick} />;
}
if (connectionState === 'error') {
return <ErrorStatusIcon serverName={serverName} onConfigClick={onConfigClick} />;
}
if (connectionState === 'connected') {
// Only show config button if there are customUserVars to configure
if (hasCustomUserVars) {
const isAuthenticated = tool?.authenticated || requiresOAuth;
return (
<AuthenticatedStatusIcon
serverName={serverName}
onConfigClick={onConfigClick}
isAuthenticated={isAuthenticated}
/>
);
}
return null; // No config button for connected servers without customUserVars
}
return null;
}
function InitializingStatusIcon({ serverName, onCancel, canCancel }: InitializingStatusProps) {
if (canCancel) {
return (
<button
type="button"
onClick={onCancel}
className="flex h-6 w-6 items-center justify-center rounded p-1 hover:bg-red-100 dark:hover:bg-red-900/20"
aria-label={localize('com_ui_cancel')}
title={localize('com_ui_cancel')}
>
<div className="group relative h-4 w-4">
<Loader2 className="h-4 w-4 animate-spin text-blue-500 group-hover:opacity-0" />
<X className="absolute inset-0 h-4 w-4 text-red-500 opacity-0 group-hover:opacity-100" />
</div>
</button>
);
}
return (
<div className="flex h-6 w-6 items-center justify-center rounded p-1">
<Loader2
className="h-4 w-4 animate-spin text-blue-500"
aria-label={localize('com_nav_mcp_status_connecting', { 0: serverName })}
/>
</div>
);
}
function ConnectingStatusIcon({ serverName }: StatusIconProps) {
return (
<div className="flex h-6 w-6 items-center justify-center rounded p-1">
<Loader2
className="h-4 w-4 animate-spin text-blue-500"
aria-label={localize('com_nav_mcp_status_connecting', { 0: serverName })}
/>
</div>
);
}
function DisconnectedOAuthStatusIcon({ serverName, onConfigClick }: StatusIconProps) {
return (
<button
type="button"
onClick={onConfigClick}
className="flex h-6 w-6 items-center justify-center rounded p-1 hover:bg-surface-secondary"
aria-label={localize('com_nav_mcp_configure_server', { 0: serverName })}
>
<KeyRound className="h-4 w-4 text-amber-500" />
</button>
);
}
function DisconnectedStatusIcon({ serverName, onConfigClick }: StatusIconProps) {
return (
<button
type="button"
onClick={onConfigClick}
className="flex h-6 w-6 items-center justify-center rounded p-1 hover:bg-surface-secondary"
aria-label={localize('com_nav_mcp_configure_server', { 0: serverName })}
>
<PlugZap className="h-4 w-4 text-orange-500" />
</button>
);
}
function ErrorStatusIcon({ serverName, onConfigClick }: StatusIconProps) {
return (
<button
type="button"
onClick={onConfigClick}
className="flex h-6 w-6 items-center justify-center rounded p-1 hover:bg-surface-secondary"
aria-label={localize('com_nav_mcp_configure_server', { 0: serverName })}
>
<AlertTriangle className="h-4 w-4 text-red-500" />
</button>
);
}
interface AuthenticatedStatusProps extends StatusIconProps {
isAuthenticated: boolean;
}
function AuthenticatedStatusIcon({
serverName,
onConfigClick,
isAuthenticated,
}: AuthenticatedStatusProps) {
return (
<button
type="button"
onClick={onConfigClick}
className="flex h-6 w-6 items-center justify-center rounded p-1 hover:bg-surface-secondary"
aria-label={localize('com_nav_mcp_configure_server', { 0: serverName })}
>
<SettingsIcon className={`h-4 w-4 ${isAuthenticated ? 'text-green-500' : 'text-gray-400'}`} />
</button>
);
}

View file

@ -1,131 +0,0 @@
import { RefreshCw, Link } from 'lucide-react';
import React, { useState, useCallback } from 'react';
import { useMCPServerInitialization } from '~/hooks/MCP/useMCPServerInitialization';
import { Button } from '~/components/ui';
import { useLocalize } from '~/hooks';
interface ServerInitializationSectionProps {
serverName: string;
requiresOAuth: boolean;
}
export default function ServerInitializationSection({
serverName,
requiresOAuth,
}: ServerInitializationSectionProps) {
const localize = useLocalize();
const [oauthUrl, setOauthUrl] = useState<string | null>(null);
// Use the shared initialization hook
const { initializeServer, isLoading, connectionStatus, cancelOAuthFlow, isCancellable } =
useMCPServerInitialization({
onOAuthStarted: (name, url) => {
// Store the OAuth URL locally for display
setOauthUrl(url);
},
onSuccess: () => {
// Clear OAuth URL on success
setOauthUrl(null);
},
});
const serverStatus = connectionStatus[serverName];
const isConnected = serverStatus?.connectionState === 'connected';
const canCancel = isCancellable(serverName);
const handleInitializeClick = useCallback(() => {
setOauthUrl(null);
initializeServer(serverName);
}, [initializeServer, serverName]);
const handleCancelClick = useCallback(() => {
setOauthUrl(null);
cancelOAuthFlow(serverName);
}, [cancelOAuthFlow, serverName]);
// Show subtle reinitialize option if connected
if (isConnected) {
return (
<div className="flex justify-start">
<button
onClick={handleInitializeClick}
disabled={isLoading}
className="flex items-center gap-1 text-xs text-gray-400 hover:text-gray-600 disabled:opacity-50 dark:text-gray-500 dark:hover:text-gray-400"
>
<RefreshCw className={`h-3 w-3 ${isLoading ? 'animate-spin' : ''}`} />
{isLoading ? localize('com_ui_loading') : localize('com_ui_reinitialize')}
</button>
</div>
);
}
return (
<div className="rounded-lg border border-[#991b1b] bg-[#2C1315] p-4">
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<span className="text-sm font-medium text-red-700 dark:text-red-300">
{requiresOAuth
? localize('com_ui_mcp_not_authenticated', { 0: serverName })
: localize('com_ui_mcp_not_initialized', { 0: serverName })}
</span>
</div>
{/* Only show authenticate button when OAuth URL is not present */}
{!oauthUrl && (
<Button
onClick={handleInitializeClick}
disabled={isLoading}
className="flex items-center gap-2 bg-blue-600 px-4 py-2 text-white hover:bg-blue-700 dark:hover:bg-blue-800"
>
{isLoading ? (
<>
<RefreshCw className="h-4 w-4 animate-spin" />
{localize('com_ui_loading')}
</>
) : (
<>
<RefreshCw className="h-4 w-4" />
{requiresOAuth
? localize('com_ui_authenticate')
: localize('com_ui_mcp_initialize')}
</>
)}
</Button>
)}
</div>
{/* OAuth URL display */}
{oauthUrl && (
<div className="mt-4 rounded-lg border border-blue-200 bg-blue-50 p-3 dark:border-blue-700 dark:bg-blue-900/20">
<div className="mb-2 flex items-center gap-2">
<div className="flex h-4 w-4 items-center justify-center rounded-full bg-blue-500">
<Link className="h-2.5 w-2.5 text-white" />
</div>
<span className="text-sm font-medium text-blue-700 dark:text-blue-300">
{localize('com_ui_auth_url')}
</span>
</div>
<div className="flex items-center gap-2">
<Button
onClick={() => window.open(oauthUrl, '_blank', 'noopener,noreferrer')}
className="flex-1 bg-blue-600 text-white hover:bg-blue-700 dark:hover:bg-blue-800"
>
{localize('com_ui_continue_oauth')}
</Button>
<Button
onClick={handleCancelClick}
disabled={!canCancel}
className="bg-gray-200 text-gray-700 hover:bg-gray-300 disabled:cursor-not-allowed disabled:opacity-50 dark:bg-gray-700 dark:text-gray-200 dark:hover:bg-gray-600"
title={!canCancel ? 'disabled' : undefined}
>
{localize('com_ui_cancel')}
</Button>
</div>
<p className="mt-2 text-xs text-blue-600 dark:text-blue-400">
{localize('com_ui_oauth_flow_desc')}
</p>
</div>
)}
</div>
);
}

View file

@ -1,186 +0,0 @@
import React, { useState, useCallback, useRef, useEffect } from 'react';
import { TranslationKeys, useLocalize } from '~/hooks';
import { Minus, Plus } from 'lucide-react';
interface ModelParametersProps {
label?: string;
ariaLabel?: string;
min?: number;
max?: number;
step?: number;
stepClick?: number;
initialValue?: number;
showButtons?: boolean;
onChange?: (value: number) => void;
disabled?: boolean;
}
const ModelParameters: React.FC<ModelParametersProps> = ({
label = 'Value',
ariaLabel = 'Value',
min = 0,
max = 100,
step = 1,
stepClick = 1,
initialValue = 0,
showButtons = true,
onChange,
disabled = false,
}) => {
const localize = useLocalize();
const [value, setValue] = useState(initialValue);
const [isHovering, setIsHovering] = useState(false);
const rangeRef = useRef<HTMLInputElement>(null);
const id = `model-parameter-${ariaLabel.toLowerCase().replace(/\s+/g, '-')}`;
const displayLabel =
label && label.startsWith('com_') ? localize(label as TranslationKeys) : label;
const getDecimalPlaces = (num: number) => {
const match = ('' + num).match(/(?:\.(\d+))?(?:[eE]([+-]?\d+))?$/);
if (!match) {
return 0;
}
return Math.max(0, (match[1] ? match[1].length : 0) - (match[2] ? +match[2] : 0));
};
const decimalPlaces = getDecimalPlaces(step);
const handleChange = useCallback(
(newValue: number) => {
const clampedValue = Math.min(Math.max(newValue, min), max);
const finalValue = Object.is(clampedValue, -0) ? 0 : clampedValue;
setValue(finalValue);
onChange?.(finalValue);
},
[min, max, onChange],
);
const handleInputChange = useCallback(
(e: React.ChangeEvent<HTMLInputElement>) => {
handleChange(parseFloat(e.target.value));
},
[handleChange],
);
const handleIncrement = useCallback(() => {
handleChange(value + stepClick);
}, [value, stepClick, handleChange]);
const handleDecrement = useCallback(() => {
handleChange(value - stepClick);
}, [value, stepClick, handleChange]);
const handleKeyDown = useCallback(
(e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.key === 'ArrowRight' || e.key === 'ArrowUp') {
e.preventDefault();
handleIncrement();
} else if (e.key === 'ArrowLeft' || e.key === 'ArrowDown') {
e.preventDefault();
handleDecrement();
}
},
[handleIncrement, handleDecrement],
);
useEffect(() => {
const rangeElement = rangeRef.current;
if (rangeElement) {
const percentage = ((value - min) / (max - min)) * 100;
rangeElement.style.backgroundSize = `${percentage}% 100%`;
}
}, [value, min, max]);
return (
<div className="w-full">
<div className="mb-2 flex items-center justify-between">
<label
htmlFor={id}
className={`text-sm font-medium ${disabled ? 'text-gray-400 dark:text-gray-400' : ''}`}
>
{displayLabel}
</label>
<div className="flex items-center gap-2">
<output
htmlFor={id}
className={`select-none text-sm font-medium ${
disabled ? 'text-gray-400 dark:text-gray-400' : ''
}`}
aria-live="polite"
>
{value.toFixed(decimalPlaces).replace('-0.00', '0.00')}
</output>
{showButtons && (
<div className="flex items-center gap-1">
<button
type="button"
onClick={handleDecrement}
className={`rounded-md p-1 transition-colors ${
disabled
? 'cursor-not-allowed text-gray-400 dark:text-gray-400'
: 'hover:bg-gray-200 dark:hover:bg-gray-700'
}`}
aria-label={`Decrease ${label}`}
disabled={disabled}
>
<Minus size={16} />
</button>
<button
type="button"
onClick={handleIncrement}
className={`rounded-md p-1 transition-colors ${
disabled
? 'cursor-not-allowed text-gray-400 dark:text-gray-400'
: 'hover:bg-gray-200 dark:hover:bg-gray-700'
}`}
aria-label={`Increase ${label}`}
disabled={disabled}
>
<Plus size={16} />
</button>
</div>
)}
</div>
</div>
<div className="relative w-full">
<input
ref={rangeRef}
type="range"
id={id}
min={min}
max={max}
step={step}
value={value}
onChange={handleInputChange}
onKeyDown={handleKeyDown}
onMouseEnter={() => setIsHovering(true)}
onMouseLeave={() => setIsHovering(false)}
className={`slider-thumb h-2 w-full appearance-none rounded-lg bg-gradient-to-r from-gray-500 to-gray-500 bg-no-repeat focus:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 ${
disabled ? 'cursor-not-allowed opacity-50' : 'cursor-pointer'
}`}
tabIndex={0}
style={{
backgroundSize: '50% 100%',
backgroundPosition: 'left',
}}
aria-valuemin={min}
aria-valuemax={max}
aria-valuenow={value}
aria-valuetext={`${value.toFixed(decimalPlaces).replace('-0.00', '0.00')}`}
disabled={disabled}
/>
{isHovering ? (
<div className="trab mt-1 flex justify-between">
<span className="text-xs text-gray-500">{min}</span>
<span className="text-xs text-gray-500">{max}</span>
</div>
) : (
<div className="mt-1" style={{ height: '1rem' }}></div>
)}
</div>
</div>
);
};
export default React.memo(ModelParameters);

View file

@ -1,157 +0,0 @@
import { Search, X } from 'lucide-react';
import React, { useState, useMemo, useCallback, useRef } from 'react';
import { useLocalize } from '~/hooks';
import { cn } from '~/utils';
/** This is a generic that can be added to Menu and Select components */
export default function MultiSearch({
value,
onChange,
placeholder,
className = '',
}: {
value: string | null;
onChange: (filter: string) => void;
placeholder?: string;
className?: string;
}) {
const localize = useLocalize();
const inputRef = useRef<HTMLInputElement>(null);
const onChangeHandler: React.ChangeEventHandler<HTMLInputElement> = useCallback(
(e) => onChange(e.target.value),
[onChange],
);
const clearSearch = () => {
onChange('');
setTimeout(() => {
inputRef.current?.focus();
}, 0);
};
return (
<div
className={cn(
'focus:to-surface-primary/50 group sticky left-0 top-0 z-10 flex h-12 items-center gap-2 bg-gradient-to-b from-surface-tertiary-alt from-65% to-transparent px-3 py-2 text-text-primary transition-colors duration-300 focus:bg-gradient-to-b focus:from-surface-primary',
className,
)}
>
<Search
className="h-4 w-4 text-text-secondary-alt transition-colors duration-300"
aria-hidden={'true'}
/>
<input
ref={inputRef}
type="text"
value={value ?? ''}
onChange={onChangeHandler}
placeholder={placeholder ?? localize('com_ui_select_search_model')}
aria-label="Search Model"
className="flex-1 rounded-md border-none bg-transparent px-2.5 py-2 text-sm placeholder-text-secondary focus:outline-none focus:ring-1 focus:ring-ring-primary"
/>
<button
className={cn(
'relative flex h-5 w-5 items-center justify-end rounded-md text-text-secondary-alt',
value?.length ?? 0 ? 'cursor-pointer opacity-100' : 'hidden',
)}
aria-label={'Clear search'}
onClick={clearSearch}
tabIndex={0}
>
<X
aria-hidden={'true'}
className={cn(
'text-text-secondary-alt',
value?.length ?? 0 ? 'cursor-pointer opacity-100' : 'opacity-0',
)}
/>
</button>
</div>
);
}
/**
* Helper function that will take a multiSearch input
* @param node
*/
function defaultGetStringKey(node: unknown): string {
if (typeof node === 'string') {
// BUGFIX: Detect psedeo separators and make sure they don't appear in the list when filtering items
// it makes sure (for the most part) that the model name starts and ends with dashes
// The long-term fix here would be to enable seperators (model groupings) but there's no
// feature mocks for such a thing yet
if (node.startsWith('---') && node.endsWith('---')) {
return '';
}
return node.toUpperCase();
}
// This should be a noop, but it's here for redundancy
return '';
}
/**
* Hook for conditionally making a multi-element list component into a sortable component
* Returns a RenderNode for search input when search functionality is available
* @param availableOptions
* @param placeholder
* @param getTextKeyOverride
* @param className - Additional classnames to add to the search container
* @param disabled - If the search should be disabled
* @returns
*/
export function useMultiSearch<OptionsType extends unknown[]>({
availableOptions = [] as unknown as OptionsType,
placeholder,
getTextKeyOverride,
className,
disabled = false,
}: {
availableOptions?: OptionsType;
placeholder?: string;
getTextKeyOverride?: (node: OptionsType[0]) => string;
className?: string;
disabled?: boolean;
}): [OptionsType, React.ReactNode] {
const [filterValue, setFilterValue] = useState<string | null>(null);
// We conditionally show the search when there's more than 10 elements in the menu
const shouldShowSearch = availableOptions.length > 10 && !disabled;
// Define the helper function used to enable search
// If this is invalidly described, we will assume developer error - tf. avoid rendering
const getTextKeyHelper = getTextKeyOverride || defaultGetStringKey;
// Iterate said options
const filteredOptions = useMemo(() => {
const currentFilter = filterValue ?? '';
if (!shouldShowSearch || !currentFilter || !availableOptions.length) {
// Don't render if available options aren't present, there's no filter active
return availableOptions;
}
// Filter through the values, using a simple text-based search
// nothing too fancy, but we can add a better search algo later if we need
const upperFilterValue = currentFilter.toUpperCase();
return availableOptions.filter((value) =>
getTextKeyHelper(value).includes(upperFilterValue),
) as OptionsType;
}, [availableOptions, getTextKeyHelper, filterValue, shouldShowSearch]);
const onSearchChange = useCallback(
(nextFilterValue: string) => setFilterValue(nextFilterValue),
[],
);
const searchRender = shouldShowSearch ? (
<MultiSearch
value={filterValue}
className={className}
onChange={onSearchChange}
placeholder={placeholder}
/>
) : null;
return [filteredOptions, searchRender];
}

View file

@ -1,144 +0,0 @@
import React, { useRef } from 'react';
import {
Select,
SelectArrow,
SelectItem,
SelectItemCheck,
SelectLabel,
SelectPopover,
SelectProvider,
} from '@ariakit/react';
import { cn } from '~/utils';
interface MultiSelectProps<T extends string> {
items: T[];
label?: string;
placeholder?: string;
defaultSelectedValues?: T[];
onSelectedValuesChange?: (values: T[]) => void;
renderSelectedValues?: (values: T[], placeholder?: string) => React.ReactNode;
className?: string;
itemClassName?: string;
labelClassName?: string;
selectClassName?: string;
selectIcon?: React.ReactNode;
popoverClassName?: string;
selectItemsClassName?: string;
selectedValues: T[];
setSelectedValues: (values: T[]) => void;
renderItemContent?: (
value: T,
defaultContent: React.ReactNode,
isSelected: boolean,
) => React.ReactNode;
}
function defaultRender<T extends string>(values: T[], placeholder?: string) {
if (values.length === 0) {
return placeholder || 'Select...';
}
if (values.length === 1) {
return values[0];
}
return `${values.length} items selected`;
}
export default function MultiSelect<T extends string>({
items,
label,
placeholder = 'Select...',
defaultSelectedValues = [],
onSelectedValuesChange,
renderSelectedValues = defaultRender,
className,
selectIcon,
itemClassName,
labelClassName,
selectClassName,
popoverClassName,
selectItemsClassName,
selectedValues = [],
setSelectedValues,
renderItemContent,
}: MultiSelectProps<T>) {
const selectRef = useRef<HTMLButtonElement>(null);
const handleValueChange = (values: T[]) => {
setSelectedValues(values);
if (onSelectedValuesChange) {
onSelectedValuesChange(values);
}
};
return (
<div className={className}>
<SelectProvider value={selectedValues} setValue={handleValueChange}>
{label && (
<SelectLabel className={cn('mb-1 block text-sm text-text-primary', labelClassName)}>
{label}
</SelectLabel>
)}
<Select
ref={selectRef}
className={cn(
'flex items-center justify-between gap-2 rounded-xl px-3 py-2 text-sm',
'bg-surface-tertiary text-text-primary shadow-sm hover:cursor-pointer hover:bg-surface-hover',
'outline-none focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-white focus-visible:ring-opacity-75',
selectClassName,
selectedValues.length > 0 && selectItemsClassName != null && selectItemsClassName,
)}
onChange={(e) => e.stopPropagation()}
>
{selectIcon && selectIcon}
<span className="mr-auto hidden truncate md:block">
{renderSelectedValues(selectedValues, placeholder)}
</span>
<SelectArrow className="ml-1 hidden stroke-1 text-base opacity-75 md:block" />
</Select>
<SelectPopover
gutter={4}
sameWidth
modal
unmountOnHide
finalFocus={selectRef}
className={cn(
'animate-popover z-50 flex max-h-[300px]',
'flex-col overflow-auto overscroll-contain rounded-xl',
'bg-surface-secondary px-1.5 py-1 text-text-primary shadow-lg',
'border border-border-light',
'outline-none',
popoverClassName,
)}
>
{items.map((value) => {
const defaultContent = (
<>
<SelectItemCheck className="mr-0.5 text-primary" />
<span className="truncate">{value}</span>
</>
);
const isCurrentItemSelected = selectedValues.includes(value);
return (
<SelectItem
key={value}
value={value}
className={cn(
'flex items-center gap-2 rounded-lg px-2 py-1.5 hover:cursor-pointer',
'scroll-m-1 outline-none transition-colors',
'hover:bg-black/[0.075] dark:hover:bg-white/10',
'data-[active-item]:bg-black/[0.075] dark:data-[active-item]:bg-white/10',
'w-full min-w-0 text-sm',
itemClassName,
)}
>
{renderItemContent
? renderItemContent(value, defaultContent, isCurrentItemSelected)
: defaultContent}
</SelectItem>
);
})}
</SelectPopover>
</SelectProvider>
</div>
);
}

View file

@ -1,231 +0,0 @@
import React, { useState, useRef } from 'react';
import {
Listbox,
ListboxButton,
Label,
ListboxOptions,
ListboxOption,
Transition,
} from '@headlessui/react';
import { Wrench, ArrowRight } from 'lucide-react';
import { CheckMark } from '~/components/svg';
import useOnClickOutside from '~/hooks/useOnClickOutside';
import { useMultiSearch } from './MultiSearch';
import { cn } from '~/utils/';
import type { TPlugin } from 'librechat-data-provider';
export type TMultiSelectDropDownProps = {
title?: string;
value: Array<{ icon?: string; name?: string; isButton?: boolean }>;
disabled?: boolean;
setSelected: (option: string) => void;
availableValues: TPlugin[];
showAbove?: boolean;
showLabel?: boolean;
containerClassName?: string;
optionsClassName?: string;
labelClassName?: string;
isSelected: (value: string) => boolean;
className?: string;
searchPlaceholder?: string;
optionValueKey?: string;
};
function MultiSelectDropDown({
title = 'Plugins',
value,
disabled,
setSelected,
availableValues,
showAbove = false,
showLabel = true,
containerClassName,
optionsClassName = '',
labelClassName = '',
isSelected,
className,
searchPlaceholder,
optionValueKey = 'value',
}: TMultiSelectDropDownProps) {
const [isOpen, setIsOpen] = useState(false);
const menuRef = useRef(null);
const excludeIds = ['select-plugin', 'plugins-label', 'selected-plugins'];
useOnClickOutside(menuRef, () => setIsOpen(false), excludeIds);
const handleSelect: (value: string) => void = (option) => {
setSelected(option);
setIsOpen(true);
};
// input will appear near the top of the menu, allowing correct filtering of different model menu items. This will
// reset once the component is unmounted (as per a normal search)
const [filteredValues, searchRender] = useMultiSearch<TPlugin[]>({
availableOptions: availableValues,
placeholder: searchPlaceholder,
getTextKeyOverride: (option) => (option.name || '').toUpperCase(),
});
const hasSearchRender = Boolean(searchRender);
const options = hasSearchRender ? filteredValues : availableValues;
const transitionProps = { className: 'top-full mt-3' };
if (showAbove) {
transitionProps.className = 'bottom-full mb-3';
}
const openProps = { open: isOpen };
return (
<div className={cn('flex items-center justify-center gap-2', containerClassName ?? '')}>
<div className="relative w-full">
{/* the function typing is correct but there's still an issue here */}
{/* @ts-ignore */}
<Listbox value={value} onChange={handleSelect} disabled={disabled}>
{() => (
<>
<ListboxButton
className={cn(
'relative flex w-full cursor-default flex-col rounded-md border border-black/10 bg-white py-2 pl-3 pr-10 text-left focus:outline-none focus:ring-0 focus:ring-offset-0 dark:border-gray-600 dark:border-white/20 dark:bg-gray-800 sm:text-sm',
className ?? '',
)}
id={excludeIds[0]}
onClick={() => setIsOpen((prev) => !prev)}
{...openProps}
>
{' '}
{showLabel && (
<Label
className={cn('block text-xs text-gray-700 dark:text-gray-500', labelClassName)}
id={excludeIds[1]}
data-headlessui-state=""
>
{title}
</Label>
)}
<span className="inline-flex w-full truncate" id={excludeIds[2]}>
<span
className={cn(
'flex h-6 items-center gap-1 truncate text-sm text-gray-800 dark:text-white',
!showLabel ? 'text-xs' : '',
)}
>
{!showLabel && title.length > 0 && (
<span className="text-xs text-gray-700 dark:text-gray-500">{title}:</span>
)}
<span className="flex h-6 items-center gap-1 truncate">
<div className="flex gap-1">
{value.map((v, i) => (
<div
key={i}
className="relative"
style={{ width: '16px', height: '16px' }}
>
{v.icon ? (
<img
src={v.icon}
alt={`${v} logo`}
className="h-full w-full rounded-sm bg-white"
/>
) : (
<Wrench className="h-full w-full rounded-sm bg-white" />
)}
<div className="absolute inset-0 rounded-sm ring-1 ring-inset ring-black/10" />
</div>
))}
</div>
</span>
</span>
</span>
<span className="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-2">
<svg
stroke="currentColor"
fill="none"
strokeWidth="2"
viewBox="0 0 24 24"
strokeLinecap="round"
strokeLinejoin="round"
className="h-4 w-4 text-gray-400"
height="1em"
width="1em"
xmlns="http://www.w3.org/2000/svg"
style={showAbove ? { transform: 'scaleY(-1)' } : {}}
>
<polyline points="6 9 12 15 18 9"></polyline>
</svg>
</span>
</ListboxButton>
<Transition
show={isOpen}
as={React.Fragment}
leave="transition ease-in duration-150"
leaveFrom="opacity-100"
leaveTo="opacity-0"
{...transitionProps}
>
<ListboxOptions
ref={menuRef}
className={cn(
'absolute z-50 mt-2 max-h-60 w-full overflow-auto rounded bg-white text-base text-xs ring-1 ring-black/10 focus:outline-none dark:bg-gray-800 dark:ring-white/20 dark:last:border-0 md:w-[100%]',
optionsClassName,
)}
>
{searchRender}
{options.map((option, i: number) => {
if (!option) {
return null;
}
const selected = isSelected(option[optionValueKey]);
return (
<ListboxOption
key={i}
value={option[optionValueKey]}
className="group relative flex h-[42px] cursor-pointer select-none items-center overflow-hidden border-b border-black/10 pl-3 pr-9 text-gray-800 last:border-0 hover:bg-gray-20 dark:border-white/20 dark:text-white dark:hover:bg-gray-700"
>
<span className="flex items-center gap-1.5 truncate">
{!option.isButton && (
<span className="h-6 w-6 shrink-0">
<div className="relative" style={{ width: '100%', height: '100%' }}>
{option.icon ? (
<img
src={option.icon}
alt={`${option.name} logo`}
className="h-full w-full rounded-sm bg-white"
/>
) : (
<Wrench className="h-full w-full rounded-sm bg-white" />
)}
<div className="absolute inset-0 rounded-sm ring-1 ring-inset ring-black/10"></div>
</div>
</span>
)}
<span
className={cn(
'flex h-6 items-center gap-1 text-gray-800 dark:text-gray-200',
selected ? 'font-semibold' : '',
)}
>
{option.name}
</span>
{option.isButton && (
<span className="absolute inset-y-0 right-0 flex items-center pr-3 text-gray-800 dark:text-gray-200">
<ArrowRight />
</span>
)}
{selected && !option.isButton && (
<span className="absolute inset-y-0 right-0 flex items-center pr-3 text-gray-800 dark:text-gray-200">
<CheckMark />
</span>
)}
</span>
</ListboxOption>
);
})}
</ListboxOptions>
</Transition>
</>
)}
</Listbox>
</div>
</div>
);
}
export default MultiSelectDropDown;

View file

@ -1,156 +0,0 @@
import { Wrench } from 'lucide-react';
import { Root, Trigger, Content, Portal } from '@radix-ui/react-popover';
import type { TPlugin } from 'librechat-data-provider';
import MenuItem from '~/components/Chat/Menus/UI/MenuItem';
import { useMultiSearch } from './MultiSearch';
import { cn } from '~/utils/';
type SelectDropDownProps = {
title?: string;
value: Array<{ icon?: string; name?: string; isButton?: boolean }>;
disabled?: boolean;
setSelected: (option: string) => void;
availableValues: TPlugin[];
showAbove?: boolean;
showLabel?: boolean;
containerClassName?: string;
isSelected: (value: string) => boolean;
className?: string;
optionValueKey?: string;
searchPlaceholder?: string;
};
function MultiSelectPop({
title: _title = 'Plugins',
value,
setSelected,
availableValues,
showAbove = false,
showLabel = true,
containerClassName,
isSelected,
optionValueKey = 'value',
searchPlaceholder,
}: SelectDropDownProps) {
// const localize = useLocalize();
const title = _title;
const excludeIds = ['select-plugin', 'plugins-label', 'selected-plugins'];
// Detemine if we should to convert this component into a searchable select
const [filteredValues, searchRender] = useMultiSearch<TPlugin[]>({
availableOptions: availableValues,
placeholder: searchPlaceholder,
getTextKeyOverride: (option) => (option.name || '').toUpperCase(),
});
const hasSearchRender = Boolean(searchRender);
const options = hasSearchRender ? filteredValues : availableValues;
return (
<Root>
<div className={cn('flex items-center justify-center gap-2', containerClassName ?? '')}>
<div className="relative">
<Trigger asChild>
<button
data-testid="select-dropdown-button"
className={cn(
'relative flex flex-col rounded-md border border-black/10 bg-white py-2 pl-3 pr-10 text-left focus:outline-none focus:ring-0 focus:ring-offset-0 dark:border-gray-700 dark:bg-gray-800 dark:bg-gray-800 sm:text-sm',
'pointer-cursor font-normal',
'hover:bg-gray-50 radix-state-open:bg-gray-50 dark:hover:bg-gray-700 dark:radix-state-open:bg-gray-700',
)}
>
{' '}
{showLabel && (
<label className="block text-xs text-gray-700 dark:text-gray-500 ">{title}</label>
)}
<span className="inline-flex" id={excludeIds[2]}>
<span
className={cn(
'flex h-6 items-center gap-1 text-sm text-gray-800 dark:text-white',
!showLabel ? 'text-xs' : '',
)}
>
{/* {!showLabel && title.length > 0 && (
<span className="text-xs text-gray-700 dark:text-gray-500">{title}:</span>
)} */}
<span className="flex items-center gap-1 ">
<div className="flex gap-1">
{value.length === 0 && 'None selected'}
{value.map((v, i) => (
<div key={i} className="relative">
{v.icon ? (
<img src={v.icon} alt={`${v} logo`} className="icon-lg rounded-sm" />
) : (
<Wrench className="icon-lg rounded-sm bg-white" />
)}
<div className="absolute inset-0 rounded-sm ring-1 ring-inset ring-black/10" />
</div>
))}
</div>
</span>
</span>
</span>
<span className="absolute inset-y-0 right-0 flex items-center pr-2">
<svg
stroke="currentColor"
fill="none"
strokeWidth="2"
viewBox="0 0 24 24"
strokeLinecap="round"
strokeLinejoin="round"
className="h-4 w-4 text-gray-400"
height="1em"
width="1em"
xmlns="http://www.w3.org/2000/svg"
style={showAbove ? { transform: 'scaleY(-1)' } : {}}
>
<polyline points="6 9 12 15 18 9"></polyline>
</svg>
</span>
</button>
</Trigger>
<Portal>
<Content
side="bottom"
align="center"
className={cn(
'mt-2 max-h-[52vh] min-w-full overflow-hidden overflow-y-auto rounded-lg border border-gray-200 bg-white shadow-lg dark:border-gray-700 dark:bg-gray-700 dark:text-white',
hasSearchRender && 'relative',
)}
>
{searchRender}
{options.map((option) => {
if (!option) {
return null;
}
const selected = isSelected(option[optionValueKey]);
return (
<MenuItem
key={`${option[optionValueKey]}`}
title={option.name}
value={option[optionValueKey]}
selected={selected}
onClick={() => setSelected(option.pluginKey)}
icon={
option.icon ? (
<img
src={option.icon}
alt={`${option.name} logo`}
className="icon-sm mr-1 rounded-sm bg-cover"
/>
) : (
<Wrench className="icon-sm mr-1 rounded-sm bg-white bg-cover dark:bg-gray-800" />
)
}
/>
);
})}
</Content>
</Portal>
</div>
</div>
</Root>
);
}
export default MultiSelectPop;

View file

@ -1,109 +0,0 @@
import { forwardRef, ReactNode, Ref } from 'react';
import {
OGDialogTitle,
OGDialogClose,
OGDialogFooter,
OGDialogHeader,
OGDialogContent,
OGDialogDescription,
} from './OriginalDialog';
import { useLocalize } from '~/hooks';
import { Button } from './Button';
import { Spinner } from '../svg';
import { cn } from '~/utils/';
type SelectionProps = {
selectHandler?: () => void;
selectClasses?: string;
selectText?: string | ReactNode;
isLoading?: boolean;
};
type DialogTemplateProps = {
title: string;
description?: string;
main?: ReactNode;
buttons?: ReactNode;
leftButtons?: ReactNode;
selection?: SelectionProps;
className?: string;
overlayClassName?: string;
headerClassName?: string;
mainClassName?: string;
footerClassName?: string;
showCloseButton?: boolean;
showCancelButton?: boolean;
onClose?: () => void;
};
const OGDialogTemplate = forwardRef((props: DialogTemplateProps, ref: Ref<HTMLDivElement>) => {
const localize = useLocalize();
const {
title,
main,
buttons,
selection,
className,
leftButtons,
description = '',
mainClassName,
headerClassName,
footerClassName,
showCloseButton,
overlayClassName,
showCancelButton = true,
} = props;
const { selectHandler, selectClasses, selectText, isLoading } = selection || {};
const defaultSelect =
'bg-gray-800 text-white transition-colors hover:bg-gray-700 disabled:cursor-not-allowed disabled:opacity-50 dark:bg-gray-200 dark:text-gray-800 dark:hover:bg-gray-200';
return (
<OGDialogContent
overlayClassName={overlayClassName}
showCloseButton={showCloseButton}
ref={ref}
className={cn('w-11/12 border-none bg-background text-foreground', className ?? '')}
onClick={(e) => e.stopPropagation()}
>
<OGDialogHeader className={cn(headerClassName ?? '')}>
<OGDialogTitle>{title}</OGDialogTitle>
{description && (
<OGDialogDescription className="items-center justify-center">
{description}
</OGDialogDescription>
)}
</OGDialogHeader>
<div className={cn('px-0 py-2', mainClassName)}>{main != null ? main : null}</div>
<OGDialogFooter className={footerClassName}>
<div>
{leftButtons != null ? (
<div className="mt-3 flex h-auto gap-3 max-sm:w-full max-sm:flex-col sm:mt-0 sm:flex-row">
{leftButtons}
</div>
) : null}
</div>
<div className="flex h-auto gap-3 max-sm:w-full max-sm:flex-col sm:flex-row">
{showCancelButton && (
<OGDialogClose asChild>
<Button variant="outline">{localize('com_ui_cancel')}</Button>
</OGDialogClose>
)}
{buttons != null ? buttons : null}
{selection ? (
<OGDialogClose
onClick={selectHandler}
disabled={isLoading}
className={`${
selectClasses ?? defaultSelect
} flex h-10 items-center justify-center rounded-lg border-none px-4 py-2 text-sm disabled:opacity-80 max-sm:order-first max-sm:w-full sm:order-none`}
>
{isLoading === true ? <Spinner className="size-4 text-white" /> : selectText}
</OGDialogClose>
) : null}
</div>
</OGDialogFooter>
</OGDialogContent>
);
});
export default OGDialogTemplate;

View file

@ -1,141 +0,0 @@
import * as React from 'react';
import * as DialogPrimitive from '@radix-ui/react-dialog';
import { X } from 'lucide-react';
import { cn } from '~/utils';
interface OGDialogProps extends DialogPrimitive.DialogProps {
triggerRef?: React.RefObject<HTMLButtonElement | HTMLInputElement | null>;
triggerRefs?: React.RefObject<HTMLButtonElement | HTMLInputElement | null>[];
}
const Dialog = React.forwardRef<HTMLDivElement, OGDialogProps>(
({ children, triggerRef, triggerRefs, onOpenChange, ...props }, _ref) => {
const handleOpenChange = (open: boolean) => {
if (!open && triggerRef?.current) {
setTimeout(() => {
triggerRef.current?.focus();
}, 0);
}
if (triggerRefs?.length) {
triggerRefs.forEach((ref) => {
if (ref?.current) {
setTimeout(() => {
ref.current?.focus();
}, 0);
}
});
}
onOpenChange?.(open);
};
return (
<DialogPrimitive.Root {...props} onOpenChange={handleOpenChange}>
{children}
</DialogPrimitive.Root>
);
},
);
const DialogTrigger = DialogPrimitive.Trigger;
const DialogPortal = DialogPrimitive.Portal;
const DialogClose = DialogPrimitive.Close;
export const DialogOverlay = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Overlay>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Overlay
ref={ref}
className={cn(
'fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0',
className,
)}
{...props}
/>
));
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName;
type DialogContentProps = React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content> & {
showCloseButton?: boolean;
disableScroll?: boolean;
overlayClassName?: string;
};
const DialogContent = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Content>,
DialogContentProps
>(({ className, overlayClassName, showCloseButton = true, children, ...props }, ref) => (
<DialogPortal>
<DialogOverlay className={overlayClassName} />
<DialogPrimitive.Content
ref={ref}
className={cn(
'max-w-11/12 fixed left-[50%] top-[50%] z-50 grid w-full translate-x-[-50%] translate-y-[-50%] gap-4 rounded-2xl bg-background p-6 text-text-primary shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%]',
className,
)}
{...props}
>
{children}
{showCloseButton && (
<DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-ring-primary ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground">
<X className="h-4 w-4" />
{/* eslint-disable-next-line i18next/no-literal-string */}
<span className="sr-only">Close</span>
</DialogPrimitive.Close>
)}
</DialogPrimitive.Content>
</DialogPortal>
));
DialogContent.displayName = DialogPrimitive.Content.displayName;
const DialogHeader = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
<div className={cn('flex flex-col space-y-1.5 text-center sm:text-left', className)} {...props} />
);
DialogHeader.displayName = 'DialogHeader';
const DialogFooter = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn('flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2', className)}
{...props}
/>
);
DialogFooter.displayName = 'DialogFooter';
const DialogTitle = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Title>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Title
ref={ref}
className={cn('text-lg font-semibold leading-none tracking-tight', className)}
{...props}
/>
));
DialogTitle.displayName = DialogPrimitive.Title.displayName;
const DialogDescription = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Description>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Description
ref={ref}
className={cn('text-sm text-muted-foreground', className)}
{...props}
/>
));
DialogDescription.displayName = DialogPrimitive.Description.displayName;
export {
Dialog as OGDialog,
DialogPortal as OGDialogPortal,
DialogOverlay as OGDialogOverlay,
DialogClose as OGDialogClose,
DialogTrigger as OGDialogTrigger,
DialogContent as OGDialogContent,
DialogHeader as OGDialogHeader,
DialogFooter as OGDialogFooter,
DialogTitle as OGDialogTitle,
DialogDescription as OGDialogDescription,
};

View file

@ -1,105 +0,0 @@
import * as React from 'react';
import { ChevronLeft, ChevronRight, MoreHorizontal } from 'lucide-react';
import { ButtonProps, buttonVariants } from './Button';
import { cn } from '~/utils';
const Pagination = ({ className, ...props }: React.ComponentProps<'nav'>) => (
<nav
role="navigation"
aria-label="pagination"
className={cn('mx-auto flex w-full justify-center', className)}
{...props}
/>
);
Pagination.displayName = 'Pagination';
const PaginationContent = React.forwardRef<HTMLUListElement, React.ComponentProps<'ul'>>(
({ className, ...props }, ref) => (
<ul ref={ref} className={cn('flex flex-row items-center gap-1', className)} {...props} />
),
);
PaginationContent.displayName = 'PaginationContent';
const PaginationItem = React.forwardRef<HTMLLIElement, React.ComponentProps<'li'>>(
({ className, ...props }, ref) => <li ref={ref} className={cn('', className)} {...props} />,
);
PaginationItem.displayName = 'PaginationItem';
type PaginationLinkProps = {
isActive?: boolean;
} & Pick<ButtonProps, 'size'> &
React.ComponentProps<'a'>;
const PaginationLink = ({
className,
isActive = false,
size = 'icon',
children,
...props
}: PaginationLinkProps) => (
<a
aria-current={isActive ? 'page' : undefined}
className={cn(
buttonVariants({
variant: isActive ? 'outline' : 'ghost',
size,
}),
className,
)}
{...props}
>
{children || <span className="sr-only">Page link</span>}
</a>
);
PaginationLink.displayName = 'PaginationLink';
const PaginationPrevious = ({
className,
...props
}: React.ComponentProps<typeof PaginationLink>) => (
<PaginationLink
aria-label="Go to previous page"
size="default"
className={cn('gap-1 pl-2.5', className)}
{...props}
>
<ChevronLeft className="h-4 w-4" />
<span>Previous</span>
</PaginationLink>
);
PaginationPrevious.displayName = 'PaginationPrevious';
const PaginationNext = ({ className, ...props }: React.ComponentProps<typeof PaginationLink>) => (
<PaginationLink
aria-label="Go to next page"
size="default"
className={cn('gap-1 pr-2.5', className)}
{...props}
>
<span>Next</span>
<ChevronRight className="h-4 w-4" />
</PaginationLink>
);
PaginationNext.displayName = 'PaginationNext';
const PaginationEllipsis = ({ className, ...props }: React.ComponentProps<'span'>) => (
<span
aria-hidden
className={cn('flex h-9 w-9 items-center justify-center', className)}
{...props}
>
<MoreHorizontal className="h-4 w-4" />
<span className="sr-only">More pages</span>
</span>
);
PaginationEllipsis.displayName = 'PaginationEllipsis';
export {
Pagination,
PaginationContent,
PaginationEllipsis,
PaginationItem,
PaginationLink,
PaginationNext,
PaginationPrevious,
};

View file

@ -1,376 +0,0 @@
import { useEffect, useRef, useCallback } from 'react';
import { cn } from '~/utils';
class Pixel {
width: number;
height: number;
ctx: CanvasRenderingContext2D;
x: number;
y: number;
color: string;
speed: number;
size: number;
sizeStep: number;
minSize: number;
maxSizeInteger: number;
maxSize: number;
delay: number;
counter: number;
counterStep: number;
isIdle: boolean;
isReverse: boolean;
isShimmer: boolean;
activationThreshold: number;
constructor(
canvas: HTMLCanvasElement,
context: CanvasRenderingContext2D,
x: number,
y: number,
color: string,
speed: number,
delay: number,
activationThreshold: number,
) {
this.width = canvas.width;
this.height = canvas.height;
this.ctx = context;
this.x = x;
this.y = y;
this.color = color;
this.speed = this.random(0.1, 0.9) * speed;
this.size = 0;
this.sizeStep = Math.random() * 0.4;
this.minSize = 0.5;
this.maxSizeInteger = 2;
this.maxSize = this.random(this.minSize, this.maxSizeInteger);
this.delay = delay;
this.counter = 0;
this.counterStep = Math.random() * 4 + (this.width + this.height) * 0.01;
this.isIdle = false;
this.isReverse = false;
this.isShimmer = false;
this.activationThreshold = activationThreshold;
}
private random(min: number, max: number) {
return Math.random() * (max - min) + min;
}
private draw() {
const offset = this.maxSizeInteger * 0.5 - this.size * 0.5;
this.ctx.fillStyle = this.color;
this.ctx.fillRect(this.x + offset, this.y + offset, this.size, this.size);
}
appear() {
this.isIdle = false;
if (this.counter <= this.delay) {
this.counter += this.counterStep;
return;
}
if (this.size >= this.maxSize) {
this.isShimmer = true;
}
if (this.isShimmer) {
this.shimmer();
} else {
this.size += this.sizeStep;
}
this.draw();
}
appearWithProgress(progress: number) {
const diff = progress - this.activationThreshold;
if (diff <= 0) {
this.isIdle = true;
return;
}
if (this.counter <= this.delay) {
this.counter += this.counterStep;
this.isIdle = false;
return;
}
if (this.size >= this.maxSize) {
this.isShimmer = true;
}
if (this.isShimmer) {
this.shimmer();
} else {
this.size += this.sizeStep;
}
this.isIdle = false;
this.draw();
}
disappear() {
this.isShimmer = false;
this.counter = 0;
if (this.size <= 0) {
this.isIdle = true;
return;
}
this.size -= 0.1;
this.draw();
}
private shimmer() {
if (this.size >= this.maxSize) {
this.isReverse = true;
} else if (this.size <= this.minSize) {
this.isReverse = false;
}
this.size += this.isReverse ? -this.speed : this.speed;
}
}
const getEffectiveSpeed = (value: number, reducedMotion: boolean) => {
const parsed = parseInt(String(value), 10);
const throttle = 0.001;
if (parsed <= 0 || reducedMotion) {
return 0;
}
if (parsed >= 100) {
return 100 * throttle;
}
return parsed * throttle;
};
const clamp = (n: number, min = 0, max = 1) => Math.min(Math.max(n, min), max);
const VARIANTS = {
default: { gap: 5, speed: 35, colors: '#f8fafc,#f1f5f9,#cbd5e1', noFocus: false },
blue: { gap: 10, speed: 25, colors: '#e0f2fe,#7dd3fc,#0ea5e9', noFocus: false },
yellow: { gap: 3, speed: 20, colors: '#fef08a,#fde047,#eab308', noFocus: false },
pink: { gap: 6, speed: 80, colors: '#fecdd3,#fda4af,#e11d48', noFocus: true },
} as const;
interface PixelCardProps {
variant?: keyof typeof VARIANTS;
gap?: number;
speed?: number;
colors?: string;
noFocus?: boolean;
className?: string;
progress?: number;
randomness?: number;
width?: string;
height?: string;
}
export default function PixelCard({
variant = 'default',
gap,
speed,
colors,
noFocus,
className = '',
progress,
randomness = 0.3,
width,
height,
}: PixelCardProps) {
const containerRef = useRef<HTMLDivElement>(null);
const canvasRef = useRef<HTMLCanvasElement>(null);
const pixelsRef = useRef<Pixel[]>([]);
const animationRef = useRef<number>();
const timePrevRef = useRef(performance.now());
const progressRef = useRef<number | undefined>(progress);
const reducedMotion = useRef(
window.matchMedia('(prefers-reduced-motion: reduce)').matches,
).current;
const cfg = VARIANTS[variant];
const g = gap ?? cfg.gap;
const s = speed ?? cfg.speed;
const palette = colors ?? cfg.colors;
const disableFocus = noFocus ?? cfg.noFocus;
const updateCanvasOpacity = useCallback(() => {
if (!canvasRef.current) {
return;
}
if (progressRef.current === undefined) {
canvasRef.current.style.opacity = '1';
return;
}
const fadeStart = 0.9;
const alpha =
progressRef.current >= fadeStart ? 1 - (progressRef.current - fadeStart) / 0.1 : 1;
canvasRef.current.style.opacity = String(clamp(alpha));
}, []);
const animate = useCallback(
(method: keyof Pixel) => {
animationRef.current = requestAnimationFrame(() => animate(method));
const now = performance.now();
const elapsed = now - timePrevRef.current;
if (elapsed < 1000 / 60) {
return;
}
timePrevRef.current = now - (elapsed % (1000 / 60));
const ctx = canvasRef.current?.getContext('2d');
if (!ctx || !canvasRef.current) {
return;
}
ctx.clearRect(0, 0, canvasRef.current.width, canvasRef.current.height);
let idle = true;
for (const p of pixelsRef.current) {
if (method === 'appearWithProgress') {
progressRef.current !== undefined
? p.appearWithProgress(progressRef.current)
: (p.isIdle = true);
} else {
// @ts-ignore dynamic dispatch
p[method]();
}
if (!p.isIdle) {
idle = false;
}
}
updateCanvasOpacity();
if (idle) {
cancelAnimationFrame(animationRef.current!);
}
},
[updateCanvasOpacity],
);
const startAnim = useCallback(
(m: keyof Pixel) => {
cancelAnimationFrame(animationRef.current!);
animationRef.current = requestAnimationFrame(() => animate(m));
},
[animate],
);
const initPixels = useCallback(() => {
if (!containerRef.current || !canvasRef.current) {
return;
}
const { width: cw, height: ch } = containerRef.current.getBoundingClientRect();
const ctx = canvasRef.current.getContext('2d');
canvasRef.current.width = Math.floor(cw);
canvasRef.current.height = Math.floor(ch);
const cols = palette.split(',');
const px: Pixel[] = [];
const cx = cw / 2;
const cy = ch / 2;
const maxDist = Math.hypot(cx, cy);
for (let x = 0; x < cw; x += g) {
for (let y = 0; y < ch; y += g) {
const color = cols[Math.floor(Math.random() * cols.length)];
const distNorm = Math.hypot(x - cx, y - cy) / maxDist;
const threshold = clamp(distNorm * (1 - randomness) + Math.random() * randomness);
const delay = reducedMotion ? 0 : distNorm * maxDist;
if (!ctx) {
continue;
}
px.push(
new Pixel(
canvasRef.current,
ctx,
x,
y,
color,
getEffectiveSpeed(s, reducedMotion),
delay,
threshold,
),
);
}
}
pixelsRef.current = px;
if (progressRef.current !== undefined) {
startAnim('appearWithProgress');
}
}, [g, palette, s, randomness, reducedMotion, startAnim]);
useEffect(() => {
progressRef.current = progress;
if (progress !== undefined) {
startAnim('appearWithProgress');
}
}, [progress, startAnim]);
useEffect(() => {
if (progress === undefined) {
cancelAnimationFrame(animationRef.current!);
}
}, [progress]);
useEffect(() => {
initPixels();
const obs = new ResizeObserver(initPixels);
containerRef.current && obs.observe(containerRef.current);
return () => {
obs.disconnect();
cancelAnimationFrame(animationRef.current!);
};
}, [initPixels]);
const hoverIn = () => progressRef.current === undefined && startAnim('appear');
const hoverOut = () => progressRef.current === undefined && startAnim('disappear');
const focusIn: React.FocusEventHandler<HTMLDivElement> = (e) => {
if (
!disableFocus &&
!e.currentTarget.contains(e.relatedTarget) &&
progressRef.current === undefined
) {
startAnim('appear');
}
};
const focusOut: React.FocusEventHandler<HTMLDivElement> = (e) => {
if (
!disableFocus &&
!e.currentTarget.contains(e.relatedTarget) &&
progressRef.current === undefined
) {
startAnim('disappear');
}
};
return (
<div
ref={containerRef}
style={{
width: width || '100%',
height: height || '100%',
}}
>
<div
className={cn(
'relative isolate grid select-none place-items-center overflow-hidden rounded-lg border border-border-light shadow-md transition-colors duration-200 ease-in-out',
className,
)}
style={{
width: '100%',
height: '100%',
transitionTimingFunction: 'cubic-bezier(0.5, 1, 0.89, 1)',
}}
onMouseEnter={hoverIn}
onMouseLeave={hoverOut}
onFocus={disableFocus ? undefined : focusIn}
onBlur={disableFocus ? undefined : focusOut}
tabIndex={disableFocus ? -1 : 0}
>
<canvas
ref={canvasRef}
className="pointer-events-none absolute inset-0 block"
width={width && width !== 'auto' ? parseInt(String(width)) : undefined}
height={height && height !== 'auto' ? parseInt(String(height)) : undefined}
/>
</div>
</div>
);
}

View file

@ -1,22 +0,0 @@
import * as React from 'react';
import * as ProgressPrimitive from '@radix-ui/react-progress';
import { cn } from '~/utils';
const Progress = React.forwardRef<
React.ElementRef<typeof ProgressPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof ProgressPrimitive.Root>
>(({ className, value, ...props }, ref) => (
<ProgressPrimitive.Root
ref={ref}
className={cn('relative h-2 w-full overflow-hidden rounded-full bg-primary/20', className)}
{...props}
>
<ProgressPrimitive.Indicator
className="h-full w-full flex-1 bg-primary transition-all"
style={{ transform: `translateX(-${100 - (value || 0)}%)` }}
/>
</ProgressPrimitive.Root>
));
Progress.displayName = ProgressPrimitive.Root.displayName;
export { Progress };

View file

@ -1,22 +0,0 @@
import { useLocalize } from '~/hooks';
export default function Prompt({ title, prompt }: { title: string; prompt: string }) {
const localize = useLocalize();
return (
<div
// onclick="selectPromptTemplate(0)"
className="flex w-full flex-col gap-2 rounded-md bg-gray-50 p-4 text-left hover:bg-gray-200 dark:bg-white/5 "
>
<h2 className="m-auto flex items-center gap-3 text-lg font-normal md:flex-col md:gap-2">
{title}
</h2>
<button>
<p className="w-full rounded-md bg-gray-50 p-3 hover:bg-gray-200 dark:bg-white/5 dark:hover:bg-gray-800">
{prompt}
</p>
</button>
<span className="font-medium">{localize('com_ui_use_prompt')} </span>
</div>
);
}

View file

@ -1,15 +0,0 @@
import { cn } from '~/utils';
export const QuestionMark = ({ className = '' }) => {
return (
<span>
<div
className={cn(
'border-token-border-medium text-token-text-tertiary ml-2 flex h-3.5 w-3.5 cursor-default items-center justify-center rounded-full border text-[0.5rem] font-medium leading-none',
className,
)}
>
?
</div>
</span>
);
};

View file

@ -1,62 +0,0 @@
import { GripVertical } from 'lucide-react';
import * as ResizablePrimitive from 'react-resizable-panels';
import { cn } from '~/utils';
const ResizablePanelGroup = ({
className = '',
...props
}: React.ComponentProps<typeof ResizablePrimitive.PanelGroup>) => (
<ResizablePrimitive.PanelGroup
className={cn('flex h-full w-full data-[panel-group-direction=vertical]:flex-col', className)}
{...props}
/>
);
const ResizablePanel = ResizablePrimitive.Panel;
const ResizableHandle = ({
withHandle,
className = '',
...props
}: React.ComponentProps<typeof ResizablePrimitive.PanelResizeHandle> & {
withHandle?: boolean;
}) => (
<ResizablePrimitive.PanelResizeHandle
className={cn(
'bg-border focus-visible:ring-ring relative flex w-px items-center justify-center after:absolute after:inset-y-0 after:left-1/2 after:w-1 after:-translate-x-1/2 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-offset-1 data-[panel-group-direction=vertical]:h-px data-[panel-group-direction=vertical]:w-full data-[panel-group-direction=vertical]:after:left-0 data-[panel-group-direction=vertical]:after:h-1 data-[panel-group-direction=vertical]:after:w-full data-[panel-group-direction=vertical]:after:-translate-y-1/2 data-[panel-group-direction=vertical]:after:translate-x-0 [&[data-panel-group-direction=vertical]>div]:rotate-90',
className,
)}
{...props}
>
{withHandle && (
<div className="bg-border z-10 flex h-4 w-3 items-center justify-center rounded-sm border">
<GripVertical className="h-2.5 w-2.5" />
</div>
)}
</ResizablePrimitive.PanelResizeHandle>
);
const ResizableHandleAlt = ({
withHandle,
className = '',
...props
}: React.ComponentProps<typeof ResizablePrimitive.PanelResizeHandle> & {
withHandle?: boolean;
}) => (
<ResizablePrimitive.PanelResizeHandle
className={cn(
'bg-border focus-visible:ring-ring group relative flex w-px items-center justify-center after:absolute after:inset-y-0 after:left-1/2 after:w-1 after:-translate-x-1/2 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-offset-1 data-[panel-group-direction=vertical]:h-px data-[panel-group-direction=vertical]:w-full data-[panel-group-direction=vertical]:after:left-0 data-[panel-group-direction=vertical]:after:h-1 data-[panel-group-direction=vertical]:after:w-full data-[panel-group-direction=vertical]:after:-translate-y-1/2 data-[panel-group-direction=vertical]:after:translate-x-0 [&[data-panel-group-direction=vertical]>div]:rotate-90',
className,
)}
{...props}
>
{withHandle && (
<div className="bg-border invisible z-10 flex h-4 w-3 items-center justify-center rounded-sm border group-hover:visible group-active:visible">
<GripVertical className="h-2.5 w-2.5" />
</div>
)}
</ResizablePrimitive.PanelResizeHandle>
);
export { ResizablePanelGroup, ResizablePanel, ResizableHandle, ResizableHandleAlt };

View file

@ -1,160 +0,0 @@
import * as React from 'react';
import * as SelectPrimitive from '@radix-ui/react-select';
import { CaretSortIcon, CheckIcon, ChevronDownIcon, ChevronUpIcon } from '@radix-ui/react-icons';
import { cn } from '~/utils';
const Select = SelectPrimitive.Root;
const SelectGroup = SelectPrimitive.Group;
const SelectValue = SelectPrimitive.Value;
const SelectTrigger = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger>
>(({ className = '', children, ...props }, ref) => (
<SelectPrimitive.Trigger
ref={ref}
className={cn(
'flex h-9 w-full items-center justify-between whitespace-nowrap rounded-md border border-gray-200 border-input bg-transparent px-3 py-2 text-sm shadow-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none disabled:cursor-not-allowed disabled:opacity-50 dark:border-gray-600 [&>span]:line-clamp-1',
'rounded-lg hover:bg-gray-100/50 dark:hover:bg-gray-700',
className,
)}
{...props}
>
{children}
<SelectPrimitive.Icon asChild>
<CaretSortIcon className="h-4 w-4 opacity-50" />
</SelectPrimitive.Icon>
</SelectPrimitive.Trigger>
));
SelectTrigger.displayName = SelectPrimitive.Trigger.displayName;
const SelectScrollUpButton = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.ScrollUpButton>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollUpButton>
>(({ className = '', ...props }, ref) => (
<SelectPrimitive.ScrollUpButton
ref={ref}
className={cn(
'flex cursor-default items-center justify-center py-1 dark:text-white',
className,
)}
{...props}
>
<ChevronUpIcon />
</SelectPrimitive.ScrollUpButton>
));
SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName;
const SelectScrollDownButton = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.ScrollDownButton>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollDownButton>
>(({ className = '', ...props }, ref) => (
<SelectPrimitive.ScrollDownButton
ref={ref}
className={cn(
'flex cursor-default items-center justify-center py-1 dark:text-white',
className,
)}
{...props}
>
<ChevronDownIcon />
</SelectPrimitive.ScrollDownButton>
));
SelectScrollDownButton.displayName = SelectPrimitive.ScrollDownButton.displayName;
const SelectContent = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content>
>(({ className = '', children, position = 'popper', ...props }, ref) => (
<SelectPrimitive.Portal>
<SelectPrimitive.Content
ref={ref}
className={cn(
'bg-popover text-popover-foreground relative z-50 max-h-96 min-w-[8rem] overflow-hidden rounded-md border border-gray-200 shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 dark:border-gray-600',
position === 'popper'
? 'data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1'
: '',
className,
)}
position={position}
{...props}
>
<SelectScrollUpButton />
<SelectPrimitive.Viewport
className={cn(
'p-1',
position === 'popper'
? 'h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]'
: '',
)}
>
{children}
</SelectPrimitive.Viewport>
<SelectScrollDownButton />
</SelectPrimitive.Content>
</SelectPrimitive.Portal>
));
SelectContent.displayName = SelectPrimitive.Content.displayName;
const SelectLabel = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Label>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Label>
>(({ className = '', ...props }, ref) => (
<SelectPrimitive.Label
ref={ref}
className={cn('px-2 py-1.5 text-sm font-semibold', className)}
{...props}
/>
));
SelectLabel.displayName = SelectPrimitive.Label.displayName;
const SelectItem = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Item>
>(({ className = '', children, ...props }, ref) => (
<SelectPrimitive.Item
ref={ref}
className={cn(
'relative flex w-full cursor-pointer select-none items-center rounded-sm py-1.5 pl-2 pr-8 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
'rounded-lg hover:bg-gray-100/50 dark:hover:bg-gray-700',
className,
)}
{...props}
>
<span className="absolute right-2 flex h-3.5 w-3.5 items-center justify-center">
<SelectPrimitive.ItemIndicator>
<CheckIcon className="h-4 w-4" />
</SelectPrimitive.ItemIndicator>
</span>
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
</SelectPrimitive.Item>
));
SelectItem.displayName = SelectPrimitive.Item.displayName;
const SelectSeparator = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Separator>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Separator>
>(({ className = '', ...props }, ref) => (
<SelectPrimitive.Separator
ref={ref}
className={cn('-mx-1 my-1 h-px bg-muted', className)}
{...props}
/>
));
SelectSeparator.displayName = SelectPrimitive.Separator.displayName;
export {
Select,
SelectGroup,
SelectValue,
SelectTrigger,
SelectContent,
SelectLabel,
SelectItem,
SelectSeparator,
SelectScrollUpButton,
SelectScrollDownButton,
};

View file

@ -1,285 +0,0 @@
import React, { useRef } from 'react';
import {
Label,
Listbox,
Transition,
ListboxButton,
ListboxOption,
ListboxOptions,
} from '@headlessui/react';
import type { Option, OptionWithIcon, DropdownValueSetter } from '~/common';
import CheckMark from '~/components/svg/CheckMark';
import { useMultiSearch } from './MultiSearch';
import { useLocalize } from '~/hooks';
import { cn } from '~/utils/';
type SelectDropDownProps = {
id?: string;
title?: string;
disabled?: boolean;
value: string | null | Option | OptionWithIcon;
setValue: DropdownValueSetter | ((value: string) => void);
tabIndex?: number;
availableValues?: string[] | Option[] | OptionWithIcon[];
emptyTitle?: boolean;
showAbove?: boolean;
showLabel?: boolean;
iconSide?: 'left' | 'right';
optionIconSide?: 'left' | 'right';
renderOption?: () => React.ReactNode;
containerClassName?: string;
currentValueClass?: string;
optionsListClass?: string;
optionsClass?: string;
subContainerClassName?: string;
className?: string;
placeholder?: string;
searchClassName?: string;
searchPlaceholder?: string;
showOptionIcon?: boolean;
};
function getOptionText(option: string | Option | OptionWithIcon): string {
if (typeof option === 'string') {
return option;
}
if ('label' in option) {
return option.label ?? '';
}
if ('value' in option) {
return (option.value ?? '') + '';
}
return '';
}
function SelectDropDown({
title: _title,
value,
disabled,
setValue,
availableValues,
showAbove = false,
showLabel = true,
emptyTitle = false,
iconSide = 'right',
optionIconSide = 'left',
placeholder,
containerClassName,
optionsListClass,
optionsClass,
currentValueClass,
subContainerClassName,
className,
renderOption,
searchClassName,
searchPlaceholder,
showOptionIcon = false,
}: SelectDropDownProps) {
const localize = useLocalize();
const transitionProps = { className: 'top-full mt-3' };
if (showAbove) {
transitionProps.className = 'bottom-full mb-3';
}
let title = _title;
if (emptyTitle) {
title = '';
} else if (!(title ?? '')) {
title = localize('com_ui_model');
}
const values = availableValues ?? [];
// Enable searchable select if enough items are provided.
const [filteredValues, searchRender] = useMultiSearch<string[] | Option[]>({
availableOptions: values,
placeholder: searchPlaceholder,
getTextKeyOverride: (option) => getOptionText(option).toUpperCase(),
className: searchClassName,
disabled,
});
const hasSearchRender = searchRender != null;
const options = hasSearchRender ? filteredValues : values;
const renderIcon = showOptionIcon && value != null && (value as OptionWithIcon).icon != null;
const buttonRef = useRef<HTMLButtonElement>(null);
return (
<div className={cn('flex items-center justify-center gap-2', containerClassName ?? '')}>
<div className={cn('relative w-full', subContainerClassName ?? '')}>
<Listbox value={value} onChange={setValue} disabled={disabled}>
{({ open }) => (
<>
<ListboxButton
ref={buttonRef}
data-testid="select-dropdown-button"
onKeyDown={(e) => {
if (e.key === 'Enter') {
e.preventDefault();
if (!open && buttonRef.current) {
buttonRef.current.click();
}
}
}}
className={cn(
'relative flex w-full cursor-default flex-col rounded-md border border-black/10 bg-white py-2 pl-3 pr-10 text-left focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:bg-white dark:border-gray-600 dark:bg-gray-700 sm:text-sm',
className ?? '',
)}
>
{showLabel && (
<Label
className="block text-xs text-gray-700 dark:text-gray-500"
id="headlessui-listbox-label-:r1:"
data-headlessui-state=""
>
{title}
</Label>
)}
<span className="inline-flex w-full truncate">
<span
className={cn(
'flex h-6 items-center gap-1 truncate text-sm text-gray-800 dark:text-white',
!showLabel ? 'text-xs' : '',
currentValueClass ?? '',
)}
>
{!showLabel && !emptyTitle && (
<span className="text-xs text-gray-700 dark:text-gray-500">{title}:</span>
)}
{renderIcon && optionIconSide !== 'right' && (
<span className="icon-md flex items-center">
{(value as OptionWithIcon).icon}
</span>
)}
{renderIcon && (
<span className="icon-md absolute right-0 mr-8 flex items-center">
{(value as OptionWithIcon).icon}
</span>
)}
{(() => {
if (!value) {
return <span className="text-text-secondary">{placeholder}</span>;
}
if (typeof value !== 'string') {
return value.label ?? '';
}
return value;
})()}
</span>
</span>
<span className="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-2">
<svg
stroke="currentColor"
fill="none"
strokeWidth="2"
viewBox="0 0 24 24"
strokeLinecap="round"
strokeLinejoin="round"
className="h-4 w-4 text-gray-400"
height="1em"
width="1em"
xmlns="http://www.w3.org/2000/svg"
style={showAbove ? { transform: 'scaleY(-1)' } : {}}
>
<polyline points="6 9 12 15 18 9"></polyline>
</svg>
</span>
</ListboxButton>
<Transition
show={open}
as={React.Fragment}
leave="transition ease-in duration-100"
leaveFrom="opacity-100"
leaveTo="opacity-0"
{...transitionProps}
>
<ListboxOptions
className={cn(
'absolute z-10 mt-2 max-h-60 w-full overflow-auto rounded border bg-white text-xs ring-black/10 dark:border-gray-600 dark:bg-gray-700 dark:ring-white/20 md:w-[100%]',
optionsListClass ?? '',
)}
>
{renderOption && (
<ListboxOption
key={'listbox-render-option'}
value={null}
className={cn(
'group relative flex h-[42px] cursor-pointer select-none items-center overflow-hidden pl-3 pr-9 text-gray-800 hover:bg-gray-20 dark:text-white dark:hover:bg-gray-700',
optionsClass ?? '',
)}
>
{renderOption()}
</ListboxOption>
)}
{searchRender}
{options.map((option: string | Option, i: number) => {
if (!option) {
return null;
}
const currentLabel =
typeof option === 'string' ? option : (option.label ?? option.value ?? '');
const currentValue = typeof option === 'string' ? option : (option.value ?? '');
const currentIcon =
typeof option === 'string'
? null
: ((option.icon as React.ReactNode) ?? null);
let activeValue: string | number | null | Option = value;
if (typeof activeValue !== 'string') {
activeValue = activeValue?.value ?? '';
}
return (
<ListboxOption
key={i}
value={option}
className={({ active }) =>
cn(
'group relative flex h-[42px] cursor-pointer select-none items-center overflow-hidden pl-3 pr-9 text-gray-800 hover:bg-gray-20 dark:text-white dark:hover:bg-gray-600',
active ? 'bg-surface-active text-text-primary' : '',
optionsClass ?? '',
)
}
>
<span className="flex items-center gap-1.5 truncate">
<span
className={cn(
'flex h-6 items-center gap-1 text-gray-800 dark:text-gray-200',
option === value ? 'font-semibold' : '',
iconSide === 'left' ? 'ml-4' : '',
)}
>
{currentIcon != null && (
<span
className={cn(
'mr-1',
optionIconSide === 'right' ? 'absolute right-0 pr-2' : '',
)}
>
{currentIcon}
</span>
)}
{currentLabel}
</span>
{currentValue === activeValue && (
<span
className={cn(
'absolute inset-y-0 flex items-center text-gray-800 dark:text-gray-200',
iconSide === 'left' ? 'left-0 pl-2' : 'right-0 pr-3',
)}
>
<CheckMark />
</span>
)}
</span>
</ListboxOption>
);
})}
</ListboxOptions>
</Transition>
</>
)}
</Listbox>
</div>
</div>
);
}
export default SelectDropDown;

View file

@ -1,136 +0,0 @@
import React from 'react';
import { Root, Trigger, Content, Portal } from '@radix-ui/react-popover';
import MenuItem from '~/components/Chat/Menus/UI/MenuItem';
import type { Option } from '~/common';
import { useLocalize } from '~/hooks';
import { cn } from '~/utils/';
import { useMultiSearch } from './MultiSearch';
type SelectDropDownProps = {
id?: string;
title?: string;
value: string | null | Option;
disabled?: boolean;
setValue: (value: string) => void;
availableValues: string[] | Option[];
emptyTitle?: boolean;
showAbove?: boolean;
showLabel?: boolean;
iconSide?: 'left' | 'right';
renderOption?: () => React.ReactNode;
footer?: React.ReactNode;
};
function SelectDropDownPop({
title: _title,
value,
availableValues,
setValue,
showAbove = false,
showLabel = true,
emptyTitle = false,
footer,
}: SelectDropDownProps) {
const localize = useLocalize();
const transitionProps = { className: 'top-full mt-3' };
if (showAbove) {
transitionProps.className = 'bottom-full mb-3';
}
let title = _title;
if (emptyTitle) {
title = '';
} else if (!title) {
title = localize('com_ui_model');
}
// Detemine if we should to convert this component into a searchable select. If we have enough elements, a search
// input will appear near the top of the menu, allowing correct filtering of different model menu items. This will
// reset once the component is unmounted (as per a normal search)
const [filteredValues, searchRender] = useMultiSearch<string[] | Option[]>({
availableOptions: availableValues,
});
const hasSearchRender = Boolean(searchRender);
const options = hasSearchRender ? filteredValues : availableValues;
return (
<Root>
<div className={'flex items-center justify-center gap-2'}>
<div className={'relative w-full'}>
<Trigger asChild>
<button
data-testid="select-dropdown-button"
className={cn(
'pointer-cursor relative flex flex-col rounded-lg border border-black/10 bg-white py-2 pl-3 pr-10 text-left focus:ring-0 focus:ring-offset-0 dark:border-gray-700 dark:bg-gray-800 sm:text-sm',
'hover:bg-gray-50 radix-state-open:bg-gray-50 dark:hover:bg-gray-700 dark:radix-state-open:bg-gray-700',
'min-w-[200px] max-w-[215px] sm:min-w-full sm:max-w-full',
)}
aria-label={`Select ${title}`}
aria-haspopup="false"
>
{' '}
{showLabel && (
<label className="block text-xs text-gray-700 dark:text-gray-500">{title}</label>
)}
<span className="inline-flex w-full">
<span
className={cn(
'flex h-6 items-center gap-1 text-sm text-text-primary',
!showLabel ? 'text-xs' : '',
'min-w-[75px] font-normal',
)}
>
{typeof value !== 'string' && value ? (value.label ?? '') : (value ?? '')}
</span>
</span>
<span className="absolute inset-y-0 right-0 flex items-center pr-2">
<svg
stroke="currentColor"
fill="none"
strokeWidth="2"
viewBox="0 0 24 24"
strokeLinecap="round"
strokeLinejoin="round"
className="h-4 w-4 text-gray-400"
height="1em"
width="1em"
xmlns="http://www.w3.org/2000/svg"
style={showAbove ? { transform: 'scaleY(-1)' } : {}}
>
<polyline points="6 9 12 15 18 9"></polyline>
</svg>
</span>
</button>
</Trigger>
<Portal>
<Content
side="bottom"
align="start"
className={cn(
'mr-3 mt-2 max-h-[52vh] w-full max-w-[85vw] overflow-hidden overflow-y-auto rounded-lg border border-gray-200 bg-white shadow-lg dark:border-gray-700 dark:bg-gray-700 dark:text-white sm:max-w-full lg:max-h-[52vh]',
hasSearchRender && 'relative',
)}
>
{searchRender}
{options.map((option) => {
return (
<MenuItem
key={option}
title={option}
value={option}
selected={!!(value && value === option)}
onClick={() => setValue(option)}
/>
);
})}
{footer}
</Content>
</Portal>
</div>
</div>
</Root>
);
}
export default SelectDropDownPop;

View file

@ -1,24 +0,0 @@
import * as React from 'react';
import * as SeparatorPrimitive from '@radix-ui/react-separator';
import { cn } from '~/utils';
const Separator = React.forwardRef<
React.ElementRef<typeof SeparatorPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof SeparatorPrimitive.Root>
>(({ className = '', orientation = 'horizontal', decorative = true, ...props }, ref) => (
<SeparatorPrimitive.Root
ref={ref}
decorative={decorative}
orientation={orientation}
className={cn(
'shrink-0 bg-border-light',
orientation === 'horizontal' ? 'h-[1px] w-full' : 'h-full w-[1px]',
className,
)}
{...props}
/>
));
Separator.displayName = SeparatorPrimitive.Root.displayName;
export { Separator };

View file

@ -1,15 +0,0 @@
import { cn } from '~/utils';
function Skeleton({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) {
return (
<div
className={cn(
'animate-pulse rounded-md bg-surface-tertiary opacity-50 dark:opacity-25',
className,
)}
{...props}
/>
);
}
export { Skeleton };

View file

@ -1,26 +0,0 @@
import * as React from 'react';
import * as SliderPrimitive from '@radix-ui/react-slider';
import { cn } from '~/utils';
const Slider = React.forwardRef<
React.ElementRef<typeof SliderPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof SliderPrimitive.Root> & { onDoubleClick?: () => void }
>(({ className, onDoubleClick, ...props }, ref) => (
<SliderPrimitive.Root
ref={ref}
className={cn(
'relative flex w-full cursor-pointer touch-none select-none items-center',
className,
)}
onDoubleClick={onDoubleClick}
{...props}
>
<SliderPrimitive.Track className="relative h-2 w-full grow overflow-hidden rounded-full bg-secondary">
<SliderPrimitive.Range className="absolute h-full bg-primary" />
</SliderPrimitive.Track>
<SliderPrimitive.Thumb className="block h-5 w-5 rounded-full border-2 border-primary bg-background ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50" />
</SliderPrimitive.Root>
));
Slider.displayName = SliderPrimitive.Root.displayName;
export { Slider };

View file

@ -1,38 +0,0 @@
import { render } from '@testing-library/react';
import SplitText from './SplitText';
// Mock IntersectionObserver
class MockIntersectionObserver {
observe = jest.fn();
unobserve = jest.fn();
disconnect = jest.fn();
}
Object.defineProperty(window, 'IntersectionObserver', {
writable: true,
configurable: true,
value: MockIntersectionObserver,
});
describe('SplitText', () => {
it('renders emojis correctly', () => {
const emojis = ['🚧', '❤️‍🔥', '💜', '🦎', '❌', '✅', '⚠️'];
const originalText = emojis.join('');
const { container } = render(<SplitText text={originalText} />);
const textSpans = container.querySelectorAll('p > span > span.inline-block');
// Reconstruct the text by joining all span contents
const reconstructedText = Array.from(textSpans)
.map((span) => span.textContent)
.join('')
.trim();
// Compare the reconstructed text with the original
expect(reconstructedText).toBe(originalText);
// Check the first character specifically as the reconstructed text could hide issues
for (let i = 0; i < emojis.length; i++) {
expect(Array.from(textSpans)[i].textContent).toBe(emojis[i]);
}
});
});

View file

@ -1,138 +0,0 @@
import { useSprings, animated, SpringConfig } from '@react-spring/web';
import { useEffect, useRef, useState } from 'react';
interface SplitTextProps {
text?: string;
className?: string;
delay?: number;
animationFrom?: { opacity: number; transform: string };
animationTo?: { opacity: number; transform: string };
easing?: SpringConfig['easing'];
threshold?: number;
rootMargin?: string;
textAlign?: 'left' | 'right' | 'center' | 'justify' | 'start' | 'end';
onLetterAnimationComplete?: () => void;
onLineCountChange?: (lineCount: number) => void;
}
const splitGraphemes = (text: string): string[] => {
if (typeof Intl !== 'undefined' && Intl.Segmenter) {
const segmenter = new Intl.Segmenter('en', { granularity: 'grapheme' });
const segments = segmenter.segment(text);
return Array.from(segments).map((s) => s.segment);
} else {
// Fallback for browsers without Intl.Segmenter
return [...text];
}
};
const SplitText: React.FC<SplitTextProps> = ({
text = '',
className = '',
delay = 100,
animationFrom = { opacity: 0, transform: 'translate3d(0,40px,0)' },
animationTo = { opacity: 1, transform: 'translate3d(0,0,0)' },
easing = (t: number) => t,
threshold = 0.1,
rootMargin = '-100px',
textAlign = 'center',
onLetterAnimationComplete,
onLineCountChange,
}) => {
const words = text.split(' ').map(splitGraphemes);
const letters = words.flat();
const [inView, setInView] = useState(false);
const ref = useRef<HTMLParagraphElement>(null);
const animatedCount = useRef(0);
const springs = useSprings(
letters.length,
letters.map((_, i) => ({
from: animationFrom,
to: inView
? async (next: (props: any) => Promise<void>) => {
await next(animationTo);
animatedCount.current += 1;
if (animatedCount.current === letters.length && onLetterAnimationComplete) {
onLetterAnimationComplete();
}
}
: animationFrom,
delay: i * delay,
config: { easing },
})),
);
useEffect(() => {
const observer = new IntersectionObserver(
([entry]) => {
if (entry.isIntersecting) {
setInView(true);
if (ref.current) {
observer.unobserve(ref.current);
}
}
},
{ threshold, rootMargin },
);
if (ref.current) {
observer.observe(ref.current);
}
return () => observer.disconnect();
}, [threshold, rootMargin]);
useEffect(() => {
if (ref.current && inView) {
const element = ref.current;
setTimeout(() => {
const lineHeight =
parseInt(getComputedStyle(element).lineHeight) ||
parseInt(getComputedStyle(element).fontSize) * 1.2;
const height = element.offsetHeight;
const lines = Math.round(height / lineHeight);
if (onLineCountChange) {
onLineCountChange(lines);
}
}, 100);
}
}, [inView, text, onLineCountChange]);
return (
<>
<span className="sr-only">{text}</span>
<p
ref={ref}
className={`split-parent inline overflow-hidden ${className}`}
style={{ textAlign, whiteSpace: 'normal', wordWrap: 'break-word' }}
aria-hidden="true"
>
{words.map((word, wordIndex) => (
<span key={wordIndex} style={{ display: 'inline-block', whiteSpace: 'nowrap' }}>
{word.map((letter, letterIndex) => {
const index =
words.slice(0, wordIndex).reduce((acc, w) => acc + w.length, 0) + letterIndex;
return (
<animated.span
key={index}
style={springs[index] as unknown as React.CSSProperties}
className="inline-block transform transition-opacity will-change-transform"
>
{letter}
</animated.span>
);
})}
{wordIndex < words.length - 1 && (
<span style={{ display: 'inline-block', width: '0.3em' }}>&nbsp;</span>
)}
</span>
))}
</p>
</>
);
};
export default SplitText;

View file

@ -1,26 +0,0 @@
import * as React from 'react';
import * as SwitchPrimitives from '@radix-ui/react-switch';
import { cn } from '~/utils';
const Switch = React.forwardRef<
React.ElementRef<typeof SwitchPrimitives.Root>,
React.ComponentPropsWithoutRef<typeof SwitchPrimitives.Root>
>(({ className, ...props }, ref) => (
<SwitchPrimitives.Root
className={cn(
'peer inline-flex h-6 w-11 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=unchecked]:bg-switch-unchecked',
className,
)}
{...props}
ref={ref}
>
<SwitchPrimitives.Thumb
className={cn(
'pointer-events-none block h-5 w-5 rounded-full bg-background shadow-lg ring-0 transition-transform data-[state=checked]:translate-x-5 data-[state=unchecked]:translate-x-0',
)}
/>
</SwitchPrimitives.Root>
));
Switch.displayName = SwitchPrimitives.Root.displayName;
export { Switch };

View file

@ -1,90 +0,0 @@
import * as React from 'react';
import { cn } from '~/utils';
const Table = React.forwardRef<HTMLTableElement, React.HTMLAttributes<HTMLTableElement>>(
({ className, ...props }, ref) => (
<div className="relative w-full overflow-auto">
<table ref={ref} className={cn('w-full caption-bottom text-sm', className)} {...props} />
</div>
),
);
Table.displayName = 'Table';
const TableHeader = React.forwardRef<
HTMLTableSectionElement,
React.HTMLAttributes<HTMLTableSectionElement>
>(({ className, ...props }, ref) => (
<thead ref={ref} className={cn('[&_tr]:border-b', className)} {...props} />
));
TableHeader.displayName = 'TableHeader';
const TableBody = React.forwardRef<
HTMLTableSectionElement,
React.HTMLAttributes<HTMLTableSectionElement>
>(({ className, ...props }, ref) => (
<tbody ref={ref} className={cn('[&_tr:last-child]:border-0', className)} {...props} />
));
TableBody.displayName = 'TableBody';
const TableFooter = React.forwardRef<
HTMLTableSectionElement,
React.HTMLAttributes<HTMLTableSectionElement>
>(({ className, ...props }, ref) => (
<tfoot
ref={ref}
className={cn('border-t bg-muted/50 font-medium [&>tr]:last:border-b-0', className)}
{...props}
/>
));
TableFooter.displayName = 'TableFooter';
const TableRow = React.forwardRef<HTMLTableRowElement, React.HTMLAttributes<HTMLTableRowElement>>(
({ className, ...props }, ref) => (
<tr
ref={ref}
className={cn(
'border-b border-border-light transition-colors hover:bg-muted/50 data-[state=selected]:bg-muted',
className,
)}
{...props}
/>
),
);
TableRow.displayName = 'TableRow';
const TableHead = React.forwardRef<
HTMLTableCellElement,
React.ThHTMLAttributes<HTMLTableCellElement>
>(({ className, ...props }, ref) => (
<th
ref={ref}
className={cn(
'h-12 px-4 text-left align-middle font-medium text-muted-foreground [&:has([role=checkbox])]:pr-0',
className,
)}
{...props}
/>
));
TableHead.displayName = 'TableHead';
const TableCell = React.forwardRef<
HTMLTableCellElement,
React.TdHTMLAttributes<HTMLTableCellElement>
>(({ className, ...props }, ref) => (
<td
ref={ref}
className={cn('p-4 align-middle [&:has([role=checkbox])]:pr-0', className)}
{...props}
/>
));
TableCell.displayName = 'TableCell';
const TableCaption = React.forwardRef<
HTMLTableCaptionElement,
React.HTMLAttributes<HTMLTableCaptionElement>
>(({ className, ...props }, ref) => (
<caption ref={ref} className={cn('mt-4 text-sm text-muted-foreground', className)} {...props} />
));
TableCaption.displayName = 'TableCaption';
export { Table, TableHeader, TableBody, TableFooter, TableHead, TableRow, TableCell, TableCaption };

View file

@ -1,45 +0,0 @@
import * as React from 'react';
import * as TabsPrimitive from '@radix-ui/react-tabs';
import { cn } from '~/utils';
const Tabs = TabsPrimitive.Root;
const TabsList = React.forwardRef<
React.ElementRef<typeof TabsPrimitive.List>,
React.ComponentPropsWithoutRef<typeof TabsPrimitive.List>
>(({ className = '', ...props }, ref) => (
<TabsPrimitive.List
ref={ref}
className={cn(
'inline-flex items-center justify-center rounded-md bg-surface-primary',
className,
)}
{...props}
/>
));
TabsList.displayName = TabsPrimitive.List.displayName;
const TabsTrigger = React.forwardRef<
React.ElementRef<typeof TabsPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Trigger>
>(({ className = '', ...props }, ref) => (
<TabsPrimitive.Trigger
className={cn(
'inline-flex min-w-[100px] items-center justify-center rounded-[0.185rem] px-3 py-1.5 text-sm font-medium text-gray-700 transition-all disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-white data-[state=active]:text-gray-800 data-[state=active]:shadow-sm dark:data-[state=active]:bg-gray-700 dark:data-[state=active]:text-gray-200',
className,
)}
{...props}
ref={ref}
/>
));
TabsTrigger.displayName = TabsPrimitive.Trigger.displayName;
const TabsContent = React.forwardRef<
React.ElementRef<typeof TabsPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Content>
>(({ className = '', ...props }, ref) => (
<TabsPrimitive.Content className={cn('mt-2 rounded-md p-6', className)} {...props} ref={ref} />
));
TabsContent.displayName = TabsPrimitive.Content.displayName;
export { Tabs, TabsList, TabsTrigger, TabsContent };

View file

@ -1,50 +0,0 @@
import * as React from 'react';
import { X } from 'lucide-react';
import { cn } from '~/utils';
type TagProps = React.ComponentPropsWithoutRef<'div'> & {
label: string;
labelClassName?: string;
CancelButton?: React.ReactNode;
LabelNode?: React.ReactNode;
onRemove?: (e: React.MouseEvent<HTMLButtonElement>) => void;
};
const TagPrimitiveRoot = React.forwardRef<HTMLDivElement, TagProps>(
(
{ CancelButton, LabelNode, label, onRemove, className = '', labelClassName = '', ...props },
ref,
) => (
<div
ref={ref}
{...props}
className={cn(
'flex max-h-8 items-center overflow-y-hidden rounded-3xl border-2 border-green-600 bg-green-600/20 text-xs text-green-600 dark:text-white',
className,
)}
>
<div className={cn('ml-1 whitespace-pre-wrap px-2 py-1', labelClassName)}>
{LabelNode ? <>{LabelNode} </> : null}
{label}
</div>
{CancelButton
? CancelButton
: onRemove && (
<button
onClick={(e) => {
e.stopPropagation();
onRemove(e);
}}
className="rounded-full bg-green-600/50"
aria-label={`Remove ${label}`}
>
<X className="m-[1.5px] p-1" />
</button>
)}
</div>
),
);
TagPrimitiveRoot.displayName = 'Tag';
export const Tag = React.memo(TagPrimitiveRoot);

View file

@ -1,10 +1,8 @@
import { useMemo } from 'react';
import { OGDialog, DialogTemplate, useToastContext } from '@librechat/client';
import type { TTermsOfService } from 'librechat-data-provider';
import MarkdownLite from '~/components/Chat/Messages/Content/MarkdownLite';
import DialogTemplate from '~/components/ui/DialogTemplate';
import { useAcceptTermsMutation } from '~/data-provider';
import { useToastContext } from '~/Providers';
import { OGDialog } from '~/components/ui';
import { useLocalize } from '~/hooks';
const TermsAndConditionsModal = ({
@ -73,7 +71,7 @@ const TermsAndConditionsModal = ({
main={
<section
// Motivation: This is a dialog, so its content should be focusable
// eslint-disable-next-line jsx-a11y/no-noninteractive-tabindex
tabIndex={0}
className="max-h-[60vh] overflow-y-auto p-4"
aria-label={localize('com_ui_terms_and_conditions')}

View file

@ -1,25 +0,0 @@
/* eslint-disable */
import * as React from 'react';
import TextareaAutosize from 'react-textarea-autosize';
import { cn } from '../../utils';
export interface TextareaProps extends React.TextareaHTMLAttributes<HTMLTextAreaElement> {}
const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(
({ className = '', ...props }, ref) => {
return (
<textarea
className={cn(
'flex h-20 w-full resize-none rounded-md border border-gray-300 bg-transparent px-3 py-2 text-sm placeholder:text-gray-400 focus:outline-none focus:ring-2 focus:ring-gray-400 focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 dark:border-gray-700 dark:text-gray-50 dark:focus:ring-gray-400 dark:focus:ring-offset-gray-900',
className,
)}
ref={ref}
{...props}
/>
);
},
);
Textarea.displayName = 'Textarea';
export { Textarea };

View file

@ -1,14 +0,0 @@
import { useRecoilValue } from 'recoil';
import { forwardRef, useLayoutEffect, useState } from 'react';
import ReactTextareaAutosize from 'react-textarea-autosize';
import type { TextareaAutosizeProps } from 'react-textarea-autosize';
import store from '~/store';
export const TextareaAutosize = forwardRef<HTMLTextAreaElement, TextareaAutosizeProps>(
(props, ref) => {
const [, setIsRerendered] = useState(false);
const chatDirection = useRecoilValue(store.chatDirection).toLowerCase();
useLayoutEffect(() => setIsRerendered(true), []);
return <ReactTextareaAutosize dir={chatDirection} {...props} ref={ref} />;
},
);

View file

@ -1,103 +0,0 @@
import React, { useContext, useCallback, useEffect, useState } from 'react';
import { Sun, Moon, Monitor } from 'lucide-react';
import { ThemeContext } from '~/hooks';
declare global {
interface Window {
lastThemeChange?: number;
}
}
const Theme = ({ theme, onChange }: { theme: string; onChange: (value: string) => void }) => {
const themeIcons = {
system: <Monitor />,
dark: <Moon color="white" />,
light: <Sun />,
};
const nextTheme = theme === 'dark' ? 'light' : 'dark';
const label = `Switch to ${nextTheme} theme`;
useEffect(() => {
const handleKeyPress = (e: KeyboardEvent) => {
if (e.ctrlKey && e.shiftKey && e.key.toLowerCase() === 't') {
e.preventDefault();
onChange(nextTheme);
}
};
window.addEventListener('keydown', handleKeyPress);
return () => window.removeEventListener('keydown', handleKeyPress);
}, [nextTheme, onChange]);
return (
<button
className="flex items-center gap-2 rounded-lg p-2 transition-colors hover:bg-surface-hover focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2"
aria-label={label}
aria-keyshortcuts="Ctrl+Shift+T"
onClick={(e) => {
e.preventDefault();
onChange(nextTheme);
}}
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
onChange(nextTheme);
}
}}
>
{themeIcons[theme]}
</button>
);
};
const ThemeSelector = ({ returnThemeOnly }: { returnThemeOnly?: boolean }) => {
const { theme, setTheme } = useContext(ThemeContext);
const [announcement, setAnnouncement] = useState('');
const changeTheme = useCallback(
(value: string) => {
const now = Date.now();
if (typeof window.lastThemeChange === 'number' && now - window.lastThemeChange < 500) {
return;
}
window.lastThemeChange = now;
setTheme(value);
setAnnouncement(value === 'dark' ? 'Dark theme enabled' : 'Light theme enabled');
},
[setTheme],
);
useEffect(() => {
if (theme === 'system') {
const prefersDarkScheme = window.matchMedia('(prefers-color-scheme: dark)').matches;
setTheme(prefersDarkScheme ? 'dark' : 'light');
}
}, [theme, setTheme]);
useEffect(() => {
if (announcement) {
const timeout = setTimeout(() => setAnnouncement(''), 1000);
return () => clearTimeout(timeout);
}
}, [announcement]);
if (returnThemeOnly === true) {
return <Theme theme={theme} onChange={changeTheme} />;
}
return (
<div className="flex flex-col items-center justify-center bg-white pt-6 dark:bg-gray-900 sm:pt-0">
<div className="absolute bottom-0 left-0 m-4">
<Theme theme={theme} onChange={changeTheme} />
</div>
{announcement && (
<div aria-live="polite" className="sr-only">
{announcement}
</div>
)}
</div>
);
};
export default ThemeSelector;

View file

@ -1,57 +0,0 @@
import * as RadixToast from '@radix-ui/react-toast';
import { NotificationSeverity } from '~/common/types';
import { useToast } from '~/hooks';
export default function Toast() {
const { toast, onOpenChange } = useToast();
const severityClassName = {
[NotificationSeverity.INFO]: 'border-gray-500 bg-gray-500',
[NotificationSeverity.SUCCESS]: 'border-green-500 bg-green-500',
[NotificationSeverity.WARNING]: 'border-orange-500 bg-orange-500',
[NotificationSeverity.ERROR]: 'border-red-500 bg-red-500',
};
return (
<RadixToast.Root
open={toast.open}
onOpenChange={onOpenChange}
className="toast-root"
style={{
height: '74px',
marginBottom: '0px',
}}
>
<div className="w-full p-1 text-center md:w-auto md:text-justify">
<div
className={`alert-root pointer-events-auto inline-flex flex-row gap-2 rounded-md border px-3 py-2 text-white ${
severityClassName[toast.severity]
}`}
>
{toast.showIcon && (
<div className="mt-1 flex-shrink-0 flex-grow-0">
<svg
stroke="currentColor"
fill="none"
strokeWidth="2"
viewBox="0 0 24 24"
strokeLinecap="round"
strokeLinejoin="round"
className="icon-sm"
height="1em"
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
<polygon points="7.86 2 16.14 2 22 7.86 22 16.14 16.14 22 7.86 22 2 16.14 2 7.86 7.86 2" />
<line x1="12" y1="8" x2="12" y2="12" />
<line x1="12" y1="16" x2="12.01" y2="16" />
</svg>
</div>
)}
<RadixToast.Description className="flex-1 justify-center gap-2">
<div className="whitespace-pre-wrap text-left">{toast.message}</div>
</RadixToast.Description>
</div>
</div>
</RadixToast.Root>
);
}

View file

@ -1,75 +0,0 @@
import * as Ariakit from '@ariakit/react';
import { AnimatePresence, motion } from 'framer-motion';
import { forwardRef, useMemo } from 'react';
import { cn } from '~/utils';
interface TooltipAnchorProps extends Ariakit.TooltipAnchorProps {
description: string;
side?: 'top' | 'bottom' | 'left' | 'right';
className?: string;
focusable?: boolean;
role?: string;
}
export const TooltipAnchor = forwardRef<HTMLDivElement, TooltipAnchorProps>(function TooltipAnchor(
{ description, side = 'top', className, role, ...props },
ref,
) {
const tooltip = Ariakit.useTooltipStore({ placement: side });
const mounted = Ariakit.useStoreState(tooltip, (state) => state.mounted);
const placement = Ariakit.useStoreState(tooltip, (state) => state.placement);
const { x, y } = useMemo(() => {
const dir = placement.split('-')[0];
switch (dir) {
case 'top':
return { x: 0, y: -8 };
case 'bottom':
return { x: 0, y: 8 };
case 'left':
return { x: -8, y: 0 };
case 'right':
return { x: 8, y: 0 };
default:
return { x: 0, y: 0 };
}
}, [placement]);
const handleKeyDown = (event: React.KeyboardEvent<HTMLDivElement>) => {
if (role === 'button' && event.key === 'Enter') {
event.preventDefault();
(event.target as HTMLDivElement).click();
}
};
return (
<Ariakit.TooltipProvider store={tooltip} hideTimeout={0}>
<Ariakit.TooltipAnchor
{...props}
ref={ref}
role={role}
onKeyDown={handleKeyDown}
className={cn('cursor-pointer', className)}
/>
<AnimatePresence>
{mounted === true && (
<Ariakit.Tooltip
gutter={4}
alwaysVisible
className="tooltip"
render={
<motion.div
initial={{ opacity: 0, x, y }}
animate={{ opacity: 1, x: 0, y: 0 }}
exit={{ opacity: 0, x, y }}
/>
}
>
<Ariakit.TooltipArrow />
{description}
</Ariakit.Tooltip>
)}
</AnimatePresence>
</Ariakit.TooltipProvider>
);
});

View file

@ -1,48 +1 @@
export * from './Accordion';
export * from './AnimatedTabs';
export * from './AlertDialog';
export * from './Breadcrumb';
export * from './Button';
export * from './Checkbox';
export * from './DataTableColumnHeader';
export * from './Dialog';
export * from './DropdownMenu';
export * from './HoverCard';
export * from './Input';
export * from './InputNumber';
export * from './Label';
export * from './OriginalDialog';
export * from './Prompt';
export * from './QuestionMark';
export * from './Slider';
export * from './Separator';
export * from './InputCombobox';
export * from './Skeleton';
export * from './Switch';
export * from './Table';
export * from './Tabs';
export * from './Tag';
export * from './Textarea';
export * from './TextareaAutosize';
export * from './Tooltip';
export * from './Pagination';
export * from './Progress';
export * from './InputOTP';
export { default as Badge } from './Badge';
export { default as Combobox } from './Combobox';
export { default as Dropdown } from './Dropdown';
export { default as SplitText } from './SplitText';
export { default as FormInput } from './FormInput';
export { default as PixelCard } from './PixelCard';
export { default as FileUpload } from './FileUpload';
export { default as DropdownPopup } from './DropdownPopup';
export { default as DelayedRender } from './DelayedRender';
export { default as ThemeSelector } from './ThemeSelector';
export { default as SelectDropDown } from './SelectDropDown';
export { default as MultiSelectPop } from './MultiSelectPop';
export { default as ModelParameters } from './ModelParameters';
export { default as OGDialogTemplate } from './OGDialogTemplate';
export { default as InputWithDropdown } from './InputWithDropDown';
export { default as SelectDropDownPop } from './SelectDropDownPop';
export { default as AnimatedSearchInput } from './AnimatedSearchInput';
export { default as MultiSelectDropDown } from './MultiSelectDropDown';
export { default as TermsAndConditionsModal } from './TermsAndConditionsModal';