| @@ -1,7 +1,7 @@ | |||||
| import axios from 'axios' | import axios from 'axios' | ||||
| import toCamel from 'camelcase-keys' | import toCamel from 'camelcase-keys' | ||||
| import { AnimatePresence, motion } from 'framer-motion' | import { AnimatePresence, motion } from 'framer-motion' | ||||
| import { Fragment, useState, useEffect } from 'react' | |||||
| import { Fragment, useEffect, useLayoutEffect, useRef, useState } from 'react' | |||||
| import { Link, useLocation } from 'react-router-dom' | import { Link, useLocation } from 'react-router-dom' | ||||
| import Separator from '@/components/MenuSeparator' | import Separator from '@/components/MenuSeparator' | ||||
| @@ -20,6 +20,28 @@ type Props = { user: User | null } | |||||
| export default (({ user }: Props) => { | export default (({ user }: Props) => { | ||||
| const location = useLocation () | const location = useLocation () | ||||
| const dirRef = useRef<(-1) | 1> (1) | |||||
| const itemsRef = useRef<(HTMLAnchorElement | null)[]> ([]) | |||||
| const navRef = useRef<HTMLDivElement | null> (null) | |||||
| const measure = () => { | |||||
| const nav = navRef.current | |||||
| const el = itemsRef.current[activeIdx] | |||||
| if (!(nav) || !(el) || activeIdx < 0) | |||||
| return | |||||
| const navRect = nav.getBoundingClientRect () | |||||
| const elRect = el.getBoundingClientRect () | |||||
| setHl ({ left: elRect.left - navRect.left, | |||||
| width: elRect.width, | |||||
| visible: true }) | |||||
| } | |||||
| const [hl, setHl] = useState<{ left: number; width: number; visible: boolean }> ({ | |||||
| left: 0, | |||||
| width: 0, | |||||
| visible: false }) | |||||
| const [menuOpen, setMenuOpen] = useState (false) | const [menuOpen, setMenuOpen] = useState (false) | ||||
| const [openItemIdx, setOpenItemIdx] = useState (-1) | const [openItemIdx, setOpenItemIdx] = useState (-1) | ||||
| const [postCount, setPostCount] = useState<number | null> (null) | const [postCount, setPostCount] = useState<number | null> (null) | ||||
| @@ -54,6 +76,32 @@ export default (({ user }: Props) => { | |||||
| { name: 'お前', to: `/users/${ user?.id }`, visible: false }, | { name: 'お前', to: `/users/${ user?.id }`, visible: false }, | ||||
| { name: '設定', to: '/users/settings', visible: Boolean (user) }] }] | { name: '設定', to: '/users/settings', visible: Boolean (user) }] }] | ||||
| const activeIdx = menu.findIndex (item => location.pathname.startsWith (item.base || item.to)) | |||||
| const prevActiveIdxRef = useRef<number> (activeIdx) | |||||
| if (activeIdx !== prevActiveIdxRef.current) | |||||
| { | |||||
| dirRef.current = activeIdx > prevActiveIdxRef.current ? 1 : -1 | |||||
| prevActiveIdxRef.current = activeIdx | |||||
| } | |||||
| const dir = dirRef.current | |||||
| useLayoutEffect (() => { | |||||
| if (activeIdx < 0) | |||||
| return | |||||
| const raf = requestAnimationFrame (measure) | |||||
| const onResize = () => requestAnimationFrame (measure) | |||||
| addEventListener ('resize', onResize) | |||||
| return () => { | |||||
| cancelAnimationFrame (raf) | |||||
| removeEventListener ('resize', onResize) | |||||
| } | |||||
| }, [activeIdx]) | |||||
| useEffect (() => { | useEffect (() => { | ||||
| const unsubscribe = WikiIdBus.subscribe (setWikiId) | const unsubscribe = WikiIdBus.subscribe (setWikiId) | ||||
| return () => unsubscribe () | return () => unsubscribe () | ||||
| @@ -99,16 +147,26 @@ export default (({ user }: Props) => { | |||||
| ぼざクリ タグ広場 | ぼざクリ タグ広場 | ||||
| </Link> | </Link> | ||||
| {menu.map ((item, i) => ( | |||||
| <Link key={i} | |||||
| to={item.to} | |||||
| className={cn ('hidden md:flex h-full items-center', | |||||
| (location.pathname.startsWith (item.base || item.to) | |||||
| ? 'bg-yellow-200 dark:bg-red-950 px-4 font-bold' | |||||
| : 'px-2'))}> | |||||
| {item.name} | |||||
| </Link> | |||||
| ))} | |||||
| <div ref={navRef} className="relative hidden md:flex h-full items-center"> | |||||
| <div aria-hidden | |||||
| className={cn ('absolute top-1/2 -translate-y-1/2 h-full', | |||||
| 'bg-yellow-200 dark:bg-red-950', | |||||
| 'transition-[transform,width] duration-200 ease-out')} | |||||
| style={{ width: hl.width, | |||||
| transform: `translate(${ hl.left }px, -50%)`, | |||||
| opacity: hl.visible ? 1 : 0 }}/> | |||||
| {menu.map ((item, i) => ( | |||||
| <Link key={i} | |||||
| to={item.to} | |||||
| ref={el => { | |||||
| itemsRef.current[i] = el | |||||
| }} | |||||
| className={cn ('relative z-10 flex h-full items-center px-5', | |||||
| (i === openItemIdx) && 'font-bold')}> | |||||
| {item.name} | |||||
| </Link>))} | |||||
| </div> | |||||
| </div> | </div> | ||||
| <TopNavUser user={user}/> | <TopNavUser user={user}/> | ||||
| @@ -125,16 +183,33 @@ export default (({ user }: Props) => { | |||||
| </a> | </a> | ||||
| </nav> | </nav> | ||||
| <div className="hidden md:flex bg-yellow-200 dark:bg-red-950 | |||||
| items-center w-full min-h-[40px] px-3"> | |||||
| {menu.find (item => location.pathname.startsWith (item.base || item.to))?.subMenu | |||||
| .filter (item => item.visible ?? true) | |||||
| .map ((item, i) => 'component' in item ? item.component : ( | |||||
| <Link key={i} | |||||
| to={item.to} | |||||
| className="h-full flex items-center px-3"> | |||||
| {item.name} | |||||
| </Link>))} | |||||
| <div className="relative hidden md:flex bg-yellow-200 dark:bg-red-950 | |||||
| items-center w-full min-h-[40px] overflow-hidden"> | |||||
| <AnimatePresence initial={false} custom={dir}> | |||||
| <motion.div | |||||
| key={activeIdx} | |||||
| custom={dir} | |||||
| variants={{ enter: (d: -1 | 1) => ({ y: d * 24, opacity: 0 }), | |||||
| centre: { y: 0, opacity: 1 }, | |||||
| exit: (d: -1 | 1) => ({ y: (-d) * 24, opacity: 0 }) }} | |||||
| className="absolute inset-0 flex items-center px-3" | |||||
| initial="enter" | |||||
| animate="centre" | |||||
| exit="exit" | |||||
| transition={{ duration: .2, ease: 'easeOut' }}> | |||||
| {(menu[activeIdx]?.subMenu ?? []) | |||||
| .filter (item => item.visible ?? true) | |||||
| .map ((item, i) => ( | |||||
| 'component' in item | |||||
| ? <Fragment key={`c-${ i }`}>{item.component}</Fragment> | |||||
| : ( | |||||
| <Link key={`l-${ i }`} | |||||
| to={item.to} | |||||
| className="h-full flex items-center px-3"> | |||||
| {item.name} | |||||
| </Link>)))} | |||||
| </motion.div> | |||||
| </AnimatePresence> | |||||
| </div> | </div> | ||||
| <AnimatePresence initial={false}> | <AnimatePresence initial={false}> | ||||
| @@ -167,16 +242,38 @@ export default (({ user }: Props) => { | |||||
| }}> | }}> | ||||
| {item.name} | {item.name} | ||||
| </Link> | </Link> | ||||
| {i === openItemIdx && ( | |||||
| item.subMenu | |||||
| .filter (subItem => subItem.visible ?? true) | |||||
| .map ((subItem, j) => 'component' in subItem ? subItem.component : ( | |||||
| <Link key={j} | |||||
| to={subItem.to} | |||||
| className="w-full min-h-[36px] flex items-center pl-12 | |||||
| bg-yellow-50 dark:bg-red-950"> | |||||
| {subItem.name} | |||||
| </Link>)))} | |||||
| <AnimatePresence initial={false}> | |||||
| {i === openItemIdx && ( | |||||
| <motion.div | |||||
| key={`sp-sub-${ i }`} | |||||
| className="w-full bg-yellow-50 dark:bg-red-950" | |||||
| variants={{ closed: { clipPath: 'inset(0 0 100% 0)', | |||||
| height: 0, | |||||
| opacity: 0 }, | |||||
| open: { clipPath: 'inset(0 0 0% 0)', | |||||
| height: 'auto', | |||||
| opacity: 1 } }} | |||||
| initial="closed" | |||||
| animate="open" | |||||
| exit="closed" | |||||
| transition={{ duration: .2, ease: 'easeOut' }}> | |||||
| {item.subMenu | |||||
| .filter (subItem => subItem.visible ?? true) | |||||
| .map ((subItem, j) => ( | |||||
| 'component' in subItem | |||||
| ? ( | |||||
| <Fragment key={`sp-c-${ i }-${ j }`}> | |||||
| {subItem.component} | |||||
| </Fragment>) | |||||
| : ( | |||||
| <Link key={`sp-l-${ i }-${ j }`} | |||||
| to={subItem.to} | |||||
| className="w-full min-h-[36px] flex items-center pl-12"> | |||||
| {subItem.name} | |||||
| </Link>)))} | |||||
| </motion.div>)} | |||||
| </AnimatePresence> | |||||
| </Fragment>))} | </Fragment>))} | ||||
| <TopNavUser user={user} sp/> | <TopNavUser user={user} sp/> | ||||
| <Separator/> | <Separator/> | ||||