F: Animation rework pt2

This commit is contained in:
Ivan 2024-12-11 23:37:23 +03:00
parent 725b21091c
commit ce8f2584db
19 changed files with 159 additions and 344 deletions

View File

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

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 { animateNavigation } from '@/styling/animations'; import useWindowSize from '@/hooks/useWindowSize';
import { PARAMETER } from '@/utils/constants';
import { urls } from '../urls'; import { urls } from '../urls';
import Logo from './Logo'; import Logo from './Logo';
@ -15,6 +15,7 @@ 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);
@ -33,17 +34,24 @@ function Navigation() {
)} )}
> >
<ToggleNavigation /> <ToggleNavigation />
<motion.div <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'
)} )}
initial={false} style={{
animate={!noNavigationAnimation ? 'open' : 'closed'} transitionProperty: 'height, translate',
variants={animateNavigation} transitionDuration: `${PARAMETER.moveDuration}ms`,
height: noNavigationAnimation ? '0rem' : '3rem',
translate: noNavigationAnimation ? '0 -1.5rem' : '0'
}}
> >
<div tabIndex={-1} className='flex items-center mr-auto cursor-pointer' onClick={navigateHome}> <div
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]'>
@ -52,7 +60,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>
</motion.div> </div>
</nav> </nav>
); );
} }

View File

@ -1,15 +1,13 @@
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 { animateNavigationToggle } from '@/styling/animations'; import { globals, PARAMETER } from '@/utils/constants';
import { globals } from '@/utils/constants';
function ToggleNavigation() { function ToggleNavigation() {
const { noNavigationAnimation, toggleNoNavigation } = useConceptOptions(); const { noNavigationAnimation, toggleNoNavigation } = useConceptOptions();
return ( return (
<motion.button <button
type='button' type='button'
tabIndex={-1} tabIndex={-1}
className={clsx( className={clsx(
@ -20,15 +18,18 @@ 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}
</motion.button> </button>
); );
} }

View File

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

View File

@ -4,9 +4,8 @@ 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, storage } from '@/utils/constants'; import { globals, PARAMETER, storage } from '@/utils/constants';
import { contextOutsideScope } from '@/utils/labels'; import { contextOutsideScope } from '@/utils/labels';
interface IOptionsContext { interface IOptionsContext {
@ -91,7 +90,7 @@ export const OptionsState = ({ children }: React.PropsWithChildren) => {
setNoNavigation(false); setNoNavigation(false);
} else { } else {
setNoNavigationAnimation(true); setNoNavigationAnimation(true);
setTimeout(() => setNoNavigation(true), animationDuration.navigationToggle); setTimeout(() => setNoNavigation(true), PARAMETER.moveDuration);
} }
}, [noNavigation]); }, [noNavigation]);

View File

