diff --git a/components/Menu/Menu.tsx b/components/Menu/Menu.tsx index d7f1f90..2c3296a 100644 --- a/components/Menu/Menu.tsx +++ b/components/Menu/Menu.tsx @@ -4,6 +4,7 @@ import MenuItem from './MenuItem'; import MenuToggle from './MenuToggle'; import MenuList from './MenuList'; import {MenuContext} from './MenuContext'; +import {MenuPosition} from './MenuPosition'; /** * Props. @@ -12,17 +13,19 @@ type MenuProps = { /** @default false */ initialOpen?: boolean; children?: ReactNode; -}; +} & MenuPosition; /** * @param {MenuProps} props Props. * @returns React component. */ -export default function Menu({initialOpen = false, children}: MenuProps) { +export default function Menu({initialOpen = false, children, ...position}: MenuProps) { const [isOpen, setIsOpen] = useState(initialOpen); useDisableBodyScroll(isOpen); - return {children}; + return ( + {children} + ); } Menu.Item = MenuItem; diff --git a/components/Menu/MenuContext.ts b/components/Menu/MenuContext.ts index 144964a..41ab8bc 100644 --- a/components/Menu/MenuContext.ts +++ b/components/Menu/MenuContext.ts @@ -1,4 +1,5 @@ import {createContext} from 'react'; +import {MenuPosition} from './MenuPosition'; /** * Context type. @@ -6,6 +7,7 @@ import {createContext} from 'react'; type MenuConextType = { isOpen: boolean; setIsOpen: (isOpen: boolean) => void; + position: MenuPosition; }; /** diff --git a/components/Menu/MenuItem.tsx b/components/Menu/MenuItem.tsx index 5b43799..de86c59 100644 --- a/components/Menu/MenuItem.tsx +++ b/components/Menu/MenuItem.tsx @@ -29,7 +29,7 @@ const variants = { }, }; -const itemCn = clsx('text-4xl', 'm-2', 'text-center'); +const itemCn = clsx('text-5xl', 'm-2', 'text-center'); /** * @param {MenuItemProps} props Props. diff --git a/components/Menu/MenuList.tsx b/components/Menu/MenuList.tsx index 83bacd0..ed2e059 100644 --- a/components/Menu/MenuList.tsx +++ b/components/Menu/MenuList.tsx @@ -5,6 +5,8 @@ import clsx from 'clsx'; import useScroll from '@/lib/shared/useScroll'; import useWindowDimensions from '@/lib/shared/useWindowDimensions'; import {MenuContext} from './MenuContext'; +import {MenuPosition} from './MenuPosition'; +import menuButtonSize from './menuButtonSize'; /** * Props. @@ -13,25 +15,53 @@ type MenuListProps = { children: ReactNode; }; +// eslint-disable-next-line jsdoc/require-jsdoc +function getClipPath({height, position}: {height?: number; position: MenuPosition}) { + let coordinates = ''; + + const {top, bottom, left, right} = position; + + if (left !== undefined) { + coordinates += `${left + menuButtonSize / 2}px`; + } + + if (right !== undefined) { + coordinates += `calc(100% - ${right + menuButtonSize / 2}px)`; + } + + if (top !== undefined) { + coordinates += ' ' + `${top + menuButtonSize / 2}px`; + } + + if (bottom !== undefined) { + coordinates += ' ' + `calc(100% - ${bottom + menuButtonSize / 2}px)`; + } + + const size = height ? `${height * 2 + 200}px` : '0px'; + + return `circle(${size} at ${coordinates}`; +} + const navVariants = { // eslint-disable-next-line jsdoc/require-jsdoc - open: (height: number) => ({ - clipPath: `circle(${height * 2 + 200}px at 95% 5%)`, + open: ({height, position}: {height: number; position: MenuPosition}) => ({ + clipPath: getClipPath({height, position}), transition: { type: 'spring', stiffness: 20, restDelta: 2, }, }), - closed: { - clipPath: 'circle(30px at 95% 5%)', + // eslint-disable-next-line jsdoc/require-jsdoc + closed: ({position}: {height: number; position: MenuPosition}) => ({ + clipPath: getClipPath({position}), transition: { delay: 0.5, type: 'spring', stiffness: 400, damping: 40, }, - }, + }), }; const navCn = clsx( @@ -41,7 +71,7 @@ const navCn = clsx( 'w-full', 'h-full', 'z-10', - 'bg-alternate', + 'bg-gradient-to-bl from-accent0 to-alternate', 'flex', 'flex-col', 'items-center', @@ -53,7 +83,7 @@ const navCn = clsx( * @returns React component. */ export default function MenuList({children}: MenuListProps) { - const {isOpen, setIsOpen} = useContext(MenuContext); + const {isOpen, setIsOpen, position} = useContext(MenuContext); const close = useCallback(() => setIsOpen(false), [setIsOpen]); useHotkeys('esc', close); useScroll(close); @@ -71,7 +101,7 @@ export default function MenuList({children}: MenuListProps) { > +>; diff --git a/components/Menu/MenuToggle.tsx b/components/Menu/MenuToggle.tsx index 1fecb58..c1bba75 100644 --- a/components/Menu/MenuToggle.tsx +++ b/components/Menu/MenuToggle.tsx @@ -1,11 +1,12 @@ 'use client'; -import {SVGMotionProps, Variants, m} from 'framer-motion'; +import {Variants, m} from 'framer-motion'; import {forwardRef, memo, useContext} from 'react'; import clsx from 'clsx'; -import {twMerge} from 'tailwind-merge'; import {MenuContext} from './MenuContext'; +import menuButtonSize from './menuButtonSize'; import Button from '../Button/Button'; +import MenuToggleSvgContent from './MenuToggleSvgContent'; /** * Props. @@ -20,29 +21,18 @@ const menuVariants: Variants = { hidden: {opacity: 0}, }; -const containerCn = clsx('absolute', 'top-16', 'right-16', 'z-20'); -const btnCn = clsx('rounded-full w-24 h-24'); - -// eslint-disable-next-line jsdoc/require-jsdoc -const Path = ({variants}: SVGMotionProps) => ( - -); +const containerCn = clsx('absolute', 'z-20'); +const btnCn = clsx('rounded-full'); /** * @param {MenuToggleProps} props Props. * @returns React component. */ const MenuToggle = forwardRef(function MenuToggle( - {onToggle, className}, + {onToggle}, ref, ) { - const {setIsOpen, isOpen} = useContext(MenuContext); + const {setIsOpen, isOpen, position} = useContext(MenuContext); // eslint-disable-next-line jsdoc/require-jsdoc const onClick = () => { setIsOpen(!isOpen); @@ -57,32 +47,17 @@ const MenuToggle = forwardRef(function MenuT transition={{duration: 0.75, delay: 1}} variants={menuVariants} className={containerCn} + style={position} animate={isOpen ? 'open' : 'closed'} > ); diff --git a/components/Menu/MenuToggleSvgContent.tsx b/components/Menu/MenuToggleSvgContent.tsx new file mode 100644 index 0000000..cb8fda5 --- /dev/null +++ b/components/Menu/MenuToggleSvgContent.tsx @@ -0,0 +1,59 @@ +import {SVGMotionProps, m} from 'framer-motion'; +import clsx from 'clsx'; +import menuButtonSize from './menuButtonSize'; + +const pathCn = clsx('fill-white', 'stroke-white'); + +// eslint-disable-next-line jsdoc/require-jsdoc +const Path = ({variants}: SVGMotionProps) => ( + +); + +/** + * @returns React component. + */ +export default function MenuToggleSvgContent() { + const svgViewBoxSize = menuButtonSize / 2; + const svgElSize = svgViewBoxSize / 1.5; + const svgElOffset = (svgViewBoxSize - svgElSize) / 2; + + return ( + + + + + ); +} diff --git a/components/Menu/menuButtonSize.ts b/components/Menu/menuButtonSize.ts new file mode 100644 index 0000000..1aa902d --- /dev/null +++ b/components/Menu/menuButtonSize.ts @@ -0,0 +1,3 @@ +const menuButtonSize = 80; + +export default menuButtonSize; diff --git a/components/TopMenu/TopMenu.tsx b/components/TopMenu/TopMenu.tsx index 66391d2..6aa1465 100644 --- a/components/TopMenu/TopMenu.tsx +++ b/components/TopMenu/TopMenu.tsx @@ -4,7 +4,7 @@ import {m} from 'framer-motion'; import {menuItemsConfig, socialLinksConfig} from './topMenuCongif'; import Menu from '../Menu/Menu'; -const itemsVariants = { +const variants = { open: { transition: {staggerChildren: 0.07, delayChildren: 0.2}, }, @@ -13,28 +13,35 @@ const itemsVariants = { }, }; -const socialLinksVariants = itemsVariants; - const linkCn = clsx('animated-link'); - const socialLiksListCn = clsx('flex', 'gap-8', 'mt-auto', 'mb-16'); - const socialLinkMenuItem = clsx('text-xs'); - -const itemsListCn = clsx('mt-48'); +const itemsListCn = clsx('mt-24'); +const homeMenuItem = clsx('mb-32', 'text-xl'); /** * @returns React component. */ export default function TopMenu() { return ( - + + + + SALSAVIVA + + {menuItemsConfig.map(({href, text}, idx) => ( {socialLinksConfig.map(({href, text}, idx) => ( diff --git a/package-lock.json b/package-lock.json index f2948cf..9a4af99 100644 --- a/package-lock.json +++ b/package-lock.json @@ -45,7 +45,8 @@ "eslint-plugin-jsx-a11y": "^6.7.1", "husky": "^8.0.0", "jimp": "^0.22.8", - "prettier": "^2.8.8" + "prettier": "^2.8.8", + "ts-xor": "^1.1.0" } }, "node_modules/@alloc/quick-lru": { @@ -6925,6 +6926,12 @@ "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==" }, + "node_modules/ts-xor": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/ts-xor/-/ts-xor-1.1.0.tgz", + "integrity": "sha512-9vtspo9gVrmJR0XQyuNySpr6DhZztCDWS8LT5CO4gSeifILDRi4e8QZ0ixnvCyob9hMYRaOeo+OyW3ovhngjuA==", + "dev": true + }, "node_modules/tsconfig-paths": { "version": "3.14.2", "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.14.2.tgz", @@ -12297,6 +12304,12 @@ "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==" }, + "ts-xor": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/ts-xor/-/ts-xor-1.1.0.tgz", + "integrity": "sha512-9vtspo9gVrmJR0XQyuNySpr6DhZztCDWS8LT5CO4gSeifILDRi4e8QZ0ixnvCyob9hMYRaOeo+OyW3ovhngjuA==", + "dev": true + }, "tsconfig-paths": { "version": "3.14.2", "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.14.2.tgz", diff --git a/package.json b/package.json index 8f9d03e..64cfba3 100644 --- a/package.json +++ b/package.json @@ -50,6 +50,7 @@ "eslint-plugin-jsx-a11y": "^6.7.1", "husky": "^8.0.0", "jimp": "^0.22.8", - "prettier": "^2.8.8" + "prettier": "^2.8.8", + "ts-xor": "^1.1.0" } }