mirror of
https://github.com/danny-avila/LibreChat.git
synced 2026-05-13 07:46:47 +00:00
style(Parameters): UI improvements + removed HeaderOptions, OptionsPopover, and AlternativeSettings components; add SaveAsPresetDialog component; update translations and tailwind config for improved UI consistency
This commit is contained in:
parent
13cea97c9b
commit
66e3236171
9 changed files with 75 additions and 280 deletions
|
|
@ -1,99 +0,0 @@
|
|||
import { useState } from 'react';
|
||||
import { Settings2 } from 'lucide-react';
|
||||
import { TooltipAnchor } from '@librechat/client';
|
||||
import { Root, Anchor } from '@radix-ui/react-popover';
|
||||
import { isParamEndpoint, getEndpointField, tConvoUpdateSchema } from 'librechat-data-provider';
|
||||
import type { TPreset, TInterfaceConfig } from 'librechat-data-provider';
|
||||
import { EndpointSettings, SaveAsPresetDialog, AlternativeSettings } from '~/components/Endpoints';
|
||||
import { useSetIndexOptions, useLocalize } from '~/hooks';
|
||||
import { useGetEndpointsQuery } from '~/data-provider';
|
||||
import OptionsPopover from './OptionsPopover';
|
||||
import PopoverButtons from './PopoverButtons';
|
||||
import { useChatContext } from '~/Providers';
|
||||
|
||||
export default function HeaderOptions({
|
||||
interfaceConfig,
|
||||
}: {
|
||||
interfaceConfig?: Partial<TInterfaceConfig>;
|
||||
}) {
|
||||
const { data: endpointsConfig } = useGetEndpointsQuery();
|
||||
|
||||
const [saveAsDialogShow, setSaveAsDialogShow] = useState<boolean>(false);
|
||||
const localize = useLocalize();
|
||||
|
||||
const { showPopover, conversation, setShowPopover } = useChatContext();
|
||||
const { setOption } = useSetIndexOptions();
|
||||
const { endpoint } = conversation ?? {};
|
||||
|
||||
const saveAsPreset = () => {
|
||||
setSaveAsDialogShow(true);
|
||||
};
|
||||
|
||||
if (!endpoint) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const triggerAdvancedMode = () => setShowPopover((prev) => !prev);
|
||||
|
||||
const endpointType = getEndpointField(endpointsConfig, endpoint, 'type');
|
||||
const paramEndpoint = isParamEndpoint(endpoint, endpointType);
|
||||
|
||||
return (
|
||||
<Root
|
||||
open={showPopover}
|
||||
// onOpenChange={} // called when the open state of the popover changes.
|
||||
>
|
||||
<Anchor>
|
||||
<div className="my-auto lg:max-w-2xl xl:max-w-3xl">
|
||||
<span className="flex w-full flex-col items-center justify-center gap-0 md:order-none md:m-auto md:gap-2">
|
||||
<div className="z-[61] flex w-full items-center justify-center gap-2">
|
||||
{interfaceConfig?.parameters === true && paramEndpoint === false && (
|
||||
<TooltipAnchor
|
||||
id="parameters-button"
|
||||
aria-label={localize('com_ui_model_parameters')}
|
||||
description={localize('com_ui_model_parameters')}
|
||||
tabIndex={0}
|
||||
role="button"
|
||||
onClick={triggerAdvancedMode}
|
||||
data-testid="parameters-button"
|
||||
className="inline-flex size-10 items-center justify-center rounded-lg border border-border-light bg-transparent text-text-primary transition-all ease-in-out hover:bg-surface-tertiary disabled:pointer-events-none disabled:opacity-50 radix-state-open:bg-surface-tertiary"
|
||||
>
|
||||
<Settings2 size={16} aria-hidden="true" />
|
||||
</TooltipAnchor>
|
||||
)}
|
||||
</div>
|
||||
{interfaceConfig?.parameters === true && paramEndpoint === false && (
|
||||
<OptionsPopover
|
||||
visible={showPopover}
|
||||
saveAsPreset={saveAsPreset}
|
||||
presetsDisabled={!(interfaceConfig.presets ?? false)}
|
||||
PopoverButtons={<PopoverButtons />}
|
||||
closePopover={() => setShowPopover(false)}
|
||||
>
|
||||
<div className="px-4 py-4">
|
||||
<EndpointSettings
|
||||
className="[&::-webkit-scrollbar]:w-2"
|
||||
conversation={conversation}
|
||||
setOption={setOption}
|
||||
/>
|
||||
<AlternativeSettings conversation={conversation} setOption={setOption} />
|
||||
</div>
|
||||
</OptionsPopover>
|
||||
)}
|
||||
{interfaceConfig?.presets === true && (
|
||||
<SaveAsPresetDialog
|
||||
open={saveAsDialogShow}
|
||||
onOpenChange={setSaveAsDialogShow}
|
||||
preset={
|
||||
tConvoUpdateSchema.parse({
|
||||
...conversation,
|
||||
}) as TPreset
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
</Anchor>
|
||||
</Root>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,93 +0,0 @@
|
|||
import { useRef } from 'react';
|
||||
import { Save } from 'lucide-react';
|
||||
import { Portal, Content } from '@radix-ui/react-popover';
|
||||
import { Button, CrossIcon, useOnClickOutside } from '@librechat/client';
|
||||
import type { ReactNode } from 'react';
|
||||
import { cn, removeFocusOutlines } from '~/utils';
|
||||
import { useLocalize } from '~/hooks';
|
||||
|
||||
type TOptionsPopoverProps = {
|
||||
children: ReactNode;
|
||||
visible: boolean;
|
||||
saveAsPreset: () => void;
|
||||
closePopover: () => void;
|
||||
PopoverButtons: ReactNode;
|
||||
presetsDisabled: boolean;
|
||||
};
|
||||
|
||||
export default function OptionsPopover({
|
||||
children,
|
||||
// endpoint,
|
||||
visible,
|
||||
saveAsPreset,
|
||||
closePopover,
|
||||
PopoverButtons,
|
||||
presetsDisabled,
|
||||
}: TOptionsPopoverProps) {
|
||||
const popoverRef = useRef(null);
|
||||
useOnClickOutside(
|
||||
popoverRef,
|
||||
() => closePopover(),
|
||||
['dialog-template-content', 'shadcn-button', 'advanced-settings'],
|
||||
(_target) => {
|
||||
const target = _target as Element;
|
||||
if (
|
||||
target.id === 'presets-button' ||
|
||||
(target.parentNode instanceof Element && target.parentNode.id === 'presets-button')
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
const tagName = target.tagName;
|
||||
return tagName === 'path' || tagName === 'svg' || tagName === 'circle';
|
||||
},
|
||||
);
|
||||
|
||||
const localize = useLocalize();
|
||||
const cardStyle =
|
||||
'shadow-xl rounded-md min-w-[75px] font-normal bg-white border-black/10 border dark:bg-gray-700 text-black dark:text-white';
|
||||
|
||||
if (!visible) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Portal>
|
||||
<Content sideOffset={8} align="start" ref={popoverRef} asChild>
|
||||
<div className="z-[70] flex w-screen flex-col items-center md:w-full md:px-4">
|
||||
<div
|
||||
className={cn(
|
||||
cardStyle,
|
||||
'dark:bg-gray-700',
|
||||
'border-d-0 flex w-full flex-col overflow-hidden rounded-none border-s-0 border-t bg-white px-0 pb-[10px] dark:border-white/10 md:rounded-md md:border lg:w-[736px]',
|
||||
)}
|
||||
>
|
||||
<div className="flex w-full items-center bg-gray-50 px-2 py-2 dark:bg-gray-700">
|
||||
{presetsDisabled ? null : (
|
||||
<Button
|
||||
type="button"
|
||||
className="h-auto w-[150px] justify-start rounded-md border border-gray-300/50 bg-transparent px-2 py-1 text-xs font-normal text-black hover:bg-gray-100 hover:text-black focus-visible:ring-1 focus-visible:ring-ring-primary dark:border-gray-600 dark:bg-transparent dark:text-white dark:hover:bg-gray-600 dark:focus-visible:ring-white"
|
||||
onClick={saveAsPreset}
|
||||
>
|
||||
<Save className="mr-1 w-[14px]" />
|
||||
{localize('com_endpoint_save_as_preset')}
|
||||
</Button>
|
||||
)}
|
||||
{PopoverButtons}
|
||||
<Button
|
||||
type="button"
|
||||
className={cn(
|
||||
'ml-auto h-auto bg-transparent px-3 py-2 text-xs font-normal text-black hover:bg-gray-100 hover:text-black dark:bg-transparent dark:text-white dark:hover:bg-gray-700 dark:hover:text-white',
|
||||
removeFocusOutlines,
|
||||
)}
|
||||
onClick={closePopover}
|
||||
>
|
||||
<CrossIcon />
|
||||
</Button>
|
||||
</div>
|
||||
<div>{children}</div>
|
||||
</div>
|
||||
</div>
|
||||
</Content>
|
||||
</Portal>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,24 +0,0 @@
|
|||
import { useRecoilValue } from 'recoil';
|
||||
import { SettingsViews } from 'librechat-data-provider';
|
||||
import type { TSettingsProps } from '~/common';
|
||||
import { Advanced } from './Settings';
|
||||
import { cn } from '~/utils';
|
||||
import store from '~/store';
|
||||
|
||||
export default function AlternativeSettings({
|
||||
conversation,
|
||||
setOption,
|
||||
isPreset = false,
|
||||
className = '',
|
||||
}: TSettingsProps) {
|
||||
const currentSettingsView = useRecoilValue(store.currentSettingsView);
|
||||
if (!conversation?.endpoint || currentSettingsView === SettingsViews.default) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={cn('hide-scrollbar h-[500px] overflow-y-auto md:mb-2 md:h-[350px]', className)}>
|
||||
<Advanced conversation={conversation} setOption={setOption} isPreset={isPreset} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,8 +1,6 @@
|
|||
export { default as Icon } from './Icon';
|
||||
export { default as MinimalIcon } from './MinimalIcon';
|
||||
export { default as ConvoIcon } from './ConvoIcon';
|
||||
export { default as MinimalIcon } from './MinimalIcon';
|
||||
export { default as EndpointIcon } from './EndpointIcon';
|
||||
export { default as ConvoIconURL } from './ConvoIconURL';
|
||||
export { default as EndpointSettings } from './EndpointSettings';
|
||||
export { default as SaveAsPresetDialog } from './SaveAsPresetDialog';
|
||||
export { default as AlternativeSettings } from './AlternativeSettings';
|
||||
|
|
|
|||
|
|
@ -1,9 +1,8 @@
|
|||
import React, { useMemo, useEffect } from 'react';
|
||||
import keyBy from 'lodash/keyBy';
|
||||
import { ControlCombobox } from '@librechat/client';
|
||||
import { ChevronLeft, RotateCcw } from 'lucide-react';
|
||||
import { ControlCombobox, Button } from '@librechat/client';
|
||||
import { useFormContext, useWatch, Controller } from 'react-hook-form';
|
||||
import { componentMapping } from '~/components/SidePanel/Parameters/components';
|
||||
import {
|
||||
alternateName,
|
||||
getSettingsKeys,
|
||||
|
|
@ -13,6 +12,7 @@ import {
|
|||
agentParamSettings,
|
||||
} from 'librechat-data-provider';
|
||||
import type * as t from 'librechat-data-provider';
|
||||
import { componentMapping } from '~/components/SidePanel/Parameters/components';
|
||||
import type { AgentForm, AgentModelPanelProps, StringOption } from '~/common';
|
||||
import { useGetEndpointsQuery } from '~/data-provider';
|
||||
import { useLocalize } from '~/hooks';
|
||||
|
|
@ -245,14 +245,10 @@ export default function ModelPanel({
|
|||
</div>
|
||||
)}
|
||||
{/* Reset Parameters Button */}
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleResetParameters}
|
||||
className="btn btn-neutral my-1 flex w-full items-center justify-center gap-2 px-4 py-2 text-sm"
|
||||
>
|
||||
<Button variant="outline" onClick={handleResetParameters}>
|
||||
<RotateCcw className="h-4 w-4" aria-hidden="true" />
|
||||
{localize('com_ui_reset_var', { 0: localize('com_ui_model_parameters') })}
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import React, { useMemo, useState, useEffect, useCallback } from 'react';
|
||||
import React, { useMemo, useEffect, useCallback, useState } from 'react';
|
||||
import keyBy from 'lodash/keyBy';
|
||||
import { RotateCcw } from 'lucide-react';
|
||||
import { Button } from '@librechat/client';
|
||||
import { RotateCcw, BookPlus } from 'lucide-react';
|
||||
import {
|
||||
excludedKeys,
|
||||
paramSettings,
|
||||
|
|
@ -10,9 +11,9 @@ import {
|
|||
tConvoUpdateSchema,
|
||||
} from 'librechat-data-provider';
|
||||
import type { TPreset } from 'librechat-data-provider';
|
||||
import { SaveAsPresetDialog } from '~/components/Endpoints';
|
||||
import { useSetIndexOptions, useLocalize } from '~/hooks';
|
||||
import { useGetEndpointsQuery } from '~/data-provider';
|
||||
import SaveAsPresetDialog from './SaveAsPresetDialog';
|
||||
import { componentMapping } from './components';
|
||||
import { useChatContext } from '~/Providers';
|
||||
import { logger } from '~/utils';
|
||||
|
|
@ -22,13 +23,14 @@ export default function Parameters() {
|
|||
const { conversation, setConversation } = useChatContext();
|
||||
const { setOption } = useSetIndexOptions();
|
||||
|
||||
const [isDialogOpen, setIsDialogOpen] = useState(false);
|
||||
const [preset, setPreset] = useState<TPreset | null>(null);
|
||||
|
||||
const { data: endpointsConfig = {} } = useGetEndpointsQuery();
|
||||
const provider = conversation?.endpoint ?? '';
|
||||
const model = conversation?.model ?? '';
|
||||
|
||||
const [isDialogOpen, setIsDialogOpen] = useState(false);
|
||||
const [preset, setPreset] = useState<TPreset | null>(null);
|
||||
const [isResetting, setIsResetting] = useState(false);
|
||||
|
||||
const bedrockRegions = useMemo(() => {
|
||||
return endpointsConfig?.[conversation?.endpoint ?? '']?.availableRegions ?? [];
|
||||
}, [endpointsConfig, conversation?.endpoint]);
|
||||
|
|
@ -105,6 +107,13 @@ export default function Parameters() {
|
|||
}, [parameters, setConversation]);
|
||||
|
||||
const resetParameters = useCallback(() => {
|
||||
if (isResetting) {
|
||||
return;
|
||||
}
|
||||
|
||||
setIsResetting(true);
|
||||
setTimeout(() => setIsResetting(false), 500);
|
||||
|
||||
setConversation((prev) => {
|
||||
if (!prev) {
|
||||
return prev;
|
||||
|
|
@ -127,9 +136,9 @@ export default function Parameters() {
|
|||
logger.log('parameters', 'parameters reset, affected keys:', resetKeys);
|
||||
return updatedConversation;
|
||||
});
|
||||
}, [setConversation]);
|
||||
}, [isResetting, setConversation]);
|
||||
|
||||
const openDialog = useCallback(() => {
|
||||
const saveAsPreset = useCallback(() => {
|
||||
const newPreset = tConvoUpdateSchema.parse({
|
||||
...conversation,
|
||||
}) as TPreset;
|
||||
|
|
@ -171,23 +180,24 @@ export default function Parameters() {
|
|||
})}
|
||||
</div>
|
||||
<div className="mt-4 flex justify-center">
|
||||
<button
|
||||
type="button"
|
||||
<Button
|
||||
className="w-full"
|
||||
variant="outline"
|
||||
onClick={resetParameters}
|
||||
className="btn btn-neutral flex w-full items-center justify-center gap-2 px-4 py-2 text-sm"
|
||||
disabled={isResetting}
|
||||
>
|
||||
<RotateCcw className="h-4 w-4" aria-hidden="true" />
|
||||
<RotateCcw
|
||||
className={`h-4 w-4 ${isResetting ? 'animate-spin-reset' : ''}`}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
{localize('com_ui_reset_var', { 0: localize('com_ui_model_parameters') })}
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
<div className="mt-2 flex justify-center">
|
||||
<button
|
||||
onClick={openDialog}
|
||||
className="btn btn-primary focus:shadow-outline flex w-full items-center justify-center px-4 py-2 font-semibold text-white hover:bg-green-600 focus:border-green-500"
|
||||
type="button"
|
||||
>
|
||||
<Button className="w-full" variant="default" onClick={saveAsPreset} type="button">
|
||||
<BookPlus className="h-4 w-4" aria-hidden="true" />
|
||||
{localize('com_endpoint_save_as_preset')}
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
{preset && (
|
||||
<SaveAsPresetDialog open={isDialogOpen} onOpenChange={setIsDialogOpen} preset={preset} />
|
||||
|
|
|
|||
|
|
@ -1,16 +1,26 @@
|
|||
import React, { useEffect, useState } from 'react';
|
||||
import { useCreatePresetMutation } from 'librechat-data-provider/react-query';
|
||||
import { OGDialogTemplate, OGDialog, Input, Label, useToastContext } from '@librechat/client';
|
||||
import {
|
||||
Input,
|
||||
Label,
|
||||
Button,
|
||||
Spinner,
|
||||
useToastContext,
|
||||
OGDialog,
|
||||
OGDialogTemplate,
|
||||
} from '@librechat/client';
|
||||
import type { TEditPresetProps } from '~/common';
|
||||
import { cn, removeFocusOutlines, cleanupPreset, defaultTextProps } from '~/utils';
|
||||
import { NotificationSeverity } from '~/common';
|
||||
import { cleanupPreset, logger } from '~/utils';
|
||||
import { useLocalize } from '~/hooks';
|
||||
|
||||
const SaveAsPresetDialog = ({ open, onOpenChange, preset }: TEditPresetProps) => {
|
||||
const [title, setTitle] = useState<string>(preset.title ?? 'My Preset');
|
||||
const createPresetMutation = useCreatePresetMutation();
|
||||
const { showToast } = useToastContext();
|
||||
const localize = useLocalize();
|
||||
const { showToast } = useToastContext();
|
||||
const createPresetMutation = useCreatePresetMutation();
|
||||
const isLoading = createPresetMutation.isLoading;
|
||||
|
||||
const [title, setTitle] = useState<string>(preset.title ?? 'My Preset');
|
||||
|
||||
const submitPreset = () => {
|
||||
const _preset = cleanupPreset({
|
||||
|
|
@ -30,7 +40,8 @@ const SaveAsPresetDialog = ({ open, onOpenChange, preset }: TEditPresetProps) =>
|
|||
});
|
||||
onOpenChange(false); // Close the dialog on success
|
||||
},
|
||||
onError: () => {
|
||||
onError: (error) => {
|
||||
logger.error('Error saving preset:', error);
|
||||
showToast({
|
||||
message: localize('com_endpoint_preset_save_error'),
|
||||
severity: NotificationSeverity.ERROR,
|
||||
|
|
@ -44,7 +55,6 @@ const SaveAsPresetDialog = ({ open, onOpenChange, preset }: TEditPresetProps) =>
|
|||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [open]);
|
||||
|
||||
// Handle Enter key press
|
||||
const handleKeyDown = (event: React.KeyboardEvent) => {
|
||||
if (event.key === 'Enter') {
|
||||
event.preventDefault();
|
||||
|
|
@ -56,36 +66,28 @@ const SaveAsPresetDialog = ({ open, onOpenChange, preset }: TEditPresetProps) =>
|
|||
<OGDialog open={open} onOpenChange={onOpenChange}>
|
||||
<OGDialogTemplate
|
||||
title={localize('com_endpoint_save_as_preset')}
|
||||
className="z-[90] w-11/12 sm:w-1/4"
|
||||
overlayClassName="z-[80]"
|
||||
className="w-11/12 max-w-lg"
|
||||
showCloseButton={false}
|
||||
main={
|
||||
<div className="flex w-full flex-col items-center gap-2">
|
||||
<div className="grid w-full items-center gap-2">
|
||||
<Label htmlFor="preset-custom-name" className="text-left text-sm font-medium">
|
||||
{localize('com_endpoint_preset_name')}
|
||||
</Label>
|
||||
<Input
|
||||
id="preset-custom-name"
|
||||
value={title || ''}
|
||||
onChange={(e) => setTitle(e.target.value || '')}
|
||||
onKeyDown={handleKeyDown}
|
||||
placeholder={localize('com_endpoint_preset_custom_name_placeholder')}
|
||||
aria-label={localize('com_endpoint_preset_name')}
|
||||
className={cn(
|
||||
defaultTextProps,
|
||||
'flex h-10 max-h-10 w-full resize-none border-border-medium px-3 py-2',
|
||||
removeFocusOutlines,
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<Label htmlFor="preset-custom-name" className="text-sm font-medium">
|
||||
{localize('com_endpoint_preset_name')}
|
||||
</Label>
|
||||
<Input
|
||||
id="preset-custom-name"
|
||||
value={title || ''}
|
||||
onChange={(e) => setTitle(e.target.value || '')}
|
||||
onKeyDown={handleKeyDown}
|
||||
placeholder={localize('com_endpoint_enter_name_placeholder')}
|
||||
aria-label={localize('com_endpoint_preset_name')}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
selection={{
|
||||
selectHandler: submitPreset,
|
||||
selectClasses: 'bg-green-500 hover:bg-green-600 dark:hover:bg-green-600 text-white',
|
||||
selectText: localize('com_ui_save'),
|
||||
}}
|
||||
selection={
|
||||
<Button variant="submit" onClick={submitPreset}>
|
||||
{isLoading ? <Spinner /> : localize('com_ui_save')}
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
</OGDialog>
|
||||
);
|
||||
|
|
@ -308,7 +308,7 @@
|
|||
"com_endpoint_plug_resend_files": "Resend Files",
|
||||
"com_endpoint_presence_penalty": "Presence Penalty",
|
||||
"com_endpoint_preset": "preset",
|
||||
"com_endpoint_preset_custom_name_placeholder": "something needs to go here. was empty",
|
||||
"com_endpoint_enter_name_placeholder": "Enter a name",
|
||||
"com_endpoint_preset_default": "is now the default preset.",
|
||||
"com_endpoint_preset_default_item": "Default:",
|
||||
"com_endpoint_preset_default_none": "No default preset active.",
|
||||
|
|
@ -884,7 +884,7 @@
|
|||
"com_ui_delete_confirm_strong": "This will delete <strong>{{title}}</strong>",
|
||||
"com_ui_delete_conversation": "Delete chat?",
|
||||
"com_ui_delete_conversation_tooltip": "Delete conversation",
|
||||
"com_ui_delete_memory": "Delete Memory",
|
||||
"com_ui_delete_memory": "Delete Memory?",
|
||||
"com_ui_delete_not_allowed": "Delete operation is not allowed",
|
||||
"com_ui_delete_preset": "Delete Preset?",
|
||||
"com_ui_delete_prompt": "Delete Prompt?",
|
||||
|
|
|
|||
|
|
@ -47,6 +47,10 @@ module.exports = {
|
|||
'0%': { transform: 'translateX(0)' },
|
||||
'100%': { transform: 'translateX(100%)' },
|
||||
},
|
||||
'spin-reset': {
|
||||
'0%': { transform: 'rotate(0deg)' },
|
||||
'100%': { transform: 'rotate(-360deg)' },
|
||||
},
|
||||
},
|
||||
animation: {
|
||||
'fade-in': 'fadeIn 0.5s ease-out forwards',
|
||||
|
|
@ -56,6 +60,7 @@ module.exports = {
|
|||
'slide-in-left': 'slide-in-left 300ms cubic-bezier(0.25, 0.1, 0.25, 1)',
|
||||
'slide-out-left': 'slide-out-left 300ms cubic-bezier(0.25, 0.1, 0.25, 1)',
|
||||
'slide-out-right': 'slide-out-right 300ms cubic-bezier(0.25, 0.1, 0.25, 1)',
|
||||
'spin-reset': 'spin-reset 500ms ease-in-out',
|
||||
},
|
||||
colors: {
|
||||
gray: {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue