Compare commits

..

No commits in common. "954936599ed99cd25f5de110bef4e8e0a5dd715d" and "a2615a923699f61361d00ae80cffb98edf14fc75" have entirely different histories.

105 changed files with 1871 additions and 1517 deletions

View File

@ -66,7 +66,6 @@
"ADVB", "ADVB",
"Analyse", "Analyse",
"Backquote", "Backquote",
"bezier",
"BIGPR", "BIGPR",
"cctext", "cctext",
"Certbot", "Certbot",

View File

@ -33,6 +33,7 @@ This readme file is used mostly to document project dependencies and conventions
- react-icons - react-icons
- react-router - react-router
- react-toastify - react-toastify
- react-loader-spinner
- react-tabs - react-tabs
- react-intl - react-intl
- react-select - react-select
@ -42,6 +43,7 @@ This readme file is used mostly to document project dependencies and conventions
- reactflow - reactflow
- js-file-download - js-file-download
- use-debounce - use-debounce
- framer-motion
- html-to-image - html-to-image
- @tanstack/react-table - @tanstack/react-table
- @uiw/react-codemirror - @uiw/react-codemirror

View File

@ -17,14 +17,15 @@ For more specific TODOs see comments in code
- Static analyzer for RSForm as a whole: check term duplication and empty conventions - Static analyzer for RSForm as a whole: check term duplication and empty conventions
- OSS clone and versioning - OSS clone and versioning
- Clone with saving info connection
- Semantic diff for library items
- Focus on codemirror editor when label is clicked (need React 19 ref for clean code solution) - Focus on codemirror editor when label is clicked (need React 19 ref for clean code solution)
- Draggable rows in constituents table - Draggable rows in constituents table
- Search functionality for Help Manuals - use google search integration filtered by site? - M-graph visualization for typification and RSForm in general
- Export PDF (Items list, Graph)
- replace reagraph with react-flow in TermGraph and FormulaGraph
- Search functionality for Help Manuals
- Export PDF (Items list, Graph) - use google search integration filtered by site?
- ARIA (accessibility considerations) - for now machine reading not supported - ARIA (accessibility considerations) - for now machine reading not supported
- Internationalization - at least english version. Consider react.intl - Internationalization - at least english version. Consider react.intl
- Sitemap for better SEO and crawler optimization - Sitemap for better SEO and crawler optimization
@ -32,6 +33,7 @@ For more specific TODOs see comments in code
[Functionality - CANCELED] [Functionality - CANCELED]
- User notifications on edit - consider spam prevention and change aggregation - User notifications on edit - consider spam prevention and change aggregation
- Content based search in Library - Content based search in Library
- Home page (user specific)
- Private projects. Consider cooperative editing - Private projects. Consider cooperative editing
- OSS: synthesis table: auto substitution for diamond synthesis - OSS: synthesis table: auto substitution for diamond synthesis
@ -40,8 +42,10 @@ For more specific TODOs see comments in code
- duplicate syntax parsing and type info calculations to client. Consider moving backend to Nodejs or embedding c++ lib - duplicate syntax parsing and type info calculations to client. Consider moving backend to Nodejs or embedding c++ lib
- add debounce to some search fields. Consider pagination and dynamic loading - add debounce to some search fields. Consider pagination and dynamic loading
- move autopep8 and isort settings from vscode settings to pyproject.toml - move autopep8 and isort settings from vscode settings to pyproject.toml
- Test UI for #enable-force-dark Chrome setting
- Testing: frontend react components, testplane / playwright? - Testing: frontend react components, testplane / playwright?
- Documentation: frontend base components at least
[Deployment] [Deployment]

View File

@ -3,18 +3,17 @@ Django==5.1.3
djangorestframework==3.15.2 djangorestframework==3.15.2
django-cors-headers==4.6.0 django-cors-headers==4.6.0
django-filter==24.3 django-filter==24.3
drf-spectacular==0.28.0 drf-spectacular==0.27.2
drf-spectacular-sidecar==2024.12.1 drf-spectacular-sidecar==2024.11.1
coreapi==2.3.3 coreapi==2.3.3
django-rest-passwordreset==1.5.0 django-rest-passwordreset==1.5.0
cctext==0.1.4 cctext==0.1.4
pyconcept==0.1.12 pyconcept==0.1.11
psycopg2-binary==2.9.10 psycopg2-binary==2.9.10
gunicorn==23.0.0 gunicorn==23.0.0
djangorestframework-stubs==3.15.1 djangorestframework-stubs==3.15.1
django-extensions==3.2.3 django-extensions==3.2.3
django-stubs==5.1.1 mypy==1.11.2
mypy==1.13.0 pylint==3.3.1
pylint==3.3.2 coverage==7.6.5
coverage==7.6.8

View File

@ -15,7 +15,6 @@ gunicorn
djangorestframework-stubs[compatible-mypy] djangorestframework-stubs[compatible-mypy]
django-extensions django-extensions
django-stubs
mypy mypy
pylint pylint
coverage coverage

View File

@ -3,12 +3,12 @@ Django==5.1.3
djangorestframework==3.15.2 djangorestframework==3.15.2
django-cors-headers==4.6.0 django-cors-headers==4.6.0
django-filter==24.3 django-filter==24.3
drf-spectacular==0.28.0 drf-spectacular==0.27.2
drf-spectacular-sidecar==2024.12.1 drf-spectacular-sidecar==2024.11.1
coreapi==2.3.3 coreapi==2.3.3
django-rest-passwordreset==1.5.0 django-rest-passwordreset==1.5.0
cctext==0.1.4 cctext==0.1.4
pyconcept==0.1.12 pyconcept==0.1.11
psycopg2-binary==2.9.10 psycopg2-binary==2.9.10
gunicorn==23.0.0 gunicorn==23.0.0

View File

@ -21,7 +21,6 @@
<title>Концепт Портал</title> <title>Концепт Портал</title>
<!-- <script src="https://unpkg.com/react-scan/dist/auto.global.js"></script> -->
<script> <script>
let isDark = false; let isDark = false;
if ('portal.theme.dark' in localStorage) { if ('portal.theme.dark' in localStorage) {

File diff suppressed because it is too large Load Diff

View File

@ -17,17 +17,20 @@
"@tanstack/react-table": "^8.20.5", "@tanstack/react-table": "^8.20.5",
"@uiw/codemirror-themes": "^4.23.6", "@uiw/codemirror-themes": "^4.23.6",
"@uiw/react-codemirror": "^4.23.6", "@uiw/react-codemirror": "^4.23.6",
"axios": "^1.7.9", "axios": "^1.7.8",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"eslint-plugin-react-hooks": "^5.0.0",
"framer-motion": "^11.12.0",
"html-to-image": "^1.11.11", "html-to-image": "^1.11.11",
"js-file-download": "^0.4.12", "js-file-download": "^0.4.12",
"react": "^19.0.0", "react": "^18.3.1",
"react-dom": "^19.0.0", "react-dom": "^18.3.1",
"react-error-boundary": "^4.1.2", "react-error-boundary": "^4.1.2",
"react-icons": "^5.4.0", "react-icons": "^5.3.0",
"react-intl": "^7.0.4", "react-intl": "^7.0.1",
"react-router": "^7.0.2", "react-loader-spinner": "^6.1.6",
"react-select": "^5.9.0", "react-router": "^7.0.1",
"react-select": "^5.8.3",
"react-tabs": "^6.0.2", "react-tabs": "^6.0.2",
"react-toastify": "^10.0.6", "react-toastify": "^10.0.6",
"react-tooltip": "^5.28.0", "react-tooltip": "^5.28.0",
@ -36,30 +39,26 @@
"use-debounce": "^10.0.4" "use-debounce": "^10.0.4"
}, },
"devDependencies": { "devDependencies": {
"@lezer/generator": "^1.7.2", "@lezer/generator": "^1.7.1",
"@types/jest": "^29.5.14", "@types/jest": "^29.5.14",
"@types/node": "^22.10.2", "@types/node": "^22.10.1",
"@types/react": "^19.0.1", "@types/react": "^18.3.12",
"@types/react-dom": "^19.0.2", "@types/react-dom": "^18.3.1",
"@typescript-eslint/eslint-plugin": "^8.0.1", "@typescript-eslint/eslint-plugin": "^8.0.1",
"@typescript-eslint/parser": "^8.0.1", "@typescript-eslint/parser": "^8.0.1",
"@vitejs/plugin-react": "^4.3.4", "@vitejs/plugin-react": "^4.3.4",
"autoprefixer": "^10.4.20", "autoprefixer": "^10.4.20",
"eslint": "^9.16.0", "eslint": "^9.16.0",
"eslint-plugin-react": "^7.37.2", "eslint-plugin-react": "^7.37.2",
"eslint-plugin-react-hooks": "^5.1.0",
"eslint-plugin-simple-import-sort": "^12.1.1", "eslint-plugin-simple-import-sort": "^12.1.1",
"globals": "^15.13.0", "globals": "^15.13.0",
"jest": "^29.7.0", "jest": "^29.7.0",
"postcss": "^8.4.49", "postcss": "^8.4.49",
"tailwindcss": "^3.4.16", "tailwindcss": "^3.4.15",
"ts-jest": "^29.2.5", "ts-jest": "^29.2.5",
"typescript": "^5.7.2", "typescript": "^5.7.2",
"typescript-eslint": "^8.18.0", "typescript-eslint": "^8.16.0",
"vite": "^6.0.3" "vite": "^6.0.2"
},
"overrides": {
"react": "^19.0.0"
}, },
"jest": { "jest": {
"preset": "ts-jest", "preset": "ts-jest",

View File

@ -1,10 +1,8 @@
import { Suspense } from 'react';
import { Outlet } from 'react-router'; import { Outlet } from 'react-router';
import ConceptToaster from '@/app/ConceptToaster'; import ConceptToaster from '@/app/ConceptToaster';
import Footer from '@/app/Footer'; import Footer from '@/app/Footer';
import Navigation from '@/app/Navigation'; import Navigation from '@/app/Navigation';
import Loader from '@/components/ui/Loader';
import { useConceptOptions } from '@/context/ConceptOptionsContext'; import { useConceptOptions } from '@/context/ConceptOptionsContext';
import { NavigationState } from '@/context/NavigationContext'; import { NavigationState } from '@/context/NavigationContext';
import { globals } from '@/utils/constants'; import { globals } from '@/utils/constants';
@ -31,9 +29,7 @@ function ApplicationLayout() {
}} }}
> >
<main className='cc-scroll-y' style={{ overflowY: showScroll ? 'scroll' : 'auto', minHeight: mainHeight }}> <main className='cc-scroll-y' style={{ overflowY: showScroll ? 'scroll' : 'auto', minHeight: mainHeight }}>
<Suspense fallback={<Loader />}> <Outlet />
<Outlet />
</Suspense>
</main> </main>
<Footer /> <Footer />
</div> </div>

View File

@ -1,3 +1,5 @@
import clsx from 'clsx';
import { useConceptOptions } from '@/context/ConceptOptionsContext'; import { useConceptOptions } from '@/context/ConceptOptionsContext';
import useWindowSize from '@/hooks/useWindowSize'; import useWindowSize from '@/hooks/useWindowSize';
@ -8,7 +10,7 @@ function Logo() {
return ( return (
<img <img
alt='' alt=''
className='max-h-[1.6rem] w-fit max-w-[11.4rem]' className={clsx('max-h-[1.6rem] w-fit max-w-[11.4rem]')}
src={size.isSmall ? '/logo_sign.svg' : !darkMode ? '/logo_full.svg' : '/logo_full_dark.svg'} src={size.isSmall ? '/logo_sign.svg' : !darkMode ? '/logo_full.svg' : '/logo_full_dark.svg'}
/> />
); );

View File

@ -1,11 +1,11 @@
import clsx from 'clsx'; import clsx from 'clsx';
import { motion } from 'framer-motion';
import { IconLibrary2, IconManuals, IconNewItem2 } from '@/components/Icons'; import { IconLibrary2, IconManuals, IconNewItem2 } from '@/components/Icons';
import { CProps } from '@/components/props'; import { CProps } from '@/components/props';
import { useConceptOptions } from '@/context/ConceptOptionsContext'; import { useConceptOptions } from '@/context/ConceptOptionsContext';
import { useConceptNavigation } from '@/context/NavigationContext'; import { useConceptNavigation } from '@/context/NavigationContext';
import useWindowSize from '@/hooks/useWindowSize'; import { animateNavigation } from '@/styling/animations';
import { PARAMETER } from '@/utils/constants';
import { urls } from '../urls'; import { urls } from '../urls';
import Logo from './Logo'; import Logo from './Logo';
@ -15,7 +15,6 @@ import UserMenu from './UserMenu';
function Navigation() { function Navigation() {
const router = useConceptNavigation(); const router = useConceptNavigation();
const size = useWindowSize();
const { noNavigationAnimation } = useConceptOptions(); const { noNavigationAnimation } = useConceptOptions();
const navigateHome = (event: CProps.EventMouse) => router.push(urls.home, event.ctrlKey || event.metaKey); const navigateHome = (event: CProps.EventMouse) => router.push(urls.home, event.ctrlKey || event.metaKey);
@ -34,24 +33,17 @@ function Navigation() {
)} )}
> >
<ToggleNavigation /> <ToggleNavigation />
<div <motion.div
className={clsx( className={clsx(
'pl-2 pr-[1.5rem] sm:pr-[0.9rem] h-[3rem]', // prettier: split lines 'pl-2 pr-[1.5rem] sm:pr-[0.9rem] h-[3rem]', // prettier: split lines
'flex', 'flex',
'cc-shadow-border' 'cc-shadow-border'
)} )}
style={{ initial={false}
transitionProperty: 'height, translate', animate={!noNavigationAnimation ? 'open' : 'closed'}
transitionDuration: `${PARAMETER.moveDuration}ms`, variants={animateNavigation}
height: noNavigationAnimation ? '0rem' : '3rem',
translate: noNavigationAnimation ? '0 -1.5rem' : '0'
}}
> >
<div <div tabIndex={-1} className='flex items-center mr-auto cursor-pointer' onClick={navigateHome}>
tabIndex={-1}
className={clsx('flex items-center mr-auto', !size.isSmall && 'cursor-pointer')}
onClick={!size.isSmall ? navigateHome : undefined}
>
<Logo /> <Logo />
</div> </div>
<div className='flex gap-1 py-[0.3rem]'> <div className='flex gap-1 py-[0.3rem]'>
@ -60,7 +52,7 @@ function Navigation() {
<NavigationButton text='Справка' icon={<IconManuals size='1.5rem' />} onClick={navigateHelp} /> <NavigationButton text='Справка' icon={<IconManuals size='1.5rem' />} onClick={navigateHelp} />
<UserMenu /> <UserMenu />
</div> </div>
</div> </motion.div>
</nav> </nav>
); );
} }

View File

@ -3,22 +3,13 @@ import clsx from 'clsx';
import { CProps } from '@/components/props'; import { CProps } from '@/components/props';
import { globals } from '@/utils/constants'; import { globals } from '@/utils/constants';
interface NavigationButtonProps extends CProps.Titled, CProps.Styling { interface NavigationButtonProps extends CProps.Titled {
text?: string; text?: string;
icon: React.ReactNode; icon: React.ReactNode;
onClick?: (event: CProps.EventMouse) => void; onClick?: (event: CProps.EventMouse) => void;
} }
function NavigationButton({ function NavigationButton({ icon, title, titleHtml, hideTitle, onClick, text }: NavigationButtonProps) {
icon,
title,
className,
style,
titleHtml,
hideTitle,
onClick,
text
}: NavigationButtonProps) {
return ( return (
<button <button
type='button' type='button'
@ -38,10 +29,8 @@ function NavigationButton({
{ {
'px-2': text, 'px-2': text,
'px-4': !text 'px-4': !text
}, }
className
)} )}
style={style}
> >
{icon ? <span>{icon}</span> : null} {icon ? <span>{icon}</span> : null}
{text ? <span className='hidden sm:inline'>{text}</span> : null} {text ? <span className='hidden sm:inline'>{text}</span> : null}

View File

@ -1,13 +1,15 @@
import clsx from 'clsx'; import clsx from 'clsx';
import { motion } from 'framer-motion';
import { IconPin, IconUnpin } from '@/components/Icons'; import { IconPin, IconUnpin } from '@/components/Icons';
import { useConceptOptions } from '@/context/ConceptOptionsContext'; import { useConceptOptions } from '@/context/ConceptOptionsContext';
import { globals, PARAMETER } from '@/utils/constants'; import { animateNavigationToggle } from '@/styling/animations';
import { globals } from '@/utils/constants';
function ToggleNavigation() { function ToggleNavigation() {
const { noNavigationAnimation, toggleNoNavigation } = useConceptOptions(); const { noNavigationAnimation, toggleNoNavigation } = useConceptOptions();
return ( return (
<button <motion.button
type='button' type='button'
tabIndex={-1} tabIndex={-1}
className={clsx( className={clsx(
@ -18,18 +20,15 @@ function ToggleNavigation() {
'select-none' 'select-none'
)} )}
onClick={toggleNoNavigation} onClick={toggleNoNavigation}
initial={false}
animate={noNavigationAnimation ? 'off' : 'on'}
variants={animateNavigationToggle}
data-tooltip-id={globals.tooltip} data-tooltip-id={globals.tooltip}
data-tooltip-content={noNavigationAnimation ? 'Показать навигацию' : 'Скрыть навигацию'} data-tooltip-content={noNavigationAnimation ? 'Показать навигацию' : 'Скрыть навигацию'}
style={{
transitionProperty: 'height, width',
transitionDuration: `${PARAMETER.moveDuration}ms`,
height: noNavigationAnimation ? '1.2rem' : '3rem',
width: noNavigationAnimation ? '3rem' : '1.2rem'
}}
> >
{!noNavigationAnimation ? <IconPin /> : null} {!noNavigationAnimation ? <IconPin /> : null}
{noNavigationAnimation ? <IconUnpin /> : null} {noNavigationAnimation ? <IconUnpin /> : null}
</button> </motion.button>
); );
} }

View File

@ -1,5 +1,8 @@
import { AnimatePresence } from 'framer-motion';
import { IconLogin, IconUser2 } from '@/components/Icons'; import { IconLogin, IconUser2 } from '@/components/Icons';
import Loader from '@/components/ui/Loader'; import Loader from '@/components/ui/Loader';
import AnimateFade from '@/components/wrap/AnimateFade';
import { useAuth } from '@/context/AuthContext'; import { useAuth } from '@/context/AuthContext';
import { useConceptOptions } from '@/context/ConceptOptionsContext'; import { useConceptOptions } from '@/context/ConceptOptionsContext';
import { useConceptNavigation } from '@/context/NavigationContext'; import { useConceptNavigation } from '@/context/NavigationContext';
@ -16,25 +19,32 @@ function UserMenu() {
const menu = useDropdown(); const menu = useDropdown();
const navigateLogin = () => router.push(urls.login); const navigateLogin = () => router.push(urls.login);
return ( return (
<div ref={menu.ref} className='h-full w-[4rem] flex items-center justify-center'> <div ref={menu.ref} className='h-full w-[4rem] flex items-center justify-center'>
{loading ? <Loader circular scale={1.5} /> : null} <AnimatePresence mode='wait'>
{!user && !loading ? ( {loading ? (
<NavigationButton <AnimateFade key='nav_user_badge_loader'>
className='cc-fade-in' <Loader circular scale={1.5} />
title='Перейти на страницу логина' </AnimateFade>
icon={<IconLogin size='1.5rem' className='icon-primary' />} ) : null}
onClick={navigateLogin} {!user && !loading ? (
/> <AnimateFade key='nav_user_badge_login' className='h-full'>
) : null} <NavigationButton
{user && !loading ? ( title='Перейти на страницу логина'
<NavigationButton icon={<IconLogin size='1.5rem' className='icon-primary' />}
className='cc-fade-in' onClick={navigateLogin}
icon={<IconUser2 size='1.5rem' className={adminMode && user.is_staff ? 'icon-primary' : ''} />} />
onClick={menu.toggle} </AnimateFade>
/> ) : null}
) : null} {user ? (
<AnimateFade key='nav_user_badge_profile' className='h-full'>
<NavigationButton
icon={<IconUser2 size='1.5rem' className={adminMode && user.is_staff ? 'icon-primary' : ''} />}
onClick={menu.toggle}
/>
</AnimateFade>
) : null}
</AnimatePresence>
<UserDropdown isOpen={!!user && menu.isOpen} hideDropdown={() => menu.hide()} /> <UserDropdown isOpen={!!user && menu.isOpen} hideDropdown={() => menu.hide()} />
</div> </div>
); );

View File

@ -1,25 +1,23 @@
import React from 'react';
import { createBrowserRouter } from 'react-router'; import { createBrowserRouter } from 'react-router';
import CreateItemPage from '@/pages/CreateItemPage'; import CreateItemPage from '@/pages/CreateItemPage';
import DatabaseSchemaPage from '@/pages/DatabaseSchemaPage';
import HomePage from '@/pages/HomePage'; import HomePage from '@/pages/HomePage';
import IconsPage from '@/pages/IconsPage';
import LibraryPage from '@/pages/LibraryPage'; import LibraryPage from '@/pages/LibraryPage';
import LoginPage from '@/pages/LoginPage'; import LoginPage from '@/pages/LoginPage';
import ManualsPage from '@/pages/ManualsPage';
import NotFoundPage from '@/pages/NotFoundPage'; import NotFoundPage from '@/pages/NotFoundPage';
import OssPage from '@/pages/OssPage'; import OssPage from '@/pages/OssPage';
import PasswordChangePage from '@/pages/PasswordChangePage';
import RegisterPage from '@/pages/RegisterPage';
import RestorePasswordPage from '@/pages/RestorePasswordPage';
import RSFormPage from '@/pages/RSFormPage'; import RSFormPage from '@/pages/RSFormPage';
import UserProfilePage from '@/pages/UserProfilePage';
import ApplicationLayout from './ApplicationLayout'; import ApplicationLayout from './ApplicationLayout';
import { routes } from './urls'; import { routes } from './urls';
const UserProfilePage = React.lazy(() => import('@/pages/UserProfilePage'));
const RestorePasswordPage = React.lazy(() => import('@/pages/RestorePasswordPage'));
const PasswordChangePage = React.lazy(() => import('@/pages/PasswordChangePage'));
const RegisterPage = React.lazy(() => import('@/pages/RegisterPage'));
const ManualsPage = React.lazy(() => import('@/pages/ManualsPage'));
const IconsPage = React.lazy(() => import('@/pages/IconsPage'));
const DatabaseSchemaPage = React.lazy(() => import('@/pages/DatabaseSchemaPage'));
export const Router = createBrowserRouter([ export const Router = createBrowserRouter([
{ {
path: '/', path: '/',

View File

@ -22,7 +22,7 @@ export { BiShareAlt as IconShare } from 'react-icons/bi';
export { LuFilter as IconFilter } from 'react-icons/lu'; export { LuFilter as IconFilter } from 'react-icons/lu';
export { LuFilterX as IconFilterReset } from 'react-icons/lu'; export { LuFilterX as IconFilterReset } from 'react-icons/lu';
export {BiDownArrowCircle as IconOpenList } from 'react-icons/bi'; export {BiDownArrowCircle as IconOpenList } from 'react-icons/bi';
export { LuTriangleAlert as IconAlert } from 'react-icons/lu'; export { LuAlertTriangle as IconAlert } from 'react-icons/lu';
// ===== UI elements ======= // ===== UI elements =======
export { BiX as IconClose } from 'react-icons/bi'; export { BiX as IconClose } from 'react-icons/bi';
@ -36,7 +36,7 @@ export { LuFolderTree as IconFolderTree } from 'react-icons/lu';
export { LuFolder as IconFolder } from 'react-icons/lu'; export { LuFolder as IconFolder } from 'react-icons/lu';
export { LuFolderSearch as IconFolderSearch } from 'react-icons/lu'; export { LuFolderSearch as IconFolderSearch } from 'react-icons/lu';
export { LuFolders as IconSubfolders } from 'react-icons/lu'; export { LuFolders as IconSubfolders } from 'react-icons/lu';
export { LuFolderPen as IconFolderEdit } from 'react-icons/lu'; export { LuFolderEdit as IconFolderEdit } from 'react-icons/lu';
export { LuFolderOpen as IconFolderOpened } from 'react-icons/lu'; export { LuFolderOpen as IconFolderOpened } from 'react-icons/lu';
export { LuFolderClosed as IconFolderClosed } from 'react-icons/lu'; export { LuFolderClosed as IconFolderClosed } from 'react-icons/lu';
export { LuFolderDot as IconFolderEmpty } from 'react-icons/lu'; export { LuFolderDot as IconFolderEmpty } from 'react-icons/lu';
@ -55,7 +55,7 @@ export { TbCalendarRepeat as IconDateUpdate } from 'react-icons/tb';
export { PiFileCsv as IconCSV } from 'react-icons/pi'; export { PiFileCsv as IconCSV } from 'react-icons/pi';
// ==== User status ======= // ==== User status =======
export { LuCircleUserRound as IconUser } from 'react-icons/lu'; export { LuUserCircle2 as IconUser } from 'react-icons/lu';
export { FaCircleUser as IconUser2 } from 'react-icons/fa6'; export { FaCircleUser as IconUser2 } from 'react-icons/fa6';
export { TbUserEdit as IconEditor } from 'react-icons/tb'; export { TbUserEdit as IconEditor } from 'react-icons/tb';
export { TbUserSearch as IconUserSearch } from 'react-icons/tb'; export { TbUserSearch as IconUserSearch } from 'react-icons/tb';
@ -131,7 +131,7 @@ export { FaSortAmountDownAlt as IconSortList } from 'react-icons/fa';
export { LuNetwork as IconGenerateStructure } from 'react-icons/lu'; export { LuNetwork as IconGenerateStructure } from 'react-icons/lu';
export { LuCombine as IconSynthesis } from 'react-icons/lu'; export { LuCombine as IconSynthesis } from 'react-icons/lu';
export { LuBookCopy as IconInlineSynthesis } from 'react-icons/lu'; export { LuBookCopy as IconInlineSynthesis } from 'react-icons/lu';
export { LuWandSparkles as IconGenerateNames } from 'react-icons/lu'; export { LuWand2 as IconGenerateNames } from 'react-icons/lu';
export { GrConnect as IconConnect } from 'react-icons/gr'; export { GrConnect as IconConnect } from 'react-icons/gr';
export { BiPlayCircle as IconExecute } from 'react-icons/bi'; export { BiPlayCircle as IconExecute } from 'react-icons/bi';

View File

@ -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 { forwardRef, useCallback, useMemo, useRef, useState } from 'react'; import { forwardRef, useCallback, useMemo, useRef, useState } from 'react';
import Label from '@/components/ui/Label'; import Label from '@/components/ui/Label';
@ -206,20 +207,22 @@ const RefsInput = forwardRef<ReactCodeMirrorRef, RefsInputInputProps>(
return ( return (
<div className={clsx('flex flex-col gap-2', cursor)}> <div className={clsx('flex flex-col gap-2', cursor)}>
{showEditor && schema ? ( <AnimatePresence>
<DlgEditReference {showEditor && schema ? (
hideWindow={hideEditReference} <DlgEditReference
schema={schema} hideWindow={hideEditReference}
initial={{ schema={schema}
type: currentType, initial={{
refRaw: refText, type: currentType,
text: hintText, refRaw: refText,
basePosition: basePosition, text: hintText,
mainRefs: mainRefs basePosition: basePosition,
}} mainRefs: mainRefs
onSave={handleInputReference} }}
/> onSave={handleInputReference}
) : null} />
) : null}
</AnimatePresence>
<Label text={label} /> <Label text={label} />
<CodeMirror <CodeMirror
id={id} id={id}

View File

@ -1,15 +1,11 @@
import React, { Suspense } from 'react';
import TextURL from '@/components/ui/TextURL'; import TextURL from '@/components/ui/TextURL';
import Tooltip, { PlacesType } from '@/components/ui/Tooltip'; import Tooltip, { PlacesType } from '@/components/ui/Tooltip';
import { useConceptOptions } from '@/context/ConceptOptionsContext'; import { useConceptOptions } from '@/context/ConceptOptionsContext';
import { HelpTopic } from '@/models/miscellaneous'; import { HelpTopic } from '@/models/miscellaneous';
import TopicPage from '@/pages/ManualsPage/TopicPage';
import { IconHelp } from '../Icons'; import { IconHelp } from '../Icons';
import { CProps } from '../props'; import { CProps } from '../props';
import Loader from '../ui/Loader';
const TopicPage = React.lazy(() => import('@/pages/ManualsPage/TopicPage'));
interface BadgeHelpProps extends CProps.Styling { interface BadgeHelpProps extends CProps.Styling {
/** Topic to display in a tooltip. */ /** Topic to display in a tooltip. */
@ -38,14 +34,12 @@ function BadgeHelp({ topic, padding = 'p-1', ...restProps }: BadgeHelpProps) {
<div tabIndex={-1} id={`help-${topic}`} className={padding}> <div tabIndex={-1} id={`help-${topic}`} className={padding}>
<IconHelp size='1.25rem' className='icon-primary' /> <IconHelp size='1.25rem' className='icon-primary' />
<Tooltip clickable anchorSelect={`#help-${topic}`} layer='z-modalTooltip' {...restProps}> <Tooltip clickable anchorSelect={`#help-${topic}`} layer='z-modalTooltip' {...restProps}>
<Suspense fallback={<Loader />}> <div className='relative' onClick={event => event.stopPropagation()}>
<div className='relative' onClick={event => event.stopPropagation()}> <div className='absolute right-0 text-sm top-[0.4rem] clr-input'>
<div className='absolute right-0 text-sm top-[0.4rem] clr-input'> <TextURL text='Справка...' href={`/manuals?topic=${topic}`} />
<TextURL text='Справка...' href={`/manuals?topic=${topic}`} />
</div>
</div> </div>
<TopicPage topic={topic} /> </div>
</Suspense> <TopicPage topic={topic} />
</Tooltip> </Tooltip>
</div> </div>
); );

View File

@ -4,6 +4,7 @@ import clsx from 'clsx';
import { isResponseHtml } from '@/utils/utils'; import { isResponseHtml } from '@/utils/utils';
import PrettyJson from '../ui/PrettyJSON'; import PrettyJson from '../ui/PrettyJSON';
import AnimateFade from '../wrap/AnimateFade';
export type ErrorData = string | Error | AxiosError | undefined; export type ErrorData = string | Error | AxiosError | undefined;
@ -58,9 +59,8 @@ function DescribeError({ error }: { error: ErrorData }) {
function InfoError({ error }: InfoErrorProps) { function InfoError({ error }: InfoErrorProps) {
return ( return (
<div <AnimateFade
className={clsx( className={clsx(
'cc-fade-in',
'min-w-[25rem]', 'min-w-[25rem]',
'px-3 py-2 flex flex-col', 'px-3 py-2 flex flex-col',
'clr-text-red', 'clr-text-red',
@ -75,7 +75,7 @@ function InfoError({ error }: InfoErrorProps) {
</div> </div>
<DescribeError error={error} /> <DescribeError error={error} />
</div> </AnimateFade>
); );
} }

View File

@ -1,5 +1,5 @@
// =========== Module contains interfaces for common UI elements. ========== // =========== Module contains interfaces for common UI elements. ==========
import React from 'react'; import { HTMLMotionProps } from 'framer-motion';
export namespace CProps { export namespace CProps {
/** /**
@ -88,6 +88,16 @@ export namespace CProps {
*/ */
export type Input = Titled & React.ComponentProps<'input'>; export type Input = Titled & React.ComponentProps<'input'>;
/**
* Represents `button` component with optional title and animation properties.
*/
export type AnimatedButton = Titled & Omit<HTMLMotionProps<'button'>, 'type'>;
/**
* Represents `div` component with animation properties.
*/
export type AnimatedDiv = HTMLMotionProps<'div'>;
/** /**
* Represents `mouse event` in React. * Represents `mouse event` in React.
*/ */

View File

@ -32,7 +32,9 @@ function TableBody<TData>({
onRowDoubleClicked onRowDoubleClicked
}: TableBodyProps<TData>) { }: TableBodyProps<TData>) {
function handleRowClicked(target: Row<TData>, event: CProps.EventMouse) { function handleRowClicked(target: Row<TData>, event: CProps.EventMouse) {
onRowClicked?.(target.original, event); if (onRowClicked) {
onRowClicked(target.original, event);
}
if (enableRowSelection && target.getCanSelect()) { if (enableRowSelection && target.getCanSelect()) {
if (event.shiftKey && !!lastSelected && lastSelected !== target.id) { if (event.shiftKey && !!lastSelected && lastSelected !== target.id) {
const { rows, rowsById } = table.getRowModel(); const { rows, rowsById } = table.getRowModel();

View File

@ -1,6 +1,7 @@
import clsx from 'clsx'; import clsx from 'clsx';
import { motion } from 'framer-motion';
import { PARAMETER } from '@/utils/constants'; import { animateDropdown } from '@/styling/animations';
import { CProps } from '../props'; import { CProps } from '../props';
@ -24,12 +25,11 @@ function Dropdown({
stretchTop, stretchTop,
className, className,
children, children,
style,
...restProps ...restProps
}: React.PropsWithChildren<DropdownProps>) { }: React.PropsWithChildren<DropdownProps>) {
return ( return (
<div className='relative'> <div className='relative'>
<div <motion.div
tabIndex={-1} tabIndex={-1}
className={clsx( className={clsx(
'z-topmost', 'z-topmost',
@ -45,18 +45,13 @@ function Dropdown({
}, },
className className
)} )}
style={{ initial={false}
transitionProperty: 'clip-path, transform', animate={isOpen ? 'open' : 'closed'}
transitionDuration: `${PARAMETER.dropdownDuration}ms`, variants={animateDropdown}
transitionTimingFunction: 'ease-in-out',
transform: isOpen ? 'translateY(0)' : 'translateY(-10%)',
clipPath: isOpen ? 'inset(0% 0% 0% 0%)' : 'inset(10% 0% 90% 0%)',
...style
}}
{...restProps} {...restProps}
> >
{children} {children}
</div> </motion.div>
</div> </div>
); );
} }

View File

@ -1,10 +1,12 @@
import clsx from 'clsx'; import clsx from 'clsx';
import { motion } from 'framer-motion';
import { animateDropdownItem } from '@/styling/animations';
import { globals } from '@/utils/constants'; import { globals } from '@/utils/constants';
import { CProps } from '../props'; import { CProps } from '../props';
interface DropdownButtonProps extends CProps.Button { interface DropdownButtonProps extends CProps.AnimatedButton {
/** Icon to display first (not used if children are provided). */ /** Icon to display first (not used if children are provided). */
icon?: React.ReactNode; icon?: React.ReactNode;
@ -16,7 +18,7 @@ interface DropdownButtonProps extends CProps.Button {
} }
/** /**
* `button` with optional text, icon, and click functionality styled for use in a {@link Dropdown}. * Animated `button` with optional text, icon, and click functionality.
* It supports optional children for custom content or the default text/icon display. * It supports optional children for custom content or the default text/icon display.
*/ */
function DropdownButton({ function DropdownButton({
@ -31,7 +33,7 @@ function DropdownButton({
...restProps ...restProps
}: DropdownButtonProps) { }: DropdownButtonProps) {
return ( return (
<button <motion.button
tabIndex={-1} tabIndex={-1}
type='button' type='button'
onClick={onClick} onClick={onClick}
@ -46,6 +48,7 @@ function DropdownButton({
}, },
className className
)} )}
variants={animateDropdownItem}
data-tooltip-id={!!title || !!titleHtml ? globals.tooltip : undefined} data-tooltip-id={!!title || !!titleHtml ? globals.tooltip : undefined}
data-tooltip-html={titleHtml} data-tooltip-html={titleHtml}
data-tooltip-content={title} data-tooltip-content={title}
@ -55,7 +58,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>
); );
} }

View File

@ -1,11 +1,15 @@
import clsx from 'clsx'; import clsx from 'clsx';
import { motion } from 'framer-motion';
import { animateDropdownItem } from '@/styling/animations';
import Checkbox, { CheckboxProps } from './Checkbox'; import Checkbox, { CheckboxProps } from './Checkbox';
/** Animated {@link Checkbox} inside a {@link Dropdown} item. */ /** Animated {@link Checkbox} inside a {@link Dropdown} item. */
function DropdownCheckbox({ setValue, disabled, ...restProps }: CheckboxProps) { function DropdownCheckbox({ setValue, disabled, ...restProps }: CheckboxProps) {
return ( return (
<div <motion.div
variants={animateDropdownItem}
className={clsx( className={clsx(
'px-3 py-1', 'px-3 py-1',
'text-left overflow-ellipsis whitespace-nowrap', 'text-left overflow-ellipsis whitespace-nowrap',
@ -14,7 +18,7 @@ function DropdownCheckbox({ setValue, disabled, ...restProps }: CheckboxProps) {
)} )}
> >
<Checkbox tabIndex={-1} disabled={disabled} setValue={setValue} {...restProps} /> <Checkbox tabIndex={-1} disabled={disabled} setValue={setValue} {...restProps} />
</div> </motion.div>
); );
} }

View File

@ -0,0 +1,28 @@
import clsx from 'clsx';
import { motion } from 'framer-motion';
import { animateDropdownItem } from '@/styling/animations';
import { DividerProps } from './Divider';
/**
* {@link Divider} with animation inside {@link Dropdown}.
*/
function DropdownDivider({ vertical, margins = 'mx-2', className, ...restProps }: DividerProps) {
return (
<motion.div
variants={animateDropdownItem}
className={clsx(
margins, //prettier: split-lines
className,
{
'border-x': vertical,
'border-y': !vertical
}
)}
{...restProps}
/>
);
}
export default DropdownDivider;

View File

@ -36,7 +36,9 @@ function FileInput({ id, label, acceptType, title, className, style, onChange, .
} else { } else {
setFileName(''); setFileName('');
} }
onChange?.(event); if (onChange) {
onChange(event);
}
}; };
return ( return (

View File

@ -1,7 +1,11 @@
'use client'; 'use client';
import { ThreeCircles, ThreeDots } from 'react-loader-spinner';
import { useConceptOptions } from '@/context/ConceptOptionsContext'; import { useConceptOptions } from '@/context/ConceptOptionsContext';
import AnimateFade from '../wrap/AnimateFade';
interface LoaderProps { interface LoaderProps {
/** Scale of the loader from 1 to 10. */ /** Scale of the loader from 1 to 10. */
scale?: number; scale?: number;
@ -10,85 +14,20 @@ interface LoaderProps {
circular?: boolean; circular?: boolean;
} }
const animateRotation = (duration: string) => {
return (
<animateTransform
attributeName='transform'
attributeType='XML'
type='rotate'
dur={duration}
from='0 50 50'
to='360 50 50'
repeatCount='indefinite'
/>
);
};
const animatePulse = (startBig: boolean, duration: string) => {
return (
<>
<animate
attributeName='r'
from={startBig ? '15' : '9'}
to={startBig ? '15' : '9'}
begin='0s'
dur={duration}
values={startBig ? '15;9;15' : '9;15;9'}
calcMode='linear'
repeatCount='indefinite'
/>
<animate
attributeName='fill-opacity'
from={startBig ? '1' : '.5'}
to={startBig ? '.5' : '1'}
begin='0s'
dur={duration}
values={startBig ? '1;.5;1' : '.5;1;.5'}
calcMode='linear'
repeatCount='indefinite'
/>
</>
);
};
/** /**
* Displays animated loader. * Displays animated loader.
*/ */
function Loader({ scale = 5, circular }: LoaderProps) { function Loader({ scale = 5, circular }: LoaderProps) {
const { colors } = useConceptOptions(); const { colors } = useConceptOptions();
if (circular) { return (
return ( <AnimateFade noFadeIn className='flex justify-center'>
<div className='flex justify-center' aria-label='three-circles-loading' aria-busy='true' role='progressbar'> {circular ? (
<svg height={`${scale * 20}`} width={`${scale * 20}`} viewBox='0 0 100 100' fill={colors.bgPrimary}> <ThreeCircles color={colors.bgPrimary} height={scale * 20} width={scale * 20} />
<path d='M31.6,3.5C5.9,13.6-6.6,42.7,3.5,68.4c10.1,25.7,39.2,38.3,64.9,28.1l-3.1-7.9c-21.3,8.4-45.4-2-53.8-23.3 c-8.4-21.3,2-45.4,23.3-53.8L31.6,3.5z'> ) : (
{animateRotation('2.25s')} <ThreeDots color={colors.bgPrimary} height={scale * 20} width={scale * 20} radius={scale * 2} />
</path> )}
<path d='M82,35.7C74.1,18,53.4,10.1,35.7,18S10.1,46.6,18,64.3l7.6-3.4c-6-13.5,0-29.3,13.5-35.3s29.3,0,35.3,13.5 L82,35.7z'> </AnimateFade>
{animateRotation('1.75s')} );
</path>
<path d='M42.3,39.6c5.7-4.3,13.9-3.1,18.1,2.7c4.3,5.7,3.1,13.9-2.7,18.1l4.1,5.5c8.8-6.5,10.6-19,4.1-27.7 c-6.5-8.8-19-10.6-27.7-4.1L42.3,39.6z'>
{animateRotation('0.75s')}
</path>
</svg>
</div>
);
} else {
return (
<div className='flex justify-center' aria-busy='true' role='progressbar'>
<svg height={`${scale * 20}`} width={`${scale * 20}`} viewBox='0 0 120 30' fill={colors.bgPrimary}>
<circle cx='15' cy='15' r='16'>
{animatePulse(true, '0.8s')}
</circle>
<circle cx='60' cy='15' r='10' attributeName='fill-opacity' from='1' to='0.3'>
{animatePulse(false, '0.8s')}
</circle>
<circle cx='105' cy='15' r='16'>
{animatePulse(true, '0.8s')}
</circle>
</svg>
</div>
);
}
} }
export default Loader; export default Loader;

View File

@ -1,9 +1,12 @@
'use client'; 'use client';
import clsx from 'clsx'; import clsx from 'clsx';
import { motion } from 'framer-motion';
import { useRef } from 'react';
import useEscapeKey from '@/hooks/useEscapeKey'; import useEscapeKey from '@/hooks/useEscapeKey';
import { HelpTopic } from '@/models/miscellaneous'; import { HelpTopic } from '@/models/miscellaneous';
import { animateModal } from '@/styling/animations';
import { PARAMETER } from '@/utils/constants'; import { PARAMETER } from '@/utils/constants';
import { prepareTooltip } from '@/utils/labels'; import { prepareTooltip } from '@/utils/labels';
@ -76,18 +79,19 @@ function Modal({
hideHelpWhen, hideHelpWhen,
...restProps ...restProps
}: React.PropsWithChildren<ModalProps>) { }: React.PropsWithChildren<ModalProps>) {
const ref = useRef(null);
useEscapeKey(hideWindow); useEscapeKey(hideWindow);
const handleCancel = () => { const handleCancel = () => {
hideWindow(); hideWindow();
onCancel?.(); if (onCancel) onCancel();
}; };
const handleSubmit = () => { const handleSubmit = () => {
if (beforeSubmit && !beforeSubmit()) { if (beforeSubmit && !beforeSubmit()) {
return; return;
} }
onSubmit?.(); if (onSubmit) onSubmit();
hideWindow(); hideWindow();
}; };
@ -98,12 +102,18 @@ function Modal({
className={clsx('z-navigation', 'fixed top-0 left-0', 'w-full h-full', 'cc-modal-backdrop')} className={clsx('z-navigation', 'fixed top-0 left-0', 'w-full h-full', 'cc-modal-backdrop')}
onClick={hideWindow} onClick={hideWindow}
/> />
<div <motion.div
ref={ref}
className={clsx( className={clsx(
'cc-animate-modal', 'z-modal',
'z-modal absolute bottom-1/2 left-1/2 -translate-x-1/2 translate-y-1/2', 'absolute bottom-1/2 left-1/2 -translate-x-1/2 translate-y-1/2',
'border rounded-xl clr-app' 'border rounded-xl',
'clr-app'
)} )}
initial={{ ...animateModal.initial }}
animate={{ ...animateModal.animate }}
exit={{ ...animateModal.exit }}
{...restProps}
> >
<Overlay position='right-2 top-2'> <Overlay position='right-2 top-2'>
<MiniButton <MiniButton
@ -123,14 +133,13 @@ function Modal({
<div <div
className={clsx( className={clsx(
'overscroll-contain max-h-[calc(100svh-8rem)] max-w-[100svw] xs:max-w-[calc(100svw-2rem)] outline-none', 'overscroll-contain max-h-[calc(100svh-8rem)] max-w-[100svw] xs:max-w-[calc(100svw-2rem)]',
{ {
'overflow-auto': !overflowVisible, 'overflow-auto': !overflowVisible,
'overflow-visible': overflowVisible 'overflow-visible': overflowVisible
}, },
className className
)} )}
{...restProps}
> >
{children} {children}
</div> </div>
@ -149,7 +158,7 @@ function Modal({
) : null} ) : null}
<Button text={readonly ? 'Закрыть' : 'Отмена'} className='min-w-[7rem]' onClick={handleCancel} /> <Button text={readonly ? 'Закрыть' : 'Отмена'} className='min-w-[7rem]' onClick={handleCancel} />
</div> </div>
</div> </motion.div>
</div> </div>
); );
} }

View File

@ -1,7 +1,9 @@
import clsx from 'clsx'; import clsx from 'clsx';
import { AnimatePresence, motion } from 'framer-motion';
import { useCallback, useLayoutEffect, useMemo, useState } from 'react'; import { useCallback, useLayoutEffect, useMemo, useState } from 'react';
import { globals, PARAMETER } from '@/utils/constants'; import { animateSideAppear } from '@/styling/animations';
import { globals } from '@/utils/constants';
import { IconDropArrow, IconPageRight } from '../Icons'; import { IconDropArrow, IconPageRight } from '../Icons';
import { CProps } from '../props'; import { CProps } from '../props';
@ -92,46 +94,41 @@ function SelectTree<ItemType>({
return ( return (
<div {...restProps}> <div {...restProps}>
{items.map((item, index) => { <AnimatePresence initial={false}>
const isActive = getParent(item) === item || !folded.includes(getParent(item)); {items.map((item, index) =>
return ( getParent(item) === item || !folded.includes(getParent(item)) ? (
<div <motion.div
tabIndex={-1} tabIndex={-1}
key={`${prefix}${index}`} key={`${prefix}${index}`}
className={clsx( className={clsx(
'pr-3 pl-6 border-b', 'pr-3 pl-6 py-1',
'cc-scroll-row', 'cc-scroll-row',
'clr-controls clr-hover', 'clr-controls clr-hover',
'cursor-pointer', 'cursor-pointer',
value === item && 'clr-selected' value === item && 'clr-selected'
)} )}
data-tooltip-id={globals.tooltip} data-tooltip-id={globals.tooltip}
data-tooltip-html={isActive ? getDescription(item) : undefined} data-tooltip-html={getDescription(item)}
onClick={isActive ? event => handleSetValue(event, item) : undefined} onClick={event => handleSetValue(event, item)}
style={{ initial={{ ...animateSideAppear.initial }}
borderBottomWidth: isActive ? '1px' : '0px', animate={{ ...animateSideAppear.animate }}
transitionProperty: 'height, opacity, padding', exit={{ ...animateSideAppear.exit }}
transitionDuration: `${PARAMETER.moveDuration}ms`, >
paddingTop: isActive ? '0.25rem' : '0', {foldable.has(item) ? (
paddingBottom: isActive ? '0.25rem' : '0', <Overlay position='left-[-1.3rem]' className={clsx(!folded.includes(item) && 'top-[0.1rem]')}>
height: isActive ? 'min-content' : '0', <MiniButton
opacity: isActive ? '1' : '0' noPadding
}} noHover
> icon={!folded.includes(item) ? <IconDropArrow size='1rem' /> : <IconPageRight size='1.25rem' />}
{foldable.has(item) ? ( onClick={event => handleClickFold(event, item, folded.includes(item))}
<Overlay position='left-[-1.3rem]' className={clsx(!folded.includes(item) && 'top-[0.1rem]')}> />
<MiniButton </Overlay>
noPadding ) : null}
noHover {getParent(item) === item ? getLabel(item) : `- ${getLabel(item).toLowerCase()}`}
icon={!folded.includes(item) ? <IconDropArrow size='1rem' /> : <IconPageRight size='1.25rem' />} </motion.div>
onClick={event => handleClickFold(event, item, folded.includes(item))} ) : null
/> )}
</Overlay> </AnimatePresence>
) : null}
{getParent(item) === item ? getLabel(item) : `- ${getLabel(item).toLowerCase()}`}
</div>
);
})}
</div> </div>
); );
} }

View File

@ -0,0 +1,29 @@
import { motion } from 'framer-motion';
import { animateFade } from '@/styling/animations';
import { CProps } from '../props';
interface AnimateFadeProps extends CProps.AnimatedDiv {
noFadeIn?: boolean;
noFadeOut?: boolean;
hideContent?: boolean;
}
function AnimateFade({ style, noFadeIn, noFadeOut, children, hideContent, ...restProps }: AnimateFadeProps) {
return (
<motion.div
tabIndex={-1}
initial={{ ...(!noFadeIn ? animateFade.initial : {}) }}
animate={hideContent ? 'hidden' : 'active'}
variants={animateFade.variants}
exit={{ ...(!noFadeOut ? animateFade.exit : {}) }}
style={{ display: hideContent ? 'none' : '', willChange: 'auto', ...style }}
{...restProps}
>
{children}
</motion.div>
);
}
export default AnimateFade;

View File

@ -1,25 +1,43 @@
import InfoError, { ErrorData } from '../info/InfoError'; import { AnimatePresence } from 'framer-motion';
import Loader from '../ui/Loader';
import InfoError, { ErrorData } from '../info/InfoError';
import { CProps } from '../props';
import Loader from '../ui/Loader';
import AnimateFade from './AnimateFade';
interface DataLoaderProps extends CProps.AnimatedDiv {
id: string;
interface DataLoaderProps {
isLoading?: boolean; isLoading?: boolean;
error?: ErrorData; error?: ErrorData;
hasNoData?: boolean; hasNoData?: boolean;
} }
function DataLoader({ isLoading, hasNoData, error, children }: React.PropsWithChildren<DataLoaderProps>) { function DataLoader({
if (isLoading) { id,
return <Loader />; isLoading,
} hasNoData,
if (error) { error,
return <InfoError error={error} />; className,
} children,
...restProps
if (hasNoData) { }: React.PropsWithChildren<DataLoaderProps>) {
return <div className='cc-fade-in w-full text-center p-1'>Данные не загружены</div>; return (
} else { <AnimatePresence mode='wait'>
return <>{children}</>; {!isLoading && !error && !hasNoData ? (
} <AnimateFade id={id} key={`${id}-data`} className={className} {...restProps}>
{children}
</AnimateFade>
) : null}
{!isLoading && !error && hasNoData ? (
<AnimateFade key={`${id}-no-data`} className='w-full text-center p-1' {...restProps}>
Данные не загружены
</AnimateFade>
) : null}
{isLoading ? <Loader key={`${id}-loader`} /> : null}
{error ? <InfoError key={`${id}-error`} error={error} /> : null}
</AnimatePresence>
);
} }
export default DataLoader; export default DataLoader;

View File

@ -13,7 +13,7 @@ function ExpectedAnonymous() {
} }
return ( return (
<div className='cc-fade-in flex flex-col items-center gap-3 py-6'> <div className='flex flex-col items-center gap-3 py-6'>
<p className='font-semibold'>{`Вы вошли в систему как ${user?.username ?? ''}`}</p> <p className='font-semibold'>{`Вы вошли в систему как ${user?.username ?? ''}`}</p>
<div className='flex gap-3'> <div className='flex gap-3'>
<TextURL text='Новая схема' href='/library/create' /> <TextURL text='Новая схема' href='/library/create' />

View File

@ -1,28 +1,30 @@
'use client'; 'use client';
import { AnimatePresence } from 'framer-motion';
import { useAuth } from '@/context/AuthContext'; import { useAuth } from '@/context/AuthContext';
import Loader from '../ui/Loader'; import Loader from '../ui/Loader';
import TextURL from '../ui/TextURL'; import TextURL from '../ui/TextURL';
import AnimateFade from './AnimateFade';
function RequireAuth({ children }: React.PropsWithChildren) { function RequireAuth({ children }: React.PropsWithChildren) {
const { user, loading } = useAuth(); const { user, loading } = useAuth();
if (loading) { return (
return <Loader key='auth-loader' />; <AnimatePresence mode='wait'>
} {loading ? <Loader key='auth-loader' /> : null}
if (user) { {!loading && user ? <AnimateFade key='auth-data'>{children}</AnimateFade> : null}
return <>{children}</>; {!loading && !user ? (
} else { <AnimateFade key='auth-no-user' className='flex flex-col items-center gap-1 mt-2'>
return ( <p className='mb-2'>Пожалуйста войдите в систему</p>
<div key='auth-no-user' className='flex flex-col items-center gap-1 mt-2'> <TextURL text='Войти в Портал' href='/login' />
<p className='mb-2'>Пожалуйста войдите в систему</p> <TextURL text='Зарегистрироваться' href='/signup' />
<TextURL text='Войти в Портал' href='/login' /> <TextURL text='Начальная страница' href='/' />
<TextURL text='Зарегистрироваться' href='/signup' /> </AnimateFade>
<TextURL text='Начальная страница' href='/' /> ) : null}
</div> </AnimatePresence>
); );
}
} }
export default RequireAuth; export default RequireAuth;

View File

@ -69,7 +69,7 @@ export const AuthState = ({ children }: React.PropsWithChildren) => {
} else { } else {
setUser(undefined); setUser(undefined);
} }
callback?.(); if (callback) callback();
} }
}); });
}, },
@ -85,7 +85,7 @@ export const AuthState = ({ children }: React.PropsWithChildren) => {
onError: setError, onError: setError,
onSuccess: newData => onSuccess: newData =>
reload(() => { reload(() => {
callback?.(newData); if (callback) callback(newData);
}) })
}); });
} }
@ -96,7 +96,7 @@ export const AuthState = ({ children }: React.PropsWithChildren) => {
showError: true, showError: true,
onSuccess: newData => onSuccess: newData =>
reload(() => { reload(() => {
callback?.(newData); if (callback) callback(newData);
}) })
}); });
} }
@ -111,7 +111,7 @@ export const AuthState = ({ children }: React.PropsWithChildren) => {
onSuccess: newData => onSuccess: newData =>
reload(() => { reload(() => {
users.push(newData as IUserInfo); users.push(newData as IUserInfo);
callback?.(newData); if (callback) callback(newData);
}) })
}); });
} }
@ -126,7 +126,7 @@ export const AuthState = ({ children }: React.PropsWithChildren) => {
onError: setError, onError: setError,
onSuccess: () => onSuccess: () =>
reload(() => { reload(() => {
callback?.(); if (callback) callback();
}) })
}); });
}, },
@ -143,7 +143,7 @@ export const AuthState = ({ children }: React.PropsWithChildren) => {
onError: setError, onError: setError,
onSuccess: () => onSuccess: () =>
reload(() => { reload(() => {
callback?.(); if (callback) callback();
}) })
}); });
}, },
@ -160,7 +160,7 @@ export const AuthState = ({ children }: React.PropsWithChildren) => {
onError: setError, onError: setError,
onSuccess: () => onSuccess: () =>
reload(() => { reload(() => {
callback?.(); if (callback) callback();
}) })
}); });
}, },
@ -177,7 +177,7 @@ export const AuthState = ({ children }: React.PropsWithChildren) => {
onError: setError, onError: setError,
onSuccess: () => onSuccess: () =>
reload(() => { reload(() => {
callback?.(); if (callback) callback();
}) })
}); });
}, },

