This commit is contained in:
2026-04-12 21:05:42 +09:00
parent 0c805671a0
commit 27989f3bb2
2 changed files with 56 additions and 58 deletions
+54 -57
View File
@@ -19,13 +19,15 @@ import type { Menu, MenuVisibleItem, Tag, User } from '@/types'
type Props = { user: User | null } type Props = { user: User | null }
export const menuOutline = ({ tag, wikiId, user }: { tag?: Tag | null export const menuOutline = ({ tag, wikiId, user, pathName }: {
wikiId: number | null tag?: Tag | null
user: User | null }): Menu => { wikiId: number | null
user: User | null,
pathName: string }): Menu => {
const postCount = tag?.postCount ?? 0 const postCount = tag?.postCount ?? 0
const wikiPageFlg = Boolean (/^\/wiki\/(?!new|changes)[^\/]+/.test (location.pathname) && wikiId) const wikiPageFlg = Boolean (/^\/wiki\/(?!new|changes)[^\/]+/.test (pathName) && wikiId)
const wikiTitle = location.pathname.split ('/')[2] ?? '' const wikiTitle = pathName.split ('/')[2] ?? ''
return [ return [
{ name: '広場', to: '/posts', subMenu: [ { name: '広場', to: '/posts', subMenu: [
@@ -80,7 +82,7 @@ export default (({ user }: Props) => {
const measure = (idx: number) => { const measure = (idx: number) => {
const nav = navRef.current const nav = navRef.current
const el = itemsRef.current[idx < 0 ? menu.length : idx] const el = itemsRef.current[idx < 0 ? visibleMenu.length : idx]
if (!(nav) || !(el)) if (!(nav) || !(el))
{ {
@@ -119,10 +121,10 @@ export default (({ user }: Props) => {
queryKey: tagsKeys.show (effectiveTitle), queryKey: tagsKeys.show (effectiveTitle),
queryFn: () => fetchTagByName (effectiveTitle) }) queryFn: () => fetchTagByName (effectiveTitle) })
const menu = menuOutline ({ tag, wikiId, user }) const menu = menuOutline ({ tag, wikiId, user, pathName: location.pathname })
const activeIdx = (menu const visibleMenu = menu.filter ((item): item is MenuVisibleItem => item.visible ?? true)
.filter ((item): item is MenuVisibleItem => item.visible ?? true) const activeIdx =
.findIndex (item => location.pathname.startsWith (item.base || item.to))) visibleMenu.findIndex (item => location.pathname.startsWith (item.base || item.to))
const prevActiveIdxRef = useRef<number> (activeIdx) const prevActiveIdxRef = useRef<number> (activeIdx)
@@ -135,26 +137,24 @@ export default (({ user }: Props) => {
const dir = dirRef.current const dir = dirRef.current
useLayoutEffect (() => { useLayoutEffect (() => {
const raf = requestAnimationFrame (() => measure (activeIdx)) const raf = requestAnimationFrame (() => measure (moreVsbl ? -1 : activeIdx))
const onResize = () => requestAnimationFrame (() => measure (activeIdx)) const onResize = () => requestAnimationFrame (() => measure (moreVsbl ? -1 : activeIdx))
addEventListener ('resize', onResize) addEventListener ('resize', onResize)
return () => { return () => {
cancelAnimationFrame (raf) cancelAnimationFrame (raf)
removeEventListener ('resize', onResize) removeEventListener ('resize', onResize)
} }
}, [activeIdx]) })
useEffect (() => { useEffect (() => {
const unsubscribe = WikiIdBus.subscribe (setWikiId) const unsubscribe = WikiIdBus.subscribe (setWikiId)
return () => unsubscribe () return () => unsubscribe ()
}, []) }, [activeIdx])
useEffect (() => { useEffect (() => {
setMenuOpen (false) setMenuOpen (false)
setOpenItemIdx (menu setOpenItemIdx (activeIdx)
.filter ((item): item is MenuVisibleItem => item.visible ?? true)
.findIndex (item => location.pathname.startsWith (item.base || item.to)))
}, [location]) }, [location])
return ( return (
@@ -181,30 +181,29 @@ export default (({ user }: Props) => {
transform: `translateX(${ hl.left }px)`, transform: `translateX(${ hl.left }px)`,
opacity: hl.visible ? 1 : 0 }}/> opacity: hl.visible ? 1 : 0 }}/>
{menu.filter ((item): item is MenuVisibleItem => item.visible ?? true) {visibleMenu.map ((item, i) => (
.map ((item, i) => (
<motion.div <motion.div
key={item.to} key={item.to}
layoutId={`menu-${ item.name }`} layoutId={`menu-${ item.name }`}
onMouseEnter={() => { animate={{ opacity: moreVsbl ? 0 : 1 }}
setMoreVsbl (false) transition={{ opacity: { duration: .12 },
setTimeout (() => measure (activeIdx), 300) layout: { duration: .2, ease: 'easeOut' } }}
}}> style={{ pointerEvents: moreVsbl ? 'none' : 'auto' }}
onMouseEnter={() => setMoreVsbl (false)}>
<PrefetchLink <PrefetchLink
to={item.to} to={item.to}
ref={(el: (HTMLAnchorElement | null)) => { ref={(el: (HTMLAnchorElement | null)) => {
itemsRef.current[i] = el itemsRef.current[i] = el
}} }}
className={cn ('relative z-10 flex h-full items-center px-5', className={cn ('relative z-10 flex h-full items-center px-5',
(i === openItemIdx) && 'font-bold', (i === openItemIdx) && 'font-bold')}>
moreVsbl && 'pointer-events-none')}>
{item.name} {item.name}
</PrefetchLink> </PrefetchLink>
</motion.div>))} </motion.div>))}
<PrefetchLink <PrefetchLink
to="#" to="#"
ref={(el: (HTMLAnchorElement | null)) => { ref={(el: (HTMLAnchorElement | null)) => {
itemsRef.current[menu.length] = el itemsRef.current[visibleMenu.length] = el
}} }}
onClick={e => e.preventDefault ()} onClick={e => e.preventDefault ()}
onMouseEnter={() => { onMouseEnter={() => {
@@ -235,33 +234,33 @@ export default (({ user }: Props) => {
<AnimatePresence initial={false}> <AnimatePresence initial={false}>
<motion.div <motion.div
key="submenu-shell" key="submenu-shell"
layout
className="relative hidden md:block overflow-hidden className="relative hidden md:block overflow-hidden
bg-yellow-200 dark:bg-red-950" bg-yellow-200 dark:bg-red-950"
style={{ height: moreVsbl ? 40 * menu.length : (activeIdx < 0 ? 0 : 40) }}
onMouseLeave={() => { onMouseLeave={() => {
if (!(moreVsbl)) if (moreVsbl)
return setMoreVsbl (false)
setMoreVsbl (false)
setTimeout (() => measure (activeIdx), 300)
}} }}
initial={{ height: 0 }} transition={{ layout: { duration: .2, ease: 'easeOut' } }}
animate={{ height: (moreVsbl ? 40 * menu.length : (activeIdx < 0 ? 0 : 40)) }} onAnimationComplete={() => {
exit={{ height: 0 }} measure (moreVsbl ? -1 : activeIdx)
transition={{ duration: .2, ease: 'easeOut' }}> }}>
{moreVsbl {moreVsbl
? ( ? (
menu.map ((item, i) => ( menu.map ((item, i) => (
<div key={i} className="relative h-[40px]"> <div key={i} className="relative h-[40px]">
<div className="absolute inset-0 flex items-center px-3"> <div className="absolute inset-0 flex items-center px-3">
<motion.h2 <motion.div
transition={{ duration: .2, ease: 'easeOut' }}
{...((item.visible ?? true) {...((item.visible ?? true)
? { layoutId: `menu-${ item.name }` } ? { layoutId: `menu-${ item.name }` }
: { initial: { y: -40, opacity: 0 }, : { initial: { x: 40, y: -40, opacity: 0 },
animate: { y: 0, opacity: 1 }, animate: { x: 0, y: 0, opacity: 1 },
exit: { y: -40, opacity: 0 }, exit: { x: 40, y: -40, opacity: 0 } })}
transition: { duration: .2, ease: 'easeOut' } })}
className="z-10 h-full flex items-center px-3 font-bold w-24"> className="z-10 h-full flex items-center px-3 font-bold w-24">
{item.name} <h2>{item.name}</h2>
</motion.h2> </motion.div>
{item.subMenu {item.subMenu
.filter (subItem => subItem.visible ?? true) .filter (subItem => subItem.visible ?? true)
.map ((subItem, j) => ( .map ((subItem, j) => (
@@ -269,21 +268,24 @@ export default (({ user }: Props) => {
? ( ? (
<motion.div <motion.div
key={`c-${ i }-${ j }`} key={`c-${ i }-${ j }`}
{...((menu.filter (x => x.visible ?? true)[activeIdx]?.name transition={{ duration: .2, ease: 'easeOut' }}
{...((visibleMenu[activeIdx]?.name
=== item.name) === item.name)
? { layoutId: `submenu-${ item.name }-${ j }` } ? { layoutId: `submenu-${ item.name }-${ j }` }
: { initial: { y: -40, opacity: 0 }, : { initial: { y: -40, opacity: 0 },
animate: { y: 0, opacity: 1 }, animate: { y: 0, opacity: 1 },
exit: { y: -40, opacity: 0 }, exit: { y: -40, opacity: 0 } })}>
transition: { duration: .2, ease: 'easeOut' } })}>
{subItem.component} {subItem.component}
</motion.div>) </motion.div>)
: ( : (
<motion.div <motion.div
key={`l-${ i }-${ j }`} key={`l-${ i }-${ j }`}
{...((menu.filter (x => x.visible ?? true)[activeIdx]?.name {...((visibleMenu[activeIdx]?.name
=== item.name) === item.name)
&& { layoutId: `submenu-${ item.name }-${ j }` })}> ? { layoutId: `submenu-${ item.name }-${ j }` }
: { initial: { y: -40, opacity: 0 },
animate: { y: 0, opacity: 1 },
exit: { y: -40, opacity: 0 } })}>
<PrefetchLink <PrefetchLink
to={subItem.to} to={subItem.to}
target={subItem.to.slice (0, 2) === '//' ? '_blank' : undefined} target={subItem.to.slice (0, 2) === '//' ? '_blank' : undefined}
@@ -293,7 +295,7 @@ export default (({ user }: Props) => {
</motion.div>)))} </motion.div>)))}
</div> </div>
</div>))) </div>)))
: ((menu.filter (item => item.visible ?? true)[activeIdx]?.subMenu ?? []).length > 0 : ((visibleMenu[activeIdx]?.subMenu ?? []).length > 0
&& ( && (
<div className="relative h-[40px]"> <div className="relative h-[40px]">
<AnimatePresence initial={false} custom={dir}> <AnimatePresence initial={false} custom={dir}>
@@ -308,24 +310,20 @@ export default (({ user }: Props) => {
animate="centre" animate="centre"
exit="exit" exit="exit"
transition={{ duration: .2, ease: 'easeOut' }}> transition={{ duration: .2, ease: 'easeOut' }}>
{(menu.filter (item => item.visible ?? true)[activeIdx]?.subMenu ?? []) {(visibleMenu[activeIdx]?.subMenu ?? [])
.filter (item => item.visible ?? true) .filter (item => item.visible ?? true)
.map ((item, i) => ( .map ((item, i) => (
'component' in item 'component' in item
? ( ? (
<motion.div <motion.div
key={`c-${ i }`} key={`c-${ i }`}
layoutId={`submenu-${ layoutId={`submenu-${ visibleMenu[activeIdx].name }-${ i }`}>
menu.filter (x => x.visible ?? true)[activeIdx].name }-${
i }`}>
{item.component} {item.component}
</motion.div>) </motion.div>)
: ( : (
<motion.div <motion.div
key={`l-${ i }`} key={`l-${ i }`}
layoutId={`submenu-${ layoutId={`submenu-${ visibleMenu[activeIdx].name }-${ i }`}>
menu.filter (x => x.visible ?? true)[activeIdx].name }-${
i }`}>
<PrefetchLink <PrefetchLink
to={item.to} to={item.to}
target={item.to.slice (0, 2) === '//' ? '_blank' : undefined} target={item.to.slice (0, 2) === '//' ? '_blank' : undefined}
@@ -354,8 +352,7 @@ export default (({ user }: Props) => {
exit="closed" exit="closed"
transition={{ duration: .2, ease: 'easeOut' }}> transition={{ duration: .2, ease: 'easeOut' }}>
<Separator/> <Separator/>
{menu.filter ((item): item is MenuVisibleItem => item.visible ?? true) {visibleMenu.map ((item, i) => (
.map ((item, i) => (
<Fragment key={i}> <Fragment key={i}>
<PrefetchLink <PrefetchLink
to={i === openItemIdx ? item.to : '#'} to={i === openItemIdx ? item.to : '#'}
@@ -411,7 +408,7 @@ export default (({ user }: Props) => {
<PrefetchLink <PrefetchLink
to="/more" to="/more"
ref={(el: (HTMLAnchorElement | null)) => { ref={(el: (HTMLAnchorElement | null)) => {
itemsRef.current[menu.length] = el itemsRef.current[visibleMenu.length] = el
}} }}
className={cn ('w-full min-h-[40px] flex items-center pl-8', className={cn ('w-full min-h-[40px] flex items-center pl-8',
((openItemIdx < 0) ((openItemIdx < 0)
+2 -1
View File
@@ -12,7 +12,8 @@ import type { User } from '@/types'
export default (() => { export default (() => {
const menu = menuOutline ({ tag: null, wikiId: null, user: { } as User }) const menu = menuOutline (
{ tag: null, wikiId: null, user: { } as User, pathName: location.pathname })
return ( return (
<MainArea> <MainArea>