このコミットが含まれているのは:
@@ -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')}>
|
||||
その他 »
|
||||
@@ -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'))}>
|
||||
その他 »
|
||||
</PrefetchLink>
|
||||
<TopNavUser user={user} sp/>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
{`
|
||||
|
||||
新しい課題から参照
ユーザをブロックする