From d5e35f0049e10191d2c0ba0fa8ed1a06fbc9cf15 Mon Sep 17 00:00:00 2001 From: miteruzo Date: Tue, 16 Dec 2025 00:36:55 +0900 Subject: [PATCH 1/2] #183 --- frontend/src/components/TagDetailSidebar.tsx | 2 +- frontend/src/components/TopNav.tsx | 156 +++++++++++++++---- 2 files changed, 127 insertions(+), 31 deletions(-) diff --git a/frontend/src/components/TagDetailSidebar.tsx b/frontend/src/components/TagDetailSidebar.tsx index 0006738..5d02358 100644 --- a/frontend/src/components/TagDetailSidebar.tsx +++ b/frontend/src/components/TagDetailSidebar.tsx @@ -76,7 +76,7 @@ export default (({ post }: Props) => { return ( - + {CATEGORIES.map ((cat: Category) => cat in tags && ( {categoryNames[cat]} diff --git a/frontend/src/components/TopNav.tsx b/frontend/src/components/TopNav.tsx index 35a1a57..57bf4a7 100644 --- a/frontend/src/components/TopNav.tsx +++ b/frontend/src/components/TopNav.tsx @@ -1,7 +1,7 @@ import axios from 'axios' import toCamel from 'camelcase-keys' 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 Separator from '@/components/MenuSeparator' @@ -20,9 +20,31 @@ type Props = { user: User | null } export default (({ user }: Props) => { const location = useLocation () + const itemRefs = useRef<(HTMLAnchorElement | null)[]> ([]) + const navRef = useRef (null) + + const measure = () => { + const nav = navRef.current + const el = itemRefs.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 [openItemIdx, setOpenItemIdx] = useState (-1) const [postCount, setPostCount] = useState (null) + const [subDir, setSubDir] = useState<(-1) | 1> (1) const [wikiId, setWikiId] = useState (WikiIdBus.get ()) const wikiPageFlg = Boolean (/^\/wiki\/(?!new|changes)[^\/]+/.test (location.pathname) && wikiId) @@ -54,11 +76,38 @@ export default (({ user }: Props) => { { name: 'お前', to: `/users/${ user?.id }`, visible: false }, { name: '設定', to: '/users/settings', visible: Boolean (user) }] }] + const activeIdx = menu.findIndex (item => location.pathname.startsWith (item.base || item.to)) + + const prevActiveIdxRef = useRef (activeIdx) + + useLayoutEffect (() => { + if (activeIdx < 0) + return + + const raf = requestAnimationFrame (measure) + const onResize = () => requestAnimationFrame (measure) + + addEventListener ('resize', onResize) + return () => { + cancelAnimationFrame (raf) + removeEventListener ('resize', onResize) + } + }, [activeIdx]) + useEffect (() => { const unsubscribe = WikiIdBus.subscribe (setWikiId) return () => unsubscribe () }, []) + useEffect (() => { + const prev = prevActiveIdxRef.current + if (prev !== activeIdx) + { + setSubDir (activeIdx > prev ? 1 : -1) + prevActiveIdxRef.current = activeIdx + } + }, [activeIdx]) + useEffect (() => { setMenuOpen (false) setOpenItemIdx (menu.findIndex (item => ( @@ -99,16 +148,28 @@ export default (({ user }: Props) => { ぼざクリ タグ広場 - {menu.map ((item, i) => ( - - {item.name} - - ))} +
+
+ + {menu.map ((item, i) => ( + { + itemRefs.current[i] = el + }} + className={cn ('relative z-10 flex h-full items-center px-5', + ((i === openItemIdx) + ? 'font-bold' + : 'opacity-90 hover:opacity-100'))}> + {item.name} + ))} +
@@ -126,15 +187,28 @@ export default (({ user }: Props) => {
- {menu.find (item => location.pathname.startsWith (item.base || item.to))?.subMenu - .filter (item => item.visible ?? true) - .map ((item, i) => 'component' in item ? item.component : ( - - {item.name} - ))} + items-center w-full min-h-[40px] px-3 overflow-hidden"> + + + {(menu[activeIdx]?.subMenu ?? []) + .filter (item => item.visible ?? true) + .map ((item, i) => ( + 'component' in item + ? {item.component} + : ( + + {item.name} + )))} + +
@@ -167,16 +241,38 @@ export default (({ user }: Props) => { }}> {item.name} - {i === openItemIdx && ( - item.subMenu - .filter (subItem => subItem.visible ?? true) - .map ((subItem, j) => 'component' in subItem ? subItem.component : ( - - {subItem.name} - )))} + + + {i === openItemIdx && ( + + {item.subMenu + .filter (subItem => subItem.visible ?? true) + .map ((subItem, j) => ( + 'component' in subItem + ? ( + + {subItem.component} + ) + : ( + + {subItem.name} + )))} + )} + ))} -- 2.34.1 From 8a5bb5c7062f61390c1e0ca37376de50d1cbcf66 Mon Sep 17 00:00:00 2001 From: miteruzo Date: Tue, 16 Dec 2025 22:11:58 +0900 Subject: [PATCH 2/2] #183 --- frontend/src/components/TopNav.tsx | 53 +++++++++++++++--------------- 1 file changed, 27 insertions(+), 26 deletions(-) diff --git a/frontend/src/components/TopNav.tsx b/frontend/src/components/TopNav.tsx index 57bf4a7..9762a3d 100644 --- a/frontend/src/components/TopNav.tsx +++ b/frontend/src/components/TopNav.tsx @@ -20,12 +20,13 @@ type Props = { user: User | null } export default (({ user }: Props) => { const location = useLocation () - const itemRefs = useRef<(HTMLAnchorElement | null)[]> ([]) + const dirRef = useRef<(-1) | 1> (1) + const itemsRef = useRef<(HTMLAnchorElement | null)[]> ([]) const navRef = useRef (null) const measure = () => { const nav = navRef.current - const el = itemRefs.current[activeIdx] + const el = itemsRef.current[activeIdx] if (!(nav) || !(el) || activeIdx < 0) return @@ -44,7 +45,6 @@ export default (({ user }: Props) => { const [menuOpen, setMenuOpen] = useState (false) const [openItemIdx, setOpenItemIdx] = useState (-1) const [postCount, setPostCount] = useState (null) - const [subDir, setSubDir] = useState<(-1) | 1> (1) const [wikiId, setWikiId] = useState (WikiIdBus.get ()) const wikiPageFlg = Boolean (/^\/wiki\/(?!new|changes)[^\/]+/.test (location.pathname) && wikiId) @@ -80,6 +80,14 @@ export default (({ user }: Props) => { const prevActiveIdxRef = useRef (activeIdx) + if (activeIdx !== prevActiveIdxRef.current) + { + dirRef.current = activeIdx > prevActiveIdxRef.current ? 1 : -1 + prevActiveIdxRef.current = activeIdx + } + + const dir = dirRef.current + useLayoutEffect (() => { if (activeIdx < 0) return @@ -99,15 +107,6 @@ export default (({ user }: Props) => { return () => unsubscribe () }, []) - useEffect (() => { - const prev = prevActiveIdxRef.current - if (prev !== activeIdx) - { - setSubDir (activeIdx > prev ? 1 : -1) - prevActiveIdxRef.current = activeIdx - } - }, [activeIdx]) - useEffect (() => { setMenuOpen (false) setOpenItemIdx (menu.findIndex (item => ( @@ -150,9 +149,9 @@ export default (({ user }: Props) => {
@@ -161,12 +160,10 @@ export default (({ user }: Props) => { { - itemRefs.current[i] = el + itemsRef.current[i] = el }} className={cn ('relative z-10 flex h-full items-center px-5', - ((i === openItemIdx) - ? 'font-bold' - : 'opacity-90 hover:opacity-100'))}> + (i === openItemIdx) && 'font-bold')}> {item.name} ))}
@@ -186,16 +183,20 @@ export default (({ user }: Props) => { -
- +
+ + 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) => ( -- 2.34.1