このコミットが含まれているのは:
2026-07-06 23:37:21 +09:00
コミット f2651a987a
17個のファイルの変更273行の追加68行の削除
+50 -5
ファイルの表示
@@ -1,5 +1,5 @@
import { AnimatePresence, LayoutGroup, MotionConfig, motion } from 'framer-motion' import { AnimatePresence, LayoutGroup, MotionConfig, motion } from 'framer-motion'
import { useEffect, useMemo, useState } from 'react' import { Fragment, useEffect, useMemo, useState } from 'react'
import { BrowserRouter, import { BrowserRouter,
Navigate, Navigate,
Route, Route,
@@ -14,6 +14,7 @@ import { Toaster } from '@/components/ui/toaster'
import { apiPost, isApiError } from '@/lib/api' import { apiPost, isApiError } from '@/lib/api'
import { applyClientAnimationMode, import { applyClientAnimationMode,
applyClientAppearance, applyClientAppearance,
type ClientAnimationMode,
fetchUserThemeSlots, fetchUserThemeSlots,
fetchUserSettings, fetchUserSettings,
getClientThemeMode, getClientThemeMode,
@@ -61,11 +62,51 @@ import type { Dispatch, FC, SetStateAction } from 'react'
import type { User } from '@/types' import type { User } from '@/types'
const RouteTransitionWrapper = ({ user, setUser }: { const RouteTransitionWrapper = ({ animationMode, user, setUser }: {
animationMode: ClientAnimationMode
user: User | null user: User | null
setUser: Dispatch<SetStateAction<User | null>> }) => { setUser: Dispatch<SetStateAction<User | null>> }) => {
const location = useLocation () const location = useLocation ()
if (animationMode === 'off')
{
return (
<Routes location={location}>
<Route path="/" element={<Navigate to="/posts" replace/>}/>
<Route path="/posts" element={<PostListPage/>}/>
<Route path="/posts/new" element={<PostNewPage user={user}/>}/>
<Route path="/posts/search" element={<PostSearchPage/>}/>
<Route path="/posts/:id" element={<PostDetailRoute user={user}/>}/>
<Route path="/posts/changes" element={<PostHistoryPage/>}/>
<Route path="/tags" element={<TagListPage/>}/>
<Route path="/tags/:id" element={<TagDetailPage/>}/>
<Route path="/tags/:id/deerjikists" element={<DeerjikistDetailPage/>}/>
<Route path="/tags/nico" element={<NicoTagListPage user={user}/>}/>
<Route path="/nico/tags" element={<NicoTagListPage user={user}/>}/>
<Route path="/tags/changes" element={<TagHistoryPage/>}/>
<Route path="/theatres/:id" element={<TheatreDetailPage user={user}/>}/>
<Route path="/materials" element={<MaterialBasePage/>}>
<Route index element={<MaterialListPage/>}/>
<Route path="changes" element={<MaterialHistoryPage/>}/>
<Route path="new" element={<MaterialNewPage/>}/>
<Route path="suppressions" element={<MaterialSyncSuppressionsPage/>}/>
<Route path=":id" element ={<MaterialDetailPage/>}/>
</Route>
<Route path="/wiki" element={<WikiSearchPage/>}/>
<Route path="/wiki/:title" element={<WikiDetailPage/>}/>
<Route path="/wiki/new" element={<WikiNewPage user={user}/>}/>
<Route path="/wiki/:id/edit" element={<WikiEditPage user={user}/>}/>
<Route path="/wiki/:id/diff" element={<WikiDiffPage/>}/>
<Route path="/wiki/changes" element={<WikiHistoryPage/>}/>
<Route path="/users/settings" element={<SettingPage user={user} setUser={setUser}/>}/>
<Route path="/settings" element={<Navigate to="/users/settings" replace/>}/>
<Route path="/tos" element={<TOSPage/>}/>
<Route path="/gekanator" element={<GekanatorPage user={user}/>}/>
<Route path="/more" element={<MorePage/>}/>
<Route path="*" element={<NotFound/>}/>
</Routes>)
}
return ( return (
<AnimatePresence mode="wait"> <AnimatePresence mode="wait">
<Routes location={location}> <Routes location={location}>
@@ -118,6 +159,7 @@ const App: FC = () => {
const [status, setStatus] = useState (200) const [status, setStatus] = useState (200)
const behaviourSettings = useClientBehaviourSettings () const behaviourSettings = useClientBehaviourSettings ()
const animationMode = behaviourSettings.animation ?? 'normal' const animationMode = behaviourSettings.animation ?? 'normal'
const LayoutWrapper = animationMode === 'off' ? Fragment : LayoutGroup
const appLayoutTransition = useMemo ( const appLayoutTransition = useMemo (
() => ( () => (
@@ -227,15 +269,18 @@ const App: FC = () => {
? 'user' ? 'user'
: 'always' : 'always'
}> }>
<LayoutGroup> <LayoutWrapper>
<motion.div <motion.div
layout={animationMode === 'off' ? false : 'position'} layout={animationMode === 'off' ? false : 'position'}
transition={{ layout: appLayoutTransition }} transition={{ layout: appLayoutTransition }}
className="relative flex flex-col h-dvh w-full overflow-y-hidden"> className="relative flex flex-col h-dvh w-full overflow-y-hidden">
<TopNav user={user}/> <TopNav user={user}/>
<RouteTransitionWrapper user={user} setUser={setUser}/> <RouteTransitionWrapper
animationMode={animationMode}
user={user}
setUser={setUser}/>
</motion.div> </motion.div>
</LayoutGroup> </LayoutWrapper>
</MotionConfig> </MotionConfig>
<Toaster/> <Toaster/>
+12 -2
ファイルの表示
@@ -4,6 +4,8 @@ import { motion } from 'framer-motion'
import { useRef } from 'react' import { useRef } from 'react'
import TagLink from '@/components/TagLink' import TagLink from '@/components/TagLink'
import { clientAnimationTransition } from '@/lib/clientAnimation'
import { useClientBehaviourSettings } from '@/lib/useClientBehaviourSettings'
import { cn } from '@/lib/utils' import { cn } from '@/lib/utils'
import type { CSSProperties, FC, MutableRefObject } from 'react' import type { CSSProperties, FC, MutableRefObject } from 'react'
@@ -20,6 +22,12 @@ type Props = {
const DraggableDroppableTagRow: FC<Props> = ({ tag, nestLevel, pathKey, parentTagId, suppressClickRef, sp }) => { const DraggableDroppableTagRow: FC<Props> = ({ tag, nestLevel, pathKey, parentTagId, suppressClickRef, sp }) => {
const behaviourSettings = useClientBehaviourSettings ()
const animationMode = behaviourSettings.animation ?? 'normal'
const layoutTransition = clientAnimationTransition (
animationMode,
{ normal: { duration: .2, ease: 'easeOut' as const } },
)
const dndId = `tag-node:${ pathKey }` const dndId = `tag-node:${ pathKey }`
const downPosRef = useRef<{ x: number; y: number } | null> (null) const downPosRef = useRef<{ x: number; y: number } | null> (null)
@@ -94,8 +102,10 @@ const DraggableDroppableTagRow: FC<Props> = ({ tag, nestLevel, pathKey, parentTa
{...listeners}> {...listeners}>
<motion.div <motion.div
className="flex min-w-0 max-w-full items-baseline overflow-hidden" className="flex min-w-0 max-w-full items-baseline overflow-hidden"
transition={{ layout: { duration: .2, ease: 'easeOut' } }} transition={{ layout: layoutTransition }}
layoutId={`tag-${ sp ? 'sp-' : '' }${ tag.id }`}> layoutId={animationMode === 'off'
? undefined
: `tag-${ sp ? 'sp-' : '' }${ tag.id }`}>
<TagLink tag={tag} nestLevel={nestLevel}/> <TagLink tag={tag} nestLevel={nestLevel}/>
</motion.div> </motion.div>
</div>) </div>)
+5 -1
ファイルの表示
@@ -1,3 +1,4 @@
import { useClientBehaviourSettings } from '@/lib/useClientBehaviourSettings'
import { cn } from '@/lib/utils' import { cn } from '@/lib/utils'
import { useEffect, useRef, useState } from 'react' import { useEffect, useRef, useState } from 'react'
@@ -20,6 +21,8 @@ const MIN_MARQUEE_OVERFLOW_PX = 1
const ResponsiveMarqueeText: FC<Props> = ( const ResponsiveMarqueeText: FC<Props> = (
{ text, className, truncateOnMobile = false, title }, { text, className, truncateOnMobile = false, title },
) => { ) => {
const behaviourSettings = useClientBehaviourSettings ()
const animationMode = behaviourSettings.animation ?? 'normal'
const outerRef = useRef<HTMLSpanElement | null> (null) const outerRef = useRef<HTMLSpanElement | null> (null)
const staticRef = useRef<HTMLSpanElement | null> (null) const staticRef = useRef<HTMLSpanElement | null> (null)
const animatedRef = useRef<HTMLSpanElement | null> (null) const animatedRef = useRef<HTMLSpanElement | null> (null)
@@ -98,6 +101,7 @@ const ResponsiveMarqueeText: FC<Props> = (
const animated = animatedRef.current const animated = animatedRef.current
const canMarquee = ( const canMarquee = (
active active
&& animationMode !== 'off'
&& desktopMarqueeEnabled && desktopMarqueeEnabled
&& overflowPx >= MIN_MARQUEE_OVERFLOW_PX && overflowPx >= MIN_MARQUEE_OVERFLOW_PX
&& animated != null) && animated != null)
@@ -207,7 +211,7 @@ const ResponsiveMarqueeText: FC<Props> = (
resetAnimated () resetAnimated ()
setMarqueeVisible (false) setMarqueeVisible (false)
} }
}, [active, desktopMarqueeEnabled, overflowPx, text]) }, [active, animationMode, desktopMarqueeEnabled, overflowPx, text])
return ( return (
<span <span
+14 -4
ファイルの表示
@@ -20,6 +20,7 @@ import SidebarComponent from '@/components/layout/SidebarComponent'
import { toast } from '@/components/ui/use-toast' import { toast } from '@/components/ui/use-toast'
import { CATEGORIES, CATEGORY_NAMES } from '@/consts' import { CATEGORIES, CATEGORY_NAMES } from '@/consts'
import { apiDelete, apiGet, apiPatch, apiPost } from '@/lib/api' import { apiDelete, apiGet, apiPatch, apiPost } from '@/lib/api'
import { clientAnimationTransition } from '@/lib/clientAnimation'
import { postsKeys, tagsKeys } from '@/lib/queryKeys' import { postsKeys, tagsKeys } from '@/lib/queryKeys'
import { useClientBehaviourSettings } from '@/lib/useClientBehaviourSettings' import { useClientBehaviourSettings } from '@/lib/useClientBehaviourSettings'
import { dateString, originalCreatedAtString } from '@/lib/utils' import { dateString, originalCreatedAtString } from '@/lib/utils'
@@ -189,6 +190,11 @@ const TagDetailSidebar: FC<Props> = ({ className, post, sp }) => {
const qc = useQueryClient () const qc = useQueryClient ()
const behaviourSettings = useClientBehaviourSettings () const behaviourSettings = useClientBehaviourSettings ()
const tagRelationDisplay = behaviourSettings.tagRelationDisplay ?? 'grouped' const tagRelationDisplay = behaviourSettings.tagRelationDisplay ?? 'grouped'
const animationMode = behaviourSettings.animation ?? 'normal'
const layoutTransition = clientAnimationTransition (
animationMode,
{ normal: { duration: .2, ease: 'easeOut' as const } },
)
const baseTags = useMemo<TagByCategory> (() => { const baseTags = useMemo<TagByCategory> (() => {
const tagsTmp = { } as TagByCategory const tagsTmp = { } as TagByCategory
@@ -360,8 +366,10 @@ const TagDetailSidebar: FC<Props> = ({ className, post, sp }) => {
<div className="my-3" key={cat}> <div className="my-3" key={cat}>
<SubsectionTitle> <SubsectionTitle>
<motion.div <motion.div
layoutId={`tag-${ sp ? 'sp-' : '' }${ cat }`} layoutId={animationMode === 'off'
transition={{ layout: { duration: .2, ease: 'easeOut' } }}> ? undefined
: `tag-${ sp ? 'sp-' : '' }${ cat }`}
transition={{ layout: layoutTransition }}>
{CATEGORY_NAMES[cat]} {CATEGORY_NAMES[cat]}
</motion.div> </motion.div>
</SubsectionTitle> </SubsectionTitle>
@@ -393,8 +401,10 @@ const TagDetailSidebar: FC<Props> = ({ className, post, sp }) => {
})} })}
{post && ( {post && (
<motion.div <motion.div
layoutId={`post-info-${ sp }`} layoutId={animationMode === 'off'
transition={{ layout: { duration: .2, ease: 'easeOut' } }}> ? undefined
: `post-info-${ sp }`}
transition={{ layout: layoutTransition }}>
<SectionTitle></SectionTitle> <SectionTitle></SectionTitle>
<ul> <ul>
<li>Id.: {post.id}</li> <li>Id.: {post.id}</li>
+40 -18
ファイルの表示
@@ -8,6 +8,8 @@ import SectionTitle from '@/components/common/SectionTitle'
import SidebarComponent from '@/components/layout/SidebarComponent' import SidebarComponent from '@/components/layout/SidebarComponent'
import { CATEGORIES } from '@/consts' import { CATEGORIES } from '@/consts'
import { apiGet } from '@/lib/api' import { apiGet } from '@/lib/api'
import { clientAnimationTransition } from '@/lib/clientAnimation'
import { useClientBehaviourSettings } from '@/lib/useClientBehaviourSettings'
import type { FC, MouseEvent } from 'react' import type { FC, MouseEvent } from 'react'
@@ -21,6 +23,13 @@ type Props = { posts: Post[]
const TagSidebar: FC<Props> = ({ posts, onClick }) => { const TagSidebar: FC<Props> = ({ posts, onClick }) => {
const navigate = useNavigate () const navigate = useNavigate ()
const behaviourSettings = useClientBehaviourSettings ()
const animationMode = behaviourSettings.animation ?? 'normal'
const animationsOff = animationMode === 'off'
const layoutTransition = clientAnimationTransition (
animationMode,
{ normal: { duration: .2, ease: 'easeOut' as const } },
)
const [tagsVsbl, setTagsVsbl] = useState (false) const [tagsVsbl, setTagsVsbl] = useState (false)
const [tags, setTags] = useState<TagByCategory> ({ }) const [tags, setTags] = useState<TagByCategory> ({ })
@@ -65,12 +74,18 @@ const TagSidebar: FC<Props> = ({ posts, onClick }) => {
{CATEGORIES.flatMap (cat => cat in tags ? ( {CATEGORIES.flatMap (cat => cat in tags ? (
tags[cat].map (tag => ( tags[cat].map (tag => (
<li key={tag.id} className="mb-1 min-w-0 max-w-full overflow-hidden"> <li key={tag.id} className="mb-1 min-w-0 max-w-full overflow-hidden">
<motion.div {animationsOff
className="flex min-w-0 max-w-full items-baseline overflow-hidden" ? (
transition={{ layout: { duration: .2, ease: 'easeOut' } }} <div className="flex min-w-0 max-w-full items-baseline overflow-hidden">
layoutId={`tag-${ tag.id }`}> <TagLink tag={tag} onClick={onClick}/>
<TagLink tag={tag} onClick={onClick}/> </div>)
</motion.div> : (
<motion.div
className="flex min-w-0 max-w-full items-baseline overflow-hidden"
transition={{ layout: layoutTransition }}
layoutId={`tag-${ tag.id }`}>
<TagLink tag={tag} onClick={onClick}/>
</motion.div>)}
</li>))) : [])} </li>))) : [])}
</ul> </ul>
<SectionTitle></SectionTitle> <SectionTitle></SectionTitle>
@@ -103,18 +118,25 @@ const TagSidebar: FC<Props> = ({ posts, onClick }) => {
{posts.length > 0 && TagBlock} {posts.length > 0 && TagBlock}
</div> </div>
<AnimatePresence initial={false}> {animationsOff
{tagsVsbl && ( ? (
<motion.div tagsVsbl && (
key="sptags" <div className="md:hidden overflow-hidden">
className="md:hidden overflow-hidden" {posts.length > 0 && TagBlock}
initial={{ height: 0 }} </div>))
animate={{ height: 'auto' }} : (
exit={{ height: 0 }} <AnimatePresence initial={false}>
transition={{ duration: .2, ease: 'easeOut' }}> {tagsVsbl && (
{posts.length > 0 && TagBlock} <motion.div
</motion.div>)} key="sptags"
</AnimatePresence> className="md:hidden overflow-hidden"
initial={{ height: 0 }}
animate={{ height: 'auto' }}
exit={{ height: 0 }}
transition={layoutTransition}>
{posts.length > 0 && TagBlock}
</motion.div>)}
</AnimatePresence>)}
<a href="#" <a href="#"
className="md:hidden block my-2 text-center text-sm className="md:hidden block my-2 text-center text-sm
+18 -7
ファイルの表示
@@ -1,5 +1,7 @@
import { motion } from 'framer-motion' import { motion } from 'framer-motion'
import { clientAnimationTransition } from '@/lib/clientAnimation'
import { useClientBehaviourSettings } from '@/lib/useClientBehaviourSettings'
import { cn } from '@/lib/utils' import { cn } from '@/lib/utils'
import type { FC, ReactNode } from 'react' import type { FC, ReactNode } from 'react'
@@ -9,12 +11,21 @@ type Props = {
className?: string } className?: string }
const MainArea: FC<Props> = ({ children, className }) => ( const MainArea: FC<Props> = ({ children, className }) => {
<motion.main const behaviourSettings = useClientBehaviourSettings ()
transition={{ layout: { duration: .2, ease: 'easeOut' } }} const animationMode = behaviourSettings.animation ?? 'normal'
className={cn ('flex-1 overflow-y-auto p-4', className)} const layoutTransition = clientAnimationTransition (
layout="position"> animationMode,
{children} { normal: { duration: .2, ease: 'easeOut' as const } },
</motion.main>) )
return (
<motion.main
transition={{ layout: layoutTransition }}
className={cn ('flex-1 overflow-y-auto p-4', className)}
layout={animationMode === 'off' ? false : 'position'}>
{children}
</motion.main>)
}
export default MainArea export default MainArea
+10 -2
ファイルの表示
@@ -1,6 +1,8 @@
import { motion } from 'framer-motion' import { motion } from 'framer-motion'
import { Helmet } from 'react-helmet-async' import { Helmet } from 'react-helmet-async'
import { clientAnimationTransition } from '@/lib/clientAnimation'
import { useClientBehaviourSettings } from '@/lib/useClientBehaviourSettings'
import { cn } from '@/lib/utils' import { cn } from '@/lib/utils'
import type { CSSProperties, FC, MouseEvent, PointerEvent, ReactNode } from 'react' import type { CSSProperties, FC, MouseEvent, PointerEvent, ReactNode } from 'react'
@@ -68,6 +70,12 @@ const SidebarComponent: FC<Props> = ({
sidebarKey, sidebarKey,
side = 'left', side = 'left',
}) => { }) => {
const behaviourSettings = useClientBehaviourSettings ()
const animationMode = behaviourSettings.animation ?? 'normal'
const layoutTransition = clientAnimationTransition (
animationMode,
{ normal: { duration: .2, ease: 'easeOut' as const } },
)
const rootRef = useRef<HTMLDivElement | null> (null) const rootRef = useRef<HTMLDivElement | null> (null)
const maxWidthRef = useRef (maxWidth) const maxWidthRef = useRef (maxWidth)
const getContainerWidth = useCallback (() => { const getContainerWidth = useCallback (() => {
@@ -305,8 +313,8 @@ const SidebarComponent: FC<Props> = ({
return ( return (
<motion.div <motion.div
ref={rootRef} ref={rootRef}
layout="position" layout={animationMode === 'off' ? false : 'position'}
transition={{ layout: { duration: .2, ease: 'easeOut' } }} transition={{ layout: layoutTransition }}
style={style} style={style}
className={cn ( className={cn (
'relative w-full md:w-[var(--sidebar-width)] md:shrink-0 md:h-full', 'relative w-full md:w-[var(--sidebar-width)] md:shrink-0 md:h-full',
+4 -2
ファイルの表示
@@ -137,9 +137,11 @@
:root[data-animation='off'] *::before, :root[data-animation='off'] *::before,
:root[data-animation='off'] *::after :root[data-animation='off'] *::after
{ {
animation-duration: 0.001ms !important; animation-duration: 0ms !important;
animation-delay: 0ms !important;
animation-iteration-count: 1 !important; animation-iteration-count: 1 !important;
transition-duration: 0.001ms !important; transition-duration: 0ms !important;
transition-delay: 0ms !important;
} }
a a
+34
ファイルの表示
@@ -0,0 +1,34 @@
import type { Transition } from 'framer-motion'
import type { ClientAnimationMode } from '@/lib/settings'
type ClientAnimationTransitionOptions = {
normal: Transition
reduced?: Transition
off?: Transition }
export const clientAnimationsOff = (
animationMode: ClientAnimationMode,
): boolean =>
animationMode === 'off'
export const clientAnimationTransition = (
animationMode: ClientAnimationMode,
{ normal, reduced, off }: ClientAnimationTransitionOptions,
): Transition => {
if (animationMode === 'off')
return off ?? { duration: 0 }
if (animationMode === 'reduced')
return reduced ?? { duration: .08, ease: 'linear' as const }
return normal
}
export const clientScrollBehaviour = (
animationMode: ClientAnimationMode,
): ScrollBehavior =>
animationMode === 'off' ? 'auto' : 'smooth'
+7 -1
ファイルの表示
@@ -28,6 +28,7 @@ import {
getClientGekanatorBackgroundMotion, getClientGekanatorBackgroundMotion,
setClientGekanatorBackgroundMotion, setClientGekanatorBackgroundMotion,
} from '@/lib/settings' } from '@/lib/settings'
import { useClientBehaviourSettings } from '@/lib/useClientBehaviourSettings'
import { cn } from '@/lib/utils' import { cn } from '@/lib/utils'
import type { FC } from 'react' import type { FC } from 'react'
@@ -3677,6 +3678,8 @@ const expectedAnswerFor = (
const GekanatorPage: FC<{ user: User | null }> = ({ user }) => { const GekanatorPage: FC<{ user: User | null }> = ({ user }) => {
const behaviourSettings = useClientBehaviourSettings ()
const animationMode = behaviourSettings.animation ?? 'normal'
const storedGame = useMemo (loadStoredGame, []) const storedGame = useMemo (loadStoredGame, [])
const hasStoredRestore = storedGame != null && isStoredPhase (storedGame.phase) const hasStoredRestore = storedGame != null && isStoredPhase (storedGame.phase)
const queryClient = useQueryClient () const queryClient = useQueryClient ()
@@ -4069,9 +4072,12 @@ const GekanatorPage: FC<{ user: User | null }> = ({ user }) => {
return reviewGuessedPostId === reviewCorrectPostId return reviewGuessedPostId === reviewCorrectPostId
}) () }) ()
const effectiveBackgroundMotionMode = (() => { const effectiveBackgroundMotionMode = (() => {
if (animationMode === 'off')
return 'off'
if (backgroundMotionMode === 'off') if (backgroundMotionMode === 'off')
return 'off' return 'off'
if (prefersReducedMotion) if (animationMode === 'reduced' || prefersReducedMotion)
return 'calm' return 'calm'
return backgroundMotionMode return backgroundMotionMode
+7 -2
ファイルの表示
@@ -9,14 +9,18 @@ import PageTitle from '@/components/common/PageTitle'
import Pagination from '@/components/common/Pagination' import Pagination from '@/components/common/Pagination'
import MainArea from '@/components/layout/MainArea' import MainArea from '@/components/layout/MainArea'
import { SITE_TITLE } from '@/config' import { SITE_TITLE } from '@/config'
import { clientScrollBehaviour } from '@/lib/clientAnimation'
import { fetchMaterialChanges } from '@/lib/materials' import { fetchMaterialChanges } from '@/lib/materials'
import { materialsKeys } from '@/lib/queryKeys' import { materialsKeys } from '@/lib/queryKeys'
import { useClientBehaviourSettings } from '@/lib/useClientBehaviourSettings'
import { dateString, inputClass } from '@/lib/utils' import { dateString, inputClass } from '@/lib/utils'
import type { FC, FormEvent } from 'react' import type { FC, FormEvent } from 'react'
const MaterialHistoryPage: FC = () => { const MaterialHistoryPage: FC = () => {
const behaviourSettings = useClientBehaviourSettings ()
const animationMode = behaviourSettings.animation ?? 'normal'
const location = useLocation () const location = useLocation ()
const navigate = useNavigate () const navigate = useNavigate ()
const query = new URLSearchParams (location.search) const query = new URLSearchParams (location.search)
@@ -55,8 +59,9 @@ const MaterialHistoryPage: FC = () => {
const totalPages = data ? Math.ceil (data.count / limit) : 0 const totalPages = data ? Math.ceil (data.count / limit) : 0
useEffect (() => { useEffect (() => {
document.querySelector ('table')?.scrollIntoView ({ behavior: 'smooth' }) document.querySelector ('table')?.scrollIntoView ({
}, [location.search]) behavior: clientScrollBehaviour (animationMode) })
}, [animationMode, location.search])
const handleSearch = (event: FormEvent) => { const handleSearch = (event: FormEvent) => {
event.preventDefault () event.preventDefault ()
+14 -4
ファイルの表示
@@ -12,9 +12,12 @@ import { useDialogue } from '@/components/dialogues/DialogueProvider'
import MainArea from '@/components/layout/MainArea' import MainArea from '@/components/layout/MainArea'
import { toast } from '@/components/ui/use-toast' import { toast } from '@/components/ui/use-toast'
import { SITE_TITLE } from '@/config' import { SITE_TITLE } from '@/config'
import { clientAnimationTransition,
clientScrollBehaviour } from '@/lib/clientAnimation'
import { fetchPostChanges, updatePost } from '@/lib/posts' import { fetchPostChanges, updatePost } from '@/lib/posts'
import { postsKeys, tagsKeys } from '@/lib/queryKeys' import { postsKeys, tagsKeys } from '@/lib/queryKeys'
import { fetchTag } from '@/lib/tags' import { fetchTag } from '@/lib/tags'
import { useClientBehaviourSettings } from '@/lib/useClientBehaviourSettings'
import { cn, dateString, originalCreatedAtString } from '@/lib/utils' import { cn, dateString, originalCreatedAtString } from '@/lib/utils'
import type { FC, MouseEvent } from 'react' import type { FC, MouseEvent } from 'react'
@@ -37,6 +40,12 @@ const renderDiff = (diff: { current: string | null; prev: string | null }) => (
const PostHistoryPage: FC = () => { const PostHistoryPage: FC = () => {
const dialogue = useDialogue () const dialogue = useDialogue ()
const behaviourSettings = useClientBehaviourSettings ()
const animationMode = behaviourSettings.animation ?? 'normal'
const layoutTransition = clientAnimationTransition (
animationMode,
{ normal: { duration: .2, ease: 'easeOut' as const } },
)
const location = useLocation () const location = useLocation ()
const query = new URLSearchParams (location.search) const query = new URLSearchParams (location.search)
@@ -113,8 +122,9 @@ const PostHistoryPage: FC = () => {
} }
useEffect (() => { useEffect (() => {
document.querySelector ('table')?.scrollIntoView ({ behavior: 'smooth' }) document.querySelector ('table')?.scrollIntoView ({
}, [location.search]) behavior: clientScrollBehaviour (animationMode) })
}, [animationMode, location.search])
const layoutIds: string[] = [] const layoutIds: string[] = []
@@ -198,8 +208,8 @@ const PostHistoryPage: FC = () => {
rowSpan={rowsCnt}> rowSpan={rowsCnt}>
<PrefetchLink to={`/posts/${ change.postId }`}> <PrefetchLink to={`/posts/${ change.postId }`}>
<motion.div <motion.div
layoutId={layoutId} layoutId={animationMode === 'off' ? undefined : layoutId}
transition={{ layout: { duration: .2, ease: 'easeOut' } }}> transition={{ layout: layoutTransition }}>
<img src={change.thumbnail.current <img src={change.thumbnail.current
|| change.thumbnailBase.current || change.thumbnailBase.current
|| undefined} || undefined}
+19 -8
ファイルの表示
@@ -14,6 +14,8 @@ import Pagination from '@/components/common/Pagination'
import TagInput from '@/components/common/TagInput' import TagInput from '@/components/common/TagInput'
import MainArea from '@/components/layout/MainArea' import MainArea from '@/components/layout/MainArea'
import { SITE_TITLE } from '@/config' import { SITE_TITLE } from '@/config'
import { clientAnimationTransition,
clientScrollBehaviour } from '@/lib/clientAnimation'
import { fetchPosts } from '@/lib/posts' import { fetchPosts } from '@/lib/posts'
import { postsKeys } from '@/lib/queryKeys' import { postsKeys } from '@/lib/queryKeys'
import { import {
@@ -24,6 +26,7 @@ import {
LIST_LIMIT_OPTIONS, LIST_LIMIT_OPTIONS,
setClientListSettings, setClientListSettings,
} from '@/lib/settings' } from '@/lib/settings'
import { useClientBehaviourSettings } from '@/lib/useClientBehaviourSettings'
import { useKeyboardShortcuts } from '@/lib/useKeyboardShortcuts' import { useKeyboardShortcuts } from '@/lib/useKeyboardShortcuts'
import { dateString, inputClass, originalCreatedAtString } from '@/lib/utils' import { dateString, inputClass, originalCreatedAtString } from '@/lib/utils'
@@ -67,6 +70,13 @@ const parseOrder = (value: string | null): FetchPostsOrder | null => {
const PostSearchPage: FC = () => { const PostSearchPage: FC = () => {
const behaviourSettings = useClientBehaviourSettings ()
const animationMode = behaviourSettings.animation ?? 'normal'
const thumbnailTransition = clientAnimationTransition (
animationMode,
{ normal: { type: 'spring', stiffness: 500, damping: 40, mass: .5 },
reduced: { duration: .08, ease: 'linear' as const } },
)
const location = useLocation () const location = useLocation ()
const navigate = useNavigate () const navigate = useNavigate ()
@@ -141,9 +151,11 @@ const PostSearchPage: FC = () => {
setUpdatedFrom (qUpdatedFrom) setUpdatedFrom (qUpdatedFrom)
setUpdatedTo (qUpdatedTo) setUpdatedTo (qUpdatedTo)
document.querySelector ('table')?.scrollIntoView ({ behavior: 'smooth' }) document.querySelector ('table')?.scrollIntoView ({
}, [location.search, qCreatedFrom, qCreatedTo, qMatch, qOriginalCreatedFrom, behavior: clientScrollBehaviour (animationMode) })
qOriginalCreatedTo, qTags, qTitle, qUpdatedFrom, qUpdatedTo, qURL]) }, [animationMode, location.search, qCreatedFrom, qCreatedTo, qMatch,
qOriginalCreatedFrom, qOriginalCreatedTo, qTags, qTitle, qUpdatedFrom,
qUpdatedTo, qURL])
const search = async () => { const search = async () => {
const qs = new URLSearchParams () const qs = new URLSearchParams ()
@@ -400,11 +412,10 @@ const PostSearchPage: FC = () => {
<td className="p-2"> <td className="p-2">
<PrefetchLink to={`/posts/${ row.id }`} title={row.title || undefined}> <PrefetchLink to={`/posts/${ row.id }`} title={row.title || undefined}>
<motion.div <motion.div
layoutId={`page-${ row.id }`} layoutId={animationMode === 'off'
transition={{ type: 'spring', ? undefined
stiffness: 500, : `page-${ row.id }`}
damping: 40, transition={thumbnailTransition}>
mass: .5 }}>
<img src={row.thumbnail || row.thumbnailBase || undefined} <img src={row.thumbnail || row.thumbnailBase || undefined}
alt={row.title || row.url} alt={row.title || row.url}
title={row.title || row.url || undefined} title={row.title || row.url || undefined}
+7 -2
ファイルの表示
@@ -17,8 +17,10 @@ import { toast } from '@/components/ui/use-toast'
import { SITE_TITLE } from '@/config' import { SITE_TITLE } from '@/config'
import { apiPut } from '@/lib/api' import { apiPut } from '@/lib/api'
import { extractValidationError } from '@/lib/apiErrors' import { extractValidationError } from '@/lib/apiErrors'
import { clientScrollBehaviour } from '@/lib/clientAnimation'
import { tagsKeys } from '@/lib/queryKeys' import { tagsKeys } from '@/lib/queryKeys'
import { fetchNicoTags } from '@/lib/tags' import { fetchNicoTags } from '@/lib/tags'
import { useClientBehaviourSettings } from '@/lib/useClientBehaviourSettings'
import { cn, dateString, inputClass } from '@/lib/utils' import { cn, dateString, inputClass } from '@/lib/utils'
import { canEditContent } from '@/lib/users' import { canEditContent } from '@/lib/users'
@@ -38,6 +40,8 @@ const setIf = (qs: URLSearchParams, key: string, value: string) => {
const NicoTagListPage: FC<Props> = ({ user }) => { const NicoTagListPage: FC<Props> = ({ user }) => {
const behaviourSettings = useClientBehaviourSettings ()
const animationMode = behaviourSettings.animation ?? 'normal'
const dialogue = useDialogue () const dialogue = useDialogue ()
const location = useLocation () const location = useLocation ()
const navigate = useNavigate () const navigate = useNavigate ()
@@ -152,8 +156,9 @@ const NicoTagListPage: FC<Props> = ({ user }) => {
setLinkStatus (qLinkStatus) setLinkStatus (qLinkStatus)
setEditingId (null) setEditingId (null)
document.querySelector ('table')?.scrollIntoView ({ behavior: 'smooth' }) document.querySelector ('table')?.scrollIntoView ({
}, [location.search, qLinkedTag, qLinkStatus, qName]) behavior: clientScrollBehaviour (animationMode) })
}, [animationMode, location.search, qLinkedTag, qLinkStatus, qName])
useEffect (() => { useEffect (() => {
if (!(data)) if (!(data))
+7 -2
ファイルの表示
@@ -11,8 +11,10 @@ import { toast } from '@/components/ui/use-toast'
import { SITE_TITLE } from '@/config' import { SITE_TITLE } from '@/config'
import { CATEGORY_NAMES } from '@/consts' import { CATEGORY_NAMES } from '@/consts'
import { apiPut } from '@/lib/api' import { apiPut } from '@/lib/api'
import { clientScrollBehaviour } from '@/lib/clientAnimation'
import { postsKeys, tagsKeys } from '@/lib/queryKeys' import { postsKeys, tagsKeys } from '@/lib/queryKeys'
import { fetchTagChanges } from '@/lib/tags' import { fetchTagChanges } from '@/lib/tags'
import { useClientBehaviourSettings } from '@/lib/useClientBehaviourSettings'
import { cn, dateString } from '@/lib/utils' import { cn, dateString } from '@/lib/utils'
import type { FC } from 'react' import type { FC } from 'react'
@@ -43,6 +45,8 @@ const renderStateDiff = (diff: { current: string | null; prev: string | null })
const TagHistoryPage: FC = () => { const TagHistoryPage: FC = () => {
const behaviourSettings = useClientBehaviourSettings ()
const animationMode = behaviourSettings.animation ?? 'normal'
const location = useLocation () const location = useLocation ()
const query = new URLSearchParams (location.search) const query = new URLSearchParams (location.search)
const id = query.get ('id') const id = query.get ('id')
@@ -58,8 +62,9 @@ const TagHistoryPage: FC = () => {
const qc = useQueryClient () const qc = useQueryClient ()
useEffect (() => { useEffect (() => {
document.querySelector ('table')?.scrollIntoView ({ behavior: 'smooth' }) document.querySelector ('table')?.scrollIntoView ({
}, [location.search]) behavior: clientScrollBehaviour (animationMode) })
}, [animationMode, location.search])
return ( return (
<MainArea> <MainArea>
+9 -3
ファイルの表示
@@ -13,6 +13,7 @@ import Pagination from '@/components/common/Pagination'
import MainArea from '@/components/layout/MainArea' import MainArea from '@/components/layout/MainArea'
import { SITE_TITLE } from '@/config' import { SITE_TITLE } from '@/config'
import { CATEGORIES, CATEGORY_NAMES } from '@/consts' import { CATEGORIES, CATEGORY_NAMES } from '@/consts'
import { clientScrollBehaviour } from '@/lib/clientAnimation'
import { tagsKeys } from '@/lib/queryKeys' import { tagsKeys } from '@/lib/queryKeys'
import { import {
DEFAULT_POST_LIST_LIMIT, DEFAULT_POST_LIST_LIMIT,
@@ -22,6 +23,7 @@ import {
LIST_LIMIT_OPTIONS, LIST_LIMIT_OPTIONS,
setClientListSettings, setClientListSettings,
} from '@/lib/settings' } from '@/lib/settings'
import { useClientBehaviourSettings } from '@/lib/useClientBehaviourSettings'
import { useKeyboardShortcuts } from '@/lib/useKeyboardShortcuts' import { useKeyboardShortcuts } from '@/lib/useKeyboardShortcuts'
import { fetchTags } from '@/lib/tags' import { fetchTags } from '@/lib/tags'
import { dateString, inputClass } from '@/lib/utils' import { dateString, inputClass } from '@/lib/utils'
@@ -71,6 +73,8 @@ const parseOrder = (value: string | null): FetchTagsOrder | null => {
const TagListPage: FC = () => { const TagListPage: FC = () => {
const behaviourSettings = useClientBehaviourSettings ()
const animationMode = behaviourSettings.animation ?? 'normal'
const location = useLocation () const location = useLocation ()
const navigate = useNavigate () const navigate = useNavigate ()
@@ -145,9 +149,11 @@ const TagListPage: FC = () => {
setUpdatedTo (qUpdatedTo) setUpdatedTo (qUpdatedTo)
setDeprecated (qDeprecated) setDeprecated (qDeprecated)
document.querySelector ('table')?.scrollIntoView ({ behavior: 'smooth' }) document.querySelector ('table')?.scrollIntoView ({
}, [location.search, qCategory, qCreatedFrom, qCreatedTo, qName, qPostCountGTE, behavior: clientScrollBehaviour (animationMode) })
qPostCountLTE, qUpdatedFrom, qUpdatedTo, qDeprecated]) }, [animationMode, location.search, qCategory, qCreatedFrom, qCreatedTo,
qName, qPostCountGTE, qPostCountLTE, qUpdatedFrom, qUpdatedTo,
qDeprecated])
const handleSearch = (e: FormEvent) => { const handleSearch = (e: FormEvent) => {
e.preventDefault () e.preventDefault ()
+15 -4
ファイルの表示
@@ -15,6 +15,8 @@ import { Button } from '@/components/ui/button'
import { SITE_TITLE } from '@/config' import { SITE_TITLE } from '@/config'
import { CATEGORIES, CATEGORY_NAMES } from '@/consts' import { CATEGORIES, CATEGORY_NAMES } from '@/consts'
import { apiDelete, apiGet, apiPatch, apiPost, apiPut, isApiError } from '@/lib/api' import { apiDelete, apiGet, apiPatch, apiPost, apiPut, isApiError } from '@/lib/api'
import { clientAnimationTransition,
clientScrollBehaviour } from '@/lib/clientAnimation'
import { fetchPost } from '@/lib/posts' import { fetchPost } from '@/lib/posts'
import { import {
getClientTheatreLayoutMode, getClientTheatreLayoutMode,
@@ -22,6 +24,7 @@ import {
setClientTheatreLayoutMode, setClientTheatreLayoutMode,
setClientTheatreTagFlow, setClientTheatreTagFlow,
} from '@/lib/settings' } from '@/lib/settings'
import { useClientBehaviourSettings } from '@/lib/useClientBehaviourSettings'
import { canEditContent } from '@/lib/users' import { canEditContent } from '@/lib/users'
import { cn, dateString, inputClass } from '@/lib/utils' import { cn, dateString, inputClass } from '@/lib/utils'
import { useValidationErrors } from '@/lib/useValidationErrors' import { useValidationErrors } from '@/lib/useValidationErrors'
@@ -212,6 +215,12 @@ type Props = { user: User | null }
const TheatreDetailPage: FC<Props> = ({ user }: Props) => { const TheatreDetailPage: FC<Props> = ({ user }: Props) => {
const behaviourSettings = useClientBehaviourSettings ()
const animationMode = behaviourSettings.animation ?? 'normal'
const layoutTransition = clientAnimationTransition (
animationMode,
{ normal: { duration: .2, ease: 'easeOut' as const } },
)
const { id } = useParams () const { id } = useParams ()
const dialogue = useDialogue () const dialogue = useDialogue ()
@@ -618,7 +627,9 @@ const TheatreDetailPage: FC<Props> = ({ user }: Props) => {
clearValidationErrors () clearValidationErrors ()
await apiPost (`/theatres/${ id }/comments`, { content }) await apiPost (`/theatres/${ id }/comments`, { content })
setContent ('') setContent ('')
commentsRef.current?.scrollTo ({ top: 0, behavior: 'smooth' }) commentsRef.current?.scrollTo ({
top: 0,
behavior: clientScrollBehaviour (animationMode) })
} }
catch (error) catch (error)
{ {
@@ -835,8 +846,8 @@ const TheatreDetailPage: FC<Props> = ({ user }: Props) => {
return ( return (
<motion.div <motion.div
layout="position" layout={animationMode === 'off' ? false : 'position'}
transition={{ layout: { duration: .2, ease: 'easeOut' } }} transition={{ layout: layoutTransition }}
className="min-h-0 flex-1 overflow-y-auto bg-zinc-50 text-zinc-950 className="min-h-0 flex-1 overflow-y-auto bg-zinc-50 text-zinc-950
md:overflow-hidden dark:bg-zinc-950 dark:text-zinc-50"> md:overflow-hidden dark:bg-zinc-950 dark:text-zinc-50">
<Helmet> <Helmet>
@@ -860,7 +871,7 @@ const TheatreDetailPage: FC<Props> = ({ user }: Props) => {
</SidebarComponent>)} </SidebarComponent>)}
<motion.main <motion.main
layout="position" layout={animationMode === 'off' ? false : 'position'}
className="order-1 min-w-0 flex-1 space-y-4 md:order-none className="order-1 min-w-0 flex-1 space-y-4 md:order-none
md:min-w-[360px] md:overflow-y-auto"> md:min-w-[360px] md:overflow-y-auto">
<div className="space-y-4"> <div className="space-y-4">