mirror of
https://github.com/IRBorisov/ConceptPortal.git
synced 2025-06-26 04:50:36 +03:00
Implement UI animations
using Framer Motion
This commit is contained in:
parent
e024900dfa
commit
1df94572b7
|
@ -24,6 +24,7 @@ This readme file is used mostly to document project dependencies
|
|||
- react-pdf
|
||||
- react-tooltip
|
||||
- js-file-download
|
||||
- framer-motion
|
||||
- reagraph
|
||||
- @tanstack/react-table
|
||||
- @uiw/react-codemirror
|
||||
|
|
39
rsconcept/frontend/package-lock.json
generated
39
rsconcept/frontend/package-lock.json
generated
|
@ -14,6 +14,7 @@
|
|||
"@uiw/react-codemirror": "^4.21.21",
|
||||
"axios": "^1.6.2",
|
||||
"clsx": "^2.0.0",
|
||||
"framer-motion": "^10.16.16",
|
||||
"js-file-download": "^0.4.12",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
|
@ -4985,6 +4986,44 @@
|
|||
"url": "https://github.com/sponsors/rawify"
|
||||
}
|
||||
},
|
||||
"node_modules/framer-motion": {
|
||||
"version": "10.16.16",
|
||||
"resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-10.16.16.tgz",
|
||||
"integrity": "sha512-je6j91rd7NmUX7L1XHouwJ4v3R+SO4umso2LUcgOct3rHZ0PajZ80ETYZTajzEXEl9DlKyzjyt4AvGQ+lrebOw==",
|
||||
"dependencies": {
|
||||
"tslib": "^2.4.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@emotion/is-prop-valid": "^0.8.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^18.0.0",
|
||||
"react-dom": "^18.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"react": {
|
||||
"optional": true
|
||||
},
|
||||
"react-dom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/framer-motion/node_modules/@emotion/is-prop-valid": {
|
||||
"version": "0.8.8",
|
||||
"resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-0.8.8.tgz",
|
||||
"integrity": "sha512-u5WtneEAr5IDG2Wv65yhunPSMLIpuKsbuOktRojfrEiEvRyC85LgPMZI63cr7NUqT8ZIGdSVg8ZKGxIug4lXcA==",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"@emotion/memoize": "0.7.4"
|
||||
}
|
||||
},
|
||||
"node_modules/framer-motion/node_modules/@emotion/memoize": {
|
||||
"version": "0.7.4",
|
||||
"resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.7.4.tgz",
|
||||
"integrity": "sha512-Ja/Vfqe3HpuzRsG1oBtWTHk2PGZ7GR+2Vz5iYGelAw8dx32K0y7PjVuxK6z1nMpZOqAFsRUPCkK1YjJ56qJlgw==",
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/fs-minipass": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz",
|
||||
|
|
|
@ -18,6 +18,7 @@
|
|||
"@uiw/react-codemirror": "^4.21.21",
|
||||
"axios": "^1.6.2",
|
||||
"clsx": "^2.0.0",
|
||||
"framer-motion": "^10.16.16",
|
||||
"js-file-download": "^0.4.12",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
|
|
|
@ -38,7 +38,7 @@ function Button({
|
|||
className,
|
||||
colors
|
||||
)}
|
||||
data-tooltip-id={title ? (globalIDs.tooltip) : undefined}
|
||||
data-tooltip-id={title ? globalIDs.tooltip : undefined}
|
||||
data-tooltip-content={title}
|
||||
{...restProps}
|
||||
>
|
||||
|
|
|
@ -22,7 +22,6 @@ function ConceptTooltip({
|
|||
...restProps
|
||||
}: ConceptTooltipProps) {
|
||||
const { darkMode } = useConceptTheme();
|
||||
|
||||
if (typeof window === 'undefined') {
|
||||
return null;
|
||||
}
|
||||
|
|
|
@ -1,25 +1,29 @@
|
|||
import clsx from 'clsx';
|
||||
import { motion } from 'framer-motion';
|
||||
|
||||
import { animateDropdown } from '@/utils/animations';
|
||||
|
||||
import { CProps } from '../props';
|
||||
import Overlay from './Overlay';
|
||||
|
||||
interface DropdownProps
|
||||
extends CProps.Styling {
|
||||
stretchLeft?: boolean
|
||||
isOpen: boolean
|
||||
children: React.ReactNode
|
||||
}
|
||||
|
||||
function Dropdown({
|
||||
isOpen, stretchLeft,
|
||||
className,
|
||||
stretchLeft,
|
||||
children,
|
||||
...restProps
|
||||
}: DropdownProps) {
|
||||
}: DropdownProps) {
|
||||
return (
|
||||
<Overlay
|
||||
layer='z-modal-tooltip'
|
||||
position='mt-3'
|
||||
<div className='relative'>
|
||||
<motion.div
|
||||
className={clsx(
|
||||
'z-modal-tooltip',
|
||||
'absolute mt-3',
|
||||
'flex flex-col',
|
||||
'border rounded-md shadow-lg',
|
||||
'text-sm',
|
||||
|
@ -30,10 +34,14 @@ function Dropdown({
|
|||
},
|
||||
className
|
||||
)}
|
||||
initial={false}
|
||||
animate={isOpen ? 'open' : 'closed'}
|
||||
variants={animateDropdown}
|
||||
{...restProps}
|
||||
>
|
||||
{children}
|
||||
</Overlay>);
|
||||
</motion.div>
|
||||
</div>);
|
||||
}
|
||||
|
||||
export default Dropdown;
|
|
@ -1,11 +1,13 @@
|
|||
import clsx from 'clsx';
|
||||
import { motion } from 'framer-motion';
|
||||
|
||||
import { animateDropdownItem } from '@/utils/animations';
|
||||
import { globalIDs } from '@/utils/constants';
|
||||
|
||||
import { CProps } from '../props';
|
||||
|
||||
interface DropdownButtonProps
|
||||
extends CProps.Button {
|
||||
extends CProps.AnimatedButton {
|
||||
text?: string
|
||||
icon?: React.ReactNode
|
||||
|
||||
|
@ -20,7 +22,7 @@ function DropdownButton({
|
|||
...restProps
|
||||
}: DropdownButtonProps) {
|
||||
return (
|
||||
<button type='button'
|
||||
<motion.button type='button'
|
||||
onClick={onClick}
|
||||
className={clsx(
|
||||
'px-3 py-1 inline-flex items-center gap-2',
|
||||
|
@ -33,6 +35,7 @@ function DropdownButton({
|
|||
},
|
||||
className
|
||||
)}
|
||||
variants={animateDropdownItem}
|
||||
data-tooltip-id={title ? (globalIDs.tooltip) : undefined}
|
||||
data-tooltip-content={title}
|
||||
{...restProps}
|
||||
|
@ -40,7 +43,7 @@ function DropdownButton({
|
|||
{children ? children : null}
|
||||
{!children && icon ? icon : null}
|
||||
{!children && text ? <span>{text}</span> : null}
|
||||
</button>);
|
||||
</motion.button>);
|
||||
}
|
||||
|
||||
export default DropdownButton;
|
|
@ -1,4 +1,7 @@
|
|||
import clsx from 'clsx';
|
||||
import { motion } from 'framer-motion';
|
||||
|
||||
import { animateDropdownItem } from '@/utils/animations';
|
||||
|
||||
import Checkbox from './Checkbox';
|
||||
|
||||
|
@ -12,7 +15,8 @@ interface DropdownCheckboxProps {
|
|||
|
||||
function DropdownCheckbox({ title, setValue, disabled, ...restProps }: DropdownCheckboxProps) {
|
||||
return (
|
||||
<div
|
||||
<motion.div
|
||||
variants={animateDropdownItem}
|
||||
title={title}
|
||||
className={clsx(
|
||||
'px-3 py-1',
|
||||
|
@ -26,7 +30,7 @@ function DropdownCheckbox({ title, setValue, disabled, ...restProps }: DropdownC
|
|||
setValue={setValue}
|
||||
{...restProps}
|
||||
/>
|
||||
</div>);
|
||||
</motion.div>);
|
||||
}
|
||||
|
||||
export default DropdownCheckbox;
|
|
@ -1,10 +1,12 @@
|
|||
'use client';
|
||||
|
||||
import clsx from 'clsx';
|
||||
import { motion } from 'framer-motion';
|
||||
import { useRef } from 'react';
|
||||
import { BiX } from 'react-icons/bi';
|
||||
|
||||
import useEscapeKey from '@/hooks/useEscapeKey';
|
||||
import { animateModal } from '@/utils/animations';
|
||||
|
||||
import { CProps } from '../props';
|
||||
import Button from './Button';
|
||||
|
@ -56,13 +58,16 @@ function Modal({
|
|||
'w-full h-full',
|
||||
'clr-modal-backdrop'
|
||||
)}/>
|
||||
<div ref={ref}
|
||||
<motion.div ref={ref}
|
||||
className={clsx(
|
||||
'z-modal',
|
||||
'fixed bottom-1/2 left-1/2 -translate-x-1/2 translate-y-1/2',
|
||||
'border shadow-md',
|
||||
'clr-app'
|
||||
)}
|
||||
initial={{...animateModal.initial}}
|
||||
animate={{...animateModal.animate}}
|
||||
exit={{...animateModal.exit}}
|
||||
{...restProps}
|
||||
>
|
||||
<Overlay position='right-[0.3rem] top-2'>
|
||||
|
@ -107,7 +112,7 @@ function Modal({
|
|||
onClick={handleCancel}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
</>);
|
||||
}
|
||||
|
||||
|
|
|
@ -1,10 +1,12 @@
|
|||
import clsx from 'clsx';
|
||||
import { motion } from 'framer-motion';
|
||||
import { FaSquarePlus } from 'react-icons/fa6';
|
||||
import { IoLibrary } from 'react-icons/io5';
|
||||
|
||||
import { EducationIcon } from '@/components/Icons';
|
||||
import { useConceptNavigation } from '@/context/NagivationContext';
|
||||
import { useConceptTheme } from '@/context/ThemeContext';
|
||||
import { animateNavigation } from '@/utils/animations';
|
||||
|
||||
import Logo from './Logo';
|
||||
import NavigationButton from './NavigationButton';
|
||||
|
@ -13,13 +15,13 @@ import UserMenu from './UserMenu';
|
|||
|
||||
function Navigation () {
|
||||
const router = useConceptNavigation();
|
||||
const { noNavigation } = useConceptTheme();
|
||||
const { noNavigationAnimation } = useConceptTheme();
|
||||
|
||||
const navigateHome = () => router.push('/');
|
||||
const navigateLibrary = () => router.push('/library');
|
||||
const navigateHelp = () => router.push('/manuals');
|
||||
const navigateCreateNew = () => router.push('/library/create');
|
||||
|
||||
|
||||
return (
|
||||
<nav className={clsx(
|
||||
'z-navigation',
|
||||
|
@ -28,13 +30,15 @@ function Navigation () {
|
|||
'select-none'
|
||||
)}>
|
||||
<ToggleNavigationButton />
|
||||
{!noNavigation ?
|
||||
<div
|
||||
<motion.div
|
||||
className={clsx(
|
||||
'pl-2 pr-[0.9rem] h-[3rem]',
|
||||
'flex justify-between',
|
||||
'border-b-2 rounded-none'
|
||||
'shadow-border'
|
||||
)}
|
||||
initial={false}
|
||||
animate={!noNavigationAnimation ? 'open' : 'closed'}
|
||||
variants={animateNavigation}
|
||||
>
|
||||
<div className='flex items-center mr-2 cursor-pointer' onClick={navigateHome} tabIndex={-1}>
|
||||
<Logo />
|
||||
|
@ -60,7 +64,7 @@ function Navigation () {
|
|||
/>
|
||||
<UserMenu />
|
||||
</div>
|
||||
</div> : null}
|
||||
</motion.div>
|
||||
</nav>);
|
||||
}
|
||||
|
||||
|
|
|
@ -1,36 +1,28 @@
|
|||
import clsx from 'clsx';
|
||||
import { useMemo } from 'react';
|
||||
import { motion } from 'framer-motion';
|
||||
import { RiPushpinFill, RiUnpinLine } from 'react-icons/ri';
|
||||
|
||||
import { useConceptTheme } from '@/context/ThemeContext';
|
||||
import { animateNavigationToggle } from '@/utils/animations';
|
||||
|
||||
function ToggleNavigationButton() {
|
||||
const { noNavigation, toggleNoNavigation } = useConceptTheme();
|
||||
const text = useMemo(() => (
|
||||
noNavigation ?
|
||||
'∨∨∨'
|
||||
:
|
||||
<>
|
||||
<p>{'>'}</p>
|
||||
<p>{'>'}</p>
|
||||
</>
|
||||
), [noNavigation]
|
||||
);
|
||||
const { noNavigationAnimation, toggleNoNavigation } = useConceptTheme();
|
||||
return (
|
||||
<button type='button' tabIndex={-1}
|
||||
title={noNavigation ? 'Показать навигацию' : 'Скрыть навигацию'}
|
||||
<motion.button type='button' tabIndex={-1}
|
||||
title={noNavigationAnimation ? 'Показать навигацию' : 'Скрыть навигацию'}
|
||||
className={clsx(
|
||||
'absolute top-0 right-0 z-navigation',
|
||||
'border-b-2 border-l-2 rounded-none',
|
||||
'absolute top-0 right-0 z-navigation flex items-center justify-center',
|
||||
'clr-btn-nav',
|
||||
{
|
||||
'px-1 h-[1.6rem]': noNavigation,
|
||||
'w-[1.2rem] h-[3rem]': !noNavigation
|
||||
}
|
||||
'select-none disabled:cursor-not-allowed'
|
||||
)}
|
||||
onClick={toggleNoNavigation}
|
||||
initial={false}
|
||||
animate={noNavigationAnimation ? 'off' : 'on'}
|
||||
variants={animateNavigationToggle}
|
||||
>
|
||||
{text}
|
||||
</button>);
|
||||
{!noNavigationAnimation ? <RiPushpinFill /> : null}
|
||||
{noNavigationAnimation ? <RiUnpinLine /> : null}
|
||||
</motion.button>);
|
||||
}
|
||||
|
||||
export default ToggleNavigationButton;
|
|
@ -5,10 +5,11 @@ import { useConceptNavigation } from '@/context/NagivationContext';
|
|||
import { useConceptTheme } from '@/context/ThemeContext';
|
||||
|
||||
interface UserDropdownProps {
|
||||
isOpen: boolean
|
||||
hideDropdown: () => void
|
||||
}
|
||||
|
||||
function UserDropdown({ hideDropdown }: UserDropdownProps) {
|
||||
function UserDropdown({ isOpen, hideDropdown }: UserDropdownProps) {
|
||||
const { darkMode, toggleDarkMode } = useConceptTheme();
|
||||
const router = useConceptNavigation();
|
||||
const { user, logout } = useAuth();
|
||||
|
@ -25,7 +26,7 @@ function UserDropdown({ hideDropdown }: UserDropdownProps) {
|
|||
};
|
||||
|
||||
return (
|
||||
<Dropdown className='w-36' stretchLeft>
|
||||
<Dropdown className='w-36' stretchLeft isOpen={isOpen}>
|
||||
<DropdownButton
|
||||
text={user?.username}
|
||||
title='Профиль пользователя'
|
||||
|
|
|
@ -27,10 +27,10 @@ function UserMenu() {
|
|||
icon={<FaCircleUser size='1.5rem' />}
|
||||
onClick={menu.toggle}
|
||||
/> : null}
|
||||
{(user && menu.isActive) ?
|
||||
<UserDropdown
|
||||
isOpen={!!user && menu.isOpen}
|
||||
hideDropdown={() => menu.hide()}
|
||||
/> : null}
|
||||
/>
|
||||
</div>);
|
||||
}
|
||||
|
||||
|
|
|
@ -6,6 +6,7 @@ import { createTheme } from '@uiw/codemirror-themes';
|
|||
import CodeMirror, { BasicSetupOptions, ReactCodeMirrorProps, ReactCodeMirrorRef } from '@uiw/react-codemirror';
|
||||
import clsx from 'clsx';
|
||||
import { EditorView } from 'codemirror';
|
||||
import { AnimatePresence } from 'framer-motion';
|
||||
import { RefObject, useCallback, useMemo, useRef, useState } from 'react';
|
||||
|
||||
import Label from '@/components/Common/Label';
|
||||
|
@ -164,6 +165,7 @@ function RefsInput({
|
|||
}, [thisRef]);
|
||||
|
||||
return (<>
|
||||
<AnimatePresence>
|
||||
{showEditor ?
|
||||
<DlgEditReference
|
||||
hideWindow={() => setShowEditor(false)}
|
||||
|
@ -177,6 +179,8 @@ function RefsInput({
|
|||
}}
|
||||
onSave={handleInputReference}
|
||||
/> : null}
|
||||
</AnimatePresence>
|
||||
|
||||
<div className={clsx(
|
||||
'flex flex-col gap-2',
|
||||
cursor
|
||||
|
|
4
rsconcept/frontend/src/components/props.d.ts
vendored
4
rsconcept/frontend/src/components/props.d.ts
vendored
|
@ -1,4 +1,6 @@
|
|||
// =========== Module contains interfaces for common UI elements. ==========
|
||||
import { HTMLMotionProps } from 'framer-motion';
|
||||
|
||||
export namespace CProps {
|
||||
|
||||
export type Control = {
|
||||
|
@ -33,4 +35,6 @@ export type Label = Omit<
|
|||
export type TextArea = React.DetailedHTMLProps<React.TextareaHTMLAttributes<HTMLTextAreaElement>, HTMLTextAreaElement>;
|
||||
export type Input = React.DetailedHTMLProps<React.InputHTMLAttributes<HTMLInputElement>, HTMLInputElement>;
|
||||
|
||||
export type AnimatedButton = Omit<HTMLMotionProps<'button'>, 'type'>;
|
||||
|
||||
}
|
|
@ -1,10 +1,11 @@
|
|||
'use client';
|
||||
|
||||
import clsx from 'clsx';
|
||||
import { createContext, useContext, useLayoutEffect, useMemo, useState } from 'react';
|
||||
import { createContext, useCallback, useContext, useLayoutEffect, useMemo, useState } from 'react';
|
||||
|
||||
import ConceptTooltip from '@/components/Common/ConceptTooltip';
|
||||
import useLocalStorage from '@/hooks/useLocalStorage';
|
||||
import { animationDuration } from '@/utils/animations';
|
||||
import { darkT, IColorTheme, lightT } from '@/utils/color';
|
||||
import { globalIDs } from '@/utils/constants';
|
||||
|
||||
|
@ -17,6 +18,7 @@ interface IThemeContext {
|
|||
darkMode: boolean
|
||||
toggleDarkMode: () => void
|
||||
|
||||
noNavigationAnimation: boolean
|
||||
noNavigation: boolean
|
||||
toggleNoNavigation: () => void
|
||||
|
||||
|
@ -44,6 +46,7 @@ export const ThemeState = ({ children }: ThemeStateProps) => {
|
|||
const [darkMode, setDarkMode] = useLocalStorage('darkMode', false);
|
||||
const [colors, setColors] = useState<IColorTheme>(lightT);
|
||||
const [noNavigation, setNoNavigation] = useState(false);
|
||||
const [noNavigationAnimation, setNoNavigationAnimation] = useState(false);
|
||||
const [noFooter, setNoFooter] = useState(false);
|
||||
const [showScroll, setShowScroll] = useState(false);
|
||||
|
||||
|
@ -65,6 +68,17 @@ export const ThemeState = ({ children }: ThemeStateProps) => {
|
|||
setColors(darkMode ? darkT : lightT)
|
||||
}, [darkMode, setColors]);
|
||||
|
||||
const toggleNoNavigation = useCallback(
|
||||
() => {
|
||||
if (noNavigation) {
|
||||
setNoNavigationAnimation(false);
|
||||
setNoNavigation(false);
|
||||
} else {
|
||||
setNoNavigationAnimation(true);
|
||||
setTimeout(() => setNoNavigation(true), animationDuration.navigationToggle);
|
||||
}
|
||||
}, [noNavigation]);
|
||||
|
||||
const mainHeight = useMemo(
|
||||
() => {
|
||||
return !noNavigation ?
|
||||
|
@ -82,9 +96,9 @@ export const ThemeState = ({ children }: ThemeStateProps) => {
|
|||
return (
|
||||
<ThemeContext.Provider value={{
|
||||
darkMode, colors,
|
||||
noNavigation, noFooter, showScroll,
|
||||
noNavigationAnimation, noNavigation, noFooter, showScroll,
|
||||
toggleDarkMode: () => setDarkMode(prev => !prev),
|
||||
toggleNoNavigation: () => setNoNavigation(prev => !prev),
|
||||
toggleNoNavigation: toggleNoNavigation,
|
||||
setNoFooter, setShowScroll,
|
||||
viewportHeight, mainHeight
|
||||
}}>
|
||||
|
|
|
@ -5,17 +5,17 @@ import { useRef, useState } from 'react';
|
|||
import useClickedOutside from './useClickedOutside';
|
||||
|
||||
function useDropdown() {
|
||||
const [isActive, setIsActive] = useState(false);
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const ref = useRef(null);
|
||||
|
||||
useClickedOutside({ ref, callback: () => setIsActive(false) });
|
||||
useClickedOutside({ ref, callback: () => setIsOpen(false) });
|
||||
|
||||
return {
|
||||
ref,
|
||||
isActive,
|
||||
setIsActive,
|
||||
toggle: () => setIsActive(!isActive),
|
||||
hide: () => setIsActive(false)
|
||||
isOpen,
|
||||
setIsOpen,
|
||||
toggle: () => setIsOpen(!isOpen),
|
||||
hide: () => setIsOpen(false)
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -48,8 +48,7 @@ function PickerStrategy({ value, onChange }: PickerStrategyProps) {
|
|||
text={labelLibraryFilter(value)}
|
||||
onClick={strategyMenu.toggle}
|
||||
/>
|
||||
{strategyMenu.isActive ?
|
||||
<Dropdown>
|
||||
<Dropdown isOpen={strategyMenu.isOpen}>
|
||||
{Object.values(LibraryFilterStrategy).map(
|
||||
(enumValue, index) => {
|
||||
const strategy = enumValue as LibraryFilterStrategy;
|
||||
|
@ -63,7 +62,7 @@ function PickerStrategy({ value, onChange }: PickerStrategyProps) {
|
|||
disabled={isStrategyDisabled(strategy)}
|
||||
/>);
|
||||
})}
|
||||
</Dropdown> : null}
|
||||
</Dropdown>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
'use client';
|
||||
|
||||
import { AnimatePresence } from 'framer-motion';
|
||||
import { Dispatch, SetStateAction, useMemo, useState } from 'react';
|
||||
|
||||
import { useRSForm } from '@/context/RSFormContext';
|
||||
|
@ -149,6 +150,7 @@ function EditorConstituenta({
|
|||
onEditTerm={onEditTerm}
|
||||
onRenameCst={onRenameCst}
|
||||
/>
|
||||
<AnimatePresence>
|
||||
{(showList && windowSize.width && windowSize.width >= SIDELIST_HIDE_THRESHOLD) ?
|
||||
<ViewConstituents
|
||||
schema={schema}
|
||||
|
@ -157,6 +159,7 @@ function EditorConstituenta({
|
|||
activeID={activeID}
|
||||
onOpenEdit={onOpenEdit}
|
||||
/>: null}
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
</>);
|
||||
}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
'use client';
|
||||
|
||||
import { ReactCodeMirrorRef } from '@uiw/react-codemirror';
|
||||
import { AnimatePresence } from 'framer-motion';
|
||||
import { useCallback, useLayoutEffect, useRef, useState } from 'react';
|
||||
import { BiListUl } from 'react-icons/bi';
|
||||
import { FaRegKeyboard } from 'react-icons/fa6';
|
||||
|
@ -31,7 +32,7 @@ interface EditorRSExpressionProps {
|
|||
value: string
|
||||
label: string
|
||||
placeholder?: string
|
||||
|
||||
|
||||
disabled?: boolean
|
||||
toggleReset?: boolean
|
||||
showList: boolean
|
||||
|
@ -134,12 +135,14 @@ function EditorRSExpression({
|
|||
}
|
||||
|
||||
return (<>
|
||||
<AnimatePresence>
|
||||
{showAST ?
|
||||
<DlgShowAST
|
||||
expression={expression}
|
||||
syntaxTree={syntaxTree}
|
||||
hideWindow={() => setShowAST(false)}
|
||||
/> : null}
|
||||
</AnimatePresence>
|
||||
|
||||
<div>
|
||||
<Overlay position='top-0 right-0 flex'>
|
||||
|
@ -179,18 +182,18 @@ function EditorRSExpression({
|
|||
{...restProps}
|
||||
/>
|
||||
|
||||
{showControls ?
|
||||
<RSEditorControls
|
||||
<RSEditorControls
|
||||
isOpen={showControls}
|
||||
disabled={disabled}
|
||||
onEdit={handleEdit}
|
||||
/> : null}
|
||||
/>
|
||||
|
||||
{(parseData && parseData.errors.length > 0) ?
|
||||
<ParsingResult
|
||||
isOpen={!!parseData && parseData.errors.length > 0}
|
||||
data={parseData}
|
||||
disabled={disabled}
|
||||
onShowError={onShowError}
|
||||
/>: null}
|
||||
/>
|
||||
</div>
|
||||
</>);
|
||||
}
|
||||
|
|
|
@ -1,23 +1,39 @@
|
|||
'use client';
|
||||
|
||||
import clsx from 'clsx';
|
||||
import { motion } from 'framer-motion';
|
||||
|
||||
import { IExpressionParse, IRSErrorDescription } from '@/models/rslang';
|
||||
import { animateRSControl } from '@/utils/animations';
|
||||
import { describeRSError } from '@/utils/labels';
|
||||
import { getRSErrorPrefix } from '@/utils/misc';
|
||||
|
||||
interface ParsingResultProps {
|
||||
data: IExpressionParse
|
||||
data: IExpressionParse | undefined
|
||||
disabled?: boolean
|
||||
isOpen: boolean
|
||||
onShowError: (error: IRSErrorDescription) => void
|
||||
}
|
||||
|
||||
function ParsingResult({ data, disabled, onShowError }: ParsingResultProps) {
|
||||
const errorCount = data.errors.reduce((total, error) => (error.isCritical ? total + 1 : total), 0);
|
||||
const warningsCount = data.errors.length - errorCount;
|
||||
function ParsingResult({ isOpen, data, disabled, onShowError }: ParsingResultProps) {
|
||||
const errorCount = data ? data.errors.reduce((total, error) => (error.isCritical ? total + 1 : total), 0) : 0;
|
||||
const warningsCount = data ? data.errors.length - errorCount : 0;
|
||||
|
||||
return (
|
||||
<div className='px-2 pt-1 text-sm border overflow-y-auto h-[4.5rem]'>
|
||||
<motion.div
|
||||
className={clsx(
|
||||
'px-2 pt-1',
|
||||
'h-[4.5rem] mt-3',
|
||||
'text-sm',
|
||||
'border',
|
||||
'overflow-y-auto'
|
||||
)}
|
||||
initial={false}
|
||||
animate={isOpen ? 'open' : 'closed'}
|
||||
variants={animateRSControl}
|
||||
>
|
||||
<p>Ошибок: <b>{errorCount}</b> | Предупреждений: <b>{warningsCount}</b></p>
|
||||
{data.errors.map(
|
||||
{data?.errors.map(
|
||||
(error, index) => {
|
||||
return (
|
||||
<p
|
||||
|
@ -31,7 +47,7 @@ function ParsingResult({ data, disabled, onShowError }: ParsingResultProps) {
|
|||
<span>{` ${describeRSError(error)}`}</span>
|
||||
</p>);
|
||||
})}
|
||||
</div>);
|
||||
</motion.div>);
|
||||
}
|
||||
|
||||
export default ParsingResult;
|
|
@ -1,4 +1,7 @@
|
|||
import { motion } from 'framer-motion';
|
||||
|
||||
import { TokenID } from '@/models/rslang';
|
||||
import { animateRSControl } from '@/utils/animations';
|
||||
import { prefixes } from '@/utils/constants';
|
||||
|
||||
import RSLocalButton from './RSLocalButton';
|
||||
|
@ -79,13 +82,19 @@ const SECONDARY_THIRD_ROW = [
|
|||
];
|
||||
|
||||
interface RSEditorControlsProps {
|
||||
onEdit: (id: TokenID, key?: string) => void
|
||||
isOpen: boolean
|
||||
disabled?: boolean
|
||||
onEdit: (id: TokenID, key?: string) => void
|
||||
}
|
||||
|
||||
function RSEditorControls({ onEdit, disabled }: RSEditorControlsProps) {
|
||||
function RSEditorControls({ isOpen, disabled, onEdit }: RSEditorControlsProps) {
|
||||
return (
|
||||
<div className='flex-wrap text-sm divide-solid'>
|
||||
<motion.div
|
||||
className='flex-wrap text-sm divide-solid'
|
||||
initial={false}
|
||||
animate={isOpen ? 'open' : 'closed'}
|
||||
variants={animateRSControl}
|
||||
>
|
||||
{MAIN_FIRST_ROW.map(
|
||||
(token) =>
|
||||
<RSTokenButton key={`${prefixes.rsedit_btn}${token}`}
|
||||
|
@ -118,7 +127,7 @@ function RSEditorControls({ onEdit, disabled }: RSEditorControlsProps) {
|
|||
<RSLocalButton key={`${prefixes.rsedit_btn}${title}`}
|
||||
text={text} title={title} onInsert={onEdit} disabled={disabled}
|
||||
/>)}
|
||||
</div>);
|
||||
</motion.div>);
|
||||
}
|
||||
|
||||
export default RSEditorControls;
|
|
@ -70,8 +70,7 @@ function RSListToolbar({
|
|||
disabled={!isMutable}
|
||||
onClick={insertMenu.toggle}
|
||||
/>
|
||||
{insertMenu.isActive ?
|
||||
<Dropdown>
|
||||
<Dropdown isOpen={insertMenu.isOpen}>
|
||||
{(Object.values(CstType)).map(
|
||||
(typeStr) =>
|
||||
<DropdownButton
|
||||
|
@ -81,7 +80,7 @@ function RSListToolbar({
|
|||
title={getCstTypeShortcut(typeStr as CstType)}
|
||||
/>
|
||||
)}
|
||||
</Dropdown> : null}
|
||||
</Dropdown>
|
||||
</div>
|
||||
<MiniButton
|
||||
title='Удалить выбранные [Delete]'
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
'use client';
|
||||
|
||||
import clsx from 'clsx';
|
||||
import { AnimatePresence } from 'framer-motion';
|
||||
import { useCallback, useLayoutEffect, useMemo, useState } from 'react';
|
||||
import { GraphEdge, GraphNode, LayoutTypes } from 'reagraph';
|
||||
|
||||
|
@ -196,12 +197,14 @@ function EditorTermGraph({ isMutable, onOpenEdit, onCreateCst, onDeleteCst }: Ed
|
|||
|
||||
return (
|
||||
<div tabIndex={-1} onKeyDown={handleKeyDown}>
|
||||
<AnimatePresence>
|
||||
{showParamsDialog ?
|
||||
<DlgGraphParams
|
||||
hideWindow={() => setShowParamsDialog(false)}
|
||||
initial={filterParams}
|
||||
onConfirm={handleChangeParams}
|
||||
/> : null}
|
||||
</AnimatePresence>
|
||||
|
||||
<SelectedCounter hideZero
|
||||
total={schema?.stats?.count_all ?? 0}
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
import axios from 'axios';
|
||||
import clsx from 'clsx';
|
||||
import { AnimatePresence } from 'framer-motion';
|
||||
import fileDownload from 'js-file-download';
|
||||
import { useCallback, useLayoutEffect, useMemo, useState } from 'react';
|
||||
import { TabList, TabPanel, Tabs } from 'react-tabs';
|
||||
|
@ -346,49 +347,50 @@ function RSTabs() {
|
|||
return (<>
|
||||
{loading ? <ConceptLoader /> : null}
|
||||
{error ? <ProcessError error={error} /> : null}
|
||||
|
||||
{showUpload ?
|
||||
<DlgUploadRSForm
|
||||
hideWindow={() => setShowUpload(false)}
|
||||
/> : null}
|
||||
{showClone ?
|
||||
<DlgCloneLibraryItem
|
||||
base={schema!}
|
||||
hideWindow={() => setShowClone(false)}
|
||||
/> : null}
|
||||
{showCreateCst ?
|
||||
<DlgCreateCst
|
||||
hideWindow={() => setShowCreateCst(false)}
|
||||
onCreate={handleCreateCst}
|
||||
schema={schema!}
|
||||
initial={createInitialData}
|
||||
/> : null}
|
||||
{showRenameCst ?
|
||||
<DlgRenameCst
|
||||
hideWindow={() => setShowRenameCst(false)}
|
||||
onRename={handleRenameCst}
|
||||
initial={renameInitialData!}
|
||||
/> : null}
|
||||
{showDeleteCst ?
|
||||
<DlgDeleteCst
|
||||
schema={schema!}
|
||||
hideWindow={() => setShowDeleteCst(false)}
|
||||
onDelete={handleDeleteCst}
|
||||
selected={toBeDeleted}
|
||||
/> : null}
|
||||
{showEditTerm ?
|
||||
<DlgEditWordForms
|
||||
hideWindow={() => setShowEditTerm(false)}
|
||||
onSave={handleSaveWordforms}
|
||||
target={activeCst!}
|
||||
/> : null}
|
||||
{showTemplates ?
|
||||
<DlgConstituentaTemplate
|
||||
schema={schema!}
|
||||
hideWindow={() => setShowTemplates(false)}
|
||||
insertAfter={insertCstID}
|
||||
onCreate={handleCreateCst}
|
||||
/> : null}
|
||||
<AnimatePresence>
|
||||
{showUpload ?
|
||||
<DlgUploadRSForm
|
||||
hideWindow={() => setShowUpload(false)}
|
||||
/> : null}
|
||||
{showClone ?
|
||||
<DlgCloneLibraryItem
|
||||
base={schema!}
|
||||
hideWindow={() => setShowClone(false)}
|
||||
/> : null}
|
||||
{showCreateCst ?
|
||||
<DlgCreateCst
|
||||
hideWindow={() => setShowCreateCst(false)}
|
||||
onCreate={handleCreateCst}
|
||||
schema={schema!}
|
||||
initial={createInitialData}
|
||||
/> : null}
|
||||
{showRenameCst ?
|
||||
<DlgRenameCst
|
||||
hideWindow={() => setShowRenameCst(false)}
|
||||
onRename={handleRenameCst}
|
||||
initial={renameInitialData!}
|
||||
/> : null}
|
||||
{showDeleteCst ?
|
||||
<DlgDeleteCst
|
||||
schema={schema!}
|
||||
hideWindow={() => setShowDeleteCst(false)}
|
||||
onDelete={handleDeleteCst}
|
||||
selected={toBeDeleted}
|
||||
/> : null}
|
||||
{showEditTerm ?
|
||||
<DlgEditWordForms
|
||||
hideWindow={() => setShowEditTerm(false)}
|
||||
onSave={handleSaveWordforms}
|
||||
target={activeCst!}
|
||||
/> : null}
|
||||
{showTemplates ?
|
||||
<DlgConstituentaTemplate
|
||||
schema={schema!}
|
||||
hideWindow={() => setShowTemplates(false)}
|
||||
insertAfter={insertCstID}
|
||||
onCreate={handleCreateCst}
|
||||
/> : null}
|
||||
</AnimatePresence>
|
||||
|
||||
{(schema && !loading) ?
|
||||
<Tabs
|
||||
|
|
|
@ -104,12 +104,10 @@ function RSTabsMenu({
|
|||
style={{outlineColor: 'transparent'}}
|
||||
onClick={schemaMenu.toggle}
|
||||
/>
|
||||
{schemaMenu.isActive ?
|
||||
<Dropdown>
|
||||
<DropdownButton
|
||||
<Dropdown isOpen={schemaMenu.isOpen}>
|
||||
<DropdownButton disabled={(!user || !isClaimable) && !isOwned}
|
||||
text={isOwned ? 'Вы — владелец' : 'Стать владельцем'}
|
||||
title={!user || !isClaimable ? 'Взять во владение можно общую изменяемую схему' : ''}
|
||||
icon={<LuCrown size='1rem' className={isOwned ? 'clr-text-success' : 'clr-text-controls'} />}
|
||||
icon={<LuCrown size='1rem' className={isOwned ? 'clr-text-success' : ''} />}
|
||||
onClick={(!isOwned && user && isClaimable) ? handleClaimOwner : undefined}
|
||||
/>
|
||||
<DropdownButton
|
||||
|
@ -142,7 +140,7 @@ function RSTabsMenu({
|
|||
icon={<BiPlusCircle size='1rem' className='clr-text-url' />}
|
||||
onClick={handleCreateNew}
|
||||
/>
|
||||
</Dropdown> : null}
|
||||
</Dropdown>
|
||||
</div>
|
||||
|
||||
<div ref={editMenu.ref}>
|
||||
|
@ -153,8 +151,7 @@ function RSTabsMenu({
|
|||
icon={<FiEdit size='1.25rem' className={isMutable ? 'clr-text-success' : 'clr-text-warning'}/>}
|
||||
onClick={editMenu.toggle}
|
||||
/>
|
||||
{editMenu.isActive ?
|
||||
<Dropdown>
|
||||
<Dropdown isOpen={editMenu.isOpen}>
|
||||
<DropdownButton disabled={!isMutable}
|
||||
text='Сброс имён'
|
||||
title='Присвоить порядковые имена и обновить выражения'
|
||||
|
@ -167,7 +164,7 @@ function RSTabsMenu({
|
|||
icon={<BiDiamond size='1rem' className={isMutable ? 'clr-text-success': ''} />}
|
||||
onClick={handleTemplates}
|
||||
/>
|
||||
</Dropdown>: null}
|
||||
</Dropdown>
|
||||
</div>
|
||||
|
||||
<div ref={accessMenu.ref}>
|
||||
|
@ -182,8 +179,7 @@ function RSTabsMenu({
|
|||
}
|
||||
onClick={accessMenu.toggle}
|
||||
/>
|
||||
{accessMenu.isActive ?
|
||||
<Dropdown>
|
||||
<Dropdown isOpen={accessMenu.isOpen}>
|
||||
<DropdownButton
|
||||
text={labelAccessMode(UserAccessMode.READER)}
|
||||
title={describeAccessMode(UserAccessMode.READER)}
|
||||
|
@ -202,7 +198,7 @@ function RSTabsMenu({
|
|||
icon={<BiMeteor size='1rem' className={user?.is_staff ? 'clr-text-primary': ''} />}
|
||||
onClick={() => handleChangeMode(UserAccessMode.ADMIN)}
|
||||
/>
|
||||
</Dropdown>: null}
|
||||
</Dropdown>
|
||||
</div>
|
||||
</div>);
|
||||
}
|
||||
|
|
|
@ -90,8 +90,7 @@ function ConstituentsSearch({ schema, activeID, activeExpression, setFiltered }:
|
|||
text={labelCstMathchMode(filterMatch)}
|
||||
onClick={matchModeMenu.toggle}
|
||||
/>
|
||||
{matchModeMenu.isActive ?
|
||||
<Dropdown stretchLeft>
|
||||
<Dropdown stretchLeft isOpen={matchModeMenu.isOpen}>
|
||||
{Object.values(CstMatchMode).filter(value => !isNaN(Number(value))).map(
|
||||
(value, index) => {
|
||||
const matchMode = value as CstMatchMode;
|
||||
|
@ -103,7 +102,7 @@ function ConstituentsSearch({ schema, activeID, activeExpression, setFiltered }:
|
|||
<p><b>{labelCstMathchMode(matchMode)}:</b> {describeCstMathchMode(matchMode)}</p>
|
||||
</DropdownButton>);
|
||||
})}
|
||||
</Dropdown> : null}
|
||||
</Dropdown>
|
||||
</div>
|
||||
|
||||
<div ref={sourceMenu.ref}>
|
||||
|
@ -114,8 +113,7 @@ function ConstituentsSearch({ schema, activeID, activeExpression, setFiltered }:
|
|||
text={labelCstSource(filterSource)}
|
||||
onClick={sourceMenu.toggle}
|
||||
/>
|
||||
{sourceMenu.isActive ?
|
||||
<Dropdown stretchLeft>
|
||||
<Dropdown stretchLeft isOpen={sourceMenu.isOpen}>
|
||||
{Object.values(DependencyMode).filter(value => !isNaN(Number(value))).map(
|
||||
(value, index) => {
|
||||
const source = value as DependencyMode;
|
||||
|
@ -127,7 +125,7 @@ function ConstituentsSearch({ schema, activeID, activeExpression, setFiltered }:
|
|||
<p><b>{labelCstSource(source)}:</b> {describeCstSource(source)}</p>
|
||||
</DropdownButton>);
|
||||
})}
|
||||
</Dropdown> : null}
|
||||
</Dropdown>
|
||||
</div>
|
||||
</div>);
|
||||
}
|
||||
|
|
|
@ -1,9 +1,11 @@
|
|||
'use client';
|
||||
|
||||
import { motion } from 'framer-motion';
|
||||
import { useMemo, useState } from 'react';
|
||||
|
||||
import { useConceptTheme } from '@/context/ThemeContext';
|
||||
import { IConstituenta, IRSForm } from '@/models/rsform';
|
||||
import { animateSideView } from '@/utils/animations';
|
||||
|
||||
import ConstituentsSearch from './ConstituentsSearch';
|
||||
import ConstituentsTable from './ConstituentsTable';
|
||||
|
@ -36,7 +38,12 @@ function ViewConstituents({ expression, baseHeight, schema, activeID, onOpenEdit
|
|||
}, [noNavigation, baseHeight]);
|
||||
|
||||
return (
|
||||
<div className='mt-[2.25rem] border'>
|
||||
<motion.div
|
||||
className='mt-[2.25rem] border'
|
||||
initial={{...animateSideView.initial}}
|
||||
animate={{...animateSideView.animate}}
|
||||
exit={{...animateSideView.exit}}
|
||||
>
|
||||
<ConstituentsSearch
|
||||
schema={schema}
|
||||
activeID={activeID}
|
||||
|
@ -49,7 +56,7 @@ function ViewConstituents({ expression, baseHeight, schema, activeID, onOpenEdit
|
|||
onOpenEdit={onOpenEdit}
|
||||
denseThreshold={COLUMN_EXPRESSION_HIDE_THRESHOLD}
|
||||
/>
|
||||
</div>);
|
||||
</motion.div>);
|
||||
}
|
||||
|
||||
export default ViewConstituents;
|
|
@ -1,5 +1,6 @@
|
|||
'use client';
|
||||
|
||||
import { AnimatePresence } from 'framer-motion';
|
||||
import { useMemo, useState } from 'react';
|
||||
import { FiBell, FiBellOff } from 'react-icons/fi';
|
||||
|
||||
|
@ -50,11 +51,12 @@ function UserTabs() {
|
|||
<EditorPassword />
|
||||
</div>
|
||||
</div>
|
||||
<AnimatePresence>
|
||||
{(subscriptions.length > 0 && showSubs) ?
|
||||
<div>
|
||||
<h1 className='mb-6'>Отслеживаемые схемы</h1>
|
||||
<ViewSubscriptions items={subscriptions} />
|
||||
</div> : null}
|
||||
<ViewSubscriptions
|
||||
items={subscriptions}
|
||||
/> : null}
|
||||
</AnimatePresence>
|
||||
</div> : null}
|
||||
</>);
|
||||
}
|
||||
|
|
|
@ -1,11 +1,13 @@
|
|||
'use client';
|
||||
|
||||
import { motion } from 'framer-motion';
|
||||
import { useMemo } from 'react';
|
||||
import { useIntl } from 'react-intl';
|
||||
|
||||
import DataTable, { createColumnHelper } from '@/components/DataTable';
|
||||
import { useConceptNavigation } from '@/context/NagivationContext';
|
||||
import { ILibraryItem } from '@/models/library';
|
||||
import { animateSideView } from '@/utils/animations';
|
||||
|
||||
interface ViewSubscriptionsProps {
|
||||
items: ILibraryItem[]
|
||||
|
@ -52,25 +54,32 @@ function ViewSubscriptions({items}: ViewSubscriptionsProps) {
|
|||
], [intl]);
|
||||
|
||||
return (
|
||||
<DataTable dense noFooter
|
||||
className='max-h-[23.8rem] overflow-y-auto text-sm border'
|
||||
columns={columns}
|
||||
data={items}
|
||||
headPosition='0'
|
||||
<motion.div
|
||||
initial={{...animateSideView.initial}}
|
||||
animate={{...animateSideView.animate}}
|
||||
exit={{...animateSideView.exit}}
|
||||
>
|
||||
<h1 className='mb-6'>Отслеживаемые схемы</h1>
|
||||
<DataTable dense noFooter
|
||||
className='max-h-[23.8rem] overflow-y-auto text-sm border'
|
||||
columns={columns}
|
||||
data={items}
|
||||
headPosition='0'
|
||||
|
||||
enableSorting
|
||||
initialSorting={{
|
||||
id: 'time_update',
|
||||
desc: true
|
||||
}}
|
||||
noDataComponent={
|
||||
<div className='h-[10rem]'>
|
||||
Отслеживаемые схемы отсутствуют
|
||||
</div>
|
||||
}
|
||||
enableSorting
|
||||
initialSorting={{
|
||||
id: 'time_update',
|
||||
desc: true
|
||||
}}
|
||||
noDataComponent={
|
||||
<div className='h-[10rem]'>
|
||||
Отслеживаемые схемы отсутствуют
|
||||
</div>
|
||||
}
|
||||
|
||||
onRowClicked={openRSForm}
|
||||
/>);
|
||||
onRowClicked={openRSForm}
|
||||
/>
|
||||
</motion.div>);
|
||||
}
|
||||
|
||||
export default ViewSubscriptions;
|
163
rsconcept/frontend/src/utils/animations.ts
Normal file
163
rsconcept/frontend/src/utils/animations.ts
Normal file
|
@ -0,0 +1,163 @@
|
|||
/**
|
||||
* Module: animations parameters.
|
||||
*/
|
||||
|
||||
import { Variants } from 'framer-motion';
|
||||
|
||||
/**
|
||||
* Duration constants in ms.
|
||||
*/
|
||||
export const animationDuration = {
|
||||
navigationToggle: 500
|
||||
};
|
||||
|
||||
export const animateNavigation: Variants = {
|
||||
open: {
|
||||
height: '3rem',
|
||||
translateY: 0,
|
||||
transition: {
|
||||
type: 'spring',
|
||||
bounce: 0,
|
||||
duration: animationDuration.navigationToggle / 1000
|
||||
}
|
||||
},
|
||||
closed: {
|
||||
height: 0,
|
||||
translateY: '-1.5rem',
|
||||
transition: {
|
||||
type: 'spring',
|
||||
bounce: 0,
|
||||
duration: animationDuration.navigationToggle / 1000
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const animateNavigationToggle: Variants = {
|
||||
on: {
|
||||
height: '3rem',
|
||||
width: '1.2rem',
|
||||
transition: {
|
||||
type: 'spring',
|
||||
bounce: 0,
|
||||
duration: animationDuration.navigationToggle / 1000
|
||||
}
|
||||
},
|
||||
off: {
|
||||
height: '1.2rem',
|
||||
width: '3rem',
|
||||
transition: {
|
||||
type: 'spring',
|
||||
bounce: 0,
|
||||
duration: animationDuration.navigationToggle / 1000
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const animateDropdown: Variants = {
|
||||
open: {
|
||||
clipPath: 'inset(0% 0% 0% 0%)',
|
||||
transition: {
|
||||
type: 'spring',
|
||||
bounce: 0,
|
||||
duration: 0.4,
|
||||
delayChildren: 0.2,
|
||||
staggerChildren: 0.05
|
||||
}
|
||||
},
|
||||
closed: {
|
||||
clipPath: 'inset(10% 0% 90% 0%)',
|
||||
transition: {
|
||||
type: 'spring',
|
||||
bounce: 0,
|
||||
duration: 0.3
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const animateDropdownItem: Variants = {
|
||||
open: {
|
||||
opacity: 1,
|
||||
y: 0,
|
||||
transition: {
|
||||
type: 'spring',
|
||||
duration: 0.1,
|
||||
stiffness: 300,
|
||||
damping: 24
|
||||
}
|
||||
},
|
||||
closed: {
|
||||
opacity: 0,
|
||||
y: 10,
|
||||
transition: {
|
||||
duration: 0.1
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const animateRSControl: Variants = {
|
||||
open: {
|
||||
clipPath: 'inset(0% 0% 0% 0%)',
|
||||
height: 'max-content',
|
||||
transition: {
|
||||
type: 'spring',
|
||||
bounce: 0,
|
||||
duration: 0.4
|
||||
}
|
||||
},
|
||||
closed: {
|
||||
clipPath: 'inset(0% 0% 100% 0%)',
|
||||
height: 0,
|
||||
transition: {
|
||||
type: 'spring',
|
||||
bounce: 0,
|
||||
duration: 0.3
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const animateSideView = {
|
||||
initial: {
|
||||
clipPath: 'inset(0% 100% 0% 0%)',
|
||||
},
|
||||
animate: {
|
||||
clipPath: 'inset(0% 0% 0% 0%)',
|
||||
transition: {
|
||||
type: 'spring',
|
||||
bounce: 0,
|
||||
duration: 1
|
||||
}
|
||||
},
|
||||
exit: {
|
||||
clipPath: 'inset(0% 100% 0% 0%)',
|
||||
transition: {
|
||||
type: 'spring',
|
||||
bounce: 0,
|
||||
duration: 1
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const animateModal = {
|
||||
initial: {
|
||||
clipPath: 'inset(50% 50% 50% 50%)',
|
||||
opacity: 0
|
||||
},
|
||||
animate: {
|
||||
clipPath: 'inset(0% 0% 0% 0%)',
|
||||
opacity: 1,
|
||||
transition: {
|
||||
type: 'spring',
|
||||
bounce: 0,
|
||||
duration: 0.3
|
||||
}
|
||||
},
|
||||
exit: {
|
||||
opacity: 0,
|
||||
clipPath: 'inset(50% 50% 50% 50%)',
|
||||
transition: {
|
||||
type: 'spring',
|
||||
bounce: 0,
|
||||
duration: 0.2
|
||||
}
|
||||
}
|
||||
};
|
Loading…
Reference in New Issue
Block a user