@ -1,6 +1,5 @@
'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';
@ -150,6 +149,7 @@ 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,6 +163,7 @@ function LibraryPage() {
options.location, options.location,
library.folders, library.folders,
options.setLocation, options.setLocation,
options.folderMode,
toggleFolderMode, toggleFolderMode,
promptRenameLocation, promptRenameLocation,
toggleSubfolders, toggleSubfolders,
@ -187,7 +188,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='transition-all' className='cc-animate-position'
> >
<MiniButton <MiniButton
title='Выгрузить в формате CSV' title='Выгрузить в формате CSV'
@ -219,7 +220,7 @@ function LibraryPage() {
/> />
<div className='flex'> <div className='flex'>
<AnimatePresence initial={false}>{options.folderMode ? viewLocations : null}</AnimatePresence> {viewLocations}
{viewLibrary} {viewLibrary}
</div> </div>
</DataLoader> </DataLoader>

View File

@ -1,5 +1,4 @@
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';
@ -15,12 +14,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;
@ -33,6 +32,7 @@ function ViewSideLocation({
folderTree, folderTree,
activeLocation, activeLocation,
subfolders, subfolders,
isVisible,
onChangeActiveLocation, onChangeActiveLocation,
toggleFolderMode, toggleFolderMode,
toggleSubfolders, toggleSubfolders,
@ -57,7 +57,6 @@ 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(
@ -77,11 +76,16 @@ function ViewSideLocation({
); );
return ( return (
<motion.div <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')}
initial={{ ...animations.initial }} style={{
animate={{ ...animations.animate }} transitionProperty: 'width, min-width, opacity',
exit={{ ...animations.exit }} transitionDuration: `${PARAMETER.moveDuration}ms`,
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
@ -117,7 +121,7 @@ function ViewSideLocation({
onClick={handleClickFolder} onClick={handleClickFolder}
style={{ maxHeight: maxHeight }} style={{ maxHeight: maxHeight }}
/> />
</motion.div> </div>
); );
} }

View File

@ -1,7 +1,6 @@
'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';
@ -10,8 +9,7 @@ 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 { animateSlideLeft } from '@/styling/animations'; import { PARAMETER, prefixes } from '@/utils/constants';
import { prefixes } from '@/utils/constants';
import { describeHelpTopic, labelHelpTopic } from '@/utils/labels'; import { describeHelpTopic, labelHelpTopic } from '@/utils/labels';
interface TopicsDropdownProps { interface TopicsDropdownProps {
@ -55,27 +53,26 @@ function TopicsDropdown({ activeTopic, onChangeTopic }: TopicsDropdownProps) {
className={clsx('w-[3rem] h-7 rounded-none border-l-0', menu.isOpen && 'border-b-0')} className={clsx('w-[3rem] h-7 rounded-none border-l-0', menu.isOpen && 'border-b-0')}
onClick={menu.toggle} onClick={menu.toggle}
/> />
<motion.div <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}
className={clsx( className={clsx(
'border border-l-0 divide-y rounded-none', // prettier: split-lines 'border-r border-t rounded-none', // prettier: split-lines
'cc-scroll-y', 'cc-scroll-y',
'clr-controls' 'clr-controls'
)} )}
style={{ maxHeight: calculateHeight('4rem + 2px') }} style={{
initial={false} maxHeight: calculateHeight('4rem + 2px'),
animate={menu.isOpen ? 'open' : 'closed'} transitionProperty: 'clip-path',
variants={animateSlideLeft} transitionDuration: `${PARAMETER.moveDuration}ms`,
> 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 divide-y rounded-none', 'border-x border-t rounded-none',
'clr-controls', 'clr-controls',
'text-xs sm:text-sm', 'text-xs sm:text-sm',
'select-none' 'select-none'

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 outline-none transition-all duration-500 cc-blur px-1 rounded-b-2xl' className='cc-icons cc-animate-position outline-none 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,8 +1,5 @@
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 {
@ -17,12 +14,16 @@ 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 (
<motion.div <div
tabIndex={-1} tabIndex={-1}
className='text-sm border dense cc-scroll-y' className='text-sm border dense cc-scroll-y transition-all duration-300'
initial={false} style={{
animate={isOpen ? 'open' : 'closed'} clipPath: isOpen ? 'inset(0% 0% 0% 0%)' : 'inset(0% 0% 100% 0%)',
variants={animateParseResults} marginTop: isOpen ? '0.75rem' : '0rem',
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>
@ -42,7 +43,7 @@ function ParsingResult({ isOpen, data, disabled, onShowError }: ParsingResultPro
</p> </p>
); );
})} })}
</motion.div> </div>
); );
} }

View File

@ -1,9 +1,7 @@
import clsx from 'clsx'; import clsx from 'clsx';
import { motion } from 'framer-motion';
import { TokenID } from '@/models/rslang'; import { TokenID } from '@/models/rslang';
import { animateRSControl } from '@/styling/animations'; import { PARAMETER, prefixes } from '@/utils/constants';
import { prefixes } from '@/utils/constants';
import RSLocalButton from './RSLocalButton'; import RSLocalButton from './RSLocalButton';
import RSTokenButton from './RSTokenButton'; import RSTokenButton from './RSTokenButton';
@ -90,7 +88,7 @@ interface RSEditorControlsProps {
function RSEditorControls({ isOpen, disabled, onEdit }: RSEditorControlsProps) { function RSEditorControls({ isOpen, disabled, onEdit }: RSEditorControlsProps) {
return ( return (
<motion.div <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',
@ -98,9 +96,13 @@ function RSEditorControls({ isOpen, disabled, onEdit }: RSEditorControlsProps) {
'text-xs md:text-sm', 'text-xs md:text-sm',
'select-none' 'select-none'
)} )}
initial={false} style={{
animate={isOpen ? 'open' : 'closed'} transitionProperty: 'clipPath, height',
variants={animateRSControl} transitionDuration: `${PARAMETER.moveDuration}ms`,
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} />
@ -143,7 +145,7 @@ function RSEditorControls({ isOpen, disabled, onEdit }: RSEditorControlsProps) {
disabled={disabled} disabled={disabled}
/> />
))} ))}
</motion.div> </div>
); );
} }

View File

