From 7c25dedac24440249d9f1c7fca9e19ed6d5b3403 Mon Sep 17 00:00:00 2001 From: miteruzo Date: Mon, 12 Oct 2026 15:43:32 +0900 Subject: [PATCH] #424 --- .../app/controllers/wiki_pages_controller.rb | 1 + frontend/src/components/TopNav.tsx | 121 +++++++-- frontend/src/components/TopNavUser.tsx | 4 +- frontend/src/index.css | 236 +++++++++++++++++- frontend/src/pages/help/HelpChangesPage.tsx | 11 +- frontend/src/pages/help/HelpDetailPage.tsx | 3 +- frontend/src/pages/help/HelpListPage.tsx | 2 +- 7 files changed, 343 insertions(+), 35 deletions(-) diff --git a/backend/app/controllers/wiki_pages_controller.rb b/backend/app/controllers/wiki_pages_controller.rb index c00d55c..840312b 100644 --- a/backend/app/controllers/wiki_pages_controller.rb +++ b/backend/app/controllers/wiki_pages_controller.rb @@ -22,6 +22,7 @@ class WikiPagesController < ApplicationController json = descriptions.map { |description| WikiPageRepr.tag_description(description) } json.concat(tag_groups.map { WikiPageRepr.tag_group(_1) }) + json.sort! { |a, b| b['updated_at'] <=> a['updated_at'] } json = json.first(20) if title.present? render json: end diff --git a/frontend/src/components/TopNav.tsx b/frontend/src/components/TopNav.tsx index 4853e31..623f505 100644 --- a/frontend/src/components/TopNav.tsx +++ b/frontend/src/components/TopNav.tsx @@ -20,6 +20,7 @@ import type { FC, MouseEvent } from 'react' import type { Material, Menu, MenuVisibleItem, Tag, User } from '@/types' type Props = { user: User | null } +type NavDensity = 'auto' | 'comfortable' | 'compact' export const menuOutline = ( @@ -40,6 +41,9 @@ export const menuOutline = ( const materialFlg = /^\/materials\/\d+/.test (pathName) + const helpVsbl = /\/help\/((?!changes)[^\/?]+)/.test (pathName) + const helpIdentifier = pathName.split ('/')[2] ?? '' + return [ { name: '広場', to: '/posts', subMenu: [ { name: '一覧', to: '/posts' }, @@ -84,7 +88,9 @@ export const menuOutline = ( visible: wikiPageFlg && editable }] }, { name: 'ヘルプ', to: '/help/home', base: '/help', subMenu: [ { name: '検索', to: '/help' }, - { name: '全体履歴', to: '/help/changes' }] }, + { name: '全体履歴', to: '/help/changes' }, + { component: , visible: helpVsbl }, + { name: '履歴', to: `/help/changes?identifier=${ helpIdentifier }`, visible: helpVsbl }] }, { name: 'おたのしみ', visible: false, subMenu: [ { name: '上映会 (β)', to: '/theatres/1' }, { name: 'グカネータ (β)', to: '/gekanator' }] }, @@ -107,6 +113,8 @@ const TopNav: FC = ({ user }) => { const dirRef = useRef<(-1) | 1> (1) const itemsRef = useRef<(HTMLAnchorElement | null)[]> ([]) const navRef = useRef (null) + const layoutRef = useRef (null) + const barRef = useRef (null) const measure = (idx: number) => { const nav = navRef.current @@ -131,6 +139,8 @@ const TopNav: FC = ({ user }) => { width: 0, visible: false }) const [menuOpen, setMenuOpen] = useState (false) + const [desktop, setDesktop] = useState (false) + const [density, setDensity] = useState ('auto') const [moreVsbl, setMoreVsbl] = useState (false) const [openItemIdx, setOpenItemIdx] = useState (-1) const [wikiId, setWikiId] = useState (WikiIdBus.get ()) @@ -197,15 +207,81 @@ const TopNav: FC = ({ user }) => { const dir = dirRef.current useLayoutEffect (() => { - const raf = requestAnimationFrame (() => measure (moreVsbl ? -1 : activeIdx)) - const onResize = () => requestAnimationFrame (() => measure (moreVsbl ? -1 : activeIdx)) + const layout = layoutRef.current + const bar = barRef.current + const primary = bar?.querySelector ('.top-nav-primary') + if (layout == null || bar == null || primary == null) + return - addEventListener ('resize', onResize) - return () => { - cancelAnimationFrame (raf) - removeEventListener ('resize', onResize) + const userLink = bar.querySelector ('.top-nav-user-desktop') + let raf = 0 + let active = true + + const update = () => { + if (!(window.matchMedia ('(min-width: 768px)').matches)) + { + setDesktop (false) + setDensity ('auto') + return + } + + // Measure the same desktop elements while the mobile layout is active. + layout.setAttribute ('data-measuring', '') + const previousDensity = layout.getAttribute ('data-density') ?? 'auto' + const fittingDensity = (() => { + try + { + for (const candidate of ['auto', 'comfortable', 'compact'] as const) + { + layout.setAttribute ('data-density', candidate) + const style = getComputedStyle (bar) + const required = primary.getBoundingClientRect ().width + + (userLink?.getBoundingClientRect ().width ?? 0) + + parseFloat (style.paddingLeft) + parseFloat (style.paddingRight) + + (userLink == null ? 0 : parseFloat (style.columnGap)) + if (required <= bar.clientWidth) + return candidate + } + return null + } + finally + { + layout.setAttribute ('data-density', previousDensity) + layout.removeAttribute ('data-measuring') + } + }) () + setDensity (fittingDensity ?? 'compact') + setDesktop (fittingDensity != null) + if (fittingDensity != null) + measure (moreVsbl ? -1 : activeIdx) } - }) + + const schedule = () => { + if (!(active)) + return + cancelAnimationFrame (raf) + raf = requestAnimationFrame (update) + } + + const observer = + typeof ResizeObserver === 'undefined' ? null : new ResizeObserver (schedule) + observer?.observe (bar) + observer?.observe (primary) + if (userLink != null) + observer?.observe (userLink) + update () + addEventListener ('resize', schedule) + document.fonts?.addEventListener ('loadingdone', schedule) + void document.fonts?.ready.then (schedule) + + return () => { + active = false + cancelAnimationFrame (raf) + observer?.disconnect () + removeEventListener ('resize', schedule) + document.fonts?.removeEventListener ('loadingdone', schedule) + } + }, [activeIdx, moreVsbl, desktop, density, user?.name]) useEffect (() => { const unsubscribe = WikiIdBus.subscribe (setWikiId) @@ -218,20 +294,22 @@ const TopNav: FC = ({ user }) => { }, [activeIdx, location]) return ( - <> -