このコミットが含まれているのは:
2026-04-12 20:08:06 +09:00
コミット 0c805671a0
9個のファイルの変更210行の追加127行の削除
+146 -62
ファイルの表示
@@ -14,7 +14,7 @@ import { fetchWikiPage } from '@/lib/wiki'
import type { FC, MouseEvent } from 'react'
import type { Menu, Tag, User } from '@/types'
import type { Menu, MenuVisibleItem, Tag, User } from '@/types'
type Props = { user: User | null }
@@ -78,9 +78,9 @@ export default (({ user }: Props) => {
const itemsRef = useRef<(HTMLAnchorElement | null)[]> ([])
const navRef = useRef<HTMLDivElement | null> (null)
const measure = () => {
const measure = (idx: number) => {
const nav = navRef.current
const el = itemsRef.current[activeIdx < 0 ? menu.length : activeIdx]
const el = itemsRef.current[idx < 0 ? menu.length : idx]
if (!(nav) || !(el))
{
@@ -101,6 +101,7 @@ export default (({ user }: Props) => {
width: 0,
visible: false })
const [menuOpen, setMenuOpen] = useState (false)
const [moreVsbl, setMoreVsbl] = useState (false)
const [openItemIdx, setOpenItemIdx] = useState (-1)
const [wikiId, setWikiId] = useState<number | null> (WikiIdBus.get ())
@@ -120,8 +121,8 @@ export default (({ user }: Props) => {
const menu = menuOutline ({ tag, wikiId, user })
const activeIdx = (menu
.filter (item => item.visible ?? true)
.findIndex (item => location.pathname.startsWith (item.base || item.to!)))
.filter ((item): item is MenuVisibleItem => item.visible ?? true)
.findIndex (item => location.pathname.startsWith (item.base || item.to)))
const prevActiveIdxRef = useRef<number> (activeIdx)
@@ -134,8 +135,8 @@ export default (({ user }: Props) => {
const dir = dirRef.current
useLayoutEffect (() => {
const raf = requestAnimationFrame (measure)
const onResize = () => requestAnimationFrame (measure)
const raf = requestAnimationFrame (() => measure (activeIdx))
const onResize = () => requestAnimationFrame (() => measure (activeIdx))
addEventListener ('resize', onResize)
return () => {
@@ -151,8 +152,9 @@ export default (({ user }: Props) => {
useEffect (() => {
setMenuOpen (false)
setOpenItemIdx (menu.filter (item => item.visible ?? true).findIndex (item => (
location.pathname.startsWith (item.base || item.to!))))
setOpenItemIdx (menu
.filter ((item): item is MenuVisibleItem => item.visible ?? true)
.findIndex (item => location.pathname.startsWith (item.base || item.to)))
}, [location])
return (
@@ -179,22 +181,36 @@ export default (({ user }: Props) => {
transform: `translateX(${ hl.left }px)`,
opacity: hl.visible ? 1 : 0 }}/>
{menu.filter (item => item.visible ?? true).map ((item, i) => (
<PrefetchLink
key={i}
to={item.to!}
ref={(el: (HTMLAnchorElement | null)) => {
itemsRef.current[i] = el
}}
className={cn ('relative z-10 flex h-full items-center px-5',
(i === openItemIdx) && 'font-bold')}>
{item.name}
</PrefetchLink>))}
{menu.filter ((item): item is MenuVisibleItem => item.visible ?? true)
.map ((item, i) => (
<motion.div
key={item.to}
layoutId={`menu-${ item.name }`}
onMouseEnter={() => {
setMoreVsbl (false)
setTimeout (() => measure (activeIdx), 300)
}}>
<PrefetchLink
to={item.to}
ref={(el: (HTMLAnchorElement | null)) => {
itemsRef.current[i] = el
}}
className={cn ('relative z-10 flex h-full items-center px-5',
(i === openItemIdx) && 'font-bold',
moreVsbl && 'pointer-events-none')}>
{item.name}
</PrefetchLink>
</motion.div>))}
<PrefetchLink
to="/more"
to="#"
ref={(el: (HTMLAnchorElement | null)) => {
itemsRef.current[menu.length] = el
}}
onClick={e => e.preventDefault ()}
onMouseEnter={() => {
setMoreVsbl (true)
measure (-1)
}}
className={cn ('relative z-10 flex h-full items-center px-5',
(openItemIdx < 0) && 'font-bold')}>
&raquo;
@@ -217,45 +233,110 @@ export default (({ user }: Props) => {
</nav>
<AnimatePresence initial={false}>
{(menu[activeIdx]?.subMenu ?? []).length > 0 && (
<motion.div
key="submenu-shell"
className="relative hidden md:block overflow-hidden
bg-yellow-200 dark:bg-red-950"
initial={{ height: 0 }}
animate={{ height: 40 }}
exit={{ height: 0 }}
transition={{ duration: .2, ease: 'easeOut' }}>
<div className="relative h-[40px]">
<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>
<motion.div
key="submenu-shell"
className="relative hidden md:block overflow-hidden
bg-yellow-200 dark:bg-red-950"
onMouseLeave={() => {
if (!(moreVsbl))
return
setMoreVsbl (false)
setTimeout (() => measure (activeIdx), 300)
}}
initial={{ height: 0 }}
animate={{ height: (moreVsbl ? 40 * menu.length : (activeIdx < 0 ? 0 : 40)) }}
exit={{ height: 0 }}
transition={{ duration: .2, ease: 'easeOut' }}>
{moreVsbl
? (
menu.map ((item, i) => (
<div key={i} className="relative h-[40px]">
<div className="absolute inset-0 flex items-center px-3">
<motion.h2
{...((item.visible ?? true)
? { layoutId: `menu-${ item.name }` }
: { initial: { y: -40, opacity: 0 },
animate: { y: 0, opacity: 1 },
exit: { y: -40, opacity: 0 },
transition: { duration: .2, ease: 'easeOut' } })}
className="z-10 h-full flex items-center px-3 font-bold w-24">
{item.name}
</motion.h2>
{item.subMenu
.filter (subItem => subItem.visible ?? true)
.map ((subItem, j) => (
'component' in subItem
? (
<motion.div
key={`c-${ i }-${ j }`}
{...((menu.filter (x => x.visible ?? true)[activeIdx]?.name
=== item.name)
? { layoutId: `submenu-${ item.name }-${ j }` }
: { initial: { y: -40, opacity: 0 },
animate: { y: 0, opacity: 1 },
exit: { y: -40, opacity: 0 },
transition: { duration: .2, ease: 'easeOut' } })}>
{subItem.component}
</motion.div>)
: (
<PrefetchLink
key={`l-${ i }`}
to={item.to}
target={item.to.slice (0, 2) === '//' ? '_blank' : undefined}
className="h-full flex items-center px-3">
{item.name}
</PrefetchLink>)))}
</motion.div>
</AnimatePresence>
</div>
</motion.div>)}
<motion.div
key={`l-${ i }-${ j }`}
{...((menu.filter (x => x.visible ?? true)[activeIdx]?.name
=== item.name)
&& { layoutId: `submenu-${ item.name }-${ j }` })}>
<PrefetchLink
to={subItem.to}
target={subItem.to.slice (0, 2) === '//' ? '_blank' : undefined}
className="h-full flex items-center px-3">
{subItem.name}
</PrefetchLink>
</motion.div>)))}
</div>
</div>)))
: ((menu.filter (item => item.visible ?? true)[activeIdx]?.subMenu ?? []).length > 0
&& (
<div className="relative h-[40px]">
<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.filter (item => item.visible ?? true)[activeIdx]?.subMenu ?? [])
.filter (item => item.visible ?? true)
.map ((item, i) => (
'component' in item
? (
<motion.div
key={`c-${ i }`}
layoutId={`submenu-${
menu.filter (x => x.visible ?? true)[activeIdx].name }-${
i }`}>
{item.component}
</motion.div>)
: (
<motion.div
key={`l-${ i }`}
layoutId={`submenu-${
menu.filter (x => x.visible ?? true)[activeIdx].name }-${
i }`}>
<PrefetchLink
to={item.to}
target={item.to.slice (0, 2) === '//' ? '_blank' : undefined}
className="h-full flex items-center px-3">
{item.name}
</PrefetchLink>
</motion.div>)))}
</motion.div>
</AnimatePresence>
</div>))}
</motion.div>
</AnimatePresence>
<AnimatePresence initial={false}>
@@ -273,10 +354,11 @@ export default (({ user }: Props) => {
exit="closed"
transition={{ duration: .2, ease: 'easeOut' }}>
<Separator/>
{menu.filter (item => item.visible ?? true).map ((item, i) => (
{menu.filter ((item): item is MenuVisibleItem => item.visible ?? true)
.map ((item, i) => (
<Fragment key={i}>
<PrefetchLink
to={i === openItemIdx ? item.to! : '#'}
to={i === openItemIdx ? item.to : '#'}
className={cn ('w-full min-h-[40px] flex items-center pl-8',
((i === openItemIdx)
&& 'font-bold bg-yellow-50 dark:bg-red-950'))}
@@ -331,7 +413,9 @@ export default (({ user }: Props) => {
ref={(el: (HTMLAnchorElement | null)) => {
itemsRef.current[menu.length] = el
}}
className="w-full min-h-[40px] flex items-center pl-8">
className={cn ('w-full min-h-[40px] flex items-center pl-8',
((openItemIdx < 0)
&& 'font-bold bg-yellow-50 dark:bg-red-950'))}>
&raquo;
</PrefetchLink>
<TopNavUser user={user} sp/>
+1 -2
ファイルの表示
@@ -8,7 +8,6 @@ type Props = {
export default (({ children, className }: Props) => (
<main className={cn ('flex-1 overflow-y-auto p-4 md:h-[calc(100dvh-88px)]',
className)}>
<main className={cn ('flex-1 overflow-y-auto p-4', className)}>
{children}
</main>)) satisfies FC<Props>
+1 -4
ファイルの表示
@@ -6,10 +6,7 @@ type Props = { children: ReactNode }
export default (({ children }: Props) => (
<div
className="p-4 w-full md:w-64 md:h-full
md:h-[calc(100dvh-88px)] md:overflow-y-auto
sidebar">
<div className="p-4 w-full md:w-64 md:h-full md:overflow-y-auto sidebar">
<Helmet>
<style>
{`