diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 307695e..c2aa89b 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1,5 +1,5 @@ import { AnimatePresence, LayoutGroup, MotionConfig, motion } from 'framer-motion' -import { useEffect, useMemo, useState } from 'react' +import { Fragment, useEffect, useMemo, useState } from 'react' import { BrowserRouter, Navigate, Route, @@ -14,6 +14,7 @@ import { Toaster } from '@/components/ui/toaster' import { apiPost, isApiError } from '@/lib/api' import { applyClientAnimationMode, applyClientAppearance, + type ClientAnimationMode, fetchUserThemeSlots, fetchUserSettings, getClientThemeMode, @@ -61,11 +62,51 @@ import type { Dispatch, FC, SetStateAction } from 'react' import type { User } from '@/types' -const RouteTransitionWrapper = ({ user, setUser }: { +const RouteTransitionWrapper = ({ animationMode, user, setUser }: { + animationMode: ClientAnimationMode user: User | null setUser: Dispatch> }) => { const location = useLocation () + if (animationMode === 'off') + { + return ( + + }/> + }/> + }/> + }/> + }/> + }/> + }/> + }/> + }/> + }/> + }/> + }/> + }/> + }> + }/> + }/> + }/> + }/> + }/> + + }/> + }/> + }/> + }/> + }/> + }/> + }/> + }/> + }/> + }/> + }/> + }/> + ) + } + return ( @@ -118,6 +159,7 @@ const App: FC = () => { const [status, setStatus] = useState (200) const behaviourSettings = useClientBehaviourSettings () const animationMode = behaviourSettings.animation ?? 'normal' + const LayoutWrapper = animationMode === 'off' ? Fragment : LayoutGroup const appLayoutTransition = useMemo ( () => ( @@ -227,15 +269,18 @@ const App: FC = () => { ? 'user' : 'always' }> - + - + - + diff --git a/frontend/src/components/DraggableDroppableTagRow.tsx b/frontend/src/components/DraggableDroppableTagRow.tsx index 97e8248..f465bea 100644 --- a/frontend/src/components/DraggableDroppableTagRow.tsx +++ b/frontend/src/components/DraggableDroppableTagRow.tsx @@ -4,6 +4,8 @@ import { motion } from 'framer-motion' import { useRef } from 'react' import TagLink from '@/components/TagLink' +import { clientAnimationTransition } from '@/lib/clientAnimation' +import { useClientBehaviourSettings } from '@/lib/useClientBehaviourSettings' import { cn } from '@/lib/utils' import type { CSSProperties, FC, MutableRefObject } from 'react' @@ -20,6 +22,12 @@ type Props = { const DraggableDroppableTagRow: FC = ({ 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 downPosRef = useRef<{ x: number; y: number } | null> (null) @@ -94,8 +102,10 @@ const DraggableDroppableTagRow: FC = ({ tag, nestLevel, pathKey, parentTa {...listeners}> + transition={{ layout: layoutTransition }} + layoutId={animationMode === 'off' + ? undefined + : `tag-${ sp ? 'sp-' : '' }${ tag.id }`}> ) diff --git a/frontend/src/components/ResponsiveMarqueeText.tsx b/frontend/src/components/ResponsiveMarqueeText.tsx index c859055..0960730 100644 --- a/frontend/src/components/ResponsiveMarqueeText.tsx +++ b/frontend/src/components/ResponsiveMarqueeText.tsx @@ -1,3 +1,4 @@ +import { useClientBehaviourSettings } from '@/lib/useClientBehaviourSettings' import { cn } from '@/lib/utils' import { useEffect, useRef, useState } from 'react' @@ -20,6 +21,8 @@ const MIN_MARQUEE_OVERFLOW_PX = 1 const ResponsiveMarqueeText: FC = ( { text, className, truncateOnMobile = false, title }, ) => { + const behaviourSettings = useClientBehaviourSettings () + const animationMode = behaviourSettings.animation ?? 'normal' const outerRef = useRef (null) const staticRef = useRef (null) const animatedRef = useRef (null) @@ -98,6 +101,7 @@ const ResponsiveMarqueeText: FC = ( const animated = animatedRef.current const canMarquee = ( active + && animationMode !== 'off' && desktopMarqueeEnabled && overflowPx >= MIN_MARQUEE_OVERFLOW_PX && animated != null) @@ -207,7 +211,7 @@ const ResponsiveMarqueeText: FC = ( resetAnimated () setMarqueeVisible (false) } - }, [active, desktopMarqueeEnabled, overflowPx, text]) + }, [active, animationMode, desktopMarqueeEnabled, overflowPx, text]) return ( = ({ className, post, sp }) => { const qc = useQueryClient () const behaviourSettings = useClientBehaviourSettings () const tagRelationDisplay = behaviourSettings.tagRelationDisplay ?? 'grouped' + const animationMode = behaviourSettings.animation ?? 'normal' + const layoutTransition = clientAnimationTransition ( + animationMode, + { normal: { duration: .2, ease: 'easeOut' as const } }, + ) const baseTags = useMemo (() => { const tagsTmp = { } as TagByCategory @@ -360,8 +366,10 @@ const TagDetailSidebar: FC = ({ className, post, sp }) => {
+ layoutId={animationMode === 'off' + ? undefined + : `tag-${ sp ? 'sp-' : '' }${ cat }`} + transition={{ layout: layoutTransition }}> {CATEGORY_NAMES[cat]} @@ -393,8 +401,10 @@ const TagDetailSidebar: FC = ({ className, post, sp }) => { })} {post && ( + layoutId={animationMode === 'off' + ? undefined + : `post-info-${ sp }`} + transition={{ layout: layoutTransition }}> 情報
  • Id.: {post.id}
  • diff --git a/frontend/src/components/TagSidebar.tsx b/frontend/src/components/TagSidebar.tsx index de3dc16..6ba28de 100644 --- a/frontend/src/components/TagSidebar.tsx +++ b/frontend/src/components/TagSidebar.tsx @@ -8,6 +8,8 @@ import SectionTitle from '@/components/common/SectionTitle' import SidebarComponent from '@/components/layout/SidebarComponent' import { CATEGORIES } from '@/consts' import { apiGet } from '@/lib/api' +import { clientAnimationTransition } from '@/lib/clientAnimation' +import { useClientBehaviourSettings } from '@/lib/useClientBehaviourSettings' import type { FC, MouseEvent } from 'react' @@ -21,6 +23,13 @@ type Props = { posts: Post[] const TagSidebar: FC = ({ posts, onClick }) => { 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 [tags, setTags] = useState ({ }) @@ -65,12 +74,18 @@ const TagSidebar: FC = ({ posts, onClick }) => { {CATEGORIES.flatMap (cat => cat in tags ? ( tags[cat].map (tag => (
  • - - - + {animationsOff + ? ( +
    + +
    ) + : ( + + + )}
  • ))) : [])}
関聯 @@ -103,18 +118,25 @@ const TagSidebar: FC = ({ posts, onClick }) => { {posts.length > 0 && TagBlock}
- - {tagsVsbl && ( - - {posts.length > 0 && TagBlock} - )} - + {animationsOff + ? ( + tagsVsbl && ( +
+ {posts.length > 0 && TagBlock} +
)) + : ( + + {tagsVsbl && ( + + {posts.length > 0 && TagBlock} + )} + )} - {children} - ) +const MainArea: FC = ({ children, className }) => { + const behaviourSettings = useClientBehaviourSettings () + const animationMode = behaviourSettings.animation ?? 'normal' + const layoutTransition = clientAnimationTransition ( + animationMode, + { normal: { duration: .2, ease: 'easeOut' as const } }, + ) -export default MainArea \ No newline at end of file + return ( + + {children} + ) +} + +export default MainArea diff --git a/frontend/src/components/layout/SidebarComponent.tsx b/frontend/src/components/layout/SidebarComponent.tsx index ea4b015..574f924 100644 --- a/frontend/src/components/layout/SidebarComponent.tsx +++ b/frontend/src/components/layout/SidebarComponent.tsx @@ -1,6 +1,8 @@ import { motion } from 'framer-motion' import { Helmet } from 'react-helmet-async' +import { clientAnimationTransition } from '@/lib/clientAnimation' +import { useClientBehaviourSettings } from '@/lib/useClientBehaviourSettings' import { cn } from '@/lib/utils' import type { CSSProperties, FC, MouseEvent, PointerEvent, ReactNode } from 'react' @@ -68,6 +70,12 @@ const SidebarComponent: FC = ({ sidebarKey, side = 'left', }) => { + const behaviourSettings = useClientBehaviourSettings () + const animationMode = behaviourSettings.animation ?? 'normal' + const layoutTransition = clientAnimationTransition ( + animationMode, + { normal: { duration: .2, ease: 'easeOut' as const } }, + ) const rootRef = useRef (null) const maxWidthRef = useRef (maxWidth) const getContainerWidth = useCallback (() => { @@ -305,8 +313,8 @@ const SidebarComponent: FC = ({ return ( + 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' diff --git a/frontend/src/pages/GekanatorPage.tsx b/frontend/src/pages/GekanatorPage.tsx index 2e15020..181bb0c 100644 --- a/frontend/src/pages/GekanatorPage.tsx +++ b/frontend/src/pages/GekanatorPage.tsx @@ -28,6 +28,7 @@ import { getClientGekanatorBackgroundMotion, setClientGekanatorBackgroundMotion, } from '@/lib/settings' +import { useClientBehaviourSettings } from '@/lib/useClientBehaviourSettings' import { cn } from '@/lib/utils' import type { FC } from 'react' @@ -3677,6 +3678,8 @@ const expectedAnswerFor = ( const GekanatorPage: FC<{ user: User | null }> = ({ user }) => { + const behaviourSettings = useClientBehaviourSettings () + const animationMode = behaviourSettings.animation ?? 'normal' const storedGame = useMemo (loadStoredGame, []) const hasStoredRestore = storedGame != null && isStoredPhase (storedGame.phase) const queryClient = useQueryClient () @@ -4069,9 +4072,12 @@ const GekanatorPage: FC<{ user: User | null }> = ({ user }) => { return reviewGuessedPostId === reviewCorrectPostId }) () const effectiveBackgroundMotionMode = (() => { + if (animationMode === 'off') + return 'off' + if (backgroundMotionMode === 'off') return 'off' - if (prefersReducedMotion) + if (animationMode === 'reduced' || prefersReducedMotion) return 'calm' return backgroundMotionMode diff --git a/frontend/src/pages/materials/MaterialHistoryPage.tsx b/frontend/src/pages/materials/MaterialHistoryPage.tsx index 82876db..920fbad 100644 --- a/frontend/src/pages/materials/MaterialHistoryPage.tsx +++ b/frontend/src/pages/materials/MaterialHistoryPage.tsx @@ -9,14 +9,18 @@ import PageTitle from '@/components/common/PageTitle' import Pagination from '@/components/common/Pagination' import MainArea from '@/components/layout/MainArea' import { SITE_TITLE } from '@/config' +import { clientScrollBehaviour } from '@/lib/clientAnimation' import { fetchMaterialChanges } from '@/lib/materials' import { materialsKeys } from '@/lib/queryKeys' +import { useClientBehaviourSettings } from '@/lib/useClientBehaviourSettings' import { dateString, inputClass } from '@/lib/utils' import type { FC, FormEvent } from 'react' const MaterialHistoryPage: FC = () => { + const behaviourSettings = useClientBehaviourSettings () + const animationMode = behaviourSettings.animation ?? 'normal' const location = useLocation () const navigate = useNavigate () const query = new URLSearchParams (location.search) @@ -55,8 +59,9 @@ const MaterialHistoryPage: FC = () => { const totalPages = data ? Math.ceil (data.count / limit) : 0 useEffect (() => { - document.querySelector ('table')?.scrollIntoView ({ behavior: 'smooth' }) - }, [location.search]) + document.querySelector ('table')?.scrollIntoView ({ + behavior: clientScrollBehaviour (animationMode) }) + }, [animationMode, location.search]) const handleSearch = (event: FormEvent) => { event.preventDefault () diff --git a/frontend/src/pages/posts/PostHistoryPage.tsx b/frontend/src/pages/posts/PostHistoryPage.tsx index f6f961d..71aa3c8 100644 --- a/frontend/src/pages/posts/PostHistoryPage.tsx +++ b/frontend/src/pages/posts/PostHistoryPage.tsx @@ -12,9 +12,12 @@ import { useDialogue } from '@/components/dialogues/DialogueProvider' import MainArea from '@/components/layout/MainArea' import { toast } from '@/components/ui/use-toast' import { SITE_TITLE } from '@/config' +import { clientAnimationTransition, + clientScrollBehaviour } from '@/lib/clientAnimation' import { fetchPostChanges, updatePost } from '@/lib/posts' import { postsKeys, tagsKeys } from '@/lib/queryKeys' import { fetchTag } from '@/lib/tags' +import { useClientBehaviourSettings } from '@/lib/useClientBehaviourSettings' import { cn, dateString, originalCreatedAtString } from '@/lib/utils' import type { FC, MouseEvent } from 'react' @@ -37,6 +40,12 @@ const renderDiff = (diff: { current: string | null; prev: string | null }) => ( const PostHistoryPage: FC = () => { 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 query = new URLSearchParams (location.search) @@ -113,8 +122,9 @@ const PostHistoryPage: FC = () => { } useEffect (() => { - document.querySelector ('table')?.scrollIntoView ({ behavior: 'smooth' }) - }, [location.search]) + document.querySelector ('table')?.scrollIntoView ({ + behavior: clientScrollBehaviour (animationMode) }) + }, [animationMode, location.search]) const layoutIds: string[] = [] @@ -198,8 +208,8 @@ const PostHistoryPage: FC = () => { rowSpan={rowsCnt}> + layoutId={animationMode === 'off' ? undefined : layoutId} + transition={{ layout: layoutTransition }}> { 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 navigate = useNavigate () @@ -141,9 +151,11 @@ const PostSearchPage: FC = () => { setUpdatedFrom (qUpdatedFrom) setUpdatedTo (qUpdatedTo) - document.querySelector ('table')?.scrollIntoView ({ behavior: 'smooth' }) - }, [location.search, qCreatedFrom, qCreatedTo, qMatch, qOriginalCreatedFrom, - qOriginalCreatedTo, qTags, qTitle, qUpdatedFrom, qUpdatedTo, qURL]) + document.querySelector ('table')?.scrollIntoView ({ + behavior: clientScrollBehaviour (animationMode) }) + }, [animationMode, location.search, qCreatedFrom, qCreatedTo, qMatch, + qOriginalCreatedFrom, qOriginalCreatedTo, qTags, qTitle, qUpdatedFrom, + qUpdatedTo, qURL]) const search = async () => { const qs = new URLSearchParams () @@ -400,11 +412,10 @@ const PostSearchPage: FC = () => { + layoutId={animationMode === 'off' + ? undefined + : `page-${ row.id }`} + transition={thumbnailTransition}> {row.title { const NicoTagListPage: FC = ({ user }) => { + const behaviourSettings = useClientBehaviourSettings () + const animationMode = behaviourSettings.animation ?? 'normal' const dialogue = useDialogue () const location = useLocation () const navigate = useNavigate () @@ -152,8 +156,9 @@ const NicoTagListPage: FC = ({ user }) => { setLinkStatus (qLinkStatus) setEditingId (null) - document.querySelector ('table')?.scrollIntoView ({ behavior: 'smooth' }) - }, [location.search, qLinkedTag, qLinkStatus, qName]) + document.querySelector ('table')?.scrollIntoView ({ + behavior: clientScrollBehaviour (animationMode) }) + }, [animationMode, location.search, qLinkedTag, qLinkStatus, qName]) useEffect (() => { if (!(data)) diff --git a/frontend/src/pages/tags/TagHistoryPage.tsx b/frontend/src/pages/tags/TagHistoryPage.tsx index 8ce1d1c..f95d2ae 100644 --- a/frontend/src/pages/tags/TagHistoryPage.tsx +++ b/frontend/src/pages/tags/TagHistoryPage.tsx @@ -11,8 +11,10 @@ import { toast } from '@/components/ui/use-toast' import { SITE_TITLE } from '@/config' import { CATEGORY_NAMES } from '@/consts' import { apiPut } from '@/lib/api' +import { clientScrollBehaviour } from '@/lib/clientAnimation' import { postsKeys, tagsKeys } from '@/lib/queryKeys' import { fetchTagChanges } from '@/lib/tags' +import { useClientBehaviourSettings } from '@/lib/useClientBehaviourSettings' import { cn, dateString } from '@/lib/utils' import type { FC } from 'react' @@ -43,6 +45,8 @@ const renderStateDiff = (diff: { current: string | null; prev: string | null }) const TagHistoryPage: FC = () => { + const behaviourSettings = useClientBehaviourSettings () + const animationMode = behaviourSettings.animation ?? 'normal' const location = useLocation () const query = new URLSearchParams (location.search) const id = query.get ('id') @@ -58,8 +62,9 @@ const TagHistoryPage: FC = () => { const qc = useQueryClient () useEffect (() => { - document.querySelector ('table')?.scrollIntoView ({ behavior: 'smooth' }) - }, [location.search]) + document.querySelector ('table')?.scrollIntoView ({ + behavior: clientScrollBehaviour (animationMode) }) + }, [animationMode, location.search]) return ( diff --git a/frontend/src/pages/tags/TagListPage.tsx b/frontend/src/pages/tags/TagListPage.tsx index f6c9fc6..568c5d6 100644 --- a/frontend/src/pages/tags/TagListPage.tsx +++ b/frontend/src/pages/tags/TagListPage.tsx @@ -13,6 +13,7 @@ import Pagination from '@/components/common/Pagination' import MainArea from '@/components/layout/MainArea' import { SITE_TITLE } from '@/config' import { CATEGORIES, CATEGORY_NAMES } from '@/consts' +import { clientScrollBehaviour } from '@/lib/clientAnimation' import { tagsKeys } from '@/lib/queryKeys' import { DEFAULT_POST_LIST_LIMIT, @@ -22,6 +23,7 @@ import { LIST_LIMIT_OPTIONS, setClientListSettings, } from '@/lib/settings' +import { useClientBehaviourSettings } from '@/lib/useClientBehaviourSettings' import { useKeyboardShortcuts } from '@/lib/useKeyboardShortcuts' import { fetchTags } from '@/lib/tags' import { dateString, inputClass } from '@/lib/utils' @@ -71,6 +73,8 @@ const parseOrder = (value: string | null): FetchTagsOrder | null => { const TagListPage: FC = () => { + const behaviourSettings = useClientBehaviourSettings () + const animationMode = behaviourSettings.animation ?? 'normal' const location = useLocation () const navigate = useNavigate () @@ -145,9 +149,11 @@ const TagListPage: FC = () => { setUpdatedTo (qUpdatedTo) setDeprecated (qDeprecated) - document.querySelector ('table')?.scrollIntoView ({ behavior: 'smooth' }) - }, [location.search, qCategory, qCreatedFrom, qCreatedTo, qName, qPostCountGTE, - qPostCountLTE, qUpdatedFrom, qUpdatedTo, qDeprecated]) + document.querySelector ('table')?.scrollIntoView ({ + behavior: clientScrollBehaviour (animationMode) }) + }, [animationMode, location.search, qCategory, qCreatedFrom, qCreatedTo, + qName, qPostCountGTE, qPostCountLTE, qUpdatedFrom, qUpdatedTo, + qDeprecated]) const handleSearch = (e: FormEvent) => { e.preventDefault () diff --git a/frontend/src/pages/theatres/TheatreDetailPage.tsx b/frontend/src/pages/theatres/TheatreDetailPage.tsx index 43a7e8b..1c99361 100644 --- a/frontend/src/pages/theatres/TheatreDetailPage.tsx +++ b/frontend/src/pages/theatres/TheatreDetailPage.tsx @@ -15,6 +15,8 @@ import { Button } from '@/components/ui/button' import { SITE_TITLE } from '@/config' import { CATEGORIES, CATEGORY_NAMES } from '@/consts' import { apiDelete, apiGet, apiPatch, apiPost, apiPut, isApiError } from '@/lib/api' +import { clientAnimationTransition, + clientScrollBehaviour } from '@/lib/clientAnimation' import { fetchPost } from '@/lib/posts' import { getClientTheatreLayoutMode, @@ -22,6 +24,7 @@ import { setClientTheatreLayoutMode, setClientTheatreTagFlow, } from '@/lib/settings' +import { useClientBehaviourSettings } from '@/lib/useClientBehaviourSettings' import { canEditContent } from '@/lib/users' import { cn, dateString, inputClass } from '@/lib/utils' import { useValidationErrors } from '@/lib/useValidationErrors' @@ -212,6 +215,12 @@ type Props = { user: User | null } const TheatreDetailPage: FC = ({ 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 dialogue = useDialogue () @@ -618,7 +627,9 @@ const TheatreDetailPage: FC = ({ user }: Props) => { clearValidationErrors () await apiPost (`/theatres/${ id }/comments`, { content }) setContent ('') - commentsRef.current?.scrollTo ({ top: 0, behavior: 'smooth' }) + commentsRef.current?.scrollTo ({ + top: 0, + behavior: clientScrollBehaviour (animationMode) }) } catch (error) { @@ -835,8 +846,8 @@ const TheatreDetailPage: FC = ({ user }: Props) => { return ( @@ -860,7 +871,7 @@ const TheatreDetailPage: FC = ({ user }: Props) => { )}