このコミットが含まれているのは:
2026-10-17 19:59:18 +09:00
コミット f063fc54bb
17個のファイルの変更、276行の追加、123行の削除
+30 -5
ファイルの表示
@@ -1,3 +1,4 @@
import { useQuery } from '@tanstack/react-query'
import { AnimatePresence, LayoutGroup, MotionConfig, motion } from 'framer-motion'
import { Fragment, useEffect, useMemo, useState } from 'react'
import { createBrowserRouter,
@@ -14,7 +15,8 @@ import TopNav from '@/components/TopNav'
import DialogueProvider from '@/components/dialogues/DialogueProvider'
import { Toaster } from '@/components/ui/toaster'
import { apiPost, isApiError } from '@/lib/api'
import { isLocaleCode, legacyLocalePath, switchLocalePath } from '@/lib/locales'
import { fetchLocales, isLocaleCode, legacyLocalePath, switchLocalePath } from '@/lib/locales'
import { localesKeys } from '@/lib/queryKeys'
import { useLocale } from '@/lib/useLocale'
import { applyClientAnimationMode,
applyClientAppearance,
@@ -65,6 +67,7 @@ import WikiSearchPage from '@/pages/wiki/WikiSearchPage'
import type { Dispatch, FC, SetStateAction } from 'react'
import type { ClientAnimationMode } from '@/lib/settings'
import type { AvailableLocale } from '@/lib/locales'
import type { User } from '@/types'
@@ -131,7 +134,7 @@ const PostDetailRoute = ({ user }: { user: User | null }) => {
}
const RoutedApp: FC = () => {
const RoutedApp = ({ locales }: { locales: AvailableLocale[] }) => {
const [user, setUser] = useState<User | null> (null)
const [status, setStatus] = useState (200)
const behaviourSettings = useClientBehaviourSettings ()
@@ -246,7 +249,7 @@ const RoutedApp: FC = () => {
layout={animationMode === 'off' ? false : 'position'}
transition={{ layout: appLayoutTransition }}
className="relative flex h-dvh w-full flex-col overflow-y-hidden">
<TopNav user={user}/>
<TopNav user={user} locales={locales}/>
<RouteTransitionWrapper
animationMode={animationMode}
user={user}
@@ -265,10 +268,32 @@ const RoutedApp: FC = () => {
const LocaleRoute: FC = () => {
const { locale } = useParams ()
const location = useLocation ()
if (isLocaleCode (locale))
return <RoutedApp/>
const { data, isPending, isError, refetch } = useQuery ({
queryKey: localesKeys.index,
queryFn: fetchLocales,
staleTime: 0,
refetchOnMount: 'always',
refetchOnWindowFocus: 'always',
refetchOnReconnect: 'always' })
useEffect (() => {
void refetch ()
}, [location.key, refetch])
const redirect = legacyLocalePath (location)
if (!(isLocaleCode (locale)) && redirect == null)
return <NotFound/>
if (isError)
return <ServiceUnavailable/>
if (isPending)
return null
if (isLocaleCode (locale))
{
return data.locales.some (entry => entry.code === locale)
? <RoutedApp locales={data.locales}/>
: <NotFound/>
}
return redirect == null ? <NotFound/> : <Navigate to={redirect} replace/>
}
+54 -37
ファイルの表示
@@ -1,5 +1,6 @@
import { useQuery } from '@tanstack/react-query'
import { AnimatePresence, motion } from 'framer-motion'
import { Globe } from 'lucide-react'
import { Fragment, useEffect, useLayoutEffect, useRef, useState } from 'react'
import { useLocation } from 'react-router-dom'
@@ -8,9 +9,7 @@ import Separator from '@/components/MenuSeparator'
import PrefetchLink from '@/components/PrefetchLink'
import TopNavUser from '@/components/TopNavUser'
import { WikiIdBus } from '@/lib/eventBus/WikiIdBus'
import { LOCALE_LABELS,
SUPPORTED_LOCALES,
isLocaleCode,
import { isLocaleCode,
localeFromPath,
switchLocalePath,
unlocalisedPath } from '@/lib/locales'
@@ -25,9 +24,10 @@ import { fetchWikiPage } from '@/lib/wiki'
import type { FC, MouseEvent } from 'react'
import type { AvailableLocale } from '@/lib/locales'
import type { Material, Menu, MenuVisibleItem, Tag, User } from '@/types'
type Props = { user: User | null }
type Props = { user: User | null; locales: AvailableLocale[] }
type NavDensity = 'auto' | 'comfortable' | 'compact'
@@ -116,8 +116,9 @@ export const menuOutline = (
}
const TopNav: FC<Props> = ({ user }) => {
const TopNav: FC<Props> = ({ user, locales }) => {
const locale = useLocale ()
const localeName = locales.find (entry => entry.code === locale)?.name
const navigate = useLocaleNavigate ()
const location = useLocation ()
const behaviourSettings = useClientBehaviourSettings ()
@@ -229,7 +230,7 @@ const TopNav: FC<Props> = ({ user }) => {
if (layout == null || bar == null || primary == null)
return
const userLink = bar.querySelector<HTMLElement> ('.top-nav-user-desktop')
const account = bar.querySelector<HTMLElement> ('.top-nav-account')
let raf = 0
let active = true
@@ -252,9 +253,9 @@ const TopNav: FC<Props> = ({ user }) => {
layout.setAttribute ('data-density', candidate)
const style = getComputedStyle (bar)
const required = primary.getBoundingClientRect ().width
+ (userLink?.getBoundingClientRect ().width ?? 0)
+ (account?.getBoundingClientRect ().width ?? 0)
+ parseFloat (style.paddingLeft) + parseFloat (style.paddingRight)
+ (userLink == null ? 0 : parseFloat (style.columnGap))
+ (account == null ? 0 : parseFloat (style.columnGap))
if (required <= bar.clientWidth)
return candidate
}
@@ -283,8 +284,8 @@ const TopNav: FC<Props> = ({ user }) => {
typeof ResizeObserver === 'undefined' ? null : new ResizeObserver (schedule)
observer?.observe (bar)
observer?.observe (primary)
if (userLink != null)
observer?.observe (userLink)
if (account != null)
observer?.observe (account)
update ()
addEventListener ('resize', schedule)
document.fonts?.addEventListener ('loadingdone', schedule)
@@ -297,7 +298,7 @@ const TopNav: FC<Props> = ({ user }) => {
removeEventListener ('resize', schedule)
document.fonts?.removeEventListener ('loadingdone', schedule)
}
}, [activeIdx, moreVsbl, desktop, density, user?.name])
}, [activeIdx, moreVsbl, desktop, density, user?.name, locale, locales])
useEffect (() => {
const unsubscribe = WikiIdBus.subscribe (setWikiId)
@@ -309,6 +310,32 @@ const TopNav: FC<Props> = ({ user }) => {
setOpenItemIdx (activeIdx)
}, [activeIdx, location])
const localeSelector = (
<label className="top-nav-menu-link relative flex h-8 shrink-0 items-center gap-1
rounded px-1 focus-within:ring-2">
<Globe aria-hidden="true" size={16}/>
<span className="top-nav-locale-name whitespace-nowrap text-sm">
{localeName}
</span>
<select
aria-label={localeName}
value={locale}
className="absolute inset-0 h-full w-full cursor-pointer opacity-0"
onChange={event => {
const nextLocale = event.target.value
if (isLocaleCode (nextLocale)
&& locales.some (entry => entry.code === nextLocale))
{
navigate (switchLocalePath (location, nextLocale))
}
}}>
{locales.map (entry => isLocaleCode (entry.code) && (
<option key={entry.code} value={entry.code}>
{entry.name}
</option>))}
</select>
</label>)
return (
<div ref={layoutRef} className="top-nav-layout w-full min-w-0 shrink-0"
data-desktop={desktop} data-density={density}>
@@ -374,35 +401,25 @@ const TopNav: FC<Props> = ({ user }) => {
</div>
</div>
<TopNavUser user={user}/>
<div className="top-nav-account ml-auto flex min-w-0 items-center gap-2">
<TopNavUser user={user}/>
{localeSelector}
</div>
<button
type="button"
className="top-nav-mobile-toggle shrink-0 whitespace-nowrap
ml-auto border-0 bg-transparent pr-4"
onClick={() => {
setMenuOpen (!(menuOpen))
}}>
{menuOpen ? '×' : 'Menu'}
</button>
<div className="top-nav-mobile-controls ml-auto flex shrink-0 items-center gap-2">
{menuOpen && localeSelector}
<button
type="button"
className="top-nav-mobile-toggle shrink-0 whitespace-nowrap
border-0 bg-transparent pr-4"
onClick={() => {
setMenuOpen (!(menuOpen))
}}>
{menuOpen ? '×' : 'Menu'}
</button>
</div>
</nav>
<div className="flex min-w-0 justify-end px-2">
<select
aria-label={LOCALE_LABELS[locale]}
value={locale}
className="max-w-full rounded border border-gray-300 bg-white text-xs text-gray-900
dark:border-gray-700 dark:bg-gray-900 dark:text-gray-100"
onChange={event => {
const nextLocale = event.target.value
if (isLocaleCode (nextLocale))
navigate (switchLocalePath (location, nextLocale))
}}>
{SUPPORTED_LOCALES.map (code => (
<option key={code} value={code}>{LOCALE_LABELS[code]}</option>))}
</select>
</div>
<AnimatePresence initial={false}>
<motion.div
key="submenu-shell"
+1 -2
ファイルの表示
@@ -23,8 +23,7 @@ const TopNavUser: FC<Props> = ({ user, sp }) => {
return (
<>
{sp && <Separator/>}
<PrefetchLink to="/users/settings"
className={className}>
<PrefetchLink to="/users/settings" className={className}>
{user.name || '名もなきニジラー'}
</PrefetchLink>
</>)
+31 -1
ファイルの表示
@@ -216,6 +216,7 @@ body
.top-nav-desktop-menu,
.top-nav-desktop-submenu,
.top-nav-layout .top-nav-account,
.top-nav-user-desktop
{
display: none;
@@ -242,8 +243,33 @@ body
overflow-wrap: anywhere;
}
.top-nav-account .top-nav-user-desktop
{
min-width: 0;
max-width: 24vw;
overflow: hidden;
text-overflow: ellipsis;
}
.top-nav-locale-name
{
display: none;
}
@media (min-width: 768px)
{
.top-nav-layout[data-desktop='true'] .top-nav-locale-name,
.top-nav-layout[data-measuring] .top-nav-locale-name
{
display: inline;
}
.top-nav-layout[data-desktop='true'] .top-nav-account .top-nav-user-desktop,
.top-nav-layout[data-measuring] .top-nav-account .top-nav-user-desktop
{
max-width: none;
}
.top-nav-layout .top-nav-brand-link
{
font-size: 1.125rem;
@@ -255,7 +281,9 @@ body
}
.top-nav-layout[data-desktop='true'] .top-nav-desktop-menu,
.top-nav-layout[data-measuring] .top-nav-desktop-menu
.top-nav-layout[data-measuring] .top-nav-desktop-menu,
.top-nav-layout[data-desktop='true'] .top-nav-account,
.top-nav-layout[data-measuring] .top-nav-account
{
display: flex;
}
@@ -269,6 +297,8 @@ body
.top-nav-layout[data-desktop='true'] .top-nav-mobile-toggle,
.top-nav-layout[data-measuring] .top-nav-mobile-toggle,
.top-nav-layout[data-desktop='true'] .top-nav-mobile-controls,
.top-nav-layout[data-measuring] .top-nav-mobile-controls,
.top-nav-layout[data-desktop='true'] .top-nav-mobile-menu,
.top-nav-layout[data-desktop='true'] .top-nav-user-mobile
{
+9
ファイルの表示
@@ -1,9 +1,18 @@
import { apiGet } from '@/lib/api'
import type { To } from 'react-router-dom'
export const SUPPORTED_LOCALES = ['ja', 'ain-Kana'] as const
export type LocaleCode = typeof SUPPORTED_LOCALES[number]
export type AvailableLocale = { code: string; name: string }
export const fetchLocales = (): Promise<{ locales: AvailableLocale[] }> =>
apiGet ('/locales')
export const LOCALE_LABELS: Record<LocaleCode, string> = {
ja: '日本語', 'ain-Kana': 'ヤウンモシルン アイヌ イタㇰ' }
+4
ファイルの表示
@@ -7,6 +7,10 @@ import type {
MaterialFilter,
} from '@/types'
export const localesKeys = {
root: ['locales'] as const,
index: ['locales', 'index'] as const }
export const postsKeys = {
root: ['posts'] as const,
index: (