View File

@ -4,8 +4,9 @@ import { createContext, useCallback, useContext, useLayoutEffect, useMemo, useSt
import Tooltip from '@/components/ui/Tooltip'; import Tooltip from '@/components/ui/Tooltip';
import useLocalStorage from '@/hooks/useLocalStorage'; import useLocalStorage from '@/hooks/useLocalStorage';
import { animationDuration } from '@/styling/animations';
import { darkT, IColorTheme, lightT } from '@/styling/color'; import { darkT, IColorTheme, lightT } from '@/styling/color';
import { globals, PARAMETER, storage } from '@/utils/constants'; import { globals, storage } from '@/utils/constants';
import { contextOutsideScope } from '@/utils/labels'; import { contextOutsideScope } from '@/utils/labels';
interface IOptionsContext { interface IOptionsContext {
@ -90,7 +91,7 @@ export const OptionsState = ({ children }: React.PropsWithChildren) => {
setNoNavigation(false); setNoNavigation(false);
} else { } else {
setNoNavigationAnimation(true); setNoNavigationAnimation(true);
setTimeout(() => setNoNavigation(true), PARAMETER.moveDuration); setTimeout(() => setNoNavigation(true), animationDuration.navigationToggle);
} }
}, [noNavigation]); }, [noNavigation]);

View File

@ -48,7 +48,7 @@ export const GlobalOssState = ({ children }: React.PropsWithChildren) => {
(callback?: () => void) => { (callback?: () => void) => {
reloadInternal(undefined, () => { reloadInternal(undefined, () => {
setIsValid(true); setIsValid(true);
callback?.(); if (callback) callback();
}); });
}, },
[reloadInternal] [reloadInternal]

View File

@ -157,7 +157,7 @@ export const LibraryState = ({ children }: React.PropsWithChildren) => {
onError: setLoadingError, onError: setLoadingError,
onSuccess: newData => { onSuccess: newData => {
setItems(newData); setItems(newData);
callback?.(); if (callback) callback();
} }
}); });
} else { } else {
@ -167,7 +167,7 @@ export const LibraryState = ({ children }: React.PropsWithChildren) => {
onError: setLoadingError, onError: setLoadingError,
onSuccess: newData => { onSuccess: newData => {
setItems(newData); setItems(newData);
callback?.(); if (callback) callback();
} }
}); });
} }
@ -217,7 +217,7 @@ export const LibraryState = ({ children }: React.PropsWithChildren) => {
(data: ILibraryCreateData, callback?: DataCallback<ILibraryItem>) => { (data: ILibraryCreateData, callback?: DataCallback<ILibraryItem>) => {
const onSuccess = (newSchema: ILibraryItem) => const onSuccess = (newSchema: ILibraryItem) =>
reloadItems(() => { reloadItems(() => {
callback?.(newSchema); if (callback) callback(newSchema);
}); });
setProcessingError(undefined); setProcessingError(undefined);
if (data.file) { if (data.file) {
@ -250,7 +250,7 @@ export const LibraryState = ({ children }: React.PropsWithChildren) => {
onError: setProcessingError, onError: setProcessingError,
onSuccess: () => onSuccess: () =>
reloadItems(() => { reloadItems(() => {
callback?.(); if (callback) callback();
}) })
}); });
}, },
@ -270,7 +270,7 @@ export const LibraryState = ({ children }: React.PropsWithChildren) => {
onError: setProcessingError, onError: setProcessingError,
onSuccess: newSchema => onSuccess: newSchema =>
reloadItems(() => { reloadItems(() => {
callback?.(newSchema); if (callback) callback(newSchema);
}) })
}); });
}, },
@ -287,7 +287,7 @@ export const LibraryState = ({ children }: React.PropsWithChildren) => {
onError: setProcessingError, onError: setProcessingError,
onSuccess: () => onSuccess: () =>
reloadItems(() => { reloadItems(() => {
callback?.(); if (callback) callback();
}) })
}); });
}, },

View File

@ -114,7 +114,7 @@ export const OssState = ({ itemID, children }: React.PropsWithChildren<OssStateP
const fullData: IOperationSchemaData = Object.assign(model, newData); const fullData: IOperationSchemaData = Object.assign(model, newData);
oss.setData(fullData); oss.setData(fullData);
library.localUpdateItem(newData); library.localUpdateItem(newData);
callback?.(newData); if (callback) callback(newData);
} }
}); });
}, },
@ -137,7 +137,7 @@ export const OssState = ({ itemID, children }: React.PropsWithChildren<OssStateP
onSuccess: () => { onSuccess: () => {
model.owner = newOwner; model.owner = newOwner;
library.reloadItems(() => { library.reloadItems(() => {
callback?.(); if (callback) callback();
}); });
} }
}); });
@ -161,7 +161,7 @@ export const OssState = ({ itemID, children }: React.PropsWithChildren<OssStateP
onSuccess: () => { onSuccess: () => {
model.access_policy = newPolicy; model.access_policy = newPolicy;
library.reloadItems(() => { library.reloadItems(() => {
callback?.(); if (callback) callback();
}); });
} }
}); });
@ -185,7 +185,7 @@ export const OssState = ({ itemID, children }: React.PropsWithChildren<OssStateP
onSuccess: () => { onSuccess: () => {
model.location = newLocation; model.location = newLocation;
library.reloadItems(() => { library.reloadItems(() => {
callback?.(); if (callback) callback();
}); });
} }
}); });
@ -209,7 +209,7 @@ export const OssState = ({ itemID, children }: React.PropsWithChildren<OssStateP
onSuccess: () => { onSuccess: () => {
model.editors = newEditors; model.editors = newEditors;
library.reloadItems(() => { library.reloadItems(() => {
callback?.(); if (callback) callback();
}); });
} }
}); });
@ -227,7 +227,7 @@ export const OssState = ({ itemID, children }: React.PropsWithChildren<OssStateP
onError: setProcessingError, onError: setProcessingError,
onSuccess: () => { onSuccess: () => {
library.localUpdateTimestamp(Number(itemID)); library.localUpdateTimestamp(Number(itemID));
callback?.(); if (callback) callback();
} }
}); });
}, },
@ -245,7 +245,7 @@ export const OssState = ({ itemID, children }: React.PropsWithChildren<OssStateP
onSuccess: newData => { onSuccess: newData => {
oss.setData(newData.oss); oss.setData(newData.oss);
library.localUpdateTimestamp(newData.oss.id); library.localUpdateTimestamp(newData.oss.id);
callback?.(newData.new_operation); if (callback) callback(newData.new_operation);
} }
}); });
}, },
@ -263,7 +263,7 @@ export const OssState = ({ itemID, children }: React.PropsWithChildren<OssStateP
onSuccess: newData => { onSuccess: newData => {
oss.setData(newData); oss.setData(newData);
library.reloadItems(() => { library.reloadItems(() => {
callback?.(); if (callback) callback();
}); });
} }
}); });
@ -282,7 +282,7 @@ export const OssState = ({ itemID, children }: React.PropsWithChildren<OssStateP
onSuccess: newData => { onSuccess: newData => {
oss.setData(newData.oss); oss.setData(newData.oss);
library.reloadItems(() => { library.reloadItems(() => {
callback?.(newData.new_schema); if (callback) callback(newData.new_schema);
}); });
} }
}); });
@ -304,7 +304,7 @@ export const OssState = ({ itemID, children }: React.PropsWithChildren<OssStateP
onSuccess: newData => { onSuccess: newData => {
oss.setData(newData); oss.setData(newData);
library.reloadItems(() => { library.reloadItems(() => {
callback?.(); if (callback) callback();
}); });
} }
}); });
@ -326,7 +326,7 @@ export const OssState = ({ itemID, children }: React.PropsWithChildren<OssStateP
onSuccess: newData => { onSuccess: newData => {
oss.setData(newData); oss.setData(newData);
library.reloadItems(() => { library.reloadItems(() => {
callback?.(); if (callback) callback();
}); });
} }
}); });
@ -348,7 +348,7 @@ export const OssState = ({ itemID, children }: React.PropsWithChildren<OssStateP
onSuccess: newData => { onSuccess: newData => {
oss.setData(newData); oss.setData(newData);
library.reloadItems(() => { library.reloadItems(() => {
callback?.(); if (callback) callback();
}); });
} }
}); });
@ -370,7 +370,7 @@ export const OssState = ({ itemID, children }: React.PropsWithChildren<OssStateP
onSuccess: () => { onSuccess: () => {
oss.reload(); oss.reload();
library.reloadItems(() => { library.reloadItems(() => {
callback?.(); if (callback) callback();
}); });
} }
}); });

