このコミットが含まれているのは:
2026-07-05 12:51:31 +09:00
コミット b0cce4b7ee
8個のファイルの変更319行の追加200行の削除
+3 -15
ファイルの表示
@@ -13,13 +13,11 @@ import DialogueProvider from '@/components/dialogues/DialogueProvider'
import { Toaster } from '@/components/ui/toaster' import { Toaster } from '@/components/ui/toaster'
import { apiPost, isApiError } from '@/lib/api' import { apiPost, isApiError } from '@/lib/api'
import { applyClientAppearance, import { applyClientAppearance,
CLIENT_SETTINGS_EVENT,
fetchUserSettings, fetchUserSettings,
getClientAnimationMode,
getClientThemeMode, getClientThemeMode,
seedClientThemeMode } from '@/lib/settings' seedClientThemeMode } from '@/lib/settings'
import { useClientBehaviourSettings } from '@/lib/useClientBehaviourSettings'
import type { ClientAnimationMode } from '@/lib/settings'
import { KeyboardShortcutsProvider } from '@/lib/useKeyboardShortcuts' import { KeyboardShortcutsProvider } from '@/lib/useKeyboardShortcuts'
import DeerjikistDetailPage from '@/pages/deerjikists/DeerjikistDetailPage' import DeerjikistDetailPage from '@/pages/deerjikists/DeerjikistDetailPage'
import MaterialBasePage from '@/pages/materials/MaterialBasePage' import MaterialBasePage from '@/pages/materials/MaterialBasePage'
@@ -112,8 +110,8 @@ const PostDetailRoute = ({ user }: { user: User | null }) => {
const App: FC = () => { const App: FC = () => {
const [user, setUser] = useState<User | null> (null) const [user, setUser] = useState<User | null> (null)
const [status, setStatus] = useState (200) const [status, setStatus] = useState (200)
const [animationMode, setAnimationMode] = const behaviourSettings = useClientBehaviourSettings ()
useState<ClientAnimationMode> (() => getClientAnimationMode ()) const animationMode = behaviourSettings.animation ?? 'normal'
const appLayoutTransition = useMemo ( const appLayoutTransition = useMemo (
() => ( () => (
@@ -139,16 +137,6 @@ const App: FC = () => {
return () => mediaQuery.removeEventListener ('change', handleThemeChange) return () => mediaQuery.removeEventListener ('change', handleThemeChange)
}, []) }, [])
useEffect (() => {
const handleClientSettingsChange = () => {
setAnimationMode (getClientAnimationMode ())
}
window.addEventListener (CLIENT_SETTINGS_EVENT, handleClientSettingsChange)
return () =>
window.removeEventListener (CLIENT_SETTINGS_EVENT, handleClientSettingsChange)
}, [])
useEffect (() => { useEffect (() => {
const createUser = async () => { const createUser = async () => {
const data = await apiPost<{ code: string; user: User }> ('/users') const data = await apiPost<{ code: string; user: User }> ('/users')
+3 -2
ファイルの表示
@@ -5,7 +5,7 @@ import NicoViewer from '@/components/NicoViewer'
import TwitterEmbed from '@/components/TwitterEmbed' import TwitterEmbed from '@/components/TwitterEmbed'
import { useDialogue } from '@/components/dialogues/DialogueProvider' import { useDialogue } from '@/components/dialogues/DialogueProvider'
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
import { getClientEmbedAutoLoadMode } from '@/lib/settings' import { useClientBehaviourSettings } from '@/lib/useClientBehaviourSettings'
import type { FC, RefObject } from 'react' import type { FC, RefObject } from 'react'
@@ -36,7 +36,8 @@ const PostEmbed: FC<Props> = (
onError }, onError },
) => { ) => {
const dialogue = useDialogue () const dialogue = useDialogue ()
const embedAutoLoadMode = getClientEmbedAutoLoadMode () const behaviourSettings = useClientBehaviourSettings ()
const embedAutoLoadMode = behaviourSettings.embedAutoLoad ?? 'auto'
const [manualLoadRequested, setManualLoadRequested] = useState (false) const [manualLoadRequested, setManualLoadRequested] = useState (false)
const [framed, setFramed] = useState (false) const [framed, setFramed] = useState (false)
const [youtubePlayer, setYoutubePlayer] = useState<YouTubePlayer | null> (null) const [youtubePlayer, setYoutubePlayer] = useState<YouTubePlayer | null> (null)
+13 -4
ファイルの表示
@@ -3,7 +3,7 @@ import { useRef } from 'react'
import { useLocation } from 'react-router-dom' import { useLocation } from 'react-router-dom'
import PrefetchLink from '@/components/PrefetchLink' import PrefetchLink from '@/components/PrefetchLink'
import { getClientThumbnailMode } from '@/lib/settings' import { useClientBehaviourSettings } from '@/lib/useClientBehaviourSettings'
import { cn } from '@/lib/utils' import { cn } from '@/lib/utils'
import { useSharedTransitionStore } from '@/stores/sharedTransitionStore' import { useSharedTransitionStore } from '@/stores/sharedTransitionStore'
@@ -17,7 +17,15 @@ type Props = { posts: Post[]
const PostList: FC<Props> = ({ posts, onClick }) => { const PostList: FC<Props> = ({ posts, onClick }) => {
const location = useLocation () const location = useLocation ()
const thumbnailMode = getClientThumbnailMode () const behaviourSettings = useClientBehaviourSettings ()
const thumbnailMode = behaviourSettings.thumbnailMode ?? 'normal'
const animationMode = behaviourSettings.animation ?? 'normal'
const cardLayoutTransition =
animationMode === 'off'
? { duration: 0 }
: animationMode === 'reduced'
? { duration: .08, ease: 'linear' as const }
: { duration: .2, ease: 'easeOut' as const }
const setForLocationKey = useSharedTransitionStore (s => s.setForLocationKey) const setForLocationKey = useSharedTransitionStore (s => s.setForLocationKey)
@@ -41,7 +49,8 @@ const PostList: FC<Props> = ({ posts, onClick }) => {
}}> }}>
<motion.div <motion.div
ref={cardRef} ref={cardRef}
layoutId={layoutId} layout={animationMode === 'off' ? false : true}
layoutId={animationMode === 'off' ? undefined : layoutId}
className={cn ('w-full h-full overflow-hidden rounded-xl shadow', className={cn ('w-full h-full overflow-hidden rounded-xl shadow',
'transform-gpu will-change-transform', 'transform-gpu will-change-transform',
(post.childPosts ?? []).length > 0 && 'ring-4 ring-green-500', (post.childPosts ?? []).length > 0 && 'ring-4 ring-green-500',
@@ -61,7 +70,7 @@ const PostList: FC<Props> = ({ posts, onClick }) => {
cardRef.current.style.zIndex = '' cardRef.current.style.zIndex = ''
cardRef.current.style.position = '' cardRef.current.style.position = ''
}} }}
transition={{ layout: { duration: .2, ease: 'easeOut' } }}> transition={{ layout: cardLayoutTransition }}>
{thumbnailMode === 'off' {thumbnailMode === 'off'
? ( ? (
<div className="flex h-full w-full items-center justify-center <div className="flex h-full w-full items-center justify-center
+247 -133
ファイルの表示
@@ -8,6 +8,7 @@ import PrefetchLink from '@/components/PrefetchLink'
import TopNavUser from '@/components/TopNavUser' import TopNavUser from '@/components/TopNavUser'
import { WikiIdBus } from '@/lib/eventBus/WikiIdBus' import { WikiIdBus } from '@/lib/eventBus/WikiIdBus'
import { materialsKeys, tagsKeys, wikiKeys } from '@/lib/queryKeys' import { materialsKeys, tagsKeys, wikiKeys } from '@/lib/queryKeys'
import { useClientBehaviourSettings } from '@/lib/useClientBehaviourSettings'
import { fetchTag, fetchTagByName } from '@/lib/tags' import { fetchTag, fetchTagByName } from '@/lib/tags'
import { fetchMaterial } from '@/lib/materials' import { fetchMaterial } from '@/lib/materials'
import { cn } from '@/lib/utils' import { cn } from '@/lib/utils'
@@ -91,6 +92,10 @@ export const menuOutline = (
const TopNav: FC<Props> = ({ user }) => { const TopNav: FC<Props> = ({ user }) => {
const location = useLocation () const location = useLocation ()
const behaviourSettings = useClientBehaviourSettings ()
const animationMode = behaviourSettings.animation ?? 'normal'
const animationsOff = animationMode === 'off'
const reducedAnimations = animationMode === 'reduced'
const dirRef = useRef<(-1) | 1> (1) const dirRef = useRef<(-1) | 1> (1)
const itemsRef = useRef<(HTMLAnchorElement | null)[]> ([]) const itemsRef = useRef<(HTMLAnchorElement | null)[]> ([])
@@ -155,6 +160,20 @@ const TopNav: FC<Props> = ({ user }) => {
const activeIdx = const activeIdx =
visibleMenu.findIndex (item => location.pathname.startsWith (item.base || item.to)) visibleMenu.findIndex (item => location.pathname.startsWith (item.base || item.to))
const submenuHeight = moreVsbl ? 40 * moreMenu.length : (activeIdx < 0 ? 0 : 40) const submenuHeight = moreVsbl ? 40 * moreMenu.length : (activeIdx < 0 ? 0 : 40)
const topNavTransition =
reducedAnimations
? { duration: .08, ease: 'linear' as const }
: { duration: .2, ease: 'easeOut' as const }
const opacityTransition =
reducedAnimations
? { duration: .08 }
: { duration: .12 }
const highlightTransitionClass =
animationsOff
? undefined
: reducedAnimations
? 'transition-[transform,width] duration-75 ease-linear'
: 'transition-[transform,width] duration-200 ease-out'
const prevActiveIdxRef = useRef<number> (activeIdx) const prevActiveIdxRef = useRef<number> (activeIdx)
@@ -206,7 +225,7 @@ const TopNav: FC<Props> = ({ user }) => {
<div aria-hidden <div aria-hidden
className={cn ('absolute inset-y-0 h-12', className={cn ('absolute inset-y-0 h-12',
'bg-yellow-200 dark:bg-red-950', 'bg-yellow-200 dark:bg-red-950',
'transition-[transform,width] duration-200 ease-out')} highlightTransitionClass)}
style={{ width: hl.width, style={{ width: hl.width,
transform: `translateX(${ hl.left }px)`, transform: `translateX(${ hl.left }px)`,
opacity: hl.visible ? 1 : 0 }}/> opacity: hl.visible ? 1 : 0 }}/>
@@ -214,10 +233,12 @@ const TopNav: FC<Props> = ({ user }) => {
{visibleMenu.map ((item, i) => ( {visibleMenu.map ((item, i) => (
<motion.div <motion.div
key={item.to} key={item.to}
layoutId={`menu-${ item.name }`} {...(animationsOff
? { }
: { layoutId: `menu-${ item.name }` })}
animate={{ opacity: moreVsbl ? 0 : 1 }} animate={{ opacity: moreVsbl ? 0 : 1 }}
transition={{ opacity: { duration: .12 }, transition={{ opacity: opacityTransition,
layout: { duration: .2, ease: 'easeOut' } }} layout: topNavTransition }}
style={{ pointerEvents: moreVsbl ? 'none' : 'auto' }} style={{ pointerEvents: moreVsbl ? 'none' : 'auto' }}
onMouseEnter={() => setMoreVsbl (false)}> onMouseEnter={() => setMoreVsbl (false)}>
<PrefetchLink <PrefetchLink
@@ -264,7 +285,8 @@ const TopNav: FC<Props> = ({ user }) => {
<AnimatePresence initial={false}> <AnimatePresence initial={false}>
<motion.div <motion.div
key="submenu-shell" key="submenu-shell"
layout {...(animationsOff ? { }
: { layout: true })}
className="relative z-20 hidden md:block overflow-hidden className="relative z-20 hidden md:block overflow-hidden
bg-yellow-200 dark:bg-red-950" bg-yellow-200 dark:bg-red-950"
animate={{ height: submenuHeight }} animate={{ height: submenuHeight }}
@@ -272,7 +294,7 @@ const TopNav: FC<Props> = ({ user }) => {
if (moreVsbl) if (moreVsbl)
setMoreVsbl (false) setMoreVsbl (false)
}} }}
transition={{ layout: { duration: .2, ease: 'easeOut' } }} transition={{ layout: topNavTransition }}
onAnimationComplete={() => { onAnimationComplete={() => {
measure (moreVsbl ? -1 : activeIdx) measure (moreVsbl ? -1 : activeIdx)
}}> }}>
@@ -282,12 +304,14 @@ const TopNav: FC<Props> = ({ user }) => {
<div key={i} className="relative h-[40px]"> <div key={i} className="relative h-[40px]">
<div className="absolute inset-0 flex items-center px-3"> <div className="absolute inset-0 flex items-center px-3">
<motion.div <motion.div
transition={{ duration: .2, ease: 'easeOut' }} transition={topNavTransition}
{...((item.visible ?? true) {...(animationsOff
? { }
: ((item.visible ?? true)
? { layoutId: `menu-${ item.name }` } ? { layoutId: `menu-${ item.name }` }
: { initial: { x: 40, y: -40, opacity: 0 }, : { initial: { x: 40, y: -40, opacity: 0 },
animate: { x: 0, y: 0, opacity: 1 }, animate: { x: 0, y: 0, opacity: 1 },
exit: { x: 40, y: -40, opacity: 0 } })} exit: { x: 40, y: -40, opacity: 0 } }))}
className="z-10 h-full flex items-center px-3 font-bold w-28"> className="z-10 h-full flex items-center px-3 font-bold w-28">
<h2>{item.name}</h2> <h2>{item.name}</h2>
</motion.div> </motion.div>
@@ -298,25 +322,29 @@ const TopNav: FC<Props> = ({ user }) => {
? ( ? (
<motion.div <motion.div
key={`c-${ i }-${ j }`} key={`c-${ i }-${ j }`}
transition={{ duration: .2, ease: 'easeOut' }} transition={topNavTransition}
{...((visibleMenu[activeIdx]?.name {...(animationsOff
? { }
: ((visibleMenu[activeIdx]?.name
=== item.name) === item.name)
? { layoutId: `submenu-${ item.name }-${ j }` } ? { layoutId: `submenu-${ item.name }-${ j }` }
: { initial: { y: -40, opacity: 0 }, : { initial: { y: -40, opacity: 0 },
animate: { y: 0, opacity: 1 }, animate: { y: 0, opacity: 1 },
exit: { y: -40, opacity: 0 } })}> exit: { y: -40, opacity: 0 } }))}>
{subItem.component} {subItem.component}
</motion.div>) </motion.div>)
: ( : (
<motion.div <motion.div
key={`l-${ i }-${ j }`} key={`l-${ i }-${ j }`}
transition={{ duration: .2, ease: 'easeOut' }} transition={topNavTransition}
{...((visibleMenu[activeIdx]?.name {...(animationsOff
? { }
: ((visibleMenu[activeIdx]?.name
=== item.name) === item.name)
? { layoutId: `submenu-${ item.name }-${ j }` } ? { layoutId: `submenu-${ item.name }-${ j }` }
: { initial: { y: -40, opacity: 0 }, : { initial: { y: -40, opacity: 0 },
animate: { y: 0, opacity: 1 }, animate: { y: 0, opacity: 1 },
exit: { y: -40, opacity: 0 } })}> exit: { y: -40, opacity: 0 } }))}>
<PrefetchLink <PrefetchLink
to={subItem.to} to={subItem.to}
target={subItem.to.slice (0, 2) === '//' ? '_blank' : undefined} target={subItem.to.slice (0, 2) === '//' ? '_blank' : undefined}
@@ -330,129 +358,215 @@ const TopNav: FC<Props> = ({ user }) => {
: ((visibleMenu[activeIdx]?.subMenu ?? []).length > 0 : ((visibleMenu[activeIdx]?.subMenu ?? []).length > 0
&& ( && (
<div className="relative h-[40px]"> <div className="relative h-[40px]">
<AnimatePresence initial={false} custom={dir}> {animationsOff
<motion.div ? (
key={activeIdx} <div className="absolute inset-0 flex items-center px-3">
custom={dir} {(visibleMenu[activeIdx]?.subMenu ?? [])
variants={{ enter: (d: -1 | 1) => ({ y: d * 24, opacity: 0 }), .filter (item => item.visible ?? true)
centre: { y: 0, opacity: 1 }, .map ((item, i) => (
exit: (d: -1 | 1) => ({ y: (-d) * 24, opacity: 0 }) }} 'component' in item
className="absolute inset-0 flex items-center px-3" ? (
initial="enter" <div key={`c-${ i }`}>
animate="centre" {item.component}
exit="exit" </div>)
transition={{ duration: .2, ease: 'easeOut' }}> : (
{(visibleMenu[activeIdx]?.subMenu ?? []) <div key={`l-${ i }`}>
.filter (item => item.visible ?? true) <PrefetchLink
.map ((item, i) => ( to={item.to}
'component' in item target={item.to.slice (0, 2) === '//'
? ( ? '_blank'
<motion.div : undefined}
key={`c-${ i }`} className="h-full flex items-center px-3">
transition={{ layout: { duration: .2, ease: 'easeOut' } }} {item.name}
layoutId={`submenu-${ visibleMenu[activeIdx].name }-${ i }`}> </PrefetchLink>
{item.component} </div>)))}
</motion.div>) </div>)
: ( : (
<motion.div <AnimatePresence initial={false} custom={dir}>
key={`l-${ i }`} <motion.div
transition={{ layout: { duration: .2, ease: 'easeOut' } }} key={activeIdx}
layoutId={`submenu-${ visibleMenu[activeIdx].name }-${ i }`}> custom={dir}
<PrefetchLink variants={{ enter: (d: -1 | 1) => ({ y: d * 24, opacity: 0 }),
to={item.to} centre: { y: 0, opacity: 1 },
target={item.to.slice (0, 2) === '//' ? '_blank' : undefined} exit: (d: -1 | 1) => ({ y: (-d) * 24, opacity: 0 }) }}
className="h-full flex items-center px-3"> className="absolute inset-0 flex items-center px-3"
{item.name} initial="enter"
</PrefetchLink> animate="centre"
</motion.div>)))} exit="exit"
</motion.div> transition={topNavTransition}>
</AnimatePresence> {(visibleMenu[activeIdx]?.subMenu ?? [])
.filter (item => item.visible ?? true)
.map ((item, i) => (
'component' in item
? (
<motion.div
key={`c-${ i }`}
transition={{ layout: topNavTransition }}
layoutId={`submenu-${ visibleMenu[activeIdx].name }-${ i }`}>
{item.component}
</motion.div>)
: (
<motion.div
key={`l-${ i }`}
transition={{ layout: topNavTransition }}
layoutId={`submenu-${ visibleMenu[activeIdx].name }-${ i }`}>
<PrefetchLink
to={item.to}
target={item.to.slice (0, 2) === '//'
? '_blank'
: undefined}
className="h-full flex items-center px-3">
{item.name}
</PrefetchLink>
</motion.div>)))}
</motion.div>
</AnimatePresence>)}
</div>))} </div>))}
</motion.div> </motion.div>
</AnimatePresence> </AnimatePresence>
<AnimatePresence initial={false}> {animationsOff
{menuOpen && ( ? (
<motion.div menuOpen && (
key="spmenu" <div
className={cn ('flex flex-col md:hidden', className={cn ('flex flex-col md:hidden',
'bg-yellow-200 dark:bg-red-975 items-start')} 'bg-yellow-200 dark:bg-red-975 items-start')}>
variants={{ closed: { clipPath: 'inset(0 0 100% 0)', <Separator/>
height: 0 }, {visibleMenu.map ((item, i) => (
open: { clipPath: 'inset(0 0 0% 0)', <Fragment key={i}>
height: 'auto' } }} <PrefetchLink
initial="closed" to={i === openItemIdx ? item.to : '#'}
animate="open" className={cn ('w-full min-h-[40px] flex items-center pl-8',
exit="closed" ((i === openItemIdx)
transition={{ duration: .2, ease: 'easeOut' }}> && 'font-bold bg-yellow-50 dark:bg-red-950'))}
<Separator/> onClick={(ev: MouseEvent<HTMLAnchorElement>) => {
{visibleMenu.map ((item, i) => ( if (i !== openItemIdx)
<Fragment key={i}> {
<PrefetchLink ev.preventDefault ()
to={i === openItemIdx ? item.to : '#'} setOpenItemIdx (i)
className={cn ('w-full min-h-[40px] flex items-center pl-8', }
((i === openItemIdx) }}>
&& 'font-bold bg-yellow-50 dark:bg-red-950'))} {item.name}
onClick={(ev: MouseEvent<HTMLAnchorElement>) => { </PrefetchLink>
if (i !== openItemIdx)
{
ev.preventDefault ()
setOpenItemIdx (i)
}
}}>
{item.name}
</PrefetchLink>
<AnimatePresence initial={false}> {i === openItemIdx && (
{i === openItemIdx && ( <div className="w-full bg-yellow-50 dark:bg-red-950">
<motion.div {item.subMenu
key={`sp-sub-${ i }`} .filter (subItem => subItem.visible ?? true)
className="w-full bg-yellow-50 dark:bg-red-950" .map ((subItem, j) => (
variants={{ closed: { clipPath: 'inset(0 0 100% 0)', 'component' in subItem
height: 0, ? (
opacity: 0 }, <Fragment key={`sp-c-${ i }-${ j }`}>
open: { clipPath: 'inset(0 0 0% 0)', {subItem.component}
height: 'auto', </Fragment>)
opacity: 1 } }} : (
initial="closed" <PrefetchLink
animate="open" key={`sp-l-${ i }-${ j }`}
exit="closed" to={subItem.to}
transition={{ duration: .2, ease: 'easeOut' }}> target={subItem.to.slice (0, 2) === '//'
{item.subMenu ? '_blank'
.filter (subItem => subItem.visible ?? true) : undefined}
.map ((subItem, j) => ( className="w-full min-h-[36px] flex items-center pl-12">
'component' in subItem {subItem.name}
? ( </PrefetchLink>)))}
<Fragment key={`sp-c-${ i }-${ j }`}> </div>)}
{subItem.component} </Fragment>))}
</Fragment>) <PrefetchLink
: ( to="/more"
<PrefetchLink ref={(el: (HTMLAnchorElement | null)) => {
key={`sp-l-${ i }-${ j }`} itemsRef.current[visibleMenu.length] = el
to={subItem.to} }}
target={subItem.to.slice (0, 2) === '//' className={cn ('w-full min-h-[40px] flex items-center pl-8',
? '_blank' ((openItemIdx < 0)
: undefined} && 'font-bold bg-yellow-50 dark:bg-red-950'))}>
className="w-full min-h-[36px] flex items-center pl-12"> &raquo;
{subItem.name} </PrefetchLink>
</PrefetchLink>)))} <TopNavUser user={user} sp/>
</motion.div>)} <Separator/>
</AnimatePresence> </div>))
</Fragment>))} : (
<PrefetchLink <AnimatePresence initial={false}>
to="/more" {menuOpen && (
ref={(el: (HTMLAnchorElement | null)) => { <motion.div
itemsRef.current[visibleMenu.length] = el key="spmenu"
}} className={cn ('flex flex-col md:hidden',
className={cn ('w-full min-h-[40px] flex items-center pl-8', 'bg-yellow-200 dark:bg-red-975 items-start')}
((openItemIdx < 0) variants={{ closed: { clipPath: 'inset(0 0 100% 0)',
&& 'font-bold bg-yellow-50 dark:bg-red-950'))}> height: 0 },
&raquo; open: { clipPath: 'inset(0 0 0% 0)',
</PrefetchLink> height: 'auto' } }}
<TopNavUser user={user} sp/> initial="closed"
<Separator/> animate="open"
</motion.div>)} exit="closed"
</AnimatePresence> transition={topNavTransition}>
<Separator/>
{visibleMenu.map ((item, i) => (
<Fragment key={i}>
<PrefetchLink
to={i === openItemIdx ? item.to : '#'}
className={cn ('w-full min-h-[40px] flex items-center pl-8',
((i === openItemIdx)
&& 'font-bold bg-yellow-50 dark:bg-red-950'))}
onClick={(ev: MouseEvent<HTMLAnchorElement>) => {
if (i !== openItemIdx)
{
ev.preventDefault ()
setOpenItemIdx (i)
}
}}>
{item.name}
</PrefetchLink>
<AnimatePresence initial={false}>
{i === openItemIdx && (
<motion.div
key={`sp-sub-${ i }`}
className="w-full bg-yellow-50 dark:bg-red-950"
variants={{ closed: { clipPath: 'inset(0 0 100% 0)',
height: 0,
opacity: 0 },
open: { clipPath: 'inset(0 0 0% 0)',
height: 'auto',
opacity: 1 } }}
initial="closed"
animate="open"
exit="closed"
transition={topNavTransition}>
{item.subMenu
.filter (subItem => subItem.visible ?? true)
.map ((subItem, j) => (
'component' in subItem
? (
<Fragment key={`sp-c-${ i }-${ j }`}>
{subItem.component}
</Fragment>)
: (
<PrefetchLink
key={`sp-l-${ i }-${ j }`}
to={subItem.to}
target={subItem.to.slice (0, 2) === '//'
? '_blank'
: undefined}
className="w-full min-h-[36px] flex items-center pl-12">
{subItem.name}
</PrefetchLink>)))}
</motion.div>)}
</AnimatePresence>
</Fragment>))}
<PrefetchLink
to="/more"
ref={(el: (HTMLAnchorElement | null)) => {
itemsRef.current[visibleMenu.length] = el
}}
className={cn ('w-full min-h-[40px] flex items-center pl-8',
((openItemIdx < 0)
&& 'font-bold bg-yellow-50 dark:bg-red-950'))}>
&raquo;
</PrefetchLink>
<TopNavUser user={user} sp/>
<Separator/>
</motion.div>)}
</AnimatePresence>)}
</>) </>)
} }
+14 -25
ファイルの表示
@@ -2,17 +2,16 @@ import { useEffect, useMemo, useState } from 'react'
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
import { toast } from '@/components/ui/use-toast' import { toast } from '@/components/ui/use-toast'
import { getClientBehaviorSettings, import { getClientBehaviorSettings, setClientBehaviorSettings } from '@/lib/settings'
setClientBehaviorSettings } from '@/lib/settings'
import { cn } from '@/lib/utils' import { cn } from '@/lib/utils'
import type {
ClientAnimationMode,
ClientBehaviorSettings,
ClientEmbedAutoLoadMode,
ClientThumbnailMode } from '@/lib/settings'
import type { FC, ReactNode } from 'react' import type { FC, ReactNode } from 'react'
import type { ClientAnimationMode,
ClientBehaviorSettings,
ClientEmbedAutoLoadMode,
ClientThumbnailMode } from '@/lib/settings'
type Props = { type Props = {
sectionClassName: string } sectionClassName: string }
@@ -31,25 +30,22 @@ type SettingBlockProps = {
children: ReactNode } children: ReactNode }
const animationOptions: SegmentedOption<ClientAnimationMode>[] = [ const animationOptions: SegmentedOption<ClientAnimationMode>[] = [
{ value: 'normal', label: '通常' },
{ value: 'reduced', label: '控えめ' },
{ value: 'off', label: 'なし' }, { value: 'off', label: 'なし' },
] { value: 'reduced', label: '控えめ' },
{ value: 'normal', label: '通常' }]
const embedAutoLoadOptions: SegmentedOption<ClientEmbedAutoLoadMode>[] = [ const embedAutoLoadOptions: SegmentedOption<ClientEmbedAutoLoadMode>[] = [
{ value: 'auto', label: '自動' },
{ value: 'manual', label: 'クリック時' },
{ value: 'off', label: '読み込まない' }, { value: 'off', label: '読み込まない' },
] { value: 'manual', label: 'クリック時' },
{ value: 'auto', label: '自動' }]
const thumbnailModeOptions: SegmentedOption<ClientThumbnailMode>[] = [ const thumbnailModeOptions: SegmentedOption<ClientThumbnailMode>[] = [
{ value: 'normal', label: '通常' },
{ value: 'light', label: '軽量' },
{ value: 'off', label: '非表示' }, { value: 'off', label: '非表示' },
] { value: 'light', label: '軽量' },
{ value: 'normal', label: '通常' }]
const SegmentedControl = <T extends string,>( const SegmentedControl = <T extends string,> (
{ value, options, onChange }: SegmentedControlProps<T>, { value, options, onChange }: SegmentedControlProps<T>,
) => { ) => {
return ( return (
@@ -103,8 +99,7 @@ const BehaviourSettingsSection: FC<Props> = ({ sectionClassName }) => {
const hasUnsavedChanges = useMemo ( const hasUnsavedChanges = useMemo (
() => JSON.stringify (draftSettings) !== JSON.stringify (savedSettings), () => JSON.stringify (draftSettings) !== JSON.stringify (savedSettings),
[draftSettings, savedSettings], [draftSettings, savedSettings])
)
const updateDraft = <Key extends keyof ClientBehaviorSettings,> ( const updateDraft = <Key extends keyof ClientBehaviorSettings,> (
key: Key, key: Key,
@@ -129,12 +124,6 @@ const BehaviourSettingsSection: FC<Props> = ({ sectionClassName }) => {
<div className="flex flex-wrap items-start justify-between gap-3"> <div className="flex flex-wrap items-start justify-between gap-3">
<div className="space-y-1"> <div className="space-y-1">
<h2 className="text-xl font-bold"></h2> <h2 className="text-xl font-bold"></h2>
<p className="text-sm text-muted-foreground">
</p>
<p className="text-sm text-muted-foreground">
調
</p>
{hasUnsavedChanges && ( {hasUnsavedChanges && (
<p className="text-sm text-amber-700 dark:text-amber-300"> <p className="text-sm text-amber-700 dark:text-amber-300">
+26
ファイルの表示
@@ -0,0 +1,26 @@
import { useEffect, useState } from 'react'
import {
CLIENT_SETTINGS_EVENT,
getClientBehaviorSettings,
} from '@/lib/settings'
import type { ClientBehaviorSettings } from '@/lib/settings'
export const useClientBehaviourSettings = (): ClientBehaviorSettings => {
const [settings, setSettings] =
useState<ClientBehaviorSettings> (() => getClientBehaviorSettings ())
useEffect (() => {
const handleClientSettingsChange = () => {
setSettings (getClientBehaviorSettings ())
}
window.addEventListener (CLIENT_SETTINGS_EVENT, handleClientSettingsChange)
return () =>
window.removeEventListener (CLIENT_SETTINGS_EVENT, handleClientSettingsChange)
}, [])
return settings
}
+13 -4
ファイルの表示
@@ -15,7 +15,7 @@ import { SITE_TITLE } from '@/config'
import { isApiError } from '@/lib/api' import { isApiError } from '@/lib/api'
import { fetchPost, toggleViewedFlg } from '@/lib/posts' import { fetchPost, toggleViewedFlg } from '@/lib/posts'
import { postsKeys, tagsKeys } from '@/lib/queryKeys' import { postsKeys, tagsKeys } from '@/lib/queryKeys'
import { getClientThumbnailMode } from '@/lib/settings' import { useClientBehaviourSettings } from '@/lib/useClientBehaviourSettings'
import { canEditContent } from '@/lib/users' import { canEditContent } from '@/lib/users'
import { cn } from '@/lib/utils' import { cn } from '@/lib/utils'
import NotFound from '@/pages/NotFound' import NotFound from '@/pages/NotFound'
@@ -30,8 +30,17 @@ type Props = { user: User | null }
const PostDetailPage: FC<Props> = ({ user }) => { const PostDetailPage: FC<Props> = ({ user }) => {
const editable = canEditContent (user) const editable = canEditContent (user)
const thumbnailMode = getClientThumbnailMode () const behaviourSettings = useClientBehaviourSettings ()
const thumbnailMode = behaviourSettings.thumbnailMode ?? 'normal'
const animationMode = behaviourSettings.animation ?? 'normal'
const { id } = useParams () const { id } = useParams ()
const heroLayoutId = animationMode === 'off' ? undefined : `page-${ id }`
const heroTransition =
animationMode === 'off'
? { duration: 0 }
: animationMode === 'reduced'
? { duration: .08, ease: 'linear' as const }
: { duration: .2, ease: 'easeOut' as const }
const postId = String (id ?? '') const postId = String (id ?? '')
const postKey = postsKeys.show (postId) const postKey = postsKeys.show (postId)
@@ -138,12 +147,12 @@ const PostDetailPage: FC<Props> = ({ user }) => {
{thumbnailMode !== 'off' && (post.thumbnail || post.thumbnailBase) && ( {thumbnailMode !== 'off' && (post.thumbnail || post.thumbnailBase) && (
<motion.div <motion.div
layoutId={`page-${ id }`} layoutId={heroLayoutId}
className="absolute top-4 left-4 w-[min(640px,calc(100vw-2rem))] h-[360px] className="absolute top-4 left-4 w-[min(640px,calc(100vw-2rem))] h-[360px]
overflow-hidden rounded-xl pointer-events-none z-50" overflow-hidden rounded-xl pointer-events-none z-50"
initial={{ opacity: 1 }} initial={{ opacity: 1 }}
animate={{ opacity: 0 }} animate={{ opacity: 0 }}
transition={{ duration: .2, ease: 'easeOut' }}> transition={heroTransition}>
<img src={post.thumbnail || post.thumbnailBase || undefined} <img src={post.thumbnail || post.thumbnailBase || undefined}
alt={post.title || post.url} alt={post.title || post.url}
title={post.title || post.url || undefined} title={post.title || post.url || undefined}
-17
ファイルの表示
@@ -319,23 +319,6 @@ const ThemeSection: FC<SharedSectionProps> = (
</div> </div>
</div> </div>
<div className="space-y-2 rounded-lg border border-border bg-background p-3
text-foreground shadow-sm transition-colors">
<p className="text-sm font-medium"></p>
<p className="text-xs text-muted-foreground">
: {themeLabelById (draftActiveThemeId, draftCustomThemes)}
</p>
<div className="flex flex-wrap gap-3" style={themePreviewStyle (draftTagColours)}>
{CATEGORIES.map (category => (
<TagLink
key={category}
tag={previewTags[category]}
linkFlg={false}
truncateOnMobile
withWiki={false}/>))}
</div>
</div>
<Button type="button" onClick={onThemeSubmit}> <Button type="button" onClick={onThemeSubmit}>
</Button> </Button>