@ -56,7 +56,7 @@ function StatusBar({ isModified, processing, activeCst, parseData, onAnalyze }:
'select-none', 'select-none',
'cursor-pointer', 'cursor-pointer',
'focus-frame', 'focus-frame',
'duration-500 transition-colors' 'transition-colors duration-500'
)} )}
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}

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 items-start outline-none transition-all duration-500' className='cc-icons cc-animate-position items-start outline-none'
> >
{controller.schema && controller.schema?.oss.length > 0 ? ( {controller.schema && controller.schema?.oss.length > 0 ? (
<MiniSelectorOSS <MiniSelectorOSS

View File

@ -1,7 +1,6 @@
'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';
@ -14,9 +13,8 @@ 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 { prefixes, storage } from '@/utils/constants'; import { PARAMETER, prefixes, storage } from '@/utils/constants';
interface ViewHiddenProps { interface ViewHiddenProps {
items: ConstituentaID[]; items: ConstituentaID[];
@ -60,39 +58,35 @@ function ViewHidden({ items, selected, toggleSelection, setFocus, schema, colori
onClick={() => setIsFolded(prev => !prev)} onClick={() => setIsFolded(prev => !prev)}
/> />
</Overlay> </Overlay>
<div <div className={clsx('pt-2 clr-input border-x pb-2', { 'border-b rounded-b-md': isFolded })}>
className={clsx( <div
'pt-2', // className='w-fit select-none'
'border-x', style={{
'clr-input', transitionProperty: 'margin, translate',
'select-none', transitionDuration: `${PARAMETER.fastAnimation}ms`,
{ transitionTimingFunction: 'ease-out',
'pb-2 border-b': isFolded marginLeft: isFolded ? '0.75rem' : '0',
} translate: isFolded ? '0' : 'calc(6.5rem - 50%)'
)} }}
>
<motion.div
className='w-fit'
animate={!isFolded ? 'open' : 'closed'}
variants={animateHiddenHeader}
initial={false}
> >
{`Скрытые [${localSelected.length} | ${items.length}]`} {`Скрытые [${localSelected.length} | ${items.length}]`}
</motion.div> </div>
</div> </div>
<motion.div <div
className={clsx( className={clsx(
'flex flex-wrap justify-center gap-2 py-2', 'flex flex-wrap justify-center gap-2 py-2 mt-[-0.5rem]',
'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={{ maxHeight: calculateHeight(windowSize.isSmall ? '10.4rem + 2px' : '12.5rem + 2px') }} style={{
initial={false} maxHeight: calculateHeight(windowSize.isSmall ? '10.4rem + 2px' : '12.5rem + 2px'),
animate={!isFolded ? 'open' : 'closed'} transitionProperty: 'clip-path',
variants={animateDropdown} transitionDuration: `${PARAMETER.fastAnimation}ms`,
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)!;
@ -124,7 +118,7 @@ function ViewHidden({ items, selected, toggleSelection, setFocus, schema, colori
</div> </div>
); );
})} })}
</motion.div> </div>
</div> </div>
); );
} }

View File

@ -4,97 +4,6 @@
import { Variants } from 'framer-motion'; 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 = { export const animateDropdown: Variants = {
open: { open: {
clipPath: 'inset(0% 0% 0% 0%)', clipPath: 'inset(0% 0% 0% 0%)',
@ -136,120 +45,6 @@ export const animateDropdownItem: Variants = {
} }
}; };
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 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 = { export const animateModal = {
initial: { initial: {
clipPath: 'inset(50% 50% 50% 50%)', clipPath: 'inset(50% 50% 50% 50%)',

View File

@ -16,6 +16,8 @@
--text-max-width: 75ch; --text-max-width: 75ch;
--scroll-padding: 3rem; --scroll-padding: 3rem;
--duration-move: 400ms;
/* Light Theme */ /* Light Theme */
--cl-bg-120: hsl(000, 000%, 100%); --cl-bg-120: hsl(000, 000%, 100%);
--cl-bg-100: hsl(000, 000%, 098%); --cl-bg-100: hsl(000, 000%, 098%);

View File

@ -248,4 +248,10 @@
.cc-shadow-border { .cc-shadow-border {
@apply shadow-sm shadow-[var(--cl-bg-40)] dark:shadow-[var(--cd-bg-40)]; @apply shadow-sm shadow-[var(--cl-bg-40)] dark:shadow-[var(--cd-bg-40)];
} }
.cc-animate-position {
transition-property: transform top left bottom right margin padding;
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
transition-duration: var(--duration-move);
}
} }

View File

@ -21,8 +21,9 @@ export const PARAMETER = {
ossDistanceX: 180, // pixels - insert x-distance between node centers ossDistanceX: 180, // pixels - insert x-distance between node centers
ossDistanceY: 100, // pixels - insert y-distance between node centers ossDistanceY: 100, // pixels - insert y-distance between node centers
fastAnimation: 200, // milliseconds - duration of fast animation
fadeDuration: 300, // milliseconds - duration of fade animation fadeDuration: 300, // milliseconds - duration of fade animation
moveDuration: 700, // milliseconds - duration of move animation moveDuration: 500, // milliseconds - duration of move animation
graphHandleSize: 3, // pixels - size of graph connection handle graphHandleSize: 3, // pixels - size of graph connection handle
graphNodeRadius: 20, // pixels - radius of graph node graphNodeRadius: 20, // pixels - radius of graph node