From 4f06c159be1efd90327d0a9cda09196380fb578c Mon Sep 17 00:00:00 2001 From: Marco Beretta <81851188+berry-13@users.noreply.github.com> Date: Thu, 10 Jul 2025 23:45:52 +0200 Subject: [PATCH] fix build client package --- packages/client/src/common/types.ts | 8 +- packages/client/src/components/Badge.tsx | 8 +- packages/client/src/components/DataTable.tsx | 3 +- .../client/src/components/DelayedRender.tsx | 9 +- .../src/components/DialogTemplate.spec.tsx | 17 +- .../client/src/components/DropdownNoState.tsx | 4 - .../src/components/MultiSelectDropDown.tsx | 231 ------------------ .../client/src/components/MultiSelectPop.tsx | 154 ------------ packages/client/src/components/SplitText.tsx | 69 ++++-- .../components/TermsAndConditionsModal.tsx | 111 --------- .../src/components/TextareaAutosize.tsx | 6 +- .../client/src/components/ThemeSelector.tsx | 8 +- packages/client/src/components/index.ts | 3 - packages/client/src/hooks/ThemeContext.tsx | 14 +- packages/client/src/hooks/useLocalize.ts | 6 +- packages/client/src/hooks/useToast.ts | 8 +- packages/client/src/store.ts | 20 ++ packages/client/src/svgs/ListeningIcon.tsx | 6 +- packages/client/src/svgs/SaveIcon.tsx | 7 +- packages/client/src/svgs/SpeechIcon.tsx | 6 +- packages/client/src/svgs/SwitchIcon.tsx | 7 +- packages/client/tsconfig.json | 8 +- 22 files changed, 148 insertions(+), 565 deletions(-) delete mode 100644 packages/client/src/components/MultiSelectDropDown.tsx delete mode 100644 packages/client/src/components/MultiSelectPop.tsx delete mode 100644 packages/client/src/components/TermsAndConditionsModal.tsx create mode 100644 packages/client/src/store.ts diff --git a/packages/client/src/common/types.ts b/packages/client/src/common/types.ts index 214dc349b5..2ec984954f 100644 --- a/packages/client/src/common/types.ts +++ b/packages/client/src/common/types.ts @@ -2,7 +2,7 @@ import { RefObject } from 'react'; import { FileSources, EModelEndpoint } from 'librechat-data-provider'; import type { UseMutationResult } from '@tanstack/react-query'; import type * as InputNumberPrimitive from 'rc-input-number'; -import type { SetterOrUpdater, RecoilState } from 'recoil'; +import type { PrimitiveAtom, WritableAtom } from 'jotai'; import type { ColumnDef } from '@tanstack/react-table'; import type * as t from 'librechat-data-provider'; import type { LucideIcon } from 'lucide-react'; @@ -49,9 +49,9 @@ export type AudioChunk = { export type BadgeItem = { id: string; - icon: React.ComponentType; + icon: React.ComponentType>; label: string; - atom: RecoilState; + atom: PrimitiveAtom | WritableAtom; isAvailable: boolean; }; @@ -147,7 +147,7 @@ export enum Panel { } export type FileSetter = - | SetterOrUpdater> + | GenericSetter> | React.Dispatch>>; export type ActionAuthForm = { diff --git a/packages/client/src/components/Badge.tsx b/packages/client/src/components/Badge.tsx index ed0221abb1..2469eccb26 100644 --- a/packages/client/src/components/Badge.tsx +++ b/packages/client/src/components/Badge.tsx @@ -5,7 +5,11 @@ import type { ButtonHTMLAttributes } from 'react'; import type { LucideIcon } from 'lucide-react'; import { cn } from '~/utils'; -interface BadgeProps extends ButtonHTMLAttributes { +interface BadgeProps + extends Omit< + ButtonHTMLAttributes, + 'onAnimationStart' | 'onDragStart' | 'onDragEnd' | 'onDrag' + > { icon?: LucideIcon; label: string; id?: string; @@ -70,7 +74,7 @@ export default function Badge({ }} whileTap={{ scale: isDragging ? 1.1 : isDisabled ? 1 : 0.97 }} transition={{ type: 'tween', duration: 0.1, ease: 'easeOut' }} - {...props} + {...(props as React.ComponentProps)} > {Icon && } {label} diff --git a/packages/client/src/components/DataTable.tsx b/packages/client/src/components/DataTable.tsx index 3900d99d69..0db881d5b1 100644 --- a/packages/client/src/components/DataTable.tsx +++ b/packages/client/src/components/DataTable.tsx @@ -25,8 +25,7 @@ import { AnimatedSearchInput, Skeleton, } from './'; -import { TrashIcon, Spinner } from '~/svg'; -import { LocalizeFunction } from '~/common'; +import { TrashIcon, Spinner } from '~/svgs'; import { useMediaQuery } from '~/hooks'; import { cn } from '~/utils'; diff --git a/packages/client/src/components/DelayedRender.tsx b/packages/client/src/components/DelayedRender.tsx index 216bc7ff92..0899d1dd32 100644 --- a/packages/client/src/components/DelayedRender.tsx +++ b/packages/client/src/components/DelayedRender.tsx @@ -1,5 +1,12 @@ +import React from 'react'; import { useDelayedRender } from '~/hooks'; -const DelayedRender = ({ delay, children }) => useDelayedRender(delay)(() => children); +interface DelayedRenderProps { + delay: number; + children: React.ReactNode; +} + +const DelayedRender = ({ delay, children }: DelayedRenderProps) => + useDelayedRender(delay)(() => children); export default DelayedRender; diff --git a/packages/client/src/components/DialogTemplate.spec.tsx b/packages/client/src/components/DialogTemplate.spec.tsx index f3340d537c..fedc4282ec 100644 --- a/packages/client/src/components/DialogTemplate.spec.tsx +++ b/packages/client/src/components/DialogTemplate.spec.tsx @@ -1,13 +1,12 @@ 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'; +import { Provider } from 'jotai'; describe('DialogTemplate', () => { - let mockSelectHandler; + let mockSelectHandler: jest.Mock; beforeEach(() => { mockSelectHandler = jest.fn(); @@ -15,7 +14,7 @@ describe('DialogTemplate', () => { it('renders correctly with all props', () => { const { getByText } = render( - + { selection={{ selectHandler: mockSelectHandler, selectText: 'Select' }} /> - , + , ); expect(getByText('Test Dialog')).toBeInTheDocument(); @@ -46,14 +45,14 @@ describe('DialogTemplate', () => { it('renders correctly without optional props', () => { const { queryByText } = render( - + { return; }} > - , + , ); expect(queryByText('Test Dialog')).toBeNull(); @@ -67,7 +66,7 @@ describe('DialogTemplate', () => { it('calls selectHandler when the select button is clicked', () => { const { getByText } = render( - + { @@ -79,7 +78,7 @@ describe('DialogTemplate', () => { selection={{ selectHandler: mockSelectHandler, selectText: 'Select' }} /> - , + , ); fireEvent.click(getByText('Select')); diff --git a/packages/client/src/components/DropdownNoState.tsx b/packages/client/src/components/DropdownNoState.tsx index b293b5f69c..a55caa3ce7 100644 --- a/packages/client/src/components/DropdownNoState.tsx +++ b/packages/client/src/components/DropdownNoState.tsx @@ -6,7 +6,6 @@ import { ListboxOptions, Transition, } from '@headlessui/react'; -import { AnchorPropsWithSelection } from '@headlessui/react/dist/internal/floating'; import type { Option } from '~/common'; import { cn } from '~/utils/'; @@ -16,7 +15,6 @@ interface DropdownProps { onChange: (value: string | Option) => void; options: (string | Option)[]; className?: string; - anchor?: AnchorPropsWithSelection; sizeClasses?: string; testId?: string; } @@ -31,7 +29,6 @@ const Dropdown: FC = ({ onChange, options, className = '', - anchor, sizeClasses, testId = 'dropdown-menu', }) => { @@ -90,7 +87,6 @@ const Dropdown: FC = ({ sizeClasses, className, )} - anchor={anchor} aria-label="List of options" > {options.map((item, index) => ( diff --git a/packages/client/src/components/MultiSelectDropDown.tsx b/packages/client/src/components/MultiSelectDropDown.tsx deleted file mode 100644 index f395531ad8..0000000000 --- a/packages/client/src/components/MultiSelectDropDown.tsx +++ /dev/null @@ -1,231 +0,0 @@ -import React, { useState, useRef } from 'react'; -import { Wrench, ArrowRight } from 'lucide-react'; -import { - Listbox, - ListboxButton, - Label, - ListboxOptions, - ListboxOption, - Transition, -} from '@headlessui/react'; -import type { TPlugin } from 'librechat-data-provider'; -import { useMultiSearch } from './MultiSearch'; -import { useOnClickOutside } from '~/hooks'; -import { CheckMark } from '~/svgs'; -import { cn } from '~/utils/'; - -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({ - 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 ( -
-
- {/* the function typing is correct but there's still an issue here */} - {/* @ts-ignore */} - - {() => ( - <> - setIsOpen((prev) => !prev)} - {...openProps} - > - {' '} - {showLabel && ( - - )} - - - {!showLabel && title.length > 0 && ( - {title}: - )} - -
- {value.map((v, i) => ( -
- {v.icon ? ( - {`${v} - ) : ( - - )} -
-
- ))} -
- - - - - - - - - - - - {searchRender} - {options.map((option, i: number) => { - if (!option) { - return null; - } - const selected = isSelected(option[optionValueKey]); - return ( - - - {!option.isButton && ( - -
- {option.icon ? ( - {`${option.name} - ) : ( - - )} -
-
-
- )} - - {option.name} - - {option.isButton && ( - - - - )} - {selected && !option.isButton && ( - - - - )} -
-
- ); - })} -
-
- - )} - -
-
- ); -} - -export default MultiSelectDropDown; diff --git a/packages/client/src/components/MultiSelectPop.tsx b/packages/client/src/components/MultiSelectPop.tsx deleted file mode 100644 index 7d058cc410..0000000000 --- a/packages/client/src/components/MultiSelectPop.tsx +++ /dev/null @@ -1,154 +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 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({ - availableOptions: availableValues, - placeholder: searchPlaceholder, - getTextKeyOverride: (option) => (option.name || '').toUpperCase(), - }); - const hasSearchRender = Boolean(searchRender); - const options = hasSearchRender ? filteredValues : availableValues; - - return ( - -
-
- - - - - - {searchRender} - {options.map((option) => { - if (!option) { - return null; - } - const selected = isSelected(option[optionValueKey]); - return ( - setSelected(option.pluginKey)} - icon={ - option.icon ? ( - {`${option.name} - ) : ( - - ) - } - /> - ); - })} - - -
-
-
- ); -} - -export default MultiSelectPop; diff --git a/packages/client/src/components/SplitText.tsx b/packages/client/src/components/SplitText.tsx index cde04eb8a1..f7cd8d4c8f 100644 --- a/packages/client/src/components/SplitText.tsx +++ b/packages/client/src/components/SplitText.tsx @@ -1,6 +1,36 @@ import { useSprings, animated, SpringConfig } from '@react-spring/web'; import { useEffect, useRef, useState } from 'react'; +interface SegmenterOptions { + granularity?: 'grapheme' | 'word' | 'sentence'; + localeMatcher?: 'lookup' | 'best fit'; +} + +interface SegmentData { + segment: string; + index: number; + input: string; + isWordLike?: boolean; +} + +interface Segments { + [Symbol.iterator](): IterableIterator; +} + +interface IntlSegmenter { + segment(input: string): Segments; +} + +interface IntlSegmenterConstructor { + new (locales?: string | string[], options?: SegmenterOptions): IntlSegmenter; +} + +declare global { + interface Intl { + Segmenter: IntlSegmenterConstructor; + } +} + interface SplitTextProps { text?: string; className?: string; @@ -16,12 +46,14 @@ interface SplitTextProps { } const splitGraphemes = (text: string): string[] => { - if (typeof Intl !== 'undefined' && Intl.Segmenter) { - const segmenter = new Intl.Segmenter('en', { granularity: 'grapheme' }); + if (typeof Intl !== 'undefined' && 'Segmenter' in Intl) { + const segmenter = new (Intl as typeof Intl & { Segmenter: IntlSegmenterConstructor }).Segmenter( + 'en', + { granularity: 'grapheme' }, + ); const segments = segmenter.segment(text); - return Array.from(segments).map((s) => s.segment); + return Array.from(segments).map((s: SegmentData) => s.segment); } else { - // Fallback for browsers without Intl.Segmenter return [...text]; } }; @@ -45,23 +77,20 @@ const SplitText: React.FC = ({ const ref = useRef(null); const animatedCount = useRef(0); - const springs = useSprings( - letters.length, - letters.map((_, i) => ({ - from: animationFrom, - to: inView - ? async (next: (props: any) => Promise) => { - await next(animationTo); - animatedCount.current += 1; - if (animatedCount.current === letters.length && onLetterAnimationComplete) { - onLetterAnimationComplete(); - } + const springs = useSprings(letters.length, (i) => ({ + from: animationFrom, + to: inView + ? async (next) => { + await next(animationTo); + animatedCount.current += 1; + if (animatedCount.current === letters.length && onLetterAnimationComplete) { + onLetterAnimationComplete(); } - : animationFrom, - delay: i * delay, - config: { easing }, - })), - ); + } + : animationFrom, + delay: i * delay, + config: { easing }, + })); useEffect(() => { const observer = new IntersectionObserver( diff --git a/packages/client/src/components/TermsAndConditionsModal.tsx b/packages/client/src/components/TermsAndConditionsModal.tsx deleted file mode 100644 index 5498c53840..0000000000 --- a/packages/client/src/components/TermsAndConditionsModal.tsx +++ /dev/null @@ -1,111 +0,0 @@ -import { useMemo } from 'react'; -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 './OriginalDialog'; -import { useLocalize } from '~/hooks'; - -const TermsAndConditionsModal = ({ - open, - onOpenChange, - onAccept, - onDecline, - title, - modalContent, -}: { - open: boolean; - onOpenChange: (isOpen: boolean) => void; - onAccept: () => void; - onDecline: () => void; - title?: string; - contentUrl?: string; - modalContent?: TTermsOfService['modalContent']; -}) => { - const localize = useLocalize(); - const { showToast } = useToastContext(); - const acceptTermsMutation = useAcceptTermsMutation({ - onSuccess: () => { - onAccept(); - onOpenChange(false); - }, - onError: () => { - showToast({ message: 'Failed to accept terms' }); - }, - }); - - const handleAccept = () => { - acceptTermsMutation.mutate(); - }; - - const handleDecline = () => { - onDecline(); - onOpenChange(false); - }; - - const handleOpenChange = (isOpen: boolean) => { - if (open && !isOpen) { - return; - } - onOpenChange(isOpen); - }; - - const content = useMemo(() => { - if (typeof modalContent === 'string') { - return modalContent; - } - - if (Array.isArray(modalContent)) { - return modalContent.join('\n'); - } - - return ''; - }, [modalContent]); - - return ( - - -
- {content !== '' ? ( - - ) : ( -

{localize('com_ui_no_terms_content')}

- )} -
- - } - buttons={ - <> - - - - } - /> -
- ); -}; - -export default TermsAndConditionsModal; diff --git a/packages/client/src/components/TextareaAutosize.tsx b/packages/client/src/components/TextareaAutosize.tsx index eeaa94d4aa..78083ab7a5 100644 --- a/packages/client/src/components/TextareaAutosize.tsx +++ b/packages/client/src/components/TextareaAutosize.tsx @@ -1,13 +1,13 @@ -import { useRecoilValue } from 'recoil'; +import { useAtomValue } from 'jotai'; import { forwardRef, useLayoutEffect, useState } from 'react'; import ReactTextareaAutosize from 'react-textarea-autosize'; import type { TextareaAutosizeProps } from 'react-textarea-autosize'; -import store from '~/store'; +import { chatDirectionAtom } from '~/store'; export const TextareaAutosize = forwardRef( (props, ref) => { const [, setIsRerendered] = useState(false); - const chatDirection = useRecoilValue(store.chatDirection).toLowerCase(); + const chatDirection = useAtomValue(chatDirectionAtom).toLowerCase(); useLayoutEffect(() => setIsRerendered(true), []); return ; }, diff --git a/packages/client/src/components/ThemeSelector.tsx b/packages/client/src/components/ThemeSelector.tsx index e643f05d87..47c18df46c 100644 --- a/packages/client/src/components/ThemeSelector.tsx +++ b/packages/client/src/components/ThemeSelector.tsx @@ -1,4 +1,4 @@ -import React, { useContext, useCallback, useEffect, useState } from 'react'; +import { useContext, useCallback, useEffect, useState } from 'react'; import { Sun, Moon, Monitor } from 'lucide-react'; import { ThemeContext } from '~/hooks'; @@ -8,8 +8,10 @@ declare global { } } +type ThemeType = 'system' | 'dark' | 'light'; + const Theme = ({ theme, onChange }: { theme: string; onChange: (value: string) => void }) => { - const themeIcons = { + const themeIcons: Record = { system: , dark: , light: , @@ -45,7 +47,7 @@ const Theme = ({ theme, onChange }: { theme: string; onChange: (value: string) = } }} > - {themeIcons[theme]} + {themeIcons[theme as ThemeType]} ); }; diff --git a/packages/client/src/components/index.ts b/packages/client/src/components/index.ts index b3ae7c5d2f..9a9fc34878 100644 --- a/packages/client/src/components/index.ts +++ b/packages/client/src/components/index.ts @@ -38,10 +38,7 @@ 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 '../../../../client/src/components/Input/ModelSelect/SelectDropDownPop'; export { default as AnimatedSearchInput } from './AnimatedSearchInput'; -export { default as MultiSelectDropDown } from './MultiSelectDropDown'; diff --git a/packages/client/src/hooks/ThemeContext.tsx b/packages/client/src/hooks/ThemeContext.tsx index 76b2725980..74516fcab0 100644 --- a/packages/client/src/hooks/ThemeContext.tsx +++ b/packages/client/src/hooks/ThemeContext.tsx @@ -1,9 +1,9 @@ //ThemeContext.js // source: https://plainenglish.io/blog/light-and-dark-mode-in-react-web-application-with-tailwind-css-89674496b942 -import { useSetRecoilState } from 'recoil'; +import { useSetAtom } from 'jotai'; import React, { createContext, useState, useEffect } from 'react'; import { getInitialTheme, applyFontSize } from '~/utils'; -import store from '~/store'; +import { fontSizeAtom } from '~/store'; type ProviderValue = { theme: string; @@ -26,9 +26,15 @@ export const isDark = (theme: string): boolean => { export const ThemeContext = createContext(defaultContextValue); -export const ThemeProvider = ({ initialTheme, children }) => { +export const ThemeProvider = ({ + initialTheme, + children, +}: { + initialTheme?: string; + children: React.ReactNode; +}) => { const [theme, setTheme] = useState(getInitialTheme); - const setFontSize = useSetRecoilState(store.fontSize); + const setFontSize = useSetAtom(fontSizeAtom); const rawSetTheme = (rawTheme: string) => { const root = window.document.documentElement; diff --git a/packages/client/src/hooks/useLocalize.ts b/packages/client/src/hooks/useLocalize.ts index 6b574d25b1..83366cff16 100644 --- a/packages/client/src/hooks/useLocalize.ts +++ b/packages/client/src/hooks/useLocalize.ts @@ -1,14 +1,14 @@ import { useEffect } from 'react'; import { TOptions } from 'i18next'; -import { useRecoilValue } from 'recoil'; +import { useAtomValue } from 'jotai'; import { useTranslation } from 'react-i18next'; import { resources } from '~/locales/i18n'; -import store from '~/store'; +import { langAtom } from '~/store'; export type TranslationKeys = keyof typeof resources.en.translation; export default function useLocalize() { - const lang = useRecoilValue(store.lang); + const lang = useAtomValue(langAtom); const { t, i18n } = useTranslation(); useEffect(() => { diff --git a/packages/client/src/hooks/useToast.ts b/packages/client/src/hooks/useToast.ts index 92f7bbfe17..9937c640dd 100644 --- a/packages/client/src/hooks/useToast.ts +++ b/packages/client/src/hooks/useToast.ts @@ -1,11 +1,11 @@ -import { useRecoilState } from 'recoil'; +import { useAtom } from 'jotai'; import { useRef, useEffect } from 'react'; import type { TShowToast } from '~/common'; import { NotificationSeverity } from '~/common'; -import store from '~/store'; +import { toastState, type ToastState } from '~/store'; export default function useToast(showDelay = 100) { - const [toast, setToast] = useRecoilState(store.toastState); + const [toast, setToast] = useAtom(toastState); const showTimerRef = useRef(null); const hideTimerRef = useRef(null); @@ -45,7 +45,7 @@ export default function useToast(showDelay = 100) { }); // Hides the toast after the specified duration hideTimerRef.current = window.setTimeout(() => { - setToast((prevToast) => ({ ...prevToast, open: false })); + setToast((prevToast: ToastState) => ({ ...prevToast, open: false })); }, duration); }, showDelay); }; diff --git a/packages/client/src/store.ts b/packages/client/src/store.ts new file mode 100644 index 0000000000..6e23be6e04 --- /dev/null +++ b/packages/client/src/store.ts @@ -0,0 +1,20 @@ +import { atom } from 'jotai'; +import { NotificationSeverity } from '~/common'; + +export const langAtom = atom('en'); +export const chatDirectionAtom = atom('ltr'); +export const fontSizeAtom = atom('text-base'); + +export type ToastState = { + open: boolean; + message: string; + severity: NotificationSeverity; + showIcon: boolean; +}; + +export const toastState = atom({ + open: false, + message: '', + severity: NotificationSeverity.SUCCESS, + showIcon: true, +}); diff --git a/packages/client/src/svgs/ListeningIcon.tsx b/packages/client/src/svgs/ListeningIcon.tsx index e81c7e37e9..ef8de6a4ed 100644 --- a/packages/client/src/svgs/ListeningIcon.tsx +++ b/packages/client/src/svgs/ListeningIcon.tsx @@ -1,6 +1,10 @@ import { cn } from '~/utils/'; -export default function ListeningIcon({ className }) { +type ListeningIconProps = { + className?: string; +}; + +export default function ListeningIcon({ className }: ListeningIconProps) { return (