View File

@ -147,7 +147,7 @@ export const RSFormState = ({ itemID, versionID, children }: React.PropsWithChil
setSchema(Object.assign(schema, newData)); setSchema(Object.assign(schema, newData));
library.localUpdateItem(newData); library.localUpdateItem(newData);
oss.invalidateItem(newData.id); oss.invalidateItem(newData.id);
callback?.(newData); if (callback) callback(newData);
} }
}); });
}, },
@ -168,7 +168,7 @@ export const RSFormState = ({ itemID, versionID, children }: React.PropsWithChil
onSuccess: newData => { onSuccess: newData => {
setSchema(newData); setSchema(newData);
library.localUpdateItem(newData); library.localUpdateItem(newData);
callback?.(); if (callback) callback();
} }
}); });
}, },
@ -191,7 +191,7 @@ export const RSFormState = ({ itemID, versionID, children }: React.PropsWithChil
onSuccess: () => { onSuccess: () => {
schema.owner = newOwner; schema.owner = newOwner;
library.localUpdateItem(schema); library.localUpdateItem(schema);
callback?.(); if (callback) callback();
} }
}); });
}, },
@ -214,7 +214,7 @@ export const RSFormState = ({ itemID, versionID, children }: React.PropsWithChil
onSuccess: () => { onSuccess: () => {
schema.access_policy = newPolicy; schema.access_policy = newPolicy;
library.localUpdateItem(schema); library.localUpdateItem(schema);
callback?.(); if (callback) callback();
} }
}); });
}, },
@ -258,7 +258,7 @@ export const RSFormState = ({ itemID, versionID, children }: React.PropsWithChil
onError: setProcessingError, onError: setProcessingError,
onSuccess: () => { onSuccess: () => {
schema.editors = newEditors; schema.editors = newEditors;
callback?.(); if (callback) callback();
} }
}); });
}, },
@ -279,7 +279,7 @@ export const RSFormState = ({ itemID, versionID, children }: React.PropsWithChil
setSchema(newData); setSchema(newData);
library.localUpdateTimestamp(newData.id); library.localUpdateTimestamp(newData.id);
oss.invalidateItem(newData.id); oss.invalidateItem(newData.id);
callback?.(); if (callback) callback();
} }
}); });
}, },
@ -299,7 +299,7 @@ export const RSFormState = ({ itemID, versionID, children }: React.PropsWithChil
onSuccess: newData => { onSuccess: newData => {
setSchema(newData); setSchema(newData);
library.localUpdateTimestamp(newData.id); library.localUpdateTimestamp(newData.id);
callback?.(); if (callback) callback();
} }
}); });
}, },
@ -318,7 +318,7 @@ export const RSFormState = ({ itemID, versionID, children }: React.PropsWithChil
setSchema(newData.schema); setSchema(newData.schema);
library.localUpdateTimestamp(newData.schema.id); library.localUpdateTimestamp(newData.schema.id);
oss.invalidateItem(newData.schema.id); oss.invalidateItem(newData.schema.id);
callback?.(newData.cst_list); if (callback) callback(newData.cst_list);
} }
}); });
}, },
@ -350,7 +350,7 @@ export const RSFormState = ({ itemID, versionID, children }: React.PropsWithChil
setSchema(newData.schema); setSchema(newData.schema);
library.localUpdateTimestamp(newData.schema.id); library.localUpdateTimestamp(newData.schema.id);
oss.invalidateItem(newData.schema.id); oss.invalidateItem(newData.schema.id);
callback?.(newData.new_cst); if (callback) callback(newData.new_cst);
} }
}); });
}, },
@ -369,7 +369,7 @@ export const RSFormState = ({ itemID, versionID, children }: React.PropsWithChil
setSchema(newData); setSchema(newData);
library.localUpdateTimestamp(newData.id); library.localUpdateTimestamp(newData.id);
oss.invalidateItem(newData.id); oss.invalidateItem(newData.id);
callback?.(); if (callback) callback();
} }
}); });
}, },
@ -388,7 +388,7 @@ export const RSFormState = ({ itemID, versionID, children }: React.PropsWithChil
reload(setProcessing, () => { reload(setProcessing, () => {
library.localUpdateTimestamp(Number(itemID)); library.localUpdateTimestamp(Number(itemID));
oss.invalidateItem(Number(itemID)); oss.invalidateItem(Number(itemID));
callback?.(newData); if (callback) callback(newData);
}) })
}); });
}, },
@ -407,7 +407,7 @@ export const RSFormState = ({ itemID, versionID, children }: React.PropsWithChil
setSchema(newData.schema); setSchema(newData.schema);
library.localUpdateTimestamp(newData.schema.id); library.localUpdateTimestamp(newData.schema.id);
oss.invalidateItem(newData.schema.id); oss.invalidateItem(newData.schema.id);
callback?.(newData.new_cst); if (callback) callback(newData.new_cst);
} }
}); });
}, },
@ -426,7 +426,7 @@ export const RSFormState = ({ itemID, versionID, children }: React.PropsWithChil
setSchema(newData); setSchema(newData);
library.localUpdateTimestamp(newData.id); library.localUpdateTimestamp(newData.id);
oss.invalidateItem(newData.id); oss.invalidateItem(newData.id);
callback?.(); if (callback) callback();
} }
}); });
}, },
@ -444,7 +444,7 @@ export const RSFormState = ({ itemID, versionID, children }: React.PropsWithChil
onSuccess: newData => { onSuccess: newData => {
setSchema(newData); setSchema(newData);
library.localUpdateTimestamp(Number(itemID)); library.localUpdateTimestamp(Number(itemID));
callback?.(); if (callback) callback();
} }
}); });
}, },
@ -462,7 +462,7 @@ export const RSFormState = ({ itemID, versionID, children }: React.PropsWithChil
onSuccess: newData => { onSuccess: newData => {
setSchema(newData.schema); setSchema(newData.schema);
library.localUpdateTimestamp(Number(itemID)); library.localUpdateTimestamp(Number(itemID));
callback?.(newData.version); if (callback) callback(newData.version);
} }
}); });
}, },
@ -499,7 +499,7 @@ export const RSFormState = ({ itemID, versionID, children }: React.PropsWithChil
} }
}); });
setSchema(schema); setSchema(schema);
callback?.(); if (callback) callback();
} }
}); });
}, },
@ -516,7 +516,7 @@ export const RSFormState = ({ itemID, versionID, children }: React.PropsWithChil
onSuccess: () => { onSuccess: () => {
schema!.versions = schema!.versions.filter(prev => prev.id !== target); schema!.versions = schema!.versions.filter(prev => prev.id !== target);
setSchema(schema); setSchema(schema);
callback?.(); if (callback) callback();
} }
}); });
}, },
@ -533,7 +533,7 @@ export const RSFormState = ({ itemID, versionID, children }: React.PropsWithChil
onSuccess: newData => { onSuccess: newData => {
setSchema(newData); setSchema(newData);
library.localUpdateItem(newData); library.localUpdateItem(newData);
callback?.(); if (callback) callback();
} }
}); });
}, },
@ -552,7 +552,7 @@ export const RSFormState = ({ itemID, versionID, children }: React.PropsWithChil
setSchema(newData); setSchema(newData);
library.localUpdateTimestamp(newData.id); library.localUpdateTimestamp(newData.id);
oss.invalidateItem(newData.id); oss.invalidateItem(newData.id);
callback?.(newData); if (callback) callback(newData);
} }
}); });
}, },

View File

@ -64,7 +64,7 @@ export const UserProfileState = ({ children }: React.PropsWithChildren) => {
libraryUser.first_name = newData.first_name; libraryUser.first_name = newData.first_name;
libraryUser.last_name = newData.last_name; libraryUser.last_name = newData.last_name;
} }
callback?.(newData); if (callback) callback(newData);
} }
}); });
}, },

View File

@ -69,7 +69,7 @@ export const UsersState = ({ children }: React.PropsWithChildren) => {
} }
}); });
setUsers(newData); setUsers(newData);
callback?.(); if (callback) callback();
} }
}); });
}, },

View File

@ -6,6 +6,7 @@ import { TabList, TabPanel, Tabs } from 'react-tabs';
import Modal, { ModalProps } from '@/components/ui/Modal'; import Modal, { ModalProps } from '@/components/ui/Modal';
import TabLabel from '@/components/ui/TabLabel'; import TabLabel from '@/components/ui/TabLabel';
import AnimateFade from '@/components/wrap/AnimateFade';
import { useLibrary } from '@/context/LibraryContext'; import { useLibrary } from '@/context/LibraryContext';
import usePartialUpdate from '@/hooks/usePartialUpdate'; import usePartialUpdate from '@/hooks/usePartialUpdate';
import { HelpTopic } from '@/models/miscellaneous'; import { HelpTopic } from '@/models/miscellaneous';
@ -144,9 +145,9 @@ function DlgConstituentaTemplate({ hideWindow, schema, onCreate, insertAfter }:
const editorPanel = useMemo( const editorPanel = useMemo(
() => ( () => (
<TabPanel> <TabPanel>
<div className='cc-fade-in cc-column'> <AnimateFade className='cc-column'>
<FormCreateCst state={constituenta} partialUpdate={updateConstituenta} schema={schema} /> <FormCreateCst state={constituenta} partialUpdate={updateConstituenta} schema={schema} />
</div> </AnimateFade>
</TabPanel> </TabPanel>
), ),
[constituenta, updateConstituenta, schema] [constituenta, updateConstituenta, schema]

View File

@ -10,6 +10,7 @@ import PickConstituenta from '@/components/select/PickConstituenta';
import DataTable, { IConditionalStyle } from '@/components/ui/DataTable'; import DataTable, { IConditionalStyle } from '@/components/ui/DataTable';
import MiniButton from '@/components/ui/MiniButton'; import MiniButton from '@/components/ui/MiniButton';
import NoData from '@/components/ui/NoData'; import NoData from '@/components/ui/NoData';
import AnimateFade from '@/components/wrap/AnimateFade';
import { useConceptOptions } from '@/context/ConceptOptionsContext'; import { useConceptOptions } from '@/context/ConceptOptionsContext';
import { IConstituenta, IRSForm } from '@/models/rsform'; import { IConstituenta, IRSForm } from '@/models/rsform';
import { IArgumentValue } from '@/models/rslang'; import { IArgumentValue } from '@/models/rslang';
@ -146,7 +147,7 @@ function TabArguments({ state, schema, partialUpdate }: TabArgumentsProps) {
); );
return ( return (
<div className='cc-fade-in'> <AnimateFade>
<DataTable <DataTable
dense dense
noFooter noFooter
@ -223,7 +224,7 @@ function TabArguments({ state, schema, partialUpdate }: TabArgumentsProps) {
height='5.1rem' height='5.1rem'
value={state.definition} value={state.definition}
/> />
</div> </AnimateFade>
); );
} }

View File

@ -6,6 +6,7 @@ import RSInput from '@/components/RSInput';
import PickConstituenta from '@/components/select/PickConstituenta'; import PickConstituenta from '@/components/select/PickConstituenta';
import SelectSingle from '@/components/ui/SelectSingle'; import SelectSingle from '@/components/ui/SelectSingle';
import TextArea from '@/components/ui/TextArea'; import TextArea from '@/components/ui/TextArea';
import AnimateFade from '@/components/wrap/AnimateFade';
import { useLibrary } from '@/context/LibraryContext'; import { useLibrary } from '@/context/LibraryContext';
import { CATEGORY_CST_TYPE, IConstituenta, IRSForm } from '@/models/rsform'; import { CATEGORY_CST_TYPE, IConstituenta, IRSForm } from '@/models/rsform';
import { applyFilterCategory } from '@/models/rsformAPI'; import { applyFilterCategory } from '@/models/rsformAPI';
@ -77,7 +78,7 @@ function TabTemplate({ state, partialUpdate, templateSchema }: TabTemplateProps)
}, [state.filterCategory, templateSchema]); }, [state.filterCategory, templateSchema]);
return ( return (
<div className='cc-fade-in'> <AnimateFade>
<div className='flex border-t border-x rounded-t-md clr-input'> <div className='flex border-t border-x rounded-t-md clr-input'>
<SelectSingle <SelectSingle
noBorder noBorder
@ -137,7 +138,7 @@ function TabTemplate({ state, partialUpdate, templateSchema }: TabTemplateProps)
height='5.1rem' height='5.1rem'
value={state.prototype?.definition_formal} value={state.prototype?.definition_formal}
/> />
</div> </AnimateFade>
); );
} }

View File

@ -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 BadgeHelp from '@/components/info/BadgeHelp'; import BadgeHelp from '@/components/info/BadgeHelp';
@ -8,6 +9,7 @@ import RSInput from '@/components/RSInput';
import SelectSingle from '@/components/ui/SelectSingle'; import SelectSingle from '@/components/ui/SelectSingle';
import TextArea from '@/components/ui/TextArea'; import TextArea from '@/components/ui/TextArea';
import TextInput from '@/components/ui/TextInput'; import TextInput from '@/components/ui/TextInput';
import AnimateFade from '@/components/wrap/AnimateFade';
import { HelpTopic } from '@/models/miscellaneous'; import { HelpTopic } from '@/models/miscellaneous';
import { CstType, ICstCreateData, IRSForm } from '@/models/rsform'; import { CstType, ICstCreateData, IRSForm } from '@/models/rsform';
import { generateAlias, isBaseSet, isBasicConcept, isFunctional, validateNewAlias } from '@/models/rsformAPI'; import { generateAlias, isBaseSet, isBasicConcept, isFunctional, validateNewAlias } from '@/models/rsformAPI';
@ -46,8 +48,8 @@ function FormCreateCst({ schema, state, partialUpdate, setValidated }: FormCreat
); );
return ( return (
<> <AnimatePresence>
<div className='flex items-center self-center gap-3'> <div key='dlg_cst_alias_picker' className='flex items-center self-center gap-3'>
<SelectSingle <SelectSingle
id='dlg_cst_type' id='dlg_cst_type'
placeholder='Выберите тип' placeholder='Выберите тип'
@ -70,8 +72,8 @@ function FormCreateCst({ schema, state, partialUpdate, setValidated }: FormCreat
className={clsx(PARAMETER.TOOLTIP_WIDTH, 'sm:max-w-[40rem]')} className={clsx(PARAMETER.TOOLTIP_WIDTH, 'sm:max-w-[40rem]')}
/> />
</div> </div>
<TextArea <TextArea
key='dlg_cst_term'
id='dlg_cst_term' id='dlg_cst_term'
fitContent fitContent
spellCheck spellCheck
@ -81,8 +83,7 @@ function FormCreateCst({ schema, state, partialUpdate, setValidated }: FormCreat
value={state.term_raw} value={state.term_raw}
onChange={event => partialUpdate({ term_raw: event.target.value })} onChange={event => partialUpdate({ term_raw: event.target.value })}
/> />
<AnimateFade key='dlg_cst_expression' hideContent={!state.definition_formal && isElementary}>
{!!state.definition_formal || !isElementary ? (
<RSInput <RSInput
id='dlg_cst_expression' id='dlg_cst_expression'
noTooltip noTooltip
@ -100,9 +101,8 @@ function FormCreateCst({ schema, state, partialUpdate, setValidated }: FormCreat
onChange={value => partialUpdate({ definition_formal: value })} onChange={value => partialUpdate({ definition_formal: value })}
schema={schema} schema={schema}
/> />
) : null} </AnimateFade>
<AnimateFade key='dlg_cst_definition' hideContent={!state.definition_raw && isElementary}>
{!!state.definition_raw || !isElementary ? (
<TextArea <TextArea
id='dlg_cst_definition' id='dlg_cst_definition'
spellCheck spellCheck
@ -113,10 +113,10 @@ function FormCreateCst({ schema, state, partialUpdate, setValidated }: FormCreat
value={state.definition_raw} value={state.definition_raw}
onChange={event => partialUpdate({ definition_raw: event.target.value })} onChange={event => partialUpdate({ definition_raw: event.target.value })}
/> />
) : null} </AnimateFade>
{!showConvention ? ( {!showConvention ? (
<button <button
key='dlg_cst_show_comment'
id='dlg_cst_show_comment' id='dlg_cst_show_comment'
tabIndex={-1} tabIndex={-1}
type='button' type='button'
@ -125,8 +125,10 @@ function FormCreateCst({ schema, state, partialUpdate, setValidated }: FormCreat
> >
Добавить комментарий Добавить комментарий
</button> </button>
) : ( ) : null}
<AnimateFade hideContent={!showConvention}>
<TextArea <TextArea
key='dlg_cst_convention'
id='dlg_cst_convention' id='dlg_cst_convention'
spellCheck spellCheck
fitContent fitContent
@ -136,8 +138,8 @@ function FormCreateCst({ schema, state, partialUpdate, setValidated }: FormCreat
value={state.convention} value={state.convention}
onChange={event => partialUpdate({ convention: event.target.value })} onChange={event => partialUpdate({ convention: event.target.value })}
/> />
)} </AnimateFade>
</> </AnimatePresence>
); );
} }

View File

@ -9,6 +9,7 @@ import Label from '@/components/ui/Label';
import MiniButton from '@/components/ui/MiniButton'; import MiniButton from '@/components/ui/MiniButton';
import TextArea from '@/components/ui/TextArea'; import TextArea from '@/components/ui/TextArea';
import TextInput from '@/components/ui/TextInput'; import TextInput from '@/components/ui/TextInput';
import AnimateFade from '@/components/wrap/AnimateFade';
import { useLibrary } from '@/context/LibraryContext'; import { useLibrary } from '@/context/LibraryContext';
import { ILibraryItem, LibraryItemID, LibraryItemType } from '@/models/library'; import { ILibraryItem, LibraryItemID, LibraryItemType } from '@/models/library';
import { IOperationSchema } from '@/models/oss'; import { IOperationSchema } from '@/models/oss';
@ -52,7 +53,7 @@ function TabInputOperation({
}, [createSchema, onChangeAttachedID]); }, [createSchema, onChangeAttachedID]);
return ( return (
<div className='cc-fade-in cc-column'> <AnimateFade className='cc-column'>
<TextInput <TextInput
id='operation_title' id='operation_title'
label='Полное название' label='Полное название'
@ -110,7 +111,7 @@ function TabInputOperation({
baseFilter={baseFilter} baseFilter={baseFilter}
/> />
) : null} ) : null}
</div> </AnimateFade>
); );
} }

View File

@ -2,6 +2,7 @@ import FlexColumn from '@/components/ui/FlexColumn';
import Label from '@/components/ui/Label'; import Label from '@/components/ui/Label';
import TextArea from '@/components/ui/TextArea'; import TextArea from '@/components/ui/TextArea';
import TextInput from '@/components/ui/TextInput'; import TextInput from '@/components/ui/TextInput';
import AnimateFade from '@/components/wrap/AnimateFade';
import { IOperationSchema, OperationID } from '@/models/oss'; import { IOperationSchema, OperationID } from '@/models/oss';
import PickMultiOperation from '../../components/select/PickMultiOperation'; import PickMultiOperation from '../../components/select/PickMultiOperation';
@ -30,7 +31,7 @@ function TabSynthesisOperation({
setInputs setInputs
}: TabSynthesisOperationProps) { }: TabSynthesisOperationProps) {
return ( return (
<div className='cc-fade-in cc-column'> <AnimateFade className='cc-column'>
<TextInput <TextInput
id='operation_title' id='operation_title'
label='Полное название' label='Полное название'
@ -60,7 +61,7 @@ function TabSynthesisOperation({
<Label text={`Выбор аргументов: [ ${inputs.length} ]`} /> <Label text={`Выбор аргументов: [ ${inputs.length} ]`} />
<PickMultiOperation items={oss.items} selected={inputs} setSelected={setInputs} rows={6} /> <PickMultiOperation items={oss.items} selected={inputs} setSelected={setInputs} rows={6} />
</FlexColumn> </FlexColumn>
</div> </AnimateFade>
); );
} }

View File

@ -5,6 +5,7 @@ import { useMemo } from 'react';
import PickMultiOperation from '@/components/select/PickMultiOperation'; import PickMultiOperation from '@/components/select/PickMultiOperation';
import FlexColumn from '@/components/ui/FlexColumn'; import FlexColumn from '@/components/ui/FlexColumn';
import Label from '@/components/ui/Label'; import Label from '@/components/ui/Label';
import AnimateFade from '@/components/wrap/AnimateFade';
import { IOperationSchema, OperationID } from '@/models/oss'; import { IOperationSchema, OperationID } from '@/models/oss';
interface TabArgumentsProps { interface TabArgumentsProps {
@ -21,12 +22,12 @@ function TabArguments({ oss, inputs, target, setInputs }: TabArgumentsProps) {
[oss.items, potentialCycle] [oss.items, potentialCycle]
); );
return ( return (
<div className='cc-fade-in cc-column'> <AnimateFade className='cc-column'>
<FlexColumn> <FlexColumn>
<Label text={`Выбор аргументов: [ ${inputs.length} ]`} /> <Label text={`Выбор аргументов: [ ${inputs.length} ]`} />
<PickMultiOperation items={filtered} selected={inputs} setSelected={setInputs} rows={8} /> <PickMultiOperation items={filtered} selected={inputs} setSelected={setInputs} rows={8} />
</FlexColumn> </FlexColumn>
</div> </AnimateFade>
); );
} }

View File

@ -1,5 +1,6 @@
import TextArea from '@/components/ui/TextArea'; import TextArea from '@/components/ui/TextArea';
import TextInput from '@/components/ui/TextInput'; import TextInput from '@/components/ui/TextInput';
import AnimateFade from '@/components/wrap/AnimateFade';
interface TabOperationProps { interface TabOperationProps {
alias: string; alias: string;
@ -12,7 +13,7 @@ interface TabOperationProps {
function TabOperation({ alias, onChangeAlias, title, onChangeTitle, comment, onChangeComment }: TabOperationProps) { function TabOperation({ alias, onChangeAlias, title, onChangeTitle, comment, onChangeComment }: TabOperationProps) {
return ( return (
<div className='cc-fade-in cc-column'> <AnimateFade className='cc-column'>
<TextInput <TextInput
id='operation_title' id='operation_title'
label='Полное название' label='Полное название'
@ -37,7 +38,7 @@ function TabOperation({ alias, onChangeAlias, title, onChangeTitle, comment, onC
onChange={event => onChangeComment(event.target.value)} onChange={event => onChangeComment(event.target.value)}
/> />
</div> </div>
</div> </AnimateFade>
); );
} }

View File

@ -31,23 +31,21 @@ function TabSynthesis({
}: TabSynthesisProps) { }: TabSynthesisProps) {
const { colors } = useConceptOptions(); const { colors } = useConceptOptions();
return ( return (
<DataLoader isLoading={loading} error={error}> <DataLoader id='dlg-synthesis-tab' className='cc-column mt-3' isLoading={loading} error={error}>
<div className='cc-fade-in cc-column mt-3'> <PickSubstitutions
<PickSubstitutions schemas={schemas}
schemas={schemas} prefixID={prefixes.dlg_cst_substitutes_list}
prefixID={prefixes.dlg_cst_substitutes_list} rows={8}
rows={8} substitutions={substitutions}
substitutions={substitutions} setSubstitutions={setSubstitutions}
setSubstitutions={setSubstitutions} suggestions={suggestions}
suggestions={suggestions} />
/> <TextArea
<TextArea disabled
disabled value={validationText}
value={validationText} rows={4}
rows={4} style={{ borderColor: isCorrect ? undefined : colors.fgRed }}
style={{ borderColor: isCorrect ? undefined : colors.fgRed }} />
/>
</div>
</DataLoader> </DataLoader>
); );
} }

View File

@ -6,6 +6,7 @@ import PickConstituenta from '@/components/select/PickConstituenta';
import SelectMultiGrammeme from '@/components/select/SelectMultiGrammeme'; import SelectMultiGrammeme from '@/components/select/SelectMultiGrammeme';
import Label from '@/components/ui/Label'; import Label from '@/components/ui/Label';
import TextInput from '@/components/ui/TextInput'; import TextInput from '@/components/ui/TextInput';
import AnimateFade from '@/components/wrap/AnimateFade';
import { ReferenceType } from '@/models/language'; import { ReferenceType } from '@/models/language';
import { parseEntityReference, parseGrammemes } from '@/models/languageAPI'; import { parseEntityReference, parseGrammemes } from '@/models/languageAPI';
import { CstMatchMode } from '@/models/miscellaneous'; import { CstMatchMode } from '@/models/miscellaneous';
@ -58,7 +59,7 @@ function TabEntityReference({ initial, schema, onChangeValid, onChangeReference
} }
return ( return (
<div className='cc-fade-in cc-column'> <AnimateFade className='cc-column'>
<PickConstituenta <PickConstituenta
id='dlg_reference_entity_picker' id='dlg_reference_entity_picker'
initialFilter={initial.text} initialFilter={initial.text}
@ -107,7 +108,7 @@ function TabEntityReference({ initial, schema, onChangeValid, onChangeReference
onChangeValue={setSelectedGrams} onChangeValue={setSelectedGrams}
/> />
</div> </div>
</div> </AnimateFade>
); );
} }

View File

@ -3,6 +3,7 @@
import { useEffect, useLayoutEffect, useMemo, useState } from 'react'; import { useEffect, useLayoutEffect, useMemo, useState } from 'react';
import TextInput from '@/components/ui/TextInput'; import TextInput from '@/components/ui/TextInput';
import AnimateFade from '@/components/wrap/AnimateFade';
import { ReferenceType } from '@/models/language'; import { ReferenceType } from '@/models/language';
import { parseSyntacticReference } from '@/models/languageAPI'; import { parseSyntacticReference } from '@/models/languageAPI';
@ -43,7 +44,7 @@ function TabSyntacticReference({ initial, onChangeValid, onChangeReference }: Ta
}, [nominal, offset, onChangeValid, onChangeReference]); }, [nominal, offset, onChangeValid, onChangeReference]);
return ( return (
<div className='cc-fade-in flex flex-col gap-2'> <AnimateFade className='flex flex-col gap-2'>
<TextInput <TextInput
id='dlg_reference_offset' id='dlg_reference_offset'
type='number' type='number'
@ -69,7 +70,7 @@ function TabSyntacticReference({ initial, onChangeValid, onChangeReference }: Ta
value={nominal} value={nominal}
onChange={event => setNominal(event.target.value)} onChange={event => setNominal(event.target.value)}
/> />
</div> </AnimateFade>
); );
} }

View File

@ -16,7 +16,7 @@ interface TabConstituentsProps {
function TabConstituents({ schema, error, loading, selected, setSelected }: TabConstituentsProps) { function TabConstituents({ schema, error, loading, selected, setSelected }: TabConstituentsProps) {
return ( return (
<DataLoader isLoading={loading} error={error} hasNoData={!schema}> <DataLoader id='dlg-constituents-tab' isLoading={loading} error={error} hasNoData={!schema}>
{schema ? ( {schema ? (
<PickMultiConstituenta <PickMultiConstituenta
schema={schema} schema={schema}

View File

@ -4,6 +4,7 @@ import { useMemo } from 'react';
import PickSchema from '@/components/select/PickSchema'; import PickSchema from '@/components/select/PickSchema';
import TextInput from '@/components/ui/TextInput'; import TextInput from '@/components/ui/TextInput';
import AnimateFade from '@/components/wrap/AnimateFade';
import { useLibrary } from '@/context/LibraryContext'; import { useLibrary } from '@/context/LibraryContext';
import { LibraryItemID, LibraryItemType } from '@/models/library'; import { LibraryItemID, LibraryItemType } from '@/models/library';
import { IRSForm } from '@/models/rsform'; import { IRSForm } from '@/models/rsform';
@ -21,7 +22,7 @@ function TabSchema({ selected, receiver, setSelected }: TabSchemaProps) {
const sortedItems = useMemo(() => sortItemsForInlineSynthesis(receiver, library.items), [receiver, library.items]); const sortedItems = useMemo(() => sortItemsForInlineSynthesis(receiver, library.items), [receiver, library.items]);
return ( return (
<div className='cc-fade-in flex flex-col'> <AnimateFade className='flex flex-col'>
<PickSchema <PickSchema
id='dlg_schema_picker' // prettier: split lines id='dlg_schema_picker' // prettier: split lines
items={sortedItems} items={sortedItems}
@ -42,7 +43,7 @@ function TabSchema({ selected, receiver, setSelected }: TabSchemaProps) {
dense dense
/> />
</div> </div>
</div> </AnimateFade>
); );
} }

View File

@ -40,7 +40,7 @@ function TabSubstitutions({
const schemas = useMemo(() => [...(source ? [source] : []), ...(receiver ? [receiver] : [])], [source, receiver]); const schemas = useMemo(() => [...(source ? [source] : []), ...(receiver ? [receiver] : [])], [source, receiver]);
return ( return (
<DataLoader isLoading={loading} error={error} hasNoData={!source}> <DataLoader id='dlg-substitutions-tab' className='cc-column' isLoading={loading} error={error} hasNoData={!source}>
<PickSubstitutions <PickSubstitutions
substitutions={substitutions} substitutions={substitutions}
setSubstitutions={setSubstitutions} setSubstitutions={setSubstitutions}

View File

@ -118,7 +118,7 @@ function DlgRelocateConstituents({ oss, hideWindow, initialTarget, onSubmit }: D
onSelectValue={handleSelectDestination} onSelectValue={handleSelectDestination}
/> />
</div> </div>
<DataLoader isLoading={sourceData.loading} error={sourceData.error}> <DataLoader id='dlg-relocate-constituents' isLoading={sourceData.loading} error={sourceData.error}>
{sourceData.schema ? ( {sourceData.schema ? (
<PickMultiConstituenta <PickMultiConstituenta
noBorder noBorder

View File

@ -29,7 +29,7 @@ function useCheckConstituenta({ schema }: { schema?: IRSForm }) {
onError: setError, onError: setError,
onSuccess: parse => { onSuccess: parse => {
setParseData(parse); setParseData(parse);
onSuccess?.(parse); if (onSuccess) onSuccess(parse);
} }
}); });
} }

View File

@ -13,7 +13,7 @@ function useClickedOutside(enabled: boolean, ref: React.RefObject<HTMLElement>,
function handleClickOutside(event: MouseEvent) { function handleClickOutside(event: MouseEvent) {
assertIsNode(event.target); assertIsNode(event.target);
if (ref.current && !ref.current.contains(event.target)) { if (ref.current && !ref.current.contains(event.target)) {
callback?.(); if (callback) callback();
} }
} }
document.addEventListener('mouseup', handleClickOutside); document.addEventListener('mouseup', handleClickOutside);

View File

@ -19,7 +19,7 @@ function useConceptText() {
setLoading: setProcessing, setLoading: setProcessing,
onError: setError, onError: setError,
onSuccess: data => { onSuccess: data => {
onSuccess?.(data); if (onSuccess) onSuccess(data);
} }
}); });
}, []); }, []);
@ -32,7 +32,7 @@ function useConceptText() {
setLoading: setProcessing, setLoading: setProcessing,
onError: setError, onError: setError,
onSuccess: data => { onSuccess: data => {
onSuccess?.(data); if (onSuccess) onSuccess(data);
} }
}); });
}, []); }, []);
@ -45,7 +45,7 @@ function useConceptText() {
setLoading: setProcessing, setLoading: setProcessing,
onError: setError, onError: setError,
onSuccess: data => { onSuccess: data => {
onSuccess?.(data); if (onSuccess) onSuccess(data);
} }
}); });
}, []); }, []);

View File

@ -42,7 +42,7 @@ function useOssDetails({ target, items }: { target?: string; items: ILibraryItem
}, },
onSuccess: schema => { onSuccess: schema => {
setSchema(schema); setSchema(schema);
callback?.(); if (callback) callback();
} }
}); });
}, },

View File

@ -38,7 +38,7 @@ function useRSFormDetails({ target, version }: { target?: string; version?: stri
}, },
onSuccess: schema => { onSuccess: schema => {
setSchema(schema); setSchema(schema);
callback?.(); if (callback) callback();
} }
}); });
}, },

View File

@ -1,12 +1,15 @@
import AnimateFade from '@/components/wrap/AnimateFade';
import RequireAuth from '@/components/wrap/RequireAuth'; import RequireAuth from '@/components/wrap/RequireAuth';
import FormCreateItem from './FormCreateItem'; import FormCreateItem from './FormCreateItem';
function CreateItemPage() { function CreateItemPage() {
return ( return (
<RequireAuth> <AnimateFade>
<FormCreateItem /> <RequireAuth>
</RequireAuth> <FormCreateItem />
</RequireAuth>
</AnimateFade>
); );
} }

View File

@ -122,10 +122,7 @@ function FormCreateItem() {
}, [itemType]); }, [itemType]);
return ( return (
<form <form className={clsx('cc-column', 'min-w-[30rem] max-w-[30rem] mx-auto', 'px-6 py-3')} onSubmit={handleSubmit}>
className={clsx('cc-fade-in cc-column', 'min-w-[30rem] max-w-[30rem] mx-auto', 'px-6 py-3')}
onSubmit={handleSubmit}
>
<h1 className='select-none'> <h1 className='select-none'>
{itemType == LibraryItemType.RSFORM ? ( {itemType == LibraryItemType.RSFORM ? (
<Overlay position='top-0 right-[0.5rem]'> <Overlay position='top-0 right-[0.5rem]'>

View File

@ -3,6 +3,7 @@
import { useLayoutEffect, useMemo } from 'react'; import { useLayoutEffect, useMemo } from 'react';
import { TransformComponent, TransformWrapper } from 'react-zoom-pan-pinch'; import { TransformComponent, TransformWrapper } from 'react-zoom-pan-pinch';
import AnimateFade from '@/components/wrap/AnimateFade';
import { useConceptOptions } from '@/context/ConceptOptionsContext'; import { useConceptOptions } from '@/context/ConceptOptionsContext';
import { resources } from '@/utils/constants'; import { resources } from '@/utils/constants';
@ -17,13 +18,13 @@ function DatabaseSchemaPage() {
}, [setNoFooter]); }, [setNoFooter]);
return ( return (
<div className='cc-fade-in flex justify-center overflow-hidden' style={{ maxHeight: panelHeight }}> <AnimateFade className='flex justify-center overflow-hidden' style={{ maxHeight: panelHeight }}>
<TransformWrapper> <TransformWrapper>
<TransformComponent> <TransformComponent>
<img alt='Схема базы данных' src={resources.db_schema} className='w-fit h-fit' /> <img alt='Схема базы данных' src={resources.db_schema} className='w-fit h-fit' />
</TransformComponent> </TransformComponent>
</TransformWrapper> </TransformWrapper>
</div> </AnimateFade>
); );
} }

View File

@ -1,5 +1,6 @@
'use client'; 'use client';
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 { toast } from 'react-toastify'; import { toast } from 'react-toastify';
@ -149,7 +150,6 @@ function LibraryPage() {
const viewLocations = useMemo( const viewLocations = useMemo(
() => ( () => (
<ViewSideLocation <ViewSideLocation
isVisible={options.folderMode}
activeLocation={options.location} activeLocation={options.location}
onChangeActiveLocation={options.setLocation} onChangeActiveLocation={options.setLocation}
subfolders={subfolders} subfolders={subfolders}
@ -163,7 +163,6 @@ function LibraryPage() {
options.location, options.location,
library.folders, library.folders,
options.setLocation, options.setLocation,
options.folderMode,
toggleFolderMode, toggleFolderMode,
promptRenameLocation, promptRenameLocation,
toggleSubfolders, toggleSubfolders,
@ -172,7 +171,12 @@ function LibraryPage() {
); );
return ( return (
<DataLoader isLoading={library.loading} error={library.loadingError} hasNoData={library.items.length === 0}> <DataLoader
id='library-page' // prettier: split lines
isLoading={library.loading}
error={library.loadingError}
hasNoData={library.items.length === 0}
>
{showRenameLocation ? ( {showRenameLocation ? (
<DlgChangeLocation <DlgChangeLocation
initial={options.location} initial={options.location}
@ -183,7 +187,7 @@ function LibraryPage() {
<Overlay <Overlay
position={options.noNavigation ? 'top-[0.25rem] right-[3rem]' : 'top-[0.25rem] right-0'} position={options.noNavigation ? 'top-[0.25rem] right-[3rem]' : 'top-[0.25rem] right-0'}
layer='z-tooltip' layer='z-tooltip'
className='cc-animate-position' className='transition-all'
> >
<MiniButton <MiniButton
title='Выгрузить в формате CSV' title='Выгрузить в формате CSV'
@ -214,8 +218,8 @@ function LibraryPage() {
toggleFolderMode={toggleFolderMode} toggleFolderMode={toggleFolderMode}
/> />
<div className='cc-fade-in flex'> <div className='flex'>
{viewLocations} <AnimatePresence initial={false}>{options.folderMode ? viewLocations : null}</AnimatePresence>
{viewLibrary} {viewLibrary}
</div> </div>
</DataLoader> </DataLoader>

View File

@ -161,7 +161,7 @@ function TableLibraryItems({ items, resetQuery, folderMode, toggleFolderMode }:
columns={columns} columns={columns}
data={items} data={items}
headPosition='0' headPosition='0'
className={clsx('text-xs sm:text-sm cc-scroll-y h-fit border-b', { 'border-l': folderMode })} className={clsx('text-xs sm:text-sm cc-scroll-y h-fit', { 'border-l border-b': folderMode })}
style={{ maxHeight: tableHeight }} style={{ maxHeight: tableHeight }}
noDataComponent={ noDataComponent={
<FlexColumn className='dense p-3 items-center min-h-[6rem]'> <FlexColumn className='dense p-3 items-center min-h-[6rem]'>

View File

@ -1,6 +1,7 @@
'use client'; 'use client';
import clsx from 'clsx'; import clsx from 'clsx';
import { motion } from 'framer-motion';
import { useCallback, useMemo } from 'react'; import { useCallback, useMemo } from 'react';
import { LocationIcon, VisibilityIcon } from '@/components/DomainIcons'; import { LocationIcon, VisibilityIcon } from '@/components/DomainIcons';
@ -24,6 +25,7 @@ import { useUsers } from '@/context/UsersContext';
import useDropdown from '@/hooks/useDropdown'; import useDropdown from '@/hooks/useDropdown';
import { LocationHead } from '@/models/library'; import { LocationHead } from '@/models/library';
import { UserID } from '@/models/user'; import { UserID } from '@/models/user';
import { animateDropdownItem } from '@/styling/animations';
import { prefixes } from '@/utils/constants'; import { prefixes } from '@/utils/constants';
import { describeLocationHead, labelLocationHead } from '@/utils/labels'; import { describeLocationHead, labelLocationHead } from '@/utils/labels';
import { tripleToggleColor } from '@/utils/utils'; import { tripleToggleColor } from '@/utils/utils';
@ -161,14 +163,16 @@ function ToolbarSearch({
icon={<IconEditor size='1.25rem' className={tripleToggleColor(isEditor)} />} icon={<IconEditor size='1.25rem' className={tripleToggleColor(isEditor)} />}
onClick={toggleEditor} onClick={toggleEditor}
/> />
<SelectUser <motion.div className='px-1 pb-1' variants={animateDropdownItem}>
noBorder <SelectUser
placeholder='Выберите владельца' noBorder
className='min-w-[15rem] text-sm mx-1 mb-1' placeholder='Выберите владельца'
items={users} className='min-w-[15rem] text-sm'
value={filterUser} items={users}
onSelectValue={onChangeFilterUser} value={filterUser}
/> onSelectValue={onChangeFilterUser}
/>
</motion.div>
</Dropdown> </Dropdown>
</div> </div>

View File

@ -1,4 +1,5 @@
import clsx from 'clsx'; import clsx from 'clsx';
import { motion } from 'framer-motion';
import { useCallback, useMemo } from 'react'; import { useCallback, useMemo } from 'react';
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
@ -14,12 +15,12 @@ import { useLibrary } from '@/context/LibraryContext';
import useWindowSize from '@/hooks/useWindowSize'; import useWindowSize from '@/hooks/useWindowSize';
import { FolderNode, FolderTree } from '@/models/FolderTree'; import { FolderNode, FolderTree } from '@/models/FolderTree';
import { HelpTopic } from '@/models/miscellaneous'; import { HelpTopic } from '@/models/miscellaneous';
import { animateSideMinWidth } from '@/styling/animations';
import { PARAMETER, prefixes } from '@/utils/constants'; import { PARAMETER, prefixes } from '@/utils/constants';
import { information } from '@/utils/labels'; import { information } from '@/utils/labels';
interface ViewSideLocationProps { interface ViewSideLocationProps {
folderTree: FolderTree; folderTree: FolderTree;
isVisible: boolean;
subfolders: boolean; subfolders: boolean;
activeLocation: string; activeLocation: string;
onChangeActiveLocation: (newValue: string) => void; onChangeActiveLocation: (newValue: string) => void;
@ -32,7 +33,6 @@ function ViewSideLocation({
folderTree, folderTree,
activeLocation, activeLocation,
subfolders, subfolders,
isVisible,
onChangeActiveLocation, onChangeActiveLocation,
toggleFolderMode, toggleFolderMode,
toggleSubfolders, toggleSubfolders,
@ -57,6 +57,7 @@ function ViewSideLocation({
return located.length !== 0; return located.length !== 0;
}, [activeLocation, user, items]); }, [activeLocation, user, items]);
const animations = useMemo(() => animateSideMinWidth(windowSize.isSmall ? '10rem' : '15rem'), [windowSize]);
const maxHeight = useMemo(() => calculateHeight('4.5rem'), [calculateHeight]); const maxHeight = useMemo(() => calculateHeight('4.5rem'), [calculateHeight]);
const handleClickFolder = useCallback( const handleClickFolder = useCallback(
@ -76,16 +77,11 @@ function ViewSideLocation({
); );
return ( return (
<div <motion.div
className={clsx('max-w-[10rem] sm:max-w-[15rem]', 'flex flex-col', 'text:xs sm:text-sm', 'select-none')} className={clsx('max-w-[10rem] sm:max-w-[15rem]', 'flex flex-col', 'text:xs sm:text-sm', 'select-none')}
style={{ initial={{ ...animations.initial }}
transitionProperty: 'width, min-width, opacity', animate={{ ...animations.animate }}
transitionDuration: `${PARAMETER.moveDuration}ms`, exit={{ ...animations.exit }}
transitionTimingFunction: 'ease-out',
minWidth: isVisible ? (windowSize.isSmall ? '10rem' : '15rem') : '0',
width: isVisible ? '100%' : '0',
opacity: isVisible ? 1 : 0
}}
> >
<div className='h-[2.08rem] flex justify-between items-center pr-1 pl-[0.125rem]'> <div className='h-[2.08rem] flex justify-between items-center pr-1 pl-[0.125rem]'>
<BadgeHelp <BadgeHelp
@ -121,7 +117,7 @@ function ViewSideLocation({
onClick={handleClickFolder} onClick={handleClickFolder}
style={{ maxHeight: maxHeight }} style={{ maxHeight: maxHeight }}
/> />
</div> </motion.div>
); );
} }

View File

@ -9,6 +9,7 @@ import InfoError, { ErrorData } from '@/components/info/InfoError';
import SubmitButton from '@/components/ui/SubmitButton'; import SubmitButton from '@/components/ui/SubmitButton';
import TextInput from '@/components/ui/TextInput'; import TextInput from '@/components/ui/TextInput';
import TextURL from '@/components/ui/TextURL'; import TextURL from '@/components/ui/TextURL';
import AnimateFade from '@/components/wrap/AnimateFade';
import ExpectedAnonymous from '@/components/wrap/ExpectedAnonymous'; import ExpectedAnonymous from '@/components/wrap/ExpectedAnonymous';
import { useAuth } from '@/context/AuthContext'; import { useAuth } from '@/context/AuthContext';
import { useConceptNavigation } from '@/context/NavigationContext'; import { useConceptNavigation } from '@/context/NavigationContext';
@ -51,42 +52,44 @@ function LoginPage() {
return <ExpectedAnonymous />; return <ExpectedAnonymous />;
} }
return ( return (
<form className={clsx('cc-column cc-fade-in', 'w-[24rem] mx-auto', 'pt-12 pb-6 px-6')} onSubmit={handleSubmit}> <AnimateFade>
<img alt='Концепт Портал' src={resources.logo} className='max-h-[2.5rem] min-w-[2.5rem] mb-3' /> <form className={clsx('cc-column', 'w-[24rem] mx-auto', 'pt-12 pb-6 px-6')} onSubmit={handleSubmit}>
<TextInput <img alt='Концепт Портал' src={resources.logo} className='max-h-[2.5rem] min-w-[2.5rem] mb-3' />
id='username' <TextInput
label='Логин или email' id='username'
autoComplete='username' label='Логин или email'
autoFocus autoComplete='username'
required autoFocus
allowEnter required
spellCheck={false} allowEnter
value={username} spellCheck={false}
onChange={event => setUsername(event.target.value)} value={username}
/> onChange={event => setUsername(event.target.value)}
<TextInput />
id='password' <TextInput
type='password' id='password'
label='Пароль' type='password'
autoComplete='current-password' label='Пароль'
required autoComplete='current-password'
allowEnter required
value={password} allowEnter
onChange={event => setPassword(event.target.value)} value={password}
/> onChange={event => setPassword(event.target.value)}
/>
<SubmitButton <SubmitButton
text='Войти' text='Войти'
className='self-center w-[12rem] mt-3' className='self-center w-[12rem] mt-3'
loading={loading} loading={loading}
disabled={!username || !password} disabled={!username || !password}
/> />
<div className='flex flex-col text-sm'> <div className='flex flex-col text-sm'>
<TextURL text='Восстановить пароль...' href='/restore-password' /> <TextURL text='Восстановить пароль...' href='/restore-password' />
<TextURL text='Нет аккаунта? Зарегистрируйтесь...' href='/signup' /> <TextURL text='Нет аккаунта? Зарегистрируйтесь...' href='/signup' />
</div> </div>
{error ? <ProcessError error={error} /> : null} {error ? <ProcessError error={error} /> : null}
</form> </form>
</AnimateFade>
); );
} }

View File

@ -1,6 +1,7 @@
'use client'; 'use client';
import clsx from 'clsx'; import clsx from 'clsx';
import { motion } from 'framer-motion';
import { useCallback } from 'react'; import { useCallback } from 'react';
import { IconMenuFold, IconMenuUnfold } from '@/components/Icons'; import { IconMenuFold, IconMenuUnfold } from '@/components/Icons';
@ -9,7 +10,8 @@ import SelectTree from '@/components/ui/SelectTree';
import { useConceptOptions } from '@/context/ConceptOptionsContext'; import { useConceptOptions } from '@/context/ConceptOptionsContext';
import useDropdown from '@/hooks/useDropdown'; import useDropdown from '@/hooks/useDropdown';
import { HelpTopic, topicParent } from '@/models/miscellaneous'; import { HelpTopic, topicParent } from '@/models/miscellaneous';
import { PARAMETER, prefixes } from '@/utils/constants'; import { animateSlideLeft } from '@/styling/animations';
import { prefixes } from '@/utils/constants';
import { describeHelpTopic, labelHelpTopic } from '@/utils/labels'; import { describeHelpTopic, labelHelpTopic } from '@/utils/labels';
interface TopicsDropdownProps { interface TopicsDropdownProps {
@ -50,29 +52,30 @@ function TopicsDropdown({ activeTopic, onChangeTopic }: TopicsDropdownProps) {
title='Список тем' title='Список тем'
hideTitle={menu.isOpen} hideTitle={menu.isOpen}
icon={!menu.isOpen ? <IconMenuUnfold size='1.25rem' /> : <IconMenuFold size='1.25rem' />} icon={!menu.isOpen ? <IconMenuUnfold size='1.25rem' /> : <IconMenuFold size='1.25rem' />}
className={clsx('w-[3rem] h-7 rounded-none border-l-0', menu.isOpen && 'border-b-0')} className='w-[3rem] h-7 rounded-none'
onClick={menu.toggle} onClick={menu.toggle}
/> />
<SelectTree <motion.div
items={Object.values(HelpTopic).map(item => item as HelpTopic)}
value={activeTopic}
onChangeValue={handleSelectTopic}
prefix={prefixes.topic_list}
getParent={item => topicParent.get(item) ?? item}
getLabel={labelHelpTopic}
getDescription={describeHelpTopic}
className={clsx( className={clsx(
'border-r border-t rounded-none', // prettier: split-lines 'border divide-y rounded-none', // prettier: split-lines
'cc-scroll-y', 'cc-scroll-y',
'clr-controls' 'clr-controls'
)} )}
style={{ style={{ maxHeight: calculateHeight('4rem + 2px') }}
maxHeight: calculateHeight('4rem + 2px'), initial={false}
transitionProperty: 'clip-path', animate={menu.isOpen ? 'open' : 'closed'}
transitionDuration: `${PARAMETER.moveDuration}ms`, variants={animateSlideLeft}
clipPath: menu.isOpen ? 'inset(0% 0% 0% 0%)' : 'inset(0% 100% 0% 0%)' >
}} <SelectTree
/> items={Object.values(HelpTopic).map(item => item as HelpTopic)}
value={activeTopic}
onChangeValue={handleSelectTopic}
prefix={prefixes.topic_list}
getParent={item => topicParent.get(item) ?? item}
getLabel={labelHelpTopic}
getDescription={describeHelpTopic}
/>
</motion.div>
</div> </div>
); );
} }

View File

@ -27,7 +27,7 @@ function TopicsStatic({ activeTopic, onChangeTopic }: TopicsStaticProps) {
'min-w-[14.5rem] max-w-[14.5rem] sm:min-w-[12.5rem] sm:max-w-[12.5rem] md:min-w-[14.5rem] md:max-w-[14.5rem]', 'min-w-[14.5rem] max-w-[14.5rem] sm:min-w-[12.5rem] sm:max-w-[12.5rem] md:min-w-[14.5rem] md:max-w-[14.5rem]',
'cc-scroll-y', 'cc-scroll-y',
'self-start', 'self-start',
'border-x border-t rounded-none', 'border divide-y rounded-none',
'clr-controls', 'clr-controls',
'text-xs sm:text-sm', 'text-xs sm:text-sm',
'select-none' 'select-none'

View File

@ -1,5 +1,6 @@
'use client'; 'use client';
import AnimateFade from '@/components/wrap/AnimateFade';
import { useConceptOptions } from '@/context/ConceptOptionsContext'; import { useConceptOptions } from '@/context/ConceptOptionsContext';
import { HelpTopic } from '@/models/miscellaneous'; import { HelpTopic } from '@/models/miscellaneous';
import TopicPage from '@/pages/ManualsPage/TopicPage'; import TopicPage from '@/pages/ManualsPage/TopicPage';
@ -11,13 +12,13 @@ interface ViewTopicProps {
function ViewTopic({ topic }: ViewTopicProps) { function ViewTopic({ topic }: ViewTopicProps) {
const { mainHeight } = useConceptOptions(); const { mainHeight } = useConceptOptions();
return ( return (
<div <AnimateFade
key={topic} key={topic}
className='cc-fade-in py-2 px-6 mx-auto sm:mx-0 lg:px-12 overflow-y-auto' className='py-2 px-6 mx-auto sm:mx-0 lg:px-12 overflow-y-auto'
style={{ maxHeight: mainHeight }} style={{ maxHeight: mainHeight }}
> >
<TopicPage topic={topic} /> <TopicPage topic={topic} />
</div> </AnimateFade>
); );
} }

View File

@ -3,6 +3,7 @@
import clsx from 'clsx'; import clsx from 'clsx';
import FlexColumn from '@/components/ui/FlexColumn'; import FlexColumn from '@/components/ui/FlexColumn';
import AnimateFade from '@/components/wrap/AnimateFade';
import { useOSS } from '@/context/OssContext'; import { useOSS } from '@/context/OssContext';
import EditorLibraryItem from '@/pages/RSFormPage/EditorRSFormCard/EditorLibraryItem'; import EditorLibraryItem from '@/pages/RSFormPage/EditorRSFormCard/EditorLibraryItem';
import ToolbarRSFormCard from '@/pages/RSFormPage/EditorRSFormCard/ToolbarRSFormCard'; import ToolbarRSFormCard from '@/pages/RSFormPage/EditorRSFormCard/ToolbarRSFormCard';
@ -46,14 +47,9 @@ function EditorOssCard({ isModified, onDestroy, setIsModified }: EditorOssCardPr
onDestroy={onDestroy} onDestroy={onDestroy}
controller={controller} controller={controller}
/> />
<div <AnimateFade
onKeyDown={handleInput} onKeyDown={handleInput}
className={clsx( className={clsx('md:w-fit md:max-w-fit max-w-[32rem]', 'mx-auto pt-[1.9rem]', 'flex flex-row flex-wrap px-6')}
'cc-fade-in',
'md:w-fit md:max-w-fit max-w-[32rem]',
'mx-auto pt-[1.9rem]',
'flex flex-row flex-wrap px-6'
)}
> >
<FlexColumn className='px-3'> <FlexColumn className='px-3'>
<FormOSS id={globals.library_item_editor} isModified={isModified} setIsModified={setIsModified} /> <FormOSS id={globals.library_item_editor} isModified={isModified} setIsModified={setIsModified} />
@ -61,7 +57,7 @@ function EditorOssCard({ isModified, onDestroy, setIsModified }: EditorOssCardPr
</FlexColumn> </FlexColumn>
{schema ? <OssStats stats={schema.stats} /> : null} {schema ? <OssStats stats={schema.stats} /> : null}
</div> </AnimateFade>
</> </>
); );
} }

View File

@ -18,6 +18,7 @@ import {
import { CProps } from '@/components/props'; import { CProps } from '@/components/props';
import Overlay from '@/components/ui/Overlay'; import Overlay from '@/components/ui/Overlay';
import AnimateFade from '@/components/wrap/AnimateFade';
import { useConceptOptions } from '@/context/ConceptOptionsContext'; import { useConceptOptions } from '@/context/ConceptOptionsContext';
import { useOSS } from '@/context/OssContext'; import { useOSS } from '@/context/OssContext';
import useLocalStorage from '@/hooks/useLocalStorage'; import useLocalStorage from '@/hooks/useLocalStorage';
@ -348,7 +349,7 @@ function OssFlow({ isModified, setIsModified }: OssFlowProps) {
); );
return ( return (
<div tabIndex={-1} onKeyDown={handleKeyDown}> <AnimateFade tabIndex={-1} onKeyDown={handleKeyDown}>
<Overlay position='top-[1.9rem] pt-1 right-1/2 translate-x-1/2' className='rounded-b-2xl cc-blur'> <Overlay position='top-[1.9rem] pt-1 right-1/2 translate-x-1/2' className='rounded-b-2xl cc-blur'>
<ToolbarOssGraph <ToolbarOssGraph
isModified={isModified} isModified={isModified}
@ -380,10 +381,10 @@ function OssFlow({ isModified, setIsModified }: OssFlowProps) {
{...menuProps} {...menuProps}
/> />
) : null} ) : null}
<div className='cc-fade-in relative w-[100vw]' style={{ height: mainHeight, fontFamily: 'Rubik' }}> <div className='relative w-[100vw]' style={{ height: mainHeight, fontFamily: 'Rubik' }}>
{graph} {graph}
</div> </div>
</div> </AnimateFade>
); );
} }

View File

@ -16,9 +16,9 @@ import {
IconShare IconShare
} from '@/components/Icons'; } from '@/components/Icons';
import Button from '@/components/ui/Button'; import Button from '@/components/ui/Button';
import Divider from '@/components/ui/Divider';
import Dropdown from '@/components/ui/Dropdown'; import Dropdown from '@/components/ui/Dropdown';
import DropdownButton from '@/components/ui/DropdownButton'; import DropdownButton from '@/components/ui/DropdownButton';
import DropdownDivider from '@/components/ui/DropdownDivider';
import { useAccessMode } from '@/context/AccessModeContext'; import { useAccessMode } from '@/context/AccessModeContext';
import { useAuth } from '@/context/AuthContext'; import { useAuth } from '@/context/AuthContext';
import { useConceptNavigation } from '@/context/NavigationContext'; import { useConceptNavigation } from '@/context/NavigationContext';
@ -102,7 +102,7 @@ function MenuOssTabs({ onDestroy }: MenuOssTabsProps) {
/> />
) : null} ) : null}
<Divider margins='mx-3 my-1' /> <DropdownDivider margins='mx-3 my-1' />
{user ? ( {user ? (
<DropdownButton <DropdownButton

View File

@ -1,5 +1,6 @@
'use client'; 'use client';
import { AnimatePresence } from 'framer-motion';
import { createContext, useCallback, useContext, useLayoutEffect, useMemo, useState } from 'react'; import { createContext, useCallback, useContext, useLayoutEffect, useMemo, useState } from 'react';
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
@ -217,7 +218,7 @@ export const OssEditState = ({ selected, setSelected, children }: React.PropsWit
} }
}); });
toast.success(information.changesSaved); toast.success(information.changesSaved);
callback?.(); if (callback) callback();
}); });
}, },
[model] [model]
@ -417,7 +418,7 @@ export const OssEditState = ({ selected, setSelected, children }: React.PropsWit
}} }}
> >
{model.schema ? ( {model.schema ? (
<> <AnimatePresence>
{showEditEditors ? ( {showEditEditors ? (
<DlgEditEditors <DlgEditEditors
hideWindow={() => setShowEditEditors(false)} hideWindow={() => setShowEditEditors(false)}
@ -471,7 +472,8 @@ export const OssEditState = ({ selected, setSelected, children }: React.PropsWit
onSubmit={handleRelocateConstituents} onSubmit={handleRelocateConstituents}
/> />
) : null} ) : null}
</> )
</AnimatePresence>
) : null} ) : null}
{children} {children}

View File

@ -12,6 +12,7 @@ import Loader from '@/components/ui/Loader';
import Overlay from '@/components/ui/Overlay'; import Overlay from '@/components/ui/Overlay';
import TabLabel from '@/components/ui/TabLabel'; import TabLabel from '@/components/ui/TabLabel';
import TextURL from '@/components/ui/TextURL'; import TextURL from '@/components/ui/TextURL';
import AnimateFade from '@/components/wrap/AnimateFade';
import { useAuth } from '@/context/AuthContext'; import { useAuth } from '@/context/AuthContext';
import { useConceptOptions } from '@/context/ConceptOptionsContext'; import { useConceptOptions } from '@/context/ConceptOptionsContext';
import { useLibrary } from '@/context/LibraryContext'; import { useLibrary } from '@/context/LibraryContext';
@ -150,10 +151,10 @@ function OssTabs() {
</TabList> </TabList>
</Overlay> </Overlay>
<div className='overflow-x-hidden'> <AnimateFade>
{cardPanel} {cardPanel}
{graphPanel} {graphPanel}
</div> </AnimateFade>
</Tabs> </Tabs>
) : null} ) : null}
</OssEditState> </OssEditState>

View File

@ -65,8 +65,12 @@ function PasswordChangePage() {
return <ProcessError error={error} />; return <ProcessError error={error} />;
} }
return ( return (
<DataLoader isLoading={loading} hasNoData={!isTokenValid}> <DataLoader
<form className={clsx('cc-fade-in cc-column', 'w-[24rem] mx-auto', 'px-6 mt-3')} onSubmit={handleSubmit}> id='password-change-page' //
isLoading={loading}
hasNoData={!isTokenValid}
>
<form className={clsx('cc-column', 'w-[24rem] mx-auto', 'px-6 mt-3')} onSubmit={handleSubmit}>
<TextInput <TextInput
id='new_password' id='new_password'
type='password' type='password'

View File

@ -1,6 +1,7 @@
'use client'; 'use client';
import clsx from 'clsx'; import clsx from 'clsx';
import { AnimatePresence } from 'framer-motion';
import { useMemo, useState } from 'react'; import { useMemo, useState } from 'react';
import { useConceptOptions } from '@/context/ConceptOptionsContext'; import { useConceptOptions } from '@/context/ConceptOptionsContext';
@ -87,37 +88,39 @@ function EditorConstituenta({ activeCst, isModified, setIsModified, onOpenEdit }
onReset={() => setToggleReset(prev => !prev)} onReset={() => setToggleReset(prev => !prev)}
onToggleList={() => setShowList(prev => !prev)} onToggleList={() => setShowList(prev => !prev)}
/> />
<div <div className='pt-[1.9rem] overflow-y-auto min-h-[20rem]' style={{ maxHeight: mainHeight }}>
tabIndex={-1} <div
className={clsx( tabIndex={-1}
'cc-fade-in', className={clsx(
'min-h-[20rem] max-w-[95rem] mx-auto', 'max-w-[95rem] mx-auto', // prettier: split lines
'flex pt-[1.9rem]', 'flex',
'overflow-y-auto overflow-x-clip', { 'flex-col md:items-center': isNarrow }
{ 'flex-col md:items-center': isNarrow } )}
)} onKeyDown={handleInput}
style={{ maxHeight: mainHeight }} >
onKeyDown={handleInput} <FormConstituenta
> disabled={disabled}
<FormConstituenta id={globals.constituenta_editor}
disabled={disabled} state={activeCst}
id={globals.constituenta_editor} isModified={isModified}
state={activeCst} toggleReset={toggleReset}
isModified={isModified} setIsModified={setIsModified}
toggleReset={toggleReset} onEditTerm={controller.editTermForms}
setIsModified={setIsModified} onRename={controller.renameCst}
onEditTerm={controller.editTermForms} onOpenEdit={onOpenEdit}
onRename={controller.renameCst} />
onOpenEdit={onOpenEdit} <AnimatePresence initial={false}>
/> {showList ? (
<ViewConstituents <ViewConstituents
isMounted={showList} schema={controller.schema}
schema={controller.schema} expression={activeCst?.definition_formal ?? ''}
expression={activeCst?.definition_formal ?? ''} isBottom={isNarrow}
isBottom={isNarrow} activeCst={activeCst}
activeCst={activeCst} onOpenEdit={onOpenEdit}
onOpenEdit={onOpenEdit} />
/> ) : null}
</AnimatePresence>
</div>
</div> </div>
</> </>
); );

View File

@ -1,6 +1,7 @@
'use client'; 'use client';
import clsx from 'clsx'; import clsx from 'clsx';
import { AnimatePresence } from 'framer-motion';
import { useEffect, useLayoutEffect, useMemo, useState } from 'react'; import { useEffect, useLayoutEffect, useMemo, useState } from 'react';
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
@ -11,6 +12,7 @@ import Indicator from '@/components/ui/Indicator';
import Overlay from '@/components/ui/Overlay'; import Overlay from '@/components/ui/Overlay';
import SubmitButton from '@/components/ui/SubmitButton'; import SubmitButton from '@/components/ui/SubmitButton';
import TextArea from '@/components/ui/TextArea'; import TextArea from '@/components/ui/TextArea';
import AnimateFade from '@/components/wrap/AnimateFade';
import { useRSForm } from '@/context/RSFormContext'; import { useRSForm } from '@/context/RSFormContext';
import DlgShowTypeGraph from '@/dialogs/DlgShowTypeGraph'; import DlgShowTypeGraph from '@/dialogs/DlgShowTypeGraph';
import { ConstituentaID, CstType, IConstituenta, ICstUpdateData } from '@/models/rsform'; import { ConstituentaID, CstType, IConstituenta, ICstUpdateData } from '@/models/rsform';
@ -153,10 +155,12 @@ function FormConstituenta({
} }
return ( return (
<div className='mx-0 md:mx-auto pt-[2rem] xs:pt-0'> <AnimateFade className='mx-0 md:mx-auto pt-[2rem] xs:pt-0'>
{showTypification && state ? ( <AnimatePresence>
<DlgShowTypeGraph items={typeInfo ? [typeInfo] : []} hideWindow={() => setShowTypification(false)} /> {showTypification && state ? (
) : null} <DlgShowTypeGraph items={typeInfo ? [typeInfo] : []} hideWindow={() => setShowTypification(false)} />
) : null}
</AnimatePresence>
{state ? ( {state ? (
<ControlsOverlay <ControlsOverlay
disabled={disabled} disabled={disabled}
@ -197,8 +201,12 @@ function FormConstituenta({
/> />
) : null} ) : null}
{state ? ( {state ? (
<> <AnimatePresence>
{!!state.definition_formal || !isElementary ? ( <AnimateFade
key='cst_expression_fade'
hideContent={!state.definition_formal && isElementary}
style={{ willChange: 'auto' }}
>
<EditorRSExpression <EditorRSExpression
id='cst_expression' id='cst_expression'
label={ label={
@ -221,8 +229,8 @@ function FormConstituenta({
onOpenEdit={onOpenEdit} onOpenEdit={onOpenEdit}
onShowTypeGraph={handleTypeGraph} onShowTypeGraph={handleTypeGraph}
/> />
) : null} </AnimateFade>
{!!state.definition_raw || !isElementary ? ( <AnimateFade key='cst_definition_fade' hideContent={!state.definition_raw && isElementary}>
<RefsInput <RefsInput
id='cst_definition' id='cst_definition'
label='Текстовое определение' label='Текстовое определение'
@ -237,9 +245,8 @@ function FormConstituenta({
disabled={disabled} disabled={disabled}
onChange={newValue => setTextDefinition(newValue)} onChange={newValue => setTextDefinition(newValue)}
/> />
) : null} </AnimateFade>
<AnimateFade key='cst_convention_fade' hideContent={!showConvention}>
{showConvention ? (
<TextArea <TextArea
id='cst_convention' id='cst_convention'
fitContent fitContent
@ -251,9 +258,8 @@ function FormConstituenta({
disabled={disabled || (isBasic && state.is_inherited)} disabled={disabled || (isBasic && state.is_inherited)}
onChange={event => setConvention(event.target.value)} onChange={event => setConvention(event.target.value)}
/> />
) : null} </AnimateFade>
<AnimateFade key='cst_convention_button' hideContent={showConvention || (disabled && !processing)}>
{!showConvention && (!disabled || processing) ? (
<button <button
key='cst_disable_comment' key='cst_disable_comment'
id='cst_disable_comment' id='cst_disable_comment'
@ -264,7 +270,7 @@ function FormConstituenta({
> >
Добавить комментарий Добавить комментарий
</button> </button>
) : null} </AnimateFade>
{!disabled || processing ? ( {!disabled || processing ? (
<div className='mx-auto flex'> <div className='mx-auto flex'>
@ -291,10 +297,10 @@ function FormConstituenta({
</Overlay> </Overlay>
</div> </div>
) : null} ) : null}
</> </AnimatePresence>
) : null} ) : null}
</form> </form>
</div> </AnimateFade>
); );
} }

View File

@ -51,7 +51,7 @@ function ToolbarConstituenta({
return ( return (
<Overlay <Overlay
position='cc-tab-tools right-1/2 translate-x-1/2 xs:right-4 xs:translate-x-0 md:right-1/2 md:translate-x-1/2' position='cc-tab-tools right-1/2 translate-x-1/2 xs:right-4 xs:translate-x-0 md:right-1/2 md:translate-x-1/2'
className='cc-icons cc-animate-position outline-none cc-blur px-1 rounded-b-2xl' className='cc-icons outline-none transition-all duration-500 cc-blur px-1 rounded-b-2xl'
> >
{controller.schema && controller.schema?.oss.length > 0 ? ( {controller.schema && controller.schema?.oss.length > 0 ? (
<MiniSelectorOSS <MiniSelectorOSS

View File

@ -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, useMemo, useRef, useState } from 'react'; import { useCallback, useLayoutEffect, useMemo, useRef, useState } from 'react';
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
@ -95,7 +96,7 @@ function EditorRSExpression({
args: parse.args args: parse.args
}) })
); );
callback?.(parse); if (callback) callback(parse);
}); });
} }
@ -160,10 +161,12 @@ function EditorRSExpression({
); );
return ( return (
<div className='cc-fade-in'> <div>
{showAST ? ( <AnimatePresence>
<DlgShowAST expression={expression} syntaxTree={syntaxTree} hideWindow={() => setShowAST(false)} /> {showAST ? (
) : null} <DlgShowAST expression={expression} syntaxTree={syntaxTree} hideWindow={() => setShowAST(false)} />
) : null}
</AnimatePresence>
<ToolbarRSExpression <ToolbarRSExpression
disabled={disabled} disabled={disabled}

View File

@ -1,5 +1,8 @@
import { motion } from 'framer-motion';
import { IExpressionParse, IRSErrorDescription } from '@/models/rslang'; import { IExpressionParse, IRSErrorDescription } from '@/models/rslang';
import { getRSErrorPrefix } from '@/models/rslangAPI'; import { getRSErrorPrefix } from '@/models/rslangAPI';
import { animateParseResults } from '@/styling/animations';
import { describeRSError } from '@/utils/labels'; import { describeRSError } from '@/utils/labels';
interface ParsingResultProps { interface ParsingResultProps {
@ -14,16 +17,12 @@ function ParsingResult({ isOpen, data, disabled, onShowError }: ParsingResultPro
const warningsCount = data ? data.errors.length - errorCount : 0; const warningsCount = data ? data.errors.length - errorCount : 0;
return ( return (
<div <motion.div
tabIndex={-1} tabIndex={-1}
className='text-sm border dense cc-scroll-y transition-all duration-300' className='text-sm border dense cc-scroll-y'
style={{ initial={false}
clipPath: isOpen ? 'inset(0% 0% 0% 0%)' : 'inset(0% 0% 100% 0%)', animate={isOpen ? 'open' : 'closed'}
marginTop: isOpen ? '0.75rem' : '0rem', variants={animateParseResults}
padding: isOpen ? '0.25rem 0.5rem 0.25rem 0.5rem' : '0rem 0rem 0rem 0rem',
borderWidth: isOpen ? '1px' : '0px',
height: isOpen ? '4.5rem' : '0rem'
}}
> >
<p> <p>
Ошибок: <b>{errorCount}</b> | Предупреждений: <b>{warningsCount}</b> Ошибок: <b>{errorCount}</b> | Предупреждений: <b>{warningsCount}</b>
@ -43,7 +42,7 @@ function ParsingResult({ isOpen, data, disabled, onShowError }: ParsingResultPro
</p> </p>
); );
})} })}
</div> </motion.div>
); );
} }

View File

@ -1,7 +1,9 @@
import clsx from 'clsx'; import clsx from 'clsx';
import { motion } from 'framer-motion';
import { TokenID } from '@/models/rslang'; import { TokenID } from '@/models/rslang';
import { PARAMETER, prefixes } from '@/utils/constants'; import { animateRSControl } from '@/styling/animations';
import { prefixes } from '@/utils/constants';
import RSLocalButton from './RSLocalButton'; import RSLocalButton from './RSLocalButton';
import RSTokenButton from './RSTokenButton'; import RSTokenButton from './RSTokenButton';
@ -88,7 +90,7 @@ interface RSEditorControlsProps {
function RSEditorControls({ isOpen, disabled, onEdit }: RSEditorControlsProps) { function RSEditorControls({ isOpen, disabled, onEdit }: RSEditorControlsProps) {
return ( return (
<div <motion.div
className={clsx( className={clsx(
'max-w-[28rem] min-w-[28rem] xs:max-w-[38.5rem] xs:min-w-[38.5rem] sm:max-w-[40rem] sm:min-w-[40rem] md:max-w-fit mx-1 sm:mx-0', 'max-w-[28rem] min-w-[28rem] xs:max-w-[38.5rem] xs:min-w-[38.5rem] sm:max-w-[40rem] sm:min-w-[40rem] md:max-w-fit mx-1 sm:mx-0',
'flex-wrap', 'flex-wrap',
@ -96,13 +98,9 @@ function RSEditorControls({ isOpen, disabled, onEdit }: RSEditorControlsProps) {
'text-xs md:text-sm', 'text-xs md:text-sm',
'select-none' 'select-none'
)} )}
style={{ initial={false}
transitionProperty: 'clipPath, height', animate={isOpen ? 'open' : 'closed'}
transitionDuration: `${PARAMETER.moveDuration}ms`, variants={animateRSControl}
clipPath: isOpen ? 'inset(0% 0% 0% 0%)' : 'inset(0% 0% 100% 0%)',
marginTop: isOpen ? '0.25rem' : '0rem',
height: isOpen ? 'max-content' : '0rem'
}}
> >
{MAIN_FIRST_ROW.map(token => ( {MAIN_FIRST_ROW.map(token => (
<RSTokenButton key={`${prefixes.rsedit_btn}${token}`} token={token} onInsert={onEdit} disabled={disabled} /> <RSTokenButton key={`${prefixes.rsedit_btn}${token}`} token={token} onInsert={onEdit} disabled={disabled} />
@ -145,7 +143,7 @@ function RSEditorControls({ isOpen, disabled, onEdit }: RSEditorControlsProps) {
disabled={disabled} disabled={disabled}
/> />
))} ))}
</div> </motion.div>
); );
} }

View File

@ -1,6 +1,7 @@
'use client'; 'use client';
import clsx from 'clsx'; import clsx from 'clsx';
import { AnimatePresence } from 'framer-motion';
import { useMemo } from 'react'; import { useMemo } from 'react';
import { StatusIcon } from '@/components/DomainIcons'; import { StatusIcon } from '@/components/DomainIcons';
@ -40,30 +41,29 @@ function StatusBar({ isModified, processing, activeCst, parseData, onAnalyze }:
tabIndex={0} tabIndex={0}
className={clsx( className={clsx(
'w-[10rem] h-[1.75rem]', 'w-[10rem] h-[1.75rem]',
'px-2 flex items-center justify-center', 'px-2 flex items-center justify-center gap-2',
'border', 'border',
'select-none', 'select-none',
'cursor-pointer', 'cursor-pointer',
'focus-frame', 'focus-frame',
'transition-colors duration-500' 'duration-500 transition-colors'
)} )}
style={{ backgroundColor: processing ? colors.bgDefault : colorStatusBar(status, colors) }} style={{ backgroundColor: processing ? colors.bgDefault : colorStatusBar(status, colors) }}
data-tooltip-id={globals.tooltip} data-tooltip-id={globals.tooltip}
data-tooltip-html={prepareTooltip('Проверить определение', 'Ctrl + Q')} data-tooltip-html={prepareTooltip('Проверить определение', 'Ctrl + Q')}
onClick={onAnalyze} onClick={onAnalyze}
> >
{processing ? ( <AnimatePresence mode='wait'>
<div className='cc-fade-in'> {processing ? <Loader key='status-loader' scale={3} /> : null}
{' '} {!processing ? (
<Loader scale={3} /> <>
</div> <StatusIcon key='status-icon' size='1rem' value={status} />
) : null} <span key='status-text' className='pb-[0.125rem] font-controls pr-2'>
{!processing ? ( {labelExpressionStatus(status)}
<div className='cc-fade-in flex items-center gap-2'> </span>
<StatusIcon size='1rem' value={status} /> </>
<span className='pb-[0.125rem] font-controls pr-2'>{labelExpressionStatus(status)}</span> ) : null}
</div> </AnimatePresence>
) : null}
</div> </div>
); );
} }

View File

@ -3,6 +3,7 @@
import clsx from 'clsx'; import clsx from 'clsx';
import FlexColumn from '@/components/ui/FlexColumn'; import FlexColumn from '@/components/ui/FlexColumn';
import AnimateFade from '@/components/wrap/AnimateFade';
import { useRSForm } from '@/context/RSFormContext'; import { useRSForm } from '@/context/RSFormContext';
import { globals } from '@/utils/constants'; import { globals } from '@/utils/constants';
@ -46,13 +47,9 @@ function EditorRSFormCard({ isModified, onDestroy, setIsModified }: EditorRSForm
onDestroy={onDestroy} onDestroy={onDestroy}
controller={controller} controller={controller}
/> />
<div <AnimateFade
onKeyDown={handleInput} onKeyDown={handleInput}
className={clsx( className={clsx('md:w-fit md:max-w-fit max-w-[32rem] mx-auto', 'flex flex-row flex-wrap px-6 pt-[1.9rem]')}
'cc-fade-in',
'md:w-fit md:max-w-fit max-w-[32rem] mx-auto',
'flex flex-row flex-wrap px-6 pt-[1.9rem]'
)}
> >
<FlexColumn className='flex-shrink'> <FlexColumn className='flex-shrink'>
<FormRSForm id={globals.library_item_editor} isModified={isModified} setIsModified={setIsModified} /> <FormRSForm id={globals.library_item_editor} isModified={isModified} setIsModified={setIsModified} />
@ -60,7 +57,7 @@ function EditorRSFormCard({ isModified, onDestroy, setIsModified }: EditorRSForm
</FlexColumn> </FlexColumn>
{model.schema ? <RSFormStats stats={model.schema.stats} isArchive={model.isArchive} /> : null} {model.schema ? <RSFormStats stats={model.schema.stats} isArchive={model.isArchive} /> : null}
</div> </AnimateFade>
</> </>
); );
} }

View File

@ -9,6 +9,7 @@ import { type RowSelectionState } from '@/components/ui/DataTable';
import MiniButton from '@/components/ui/MiniButton'; import MiniButton from '@/components/ui/MiniButton';
import Overlay from '@/components/ui/Overlay'; import Overlay from '@/components/ui/Overlay';
import SearchBar from '@/components/ui/SearchBar'; import SearchBar from '@/components/ui/SearchBar';
import AnimateFade from '@/components/wrap/AnimateFade';
import { useConceptOptions } from '@/context/ConceptOptionsContext'; import { useConceptOptions } from '@/context/ConceptOptionsContext';
import { CstMatchMode } from '@/models/miscellaneous'; import { CstMatchMode } from '@/models/miscellaneous';
import { ConstituentaID, CstType, IConstituenta } from '@/models/rsform'; import { ConstituentaID, CstType, IConstituenta } from '@/models/rsform';
@ -141,7 +142,7 @@ function EditorRSList({ onOpenEdit }: EditorRSListProps) {
return ( return (
<> <>
{controller.isContentEditable ? <ToolbarRSList /> : null} {controller.isContentEditable ? <ToolbarRSList /> : null}
<div tabIndex={-1} onKeyDown={handleKeyDown} className='cc-fade-in pt-[1.9rem]'> <AnimateFade tabIndex={-1} onKeyDown={handleKeyDown} className='pt-[1.9rem]'>
{controller.isContentEditable ? ( {controller.isContentEditable ? (
<div className='flex items-center border-b'> <div className='flex items-center border-b'>
<div className='px-2'> <div className='px-2'>
@ -174,7 +175,7 @@ function EditorRSList({ onOpenEdit }: EditorRSListProps) {
onEdit={onOpenEdit} onEdit={onOpenEdit}
onCreateNew={() => controller.createCst(undefined, false)} onCreateNew={() => controller.createCst(undefined, false)}
/> />
</div> </AnimateFade>
</> </>
); );
} }

View File

@ -29,7 +29,7 @@ function ToolbarRSList() {
return ( return (
<Overlay <Overlay
position='cc-tab-tools right-4 translate-x-0 md:right-1/2 md:translate-x-1/2' position='cc-tab-tools right-4 translate-x-0 md:right-1/2 md:translate-x-1/2'
className='cc-icons cc-animate-position items-start outline-none' className='cc-icons items-start outline-none transition-all duration-500'
> >
{controller.schema && controller.schema?.oss.length > 0 ? ( {controller.schema && controller.schema?.oss.length > 0 ? (
<MiniSelectorOSS <MiniSelectorOSS

View File

@ -1,6 +1,7 @@
'use client'; 'use client';
import clsx from 'clsx'; import clsx from 'clsx';
import { AnimatePresence } from 'framer-motion';
import { toPng } from 'html-to-image'; import { toPng } from 'html-to-image';
import { useCallback, useLayoutEffect, useMemo, useState } from 'react'; import { useCallback, useLayoutEffect, useMemo, useState } from 'react';
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
@ -24,6 +25,7 @@ import SelectedCounter from '@/components/info/SelectedCounter';
import { CProps } from '@/components/props'; import { CProps } from '@/components/props';
import ToolbarGraphSelection from '@/components/select/ToolbarGraphSelection'; import ToolbarGraphSelection from '@/components/select/ToolbarGraphSelection';
import Overlay from '@/components/ui/Overlay'; import Overlay from '@/components/ui/Overlay';
import AnimateFade from '@/components/wrap/AnimateFade';
import { useConceptOptions } from '@/context/ConceptOptionsContext'; import { useConceptOptions } from '@/context/ConceptOptionsContext';
import DlgGraphParams from '@/dialogs/DlgGraphParams'; import DlgGraphParams from '@/dialogs/DlgGraphParams';
import useLocalStorage from '@/hooks/useLocalStorage'; import useLocalStorage from '@/hooks/useLocalStorage';
@ -374,68 +376,70 @@ function TGFlow({ onOpenEdit }: TGFlowProps) {
return ( return (
<> <>
{showParamsDialog ? ( <AnimatePresence>
<DlgGraphParams {showParamsDialog ? (
hideWindow={() => setShowParamsDialog(false)} <DlgGraphParams
initial={filterParams} hideWindow={() => setShowParamsDialog(false)}
onConfirm={handleChangeParams} initial={filterParams}
/> onConfirm={handleChangeParams}
) : null}
<Overlay position='cc-tab-tools' className='flex flex-col items-center rounded-b-2xl cc-blur'>
<ToolbarTermGraph
noText={filterParams.noText}
foldDerived={filterParams.foldDerived}
showParamsDialog={() => setShowParamsDialog(true)}
onCreate={handleCreateCst}
onDelete={handleDeleteCst}
onFitView={() => setToggleResetView(prev => !prev)}
onSaveImage={handleSaveImage}
toggleFoldDerived={handleFoldDerived}
toggleNoText={() =>
setFilterParams(prev => ({
...prev,
noText: !prev.noText
}))
}
/>
{!focusCst ? (
<ToolbarGraphSelection
graph={controller.schema!.graph}
isCore={cstID => isBasicConcept(controller.schema?.cstByID.get(cstID)?.cst_type)}
isOwned={
controller.schema && controller.schema.inheritance.length > 0
? cstID => !controller.schema!.cstByID.get(cstID)?.is_inherited
: undefined
}
selected={controller.selected}
setSelected={handleSetSelected}
emptySelection={controller.selected.length === 0}
/> />
) : null} ) : null}
{focusCst ? ( </AnimatePresence>
<ToolbarFocusedCst
center={focusCst} <AnimateFade tabIndex={-1} onKeyDown={handleKeyDown}>
reset={() => handleSetFocus(undefined)} <Overlay position='cc-tab-tools' className='flex flex-col items-center rounded-b-2xl cc-blur'>
showInputs={filterParams.focusShowInputs} <ToolbarTermGraph
showOutputs={filterParams.focusShowOutputs} noText={filterParams.noText}
toggleShowInputs={() => foldDerived={filterParams.foldDerived}
showParamsDialog={() => setShowParamsDialog(true)}
onCreate={handleCreateCst}
onDelete={handleDeleteCst}
onFitView={() => setToggleResetView(prev => !prev)}
onSaveImage={handleSaveImage}
toggleFoldDerived={handleFoldDerived}
toggleNoText={() =>
setFilterParams(prev => ({ setFilterParams(prev => ({
...prev, ...prev,
focusShowInputs: !prev.focusShowInputs noText: !prev.noText
}))
}
toggleShowOutputs={() =>
setFilterParams(prev => ({
...prev,
focusShowOutputs: !prev.focusShowOutputs
})) }))
} }
/> />
) : null} {!focusCst ? (
</Overlay> <ToolbarGraphSelection
graph={controller.schema!.graph}
isCore={cstID => isBasicConcept(controller.schema?.cstByID.get(cstID)?.cst_type)}
isOwned={
controller.schema && controller.schema.inheritance.length > 0
? cstID => !controller.schema!.cstByID.get(cstID)?.is_inherited
: undefined
}
selected={controller.selected}
setSelected={handleSetSelected}
emptySelection={controller.selected.length === 0}
/>
) : null}
{focusCst ? (
<ToolbarFocusedCst
center={focusCst}
reset={() => handleSetFocus(undefined)}
showInputs={filterParams.focusShowInputs}
showOutputs={filterParams.focusShowOutputs}
toggleShowInputs={() =>
setFilterParams(prev => ({
...prev,
focusShowInputs: !prev.focusShowInputs
}))
}
toggleShowOutputs={() =>
setFilterParams(prev => ({
...prev,
focusShowOutputs: !prev.focusShowOutputs
}))
}
/>
) : null}
</Overlay>
<div className='cc-fade-in' tabIndex={-1} onKeyDown={handleKeyDown}>
<SelectedCounter <SelectedCounter
hideZero hideZero
totalCount={controller.schema?.stats?.count_all ?? 0} totalCount={controller.schema?.stats?.count_all ?? 0}
@ -466,9 +470,8 @@ function TGFlow({ onOpenEdit }: TGFlowProps) {
{viewHidden} {viewHidden}
</div> </div>
</Overlay> </Overlay>
{graph} {graph}
</div> </AnimateFade>
</> </>
); );
} }

View File

@ -1,6 +1,7 @@
'use client'; 'use client';
import clsx from 'clsx'; import clsx from 'clsx';
import { motion } from 'framer-motion';
import { useCallback, useMemo } from 'react'; import { useCallback, useMemo } from 'react';
import { IconDropArrow, IconDropArrowUp } from '@/components/Icons'; import { IconDropArrow, IconDropArrowUp } from '@/components/Icons';
@ -13,8 +14,9 @@ import useLocalStorage from '@/hooks/useLocalStorage';
import useWindowSize from '@/hooks/useWindowSize'; import useWindowSize from '@/hooks/useWindowSize';
import { GraphColoring } from '@/models/miscellaneous'; import { GraphColoring } from '@/models/miscellaneous';
import { ConstituentaID, IRSForm } from '@/models/rsform'; import { ConstituentaID, IRSForm } from '@/models/rsform';
import { animateDropdown, animateHiddenHeader } from '@/styling/animations';
import { colorBgGraphNode } from '@/styling/color'; import { colorBgGraphNode } from '@/styling/color';
import { PARAMETER, prefixes, storage } from '@/utils/constants'; import { prefixes, storage } from '@/utils/constants';
interface ViewHiddenProps { interface ViewHiddenProps {
items: ConstituentaID[]; items: ConstituentaID[];
@ -58,35 +60,39 @@ function ViewHidden({ items, selected, toggleSelection, setFocus, schema, colori
onClick={() => setIsFolded(prev => !prev)} onClick={() => setIsFolded(prev => !prev)}
/> />
</Overlay> </Overlay>
<div className={clsx('pt-2 clr-input border-x pb-2', { 'border-b rounded-b-md': isFolded })}>
<div
className='w-fit select-none'
style={{
transitionProperty: 'margin, translate',
transitionDuration: `${PARAMETER.fastAnimation}ms`,
transitionTimingFunction: 'ease-out',
marginLeft: isFolded ? '0.75rem' : '0',
translate: isFolded ? '0' : 'calc(6.5rem - 50%)'
}}
>
{`Скрытые [${localSelected.length} | ${items.length}]`}
</div>
</div>
<div <div
className={clsx( className={clsx(
'flex flex-wrap justify-center gap-2 py-2 mt-[-0.5rem]', 'pt-2', //
'border-x',
'clr-input',
'select-none',
{
'pb-2 border-b': isFolded
}
)}
>
<motion.div
className='w-fit'
animate={!isFolded ? 'open' : 'closed'}
variants={animateHiddenHeader}
initial={false}
>
{`Скрытые [${localSelected.length} | ${items.length}]`}
</motion.div>
</div>
<motion.div
className={clsx(
'flex flex-wrap justify-center gap-2 py-2',
'border-x border-b rounded-b-md',
'clr-input',
'text-sm', 'text-sm',
'clr-input border-x border-b rounded-b-md',
'cc-scroll-y' 'cc-scroll-y'
)} )}
style={{ style={{ maxHeight: calculateHeight(windowSize.isSmall ? '10.4rem + 2px' : '12.5rem + 2px') }}
maxHeight: calculateHeight(windowSize.isSmall ? '10.4rem + 2px' : '12.5rem + 2px'), initial={false}
transitionProperty: 'clip-path', animate={!isFolded ? 'open' : 'closed'}
transitionDuration: `${PARAMETER.fastAnimation}ms`, variants={animateDropdown}
transitionTimingFunction: 'ease-out',
clipPath: isFolded ? 'inset(10% 0% 90% 0%)' : 'inset(0% 0% 0% 0%)'
}}
> >
{items.map(cstID => { {items.map(cstID => {
const cst = schema.cstByID.get(cstID)!; const cst = schema.cstByID.get(cstID)!;
@ -118,7 +124,7 @@ function ViewHidden({ items, selected, toggleSelection, setFocus, schema, colori
</div> </div>
); );
})} })}
</div> </motion.div>
</div> </div>
); );
} }

View File

@ -27,9 +27,9 @@ import {
IconUpload IconUpload
} from '@/components/Icons'; } from '@/components/Icons';
import Button from '@/components/ui/Button'; import Button from '@/components/ui/Button';
import Divider from '@/components/ui/Divider';
import Dropdown from '@/components/ui/Dropdown'; import Dropdown from '@/components/ui/Dropdown';
import DropdownButton from '@/components/ui/DropdownButton'; import DropdownButton from '@/components/ui/DropdownButton';
import DropdownDivider from '@/components/ui/DropdownDivider';
import { useAccessMode } from '@/context/AccessModeContext'; import { useAccessMode } from '@/context/AccessModeContext';
import { useAuth } from '@/context/AuthContext'; import { useAuth } from '@/context/AuthContext';
import { useGlobalOss } from '@/context/GlobalOssContext'; import { useGlobalOss } from '@/context/GlobalOssContext';
@ -191,7 +191,7 @@ function MenuRSTabs({ onDestroy }: MenuRSTabsProps) {
/> />
) : null} ) : null}
<Divider margins='mx-3 my-1' /> <DropdownDivider margins='mx-3 my-1' />
{user ? ( {user ? (
<DropdownButton <DropdownButton
@ -244,7 +244,7 @@ function MenuRSTabs({ onDestroy }: MenuRSTabsProps) {
onClick={handleInlineSynthesis} onClick={handleInlineSynthesis}
/> />
<Divider margins='mx-3 my-1' /> <DropdownDivider margins='mx-3 my-1' />
<DropdownButton <DropdownButton
text='Упорядочить список' text='Упорядочить список'

View File

@ -1,5 +1,6 @@
'use client'; 'use client';
import { AnimatePresence } from 'framer-motion';
import fileDownload from 'js-file-download'; import fileDownload from 'js-file-download';
import { createContext, useCallback, useContext, useLayoutEffect, useMemo, useState } from 'react'; import { createContext, useCallback, useContext, useLayoutEffect, useMemo, useState } from 'react';
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
@ -325,7 +326,7 @@ export const RSEditState = ({
model.cstDelete(data, () => { model.cstDelete(data, () => {
toast.success(information.constituentsDestroyed(deletedNames)); toast.success(information.constituentsDestroyed(deletedNames));
setSelected(nextActive ? [nextActive] : []); setSelected(nextActive ? [nextActive] : []);
onDeleteCst?.(nextActive); if (onDeleteCst) onDeleteCst(nextActive);
}); });
}, },
[model, activeCst, onDeleteCst, setSelected] [model, activeCst, onDeleteCst, setSelected]
@ -683,7 +684,7 @@ export const RSEditState = ({
}} }}
> >
{model.schema ? ( {model.schema ? (
<> <AnimatePresence>
{showUpload ? <DlgUploadRSForm hideWindow={() => setShowUpload(false)} /> : null} {showUpload ? <DlgUploadRSForm hideWindow={() => setShowUpload(false)} /> : null}
{showClone ? ( {showClone ? (
<DlgCloneLibraryItem <DlgCloneLibraryItem
@ -781,7 +782,7 @@ export const RSEditState = ({
) : null} ) : null}
{showTypeGraph ? <DlgShowTypeGraph items={typeInfo} hideWindow={() => setShowTypeGraph(false)} /> : null} {showTypeGraph ? <DlgShowTypeGraph items={typeInfo} hideWindow={() => setShowTypeGraph(false)} /> : null}
</> </AnimatePresence>
) : null} ) : null}
{children} {children}

View File

@ -13,6 +13,7 @@ import Loader from '@/components/ui/Loader';
import Overlay from '@/components/ui/Overlay'; import Overlay from '@/components/ui/Overlay';
import TabLabel from '@/components/ui/TabLabel'; import TabLabel from '@/components/ui/TabLabel';
import TextURL from '@/components/ui/TextURL'; import TextURL from '@/components/ui/TextURL';
import AnimateFade from '@/components/wrap/AnimateFade';
import { useConceptOptions } from '@/context/ConceptOptionsContext'; import { useConceptOptions } from '@/context/ConceptOptionsContext';
import { useGlobalOss } from '@/context/GlobalOssContext'; import { useGlobalOss } from '@/context/GlobalOssContext';
import { useLibrary } from '@/context/LibraryContext'; import { useLibrary } from '@/context/LibraryContext';
@ -269,12 +270,12 @@ function RSTabs() {
</TabList> </TabList>
</Overlay> </Overlay>
<div className='overflow-x-hidden'> <AnimateFade className='overflow-x-hidden'>
{cardPanel} {cardPanel}
{listPanel} {listPanel}
{editorPanel} {editorPanel}
{graphPanel} {graphPanel}
</div> </AnimateFade>
</Tabs> </Tabs>
) : null} ) : null}
</RSEditState> </RSEditState>

View File

@ -1,6 +1,7 @@
'use client'; 'use client';
import clsx from 'clsx'; import clsx from 'clsx';
import { motion } from 'framer-motion';
import { useMemo, useState } from 'react'; import { useMemo, useState } from 'react';
import { useAccessMode } from '@/context/AccessModeContext'; import { useAccessMode } from '@/context/AccessModeContext';
@ -8,7 +9,7 @@ import { useConceptOptions } from '@/context/ConceptOptionsContext';
import useWindowSize from '@/hooks/useWindowSize'; import useWindowSize from '@/hooks/useWindowSize';
import { ConstituentaID, IConstituenta, IRSForm } from '@/models/rsform'; import { ConstituentaID, IConstituenta, IRSForm } from '@/models/rsform';
import { UserLevel } from '@/models/user'; import { UserLevel } from '@/models/user';
import { PARAMETER } from '@/utils/constants'; import { animateSideView } from '@/styling/animations';
import ConstituentsSearch from './ConstituentsSearch'; import ConstituentsSearch from './ConstituentsSearch';
import TableSideConstituents from './TableSideConstituents'; import TableSideConstituents from './TableSideConstituents';
@ -22,10 +23,9 @@ interface ViewConstituentsProps {
activeCst?: IConstituenta; activeCst?: IConstituenta;
schema?: IRSForm; schema?: IRSForm;
onOpenEdit: (cstID: ConstituentaID) => void; onOpenEdit: (cstID: ConstituentaID) => void;
isMounted: boolean;
} }
function ViewConstituents({ expression, schema, activeCst, isBottom, onOpenEdit, isMounted }: ViewConstituentsProps) { function ViewConstituents({ expression, schema, activeCst, isBottom, onOpenEdit }: ViewConstituentsProps) {
const { calculateHeight } = useConceptOptions(); const { calculateHeight } = useConceptOptions();
const windowSize = useWindowSize(); const windowSize = useWindowSize();
const { accessLevel } = useAccessMode(); const { accessLevel } = useAccessMode();
@ -50,7 +50,7 @@ function ViewConstituents({ expression, schema, activeCst, isBottom, onOpenEdit,
); );
return ( return (
<div <motion.div
className={clsx( className={clsx(
'border', // prettier: split-lines 'border', // prettier: split-lines
{ {
@ -58,13 +58,9 @@ function ViewConstituents({ expression, schema, activeCst, isBottom, onOpenEdit,
'mt-3 mx-6 rounded-md md:max-w-[45.8rem] overflow-hidden': isBottom 'mt-3 mx-6 rounded-md md:max-w-[45.8rem] overflow-hidden': isBottom
} }
)} )}
style={{ initial={{ ...animateSideView.initial }}
transitionProperty: 'opacity, width', animate={{ ...animateSideView.animate }}
transitionDuration: `${2 * PARAMETER.moveDuration}ms`, exit={{ ...animateSideView.exit }}
transitionTimingFunction: 'ease-in-out',
opacity: isMounted ? 1 : 0,
width: isMounted ? '100%' : '0'
}}
> >
<ConstituentsSearch <ConstituentsSearch
dense={windowSize.width && windowSize.width < COLUMN_DENSE_SEARCH_THRESHOLD ? true : undefined} dense={windowSize.width && windowSize.width < COLUMN_DENSE_SEARCH_THRESHOLD ? true : undefined}
@ -74,7 +70,7 @@ function ViewConstituents({ expression, schema, activeCst, isBottom, onOpenEdit,
setFiltered={setFilteredData} setFiltered={setFilteredData}
/> />
{table} {table}
</div> </motion.div>
); );
} }

View File

@ -72,7 +72,7 @@ function FormSignup() {
} }
} }
return ( return (
<form className={clsx('cc-fade-in cc-column', 'mx-auto w-[36rem]', 'px-6 py-3')} onSubmit={handleSubmit}> <form className={clsx('cc-column', 'mx-auto w-[36rem]', 'px-6 py-3')} onSubmit={handleSubmit}>
<h1> <h1>
<span>Новый пользователь</span> <span>Новый пользователь</span>
<Overlay id={globals.password_tooltip} position='top-[5.4rem] left-[3.5rem]'> <Overlay id={globals.password_tooltip} position='top-[5.4rem] left-[3.5rem]'>

View File

@ -1,4 +1,7 @@
import { AnimatePresence } from 'framer-motion';
import Loader from '@/components/ui/Loader'; import Loader from '@/components/ui/Loader';
import AnimateFade from '@/components/wrap/AnimateFade';
import ExpectedAnonymous from '@/components/wrap/ExpectedAnonymous'; import ExpectedAnonymous from '@/components/wrap/ExpectedAnonymous';
import { useAuth } from '@/context/AuthContext'; import { useAuth } from '@/context/AuthContext';
@ -10,11 +13,28 @@ function RegisterPage() {
if (loading) { if (loading) {
return <Loader />; return <Loader />;
} }
if (user) { if (user && !loading) {
return <ExpectedAnonymous />; return (
} else { <AnimateFade>
return <FormSignup />; <ExpectedAnonymous />
</AnimateFade>
);
} }
return (
<AnimatePresence mode='wait'>
{loading ? <Loader key='signup-loader' /> : null}
{!loading && user ? (
<AnimateFade key='signup-has-user'>
<ExpectedAnonymous />
</AnimateFade>
) : null}
{!loading && !user ? (
<AnimateFade key='signup-no-user'>
<FormSignup />
</AnimateFade>
) : null}
</AnimatePresence>
);
} }
export default RegisterPage; export default RegisterPage;

View File

@ -8,6 +8,7 @@ import InfoError, { ErrorData } from '@/components/info/InfoError';
import SubmitButton from '@/components/ui/SubmitButton'; import SubmitButton from '@/components/ui/SubmitButton';
import TextInput from '@/components/ui/TextInput'; import TextInput from '@/components/ui/TextInput';
import TextURL from '@/components/ui/TextURL'; import TextURL from '@/components/ui/TextURL';
import AnimateFade from '@/components/wrap/AnimateFade';
import { useAuth } from '@/context/AuthContext'; import { useAuth } from '@/context/AuthContext';
import { IRequestPasswordData } from '@/models/user'; import { IRequestPasswordData } from '@/models/user';
@ -31,37 +32,38 @@ function RestorePasswordPage() {
setError(undefined); setError(undefined);
}, [email, setError]); }, [email, setError]);
if (isCompleted) { return (
return ( <AnimateFade>
<div className='cc-fade-in flex flex-col items-center gap-1 mt-3'> {!isCompleted ? (
<p>На указанную почту отправлены инструкции по восстановлению пароля.</p> <form className={clsx('cc-column', 'w-[24rem] mx-auto', 'px-6 mt-3')} onSubmit={handleSubmit}>
<TextURL text='Войти в Портал' href='/login' /> <TextInput
<TextURL text='Начальная страница' href='/' /> id='email'
</div> autoComplete='email'
); required
} else { allowEnter
return ( label='Электронная почта'
<form className={clsx('cc-fade-in cc-column', 'w-[24rem] mx-auto', 'px-6 mt-3')} onSubmit={handleSubmit}> value={email}
<TextInput onChange={event => setEmail(event.target.value)}
id='email' />
autoComplete='email'
required
allowEnter
label='Электронная почта'
value={email}
onChange={event => setEmail(event.target.value)}
/>
<SubmitButton <SubmitButton
text='Запросить пароль' text='Запросить пароль'
className='self-center w-[12rem] mt-3' className='self-center w-[12rem] mt-3'
loading={loading} loading={loading}
disabled={!email} disabled={!email}
/> />
{error ? <ProcessError error={error} /> : null} {error ? <ProcessError error={error} /> : null}
</form> </form>
); ) : null}
} {isCompleted ? (
<div className='flex flex-col items-center gap-1 mt-3'>
<p>На указанную почту отправлены инструкции по восстановлению пароля.</p>
<TextURL text='Войти в Портал' href='/login' />
<TextURL text='Начальная страница' href='/' />
</div>
) : null}
</AnimateFade>
);
} }
export default RestorePasswordPage; export default RestorePasswordPage;

View File

@ -1,5 +1,6 @@
'use client'; 'use client';
import AnimateFade from '@/components/wrap/AnimateFade';
import DataLoader from '@/components/wrap/DataLoader'; import DataLoader from '@/components/wrap/DataLoader';
import { useUserProfile } from '@/context/UserProfileContext'; import { useUserProfile } from '@/context/UserProfileContext';
@ -10,14 +11,21 @@ function UserContents() {
const { user, error, loading } = useUserProfile(); const { user, error, loading } = useUserProfile();
return ( return (
<DataLoader isLoading={loading} error={error} hasNoData={!user}> <DataLoader
<div className='cc-fade-in flex gap-6 py-2 mx-auto w-fit'> id='profile-page' // prettier: split lines
<h1 className='mb-4 select-none'>Учетные данные пользователя</h1> isLoading={loading}
<div className='flex py-2'> error={error}
<EditorProfile /> hasNoData={!user}
<EditorPassword /> >
<AnimateFade className='flex gap-6 py-2 mx-auto w-fit'>
<div className='w-fit'>
<h1 className='mb-4 select-none'>Учетные данные пользователя</h1>
<div className='flex py-2'>
<EditorProfile />
<EditorPassword />
</div>
</div> </div>
</div> </AnimateFade>
</DataLoader> </DataLoader>
); );
} }

View File

@ -0,0 +1,340 @@
/**
* 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 animateSlideLeft: 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(0% 100% 0% 0%)',
transition: {
type: 'spring',
bounce: 0,
duration: 0.3
}
}
};
export const animateHiddenHeader: Variants = {
open: {
translateX: 'calc(6.5rem - 50%)',
marginLeft: 0,
transition: {
type: 'spring',
bounce: 0,
duration: 0.3
}
},
closed: {
translateX: 0,
marginLeft: '0.75rem',
transition: {
type: 'spring',
bounce: 0,
duration: 0.3
}
}
};
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%)',
marginTop: '0.25rem',
height: 'max-content',
transition: {
type: 'spring',
bounce: 0,
duration: 0.4
}
},
closed: {
clipPath: 'inset(0% 0% 100% 0%)',
marginTop: '0',
height: 0,
transition: {
type: 'spring',
bounce: 0,
duration: 0.3
}
}
};
export const animateParseResults: Variants = {
open: {
clipPath: 'inset(0% 0% 0% 0%)',
marginTop: '0.75rem',
padding: '0.25rem 0.5rem 0.25rem 0.5rem',
borderWidth: '1px',
height: '4.5rem',
transition: {
type: 'spring',
bounce: 0,
duration: 0.4
}
},
closed: {
clipPath: 'inset(0% 0% 100% 0%)',
marginTop: '0',
borderWidth: '0',
padding: '0 0 0 0',
height: 0,
transition: {
type: 'spring',
bounce: 0,
duration: 0.3
}
}
};
export const animateSideView = {
initial: {
width: 0,
opacity: 0
},
animate: {
width: 'auto',
opacity: 1,
transition: {
width: {
duration: 0.4
},
opacity: {
delay: 0.4,
duration: 0
}
}
},
exit: {
width: 0,
opacity: 0,
transition: {
width: {
duration: 0.4
},
opacity: {
duration: 0
}
}
}
};
export const animateSideMinWidth = (width: string) => ({
initial: {
minWidth: 0,
opacity: 0
},
animate: {
minWidth: width,
opacity: 1,
transition: {
width: {
duration: 0.4
},
opacity: {
delay: 0.4,
duration: 0
}
}
},
exit: {
minWidth: 0,
opacity: 0,
transition: {
width: {
duration: 0.4
},
opacity: {
duration: 0
}
}
}
});
export const animateSideAppear = {
initial: {
height: 0,
opacity: 0
},
animate: {
height: 'auto',
opacity: 1,
transition: {
height: {
duration: 0.25
},
opacity: {
delay: 0.25,
duration: 0
}
}
},
exit: {
height: 0,
opacity: 0,
transition: {
height: {
duration: 0.25
},
opacity: {
duration: 0
}
}
}
};
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
}
}
};
export const animateFade = {
initial: {
opacity: 0
},
variants: {
active: {
opacity: 1,
transition: {
type: 'tween',
ease: 'linear',
duration: 0.4
}
},
hidden: {
opacity: 0,
transition: {
type: 'tween',
ease: 'linear',
duration: 0.4
}
}
},
exit: {
opacity: 0,
transition: {
type: 'tween',
ease: 'linear',
duration: 0.4
}
}
};

Some files were not shown because too many files have changed in this diff Show More