mirror of
https://github.com/danny-avila/LibreChat.git
synced 2025-12-16 16:30:15 +01:00
💬 fix: Temporary Chat PR's broken components and improved UI (#5705)
* 💬 fix: Temporary Chat PR's broken components and improved UI * 💬 fix: bring back hover effect on AudioRecorder button * style: adjust position of Mention component popover * refactor: PromptsCommand typing and style position * refactor: virtualize mention UI --------- Co-authored-by: Danny Avila <danny@librechat.ai>
This commit is contained in:
parent
63afb317c6
commit
70e410f38b
11 changed files with 196 additions and 86 deletions
|
|
@ -13,7 +13,6 @@ export default function AudioRecorder({
|
|||
methods,
|
||||
textAreaRef,
|
||||
isSubmitting,
|
||||
isTemporary = false,
|
||||
}: {
|
||||
isRTL: boolean;
|
||||
disabled: boolean;
|
||||
|
|
@ -21,7 +20,6 @@ export default function AudioRecorder({
|
|||
methods: ReturnType<typeof useChatFormContext>;
|
||||
textAreaRef: React.RefObject<HTMLTextAreaElement>;
|
||||
isSubmitting: boolean;
|
||||
isTemporary?: boolean;
|
||||
}) {
|
||||
const { setValue, reset } = methods;
|
||||
const localize = useLocalize();
|
||||
|
|
@ -78,11 +76,7 @@ export default function AudioRecorder({
|
|||
if (isLoading === true) {
|
||||
return <Spinner className="stroke-gray-700 dark:stroke-gray-300" />;
|
||||
}
|
||||
return (
|
||||
<ListeningIcon
|
||||
className={cn(isTemporary ? 'stroke-white' : 'stroke-gray-700 dark:stroke-gray-300')}
|
||||
/>
|
||||
);
|
||||
return <ListeningIcon className="stroke-gray-700 dark:stroke-gray-300" />;
|
||||
};
|
||||
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ import { cn, removeFocusRings, checkIfScrollable } from '~/utils';
|
|||
import FileFormWrapper from './Files/FileFormWrapper';
|
||||
import { TextareaAutosize } from '~/components/ui';
|
||||
import { useGetFileConfig } from '~/data-provider';
|
||||
import { TemporaryChat } from './TemporaryChat';
|
||||
import TextareaHeader from './TextareaHeader';
|
||||
import PromptsCommand from './PromptsCommand';
|
||||
import AudioRecorder from './AudioRecorder';
|
||||
|
|
@ -47,7 +48,7 @@ const ChatForm = ({ index = 0 }) => {
|
|||
const TextToSpeech = useRecoilValue(store.textToSpeech);
|
||||
const automaticPlayback = useRecoilValue(store.automaticPlayback);
|
||||
const maximizeChatSpace = useRecoilValue(store.maximizeChatSpace);
|
||||
const isTemporary = useRecoilValue(store.isTemporary);
|
||||
const [isTemporaryChat, setIsTemporaryChat] = useRecoilState<boolean>(store.isTemporary);
|
||||
|
||||
const isSearching = useRecoilValue(store.isSearching);
|
||||
const [showStopButton, setShowStopButton] = useRecoilState(store.showStopButtonByIndex(index));
|
||||
|
|
@ -145,11 +146,8 @@ const ChatForm = ({ index = 0 }) => {
|
|||
const isUploadDisabled: boolean = endpointFileConfig?.disabled ?? false;
|
||||
|
||||
const baseClasses = cn(
|
||||
'md:py-3.5 m-0 w-full resize-none bg-surface-tertiary py-[13px] placeholder-black/50 dark:placeholder-white/50 [&:has(textarea:focus)]:shadow-[0_2px_6px_rgba(0,0,0,.05)]',
|
||||
'md:py-3.5 m-0 w-full resize-none py-[13px] bg-surface-tertiary placeholder-black/50 dark:placeholder-white/50 [&:has(textarea:focus)]:shadow-[0_2px_6px_rgba(0,0,0,.05)]',
|
||||
isCollapsed ? 'max-h-[52px]' : 'max-h-[65vh] md:max-h-[75vh]',
|
||||
isTemporary
|
||||
? 'bg-gray-600 text-white placeholder-white/20'
|
||||
: 'bg-surface-tertiary placeholder-black/50 dark:placeholder-white/50',
|
||||
);
|
||||
|
||||
const uploadActive = endpointSupportsFiles && !isUploadDisabled;
|
||||
|
|
@ -185,12 +183,11 @@ const ChatForm = ({ index = 0 }) => {
|
|||
/>
|
||||
)}
|
||||
<PromptsCommand index={index} textAreaRef={textAreaRef} submitPrompt={submitPrompt} />
|
||||
<div
|
||||
className={cn(
|
||||
'transitional-all relative flex w-full flex-grow flex-col overflow-hidden rounded-3xl text-text-primary ',
|
||||
isTemporary ? 'text-white' : 'duration-200',
|
||||
)}
|
||||
>
|
||||
<div className="transitional-all relative flex w-full flex-grow flex-col overflow-hidden rounded-3xl bg-surface-tertiary text-text-primary duration-200">
|
||||
<TemporaryChat
|
||||
isTemporaryChat={isTemporaryChat}
|
||||
setIsTemporaryChat={setIsTemporaryChat}
|
||||
/>
|
||||
<TextareaHeader addedConvo={addedConvo} setAddedConvo={setAddedConvo} />
|
||||
<FileFormWrapper disableInputs={disableInputs}>
|
||||
{endpoint && (
|
||||
|
|
@ -243,7 +240,6 @@ const ChatForm = ({ index = 0 }) => {
|
|||
textAreaRef={textAreaRef}
|
||||
disabled={!!disableInputs}
|
||||
isSubmitting={isSubmitting}
|
||||
isTemporary={isTemporary}
|
||||
/>
|
||||
)}
|
||||
{TextToSpeech && automaticPlayback && <StreamAudio index={index} />}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { useState, useRef, useEffect } from 'react';
|
||||
import { AutoSizer, List } from 'react-virtualized';
|
||||
import { EModelEndpoint } from 'librechat-data-provider';
|
||||
import type { SetterOrUpdater } from 'recoil';
|
||||
import type { MentionOption, ConvoGenerator } from '~/common';
|
||||
|
|
@ -9,6 +10,8 @@ import { useLocalize, useCombobox } from '~/hooks';
|
|||
import { removeCharIfLast } from '~/utils';
|
||||
import MentionItem from './MentionItem';
|
||||
|
||||
const ROW_HEIGHT = 40;
|
||||
|
||||
export default function Mention({
|
||||
setShowMentionPopover,
|
||||
newConversation,
|
||||
|
|
@ -121,8 +124,41 @@ export default function Mention({
|
|||
currentActiveItem?.scrollIntoView({ behavior: 'instant', block: 'nearest' });
|
||||
}, [type, activeIndex]);
|
||||
|
||||
const rowRenderer = ({
|
||||
index,
|
||||
key,
|
||||
style,
|
||||
}: {
|
||||
index: number;
|
||||
key: string;
|
||||
style: React.CSSProperties;
|
||||
}) => {
|
||||
const mention = matches[index] as MentionOption;
|
||||
return (
|
||||
<div className="absolute bottom-16 z-10 w-full space-y-2">
|
||||
<MentionItem
|
||||
type={type}
|
||||
index={index}
|
||||
key={key}
|
||||
style={style}
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
if (timeoutRef.current) {
|
||||
clearTimeout(timeoutRef.current);
|
||||
}
|
||||
timeoutRef.current = null;
|
||||
handleSelect(mention);
|
||||
}}
|
||||
name={mention.label ?? ''}
|
||||
icon={mention.icon}
|
||||
description={mention.description}
|
||||
isActive={index === activeIndex}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="absolute bottom-14 z-10 w-full space-y-2">
|
||||
<div className="popover border-token-border-light rounded-2xl border bg-white p-2 shadow-lg dark:bg-gray-700">
|
||||
<input
|
||||
// The user expects focus to transition to the input field when the popover is opened
|
||||
|
|
@ -167,27 +203,20 @@ export default function Mention({
|
|||
}}
|
||||
/>
|
||||
{open && (
|
||||
<div className="max-h-40 overflow-y-auto">
|
||||
{(matches as MentionOption[]).map((mention, index) => (
|
||||
<MentionItem
|
||||
type={type}
|
||||
index={index}
|
||||
key={`${mention.value}-${index}`}
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
if (timeoutRef.current) {
|
||||
clearTimeout(timeoutRef.current);
|
||||
}
|
||||
timeoutRef.current = null;
|
||||
handleSelect(mention);
|
||||
}}
|
||||
name={mention.label ?? ''}
|
||||
icon={mention.icon}
|
||||
description={mention.description}
|
||||
isActive={index === activeIndex}
|
||||
<div className="max-h-40">
|
||||
<AutoSizer disableHeight>
|
||||
{({ width }) => (
|
||||
<List
|
||||
width={width}
|
||||
overscanRowCount={5}
|
||||
rowHeight={ROW_HEIGHT}
|
||||
rowCount={matches.length}
|
||||
rowRenderer={rowRenderer}
|
||||
scrollToIndex={activeIndex}
|
||||
height={Math.min(matches.length * ROW_HEIGHT, 160)}
|
||||
/>
|
||||
))}
|
||||
)}
|
||||
</AutoSizer>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ export interface MentionItemProps {
|
|||
icon?: React.ReactNode;
|
||||
isActive?: boolean;
|
||||
description?: string;
|
||||
style?: React.CSSProperties;
|
||||
}
|
||||
|
||||
export default function MentionItem({
|
||||
|
|
@ -19,21 +20,28 @@ export default function MentionItem({
|
|||
icon,
|
||||
isActive,
|
||||
description,
|
||||
style,
|
||||
type = 'mention',
|
||||
}: MentionItemProps) {
|
||||
return (
|
||||
<button tabIndex={index} onClick={onClick} id={`${type}-item-${index}`} className="w-full">
|
||||
<button
|
||||
tabIndex={index}
|
||||
onClick={onClick}
|
||||
id={`${type}-item-${index}`}
|
||||
className="w-full"
|
||||
style={style}
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
'text-token-text-primary bg-token-main-surface-secondary group flex h-10 items-center gap-2 rounded-lg px-2 text-sm font-medium hover:bg-surface-secondary',
|
||||
isActive ? 'bg-surface-active' : 'bg-transparent',
|
||||
isActive === true ? 'bg-surface-active' : 'bg-transparent',
|
||||
)}
|
||||
>
|
||||
<div className="flex h-5 w-5 flex-shrink-0 items-center justify-center">{icon}</div>
|
||||
<div className="flex min-w-0 flex-grow items-center justify-between">
|
||||
<div className="truncate">
|
||||
<span className="font-medium">{name}</span>
|
||||
{description ? (
|
||||
{description != null && description ? (
|
||||
<span className="text-token-text-tertiary ml-2 text-sm font-light">
|
||||
{description}
|
||||
</span>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { useState, useRef, useEffect, useMemo, memo, useCallback } from 'react';
|
||||
import { AutoSizer, List } from 'react-virtualized';
|
||||
import { useSetRecoilState, useRecoilValue } from 'recoil';
|
||||
import { PermissionTypes, Permissions } from 'librechat-data-provider';
|
||||
import type { TPromptGroup } from 'librechat-data-provider';
|
||||
|
|
@ -42,6 +43,8 @@ const PopoverContainer = memo(
|
|||
},
|
||||
);
|
||||
|
||||
const ROW_HEIGHT = 40;
|
||||
|
||||
function PromptsCommand({
|
||||
index,
|
||||
textAreaRef,
|
||||
|
|
@ -63,8 +66,10 @@ function PromptsCommand({
|
|||
const mappedArray = data.map((group) => ({
|
||||
id: group._id,
|
||||
value: group.command ?? group.name,
|
||||
label: `${group.command ? `/${group.command} - ` : ''}${group.name}: ${
|
||||
group.oneliner?.length ? group.oneliner : group.productionPrompt?.prompt ?? ''
|
||||
label: `${group.command != null && group.command ? `/${group.command} - ` : ''}${
|
||||
group.name
|
||||
}: ${
|
||||
(group.oneliner?.length ?? 0) > 0 ? group.oneliner : group.productionPrompt?.prompt ?? ''
|
||||
}`,
|
||||
icon: <CategoryIcon category={group.category ?? ''} className="h-5 w-5" />,
|
||||
}));
|
||||
|
|
@ -85,12 +90,12 @@ function PromptsCommand({
|
|||
const [variableGroup, setVariableGroup] = useState<TPromptGroup | null>(null);
|
||||
const setShowPromptsPopover = useSetRecoilState(store.showPromptsPopoverFamily(index));
|
||||
|
||||
const prompts = useMemo(() => data?.promptGroups ?? [], [data]);
|
||||
const promptsMap = useMemo(() => data?.promptsMap ?? {}, [data]);
|
||||
const prompts = useMemo(() => data?.promptGroups, [data]);
|
||||
const promptsMap = useMemo(() => data?.promptsMap, [data]);
|
||||
|
||||
const { open, setOpen, searchValue, setSearchValue, matches } = useCombobox({
|
||||
value: '',
|
||||
options: prompts,
|
||||
options: prompts ?? [],
|
||||
});
|
||||
|
||||
const handleSelect = useCallback(
|
||||
|
|
@ -107,22 +112,20 @@ function PromptsCommand({
|
|||
removeCharIfLast(textAreaRef.current, commandChar);
|
||||
}
|
||||
|
||||
const isValidPrompt = mention && promptsMap && promptsMap[mention.id];
|
||||
|
||||
if (!isValidPrompt) {
|
||||
const group = promptsMap?.[mention.id];
|
||||
if (!group) {
|
||||
return;
|
||||
}
|
||||
|
||||
const group = promptsMap[mention.id];
|
||||
const hasVariables = detectVariables(group.productionPrompt?.prompt ?? '');
|
||||
if (group && hasVariables) {
|
||||
if (hasVariables) {
|
||||
if (e && e.key === 'Tab') {
|
||||
e.preventDefault();
|
||||
}
|
||||
setVariableGroup(group);
|
||||
setVariableDialogOpen(true);
|
||||
return;
|
||||
} else if (group) {
|
||||
} else {
|
||||
submitPrompt(group.productionPrompt?.prompt ?? '');
|
||||
}
|
||||
},
|
||||
|
|
@ -154,6 +157,37 @@ function PromptsCommand({
|
|||
return null;
|
||||
}
|
||||
|
||||
const rowRenderer = ({
|
||||
index,
|
||||
key,
|
||||
style,
|
||||
}: {
|
||||
index: number;
|
||||
key: string;
|
||||
style: React.CSSProperties;
|
||||
}) => {
|
||||
const mention = matches[index] as PromptOption;
|
||||
return (
|
||||
<MentionItem
|
||||
index={index}
|
||||
type="prompt"
|
||||
key={key}
|
||||
style={style}
|
||||
onClick={() => {
|
||||
if (timeoutRef.current) {
|
||||
clearTimeout(timeoutRef.current);
|
||||
}
|
||||
timeoutRef.current = null;
|
||||
handleSelect(mention);
|
||||
}}
|
||||
name={mention.label ?? ''}
|
||||
icon={mention.icon}
|
||||
description={mention.description}
|
||||
isActive={index === activeIndex}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<PopoverContainer
|
||||
index={index}
|
||||
|
|
@ -161,7 +195,7 @@ function PromptsCommand({
|
|||
variableGroup={variableGroup}
|
||||
setVariableDialogOpen={setVariableDialogOpen}
|
||||
>
|
||||
<div className="absolute bottom-16 z-10 w-full space-y-2">
|
||||
<div className="absolute bottom-14 z-10 w-full space-y-2">
|
||||
<div className="popover border-token-border-light rounded-2xl border bg-surface-tertiary-alt p-2 shadow-lg">
|
||||
<input
|
||||
// The user expects focus to transition to the input field when the popover is opened
|
||||
|
|
@ -213,24 +247,23 @@ function PromptsCommand({
|
|||
}
|
||||
|
||||
if (!isLoading && open) {
|
||||
return (matches as PromptOption[]).map((mention, index) => (
|
||||
<MentionItem
|
||||
index={index}
|
||||
type="prompt"
|
||||
key={`${mention.value}-${index}`}
|
||||
onClick={() => {
|
||||
if (timeoutRef.current) {
|
||||
clearTimeout(timeoutRef.current);
|
||||
}
|
||||
timeoutRef.current = null;
|
||||
handleSelect(mention);
|
||||
}}
|
||||
name={mention.label ?? ''}
|
||||
icon={mention.icon}
|
||||
description={mention.description}
|
||||
isActive={index === activeIndex}
|
||||
return (
|
||||
<div className="max-h-40">
|
||||
<AutoSizer disableHeight>
|
||||
{({ width }) => (
|
||||
<List
|
||||
width={width}
|
||||
overscanRowCount={5}
|
||||
rowHeight={ROW_HEIGHT}
|
||||
rowCount={matches.length}
|
||||
rowRenderer={rowRenderer}
|
||||
scrollToIndex={activeIndex}
|
||||
height={Math.min(matches.length * ROW_HEIGHT, 160)}
|
||||
/>
|
||||
));
|
||||
)}
|
||||
</AutoSizer>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
})()}
|
||||
|
|
|
|||
38
client/src/components/Chat/Input/TemporaryChat.tsx
Normal file
38
client/src/components/Chat/Input/TemporaryChat.tsx
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
import { MessageCircleDashed, X } from 'lucide-react';
|
||||
import { useLocalize } from '~/hooks';
|
||||
|
||||
interface TemporaryChatProps {
|
||||
isTemporaryChat: boolean;
|
||||
setIsTemporaryChat: (value: boolean) => void;
|
||||
}
|
||||
|
||||
export const TemporaryChat = ({ isTemporaryChat, setIsTemporaryChat }: TemporaryChatProps) => {
|
||||
const localize = useLocalize();
|
||||
|
||||
if (!isTemporaryChat) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="divide-token-border-light m-1.5 flex flex-col divide-y overflow-hidden rounded-b-lg rounded-t-2xl bg-surface-secondary-alt">
|
||||
<div className="flex items-start gap-4 py-2.5 pl-3 pr-1.5 text-sm">
|
||||
<span className="mt-0 flex h-6 w-6 flex-shrink-0 items-center justify-center">
|
||||
<div className="icon-md">
|
||||
<MessageCircleDashed className="icon-md" />
|
||||
</div>
|
||||
</span>
|
||||
<span className="text-token-text-secondary line-clamp-3 flex-1 py-0.5 font-semibold">
|
||||
{localize('com_ui_temporary_chat')}
|
||||
</span>
|
||||
<button
|
||||
className="text-token-text-secondary flex-shrink-0"
|
||||
type="button"
|
||||
aria-label="Close temporary chat"
|
||||
onClick={() => setIsTemporaryChat(false)}
|
||||
>
|
||||
<X className="pr-1" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
@ -4,15 +4,16 @@ import { MessageCircleDashed } from 'lucide-react';
|
|||
import { useRecoilState, useRecoilValue } from 'recoil';
|
||||
import { Constants, getConfigDefaults } from 'librechat-data-provider';
|
||||
import { useGetStartupConfig } from '~/data-provider';
|
||||
import temporaryStore from '~/store/temporary';
|
||||
import { Switch } from '~/components/ui';
|
||||
import { useLocalize } from '~/hooks';
|
||||
import { cn } from '~/utils';
|
||||
import store from '~/store';
|
||||
|
||||
export const TemporaryChat = () => {
|
||||
const localize = useLocalize();
|
||||
const { data: startupConfig } = useGetStartupConfig();
|
||||
const defaultInterface = getConfigDefaults().interface;
|
||||
const [isTemporary, setIsTemporary] = useRecoilState(temporaryStore.isTemporary);
|
||||
const [isTemporary, setIsTemporary] = useRecoilState(store.isTemporary);
|
||||
const conversation = useRecoilValue(store.conversationByIndex(0)) || undefined;
|
||||
const conversationId = conversation?.conversationId ?? '';
|
||||
const interfaceConfig = useMemo(
|
||||
|
|
@ -20,7 +21,7 @@ export const TemporaryChat = () => {
|
|||
[startupConfig],
|
||||
);
|
||||
|
||||
if (!interfaceConfig.temporaryChat) {
|
||||
if (interfaceConfig.temporaryChat === false) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
|
@ -39,20 +40,21 @@ export const TemporaryChat = () => {
|
|||
};
|
||||
|
||||
return (
|
||||
<div className="sticky bottom-0 border-t border-gray-200 bg-white px-6 py-4 dark:border-gray-700 dark:bg-gray-700">
|
||||
<div className="sticky bottom-0 border-none bg-surface-tertiary px-6 py-4 ">
|
||||
<div className="flex items-center">
|
||||
<div className={cn('flex flex-1 items-center gap-2', isActiveConvo && 'opacity-40')}>
|
||||
<MessageCircleDashed className="icon-sm" />
|
||||
<span className="text-sm text-gray-700 dark:text-gray-300">Temporary Chat</span>
|
||||
<span className="text-sm text-text-primary">{localize('com_ui_temporary_chat')}</span>
|
||||
</div>
|
||||
<div className="ml-auto flex items-center">
|
||||
<Switch
|
||||
id="enableUserMsgMarkdown"
|
||||
id="temporary-chat-switch"
|
||||
checked={isTemporary}
|
||||
onCheckedChange={onClick}
|
||||
disabled={isActiveConvo}
|
||||
className="ml-4"
|
||||
data-testid="enableUserMsgMarkdown"
|
||||
aria-label="Toggle temporary chat"
|
||||
data-testid="temporary-chat-switch"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -463,6 +463,7 @@ export default {
|
|||
com_ui_shared_link_delete_success: 'Successfully deleted shared link',
|
||||
com_ui_shared_link_bulk_delete_success: 'Successfully deleted shared links',
|
||||
com_ui_search: 'Search',
|
||||
com_ui_temporary_chat: 'Temporary Chat',
|
||||
com_auth_error_login:
|
||||
'Unable to login with the information provided. Please check your credentials and try again.',
|
||||
com_auth_error_login_rl:
|
||||
|
|
|
|||
|
|
@ -1,9 +1,6 @@
|
|||
import { atom } from 'recoil';
|
||||
import { atomWithLocalStorage } from '~/store/utils';
|
||||
|
||||
const isTemporary = atom<boolean>({
|
||||
key: 'isTemporary',
|
||||
default: false,
|
||||
});
|
||||
const isTemporary = atomWithLocalStorage('isTemporary', false);
|
||||
|
||||
export default {
|
||||
isTemporary,
|
||||
|
|
|
|||
11
package-lock.json
generated
11
package-lock.json
generated
|
|
@ -16,6 +16,7 @@
|
|||
"devDependencies": {
|
||||
"@axe-core/playwright": "^4.9.1",
|
||||
"@playwright/test": "^1.38.1",
|
||||
"@types/react-virtualized": "^9.22.0",
|
||||
"@typescript-eslint/eslint-plugin": "^5.62.0",
|
||||
"@typescript-eslint/parser": "^5.62.0",
|
||||
"cross-env": "^7.0.3",
|
||||
|
|
@ -15126,6 +15127,16 @@
|
|||
"@types/react": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/react-virtualized": {
|
||||
"version": "9.22.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/react-virtualized/-/react-virtualized-9.22.0.tgz",
|
||||
"integrity": "sha512-JL/YCCFZ123za//cj10Apk54F0UGFMrjOE0QHTuXt1KBMFrzLOGv9/x6Uc/pZ0Gaf4o6w61Fostvlw0DwuPXig==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@types/prop-types": "*",
|
||||
"@types/react": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/resolve": {
|
||||
"version": "1.20.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.20.2.tgz",
|
||||
|
|
|
|||
|
|
@ -82,6 +82,7 @@
|
|||
"devDependencies": {
|
||||
"@axe-core/playwright": "^4.9.1",
|
||||
"@playwright/test": "^1.38.1",
|
||||
"@types/react-virtualized": "^9.22.0",
|
||||
"@typescript-eslint/eslint-plugin": "^5.62.0",
|
||||
"@typescript-eslint/parser": "^5.62.0",
|
||||
"cross-env": "^7.0.3",
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue