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