このコミットが含まれているのは:
2026-11-08 15:31:02 +09:00
コミット 2b7f52978f
16個のファイルの変更、914行の追加、169行の削除
+1 -1
ファイルの表示
@@ -34,7 +34,7 @@ const PostList: FC<Props> = ({ posts, onClick }) => {
const cardRef = useRef<HTMLDivElement> (null)
return (
<div className="flex flex-wrap gap-6 p-4">
<div className="post-list flex flex-wrap gap-6 p-4">
{posts.map ((post, i) => {
const sharedId = `page-${ post.id }`
const layoutId = sharedId
+9 -5
ファイルの表示
@@ -1,9 +1,10 @@
import { useQueryClient } from '@tanstack/react-query'
import { forwardRef, useMemo } from 'react'
import { flushSync } from 'react-dom'
import { createPath } from 'react-router-dom'
import { createPath, useLocation } from 'react-router-dom'
import { localiseTo } from '@/lib/locales'
import { isVerticalPreviewPath, localeFromPath,
localiseTo, unlocalisedPath } from '@/lib/locales'
import { useLocale, useLocaleNavigate } from '@/lib/useLocale'
import { useOverlayStore } from '@/components/RouteBlockerOverlay'
import { prefetchForURL } from '@/lib/prefetchers'
@@ -38,15 +39,18 @@ export default forwardRef<HTMLAnchorElement, Props> (({
const navigate = useLocaleNavigate ()
const locale = useLocale ()
const { pathname } = useLocation ()
const qc = useQueryClient ()
const behaviourSettings = useClientBehaviourSettings ()
const linkPreloadMode = behaviourSettings.linkPreload ?? 'intent'
const path = useMemo (
() => {
const localised = localiseTo (to, locale)
return typeof localised === 'string' ? localised : createPath (localised)
const localised = localiseTo (to, locale, pathname)
const destination = typeof localised === 'string' ? localised : createPath (localised)
return (isVerticalPreviewPath (pathname) && localeFromPath (destination) === locale)
? `/mn-Mong${ unlocalisedPath (destination) }` : destination
},
[to, locale],
[to, locale, pathname],
)
const url = useMemo (() => {
return (new URL (path, currentRenderContext ()?.origin ?? 'http://localhost')).toString ()
+65 -27
ファイルの表示
@@ -4,7 +4,7 @@ import { Globe } from 'lucide-react'
import { Fragment, useEffect, useLayoutEffect, useRef, useState } from 'react'
import { useLocation } from 'react-router-dom'
import { useLocale, useLocaleNavigate } from '@/lib/useLocale'
import { useLocale, useLocaleNavigate, useVerticalPreview } from '@/lib/useLocale'
import LocalisedText, { formatUIString } from '@/components/LocalisedText'
import Separator from '@/components/MenuSeparator'
import PrefetchLink from '@/components/PrefetchLink'
@@ -137,6 +137,7 @@ export const menuOutline = (
const TopNav: FC<Props> = ({ user, locales, resolveLocaleSwitch }) => {
const { message, label } = useUITranslation ('shared')
const locale = useLocale ()
const verticalPreview = useVerticalPreview ()
const localeName = locales.find (entry => entry.code === locale)?.name
const navigate = useLocaleNavigate ()
const location = useLocation ()
@@ -180,11 +181,12 @@ const TopNav: FC<Props> = ({ user, locales, resolveLocaleSwitch }) => {
let node: HTMLElement | null = el
while (node != null && node !== nav)
{
left += node.offsetLeft
left += verticalPreview ? node.offsetTop : node.offsetLeft
const parent: Element | null = node.offsetParent
node = parent instanceof HTMLElement ? parent : null
}
const width = node === nav ? el.offsetWidth : 0
const width = node === nav
? (verticalPreview ? el.offsetHeight : el.offsetWidth) : 0
const visible = width > 0
setHL (current =>
(current.left === left && current.width === width && current.visible === visible)
@@ -202,6 +204,7 @@ const TopNav: FC<Props> = ({ user, locales, resolveLocaleSwitch }) => {
const [moreVsbl, setMoreVsbl] = useState (false)
const [openItemIdx, setOpenItemIdx] = useState (-1)
const [submenuHeight, setSubmenuHeight] = useState (0)
const [submenuWidth, setSubmenuWidth] = useState (0)
const [wikiId, setWikiId] = useState<number | null> (WikiIdBus.get ())
const wikiIdStr = String (wikiId ?? '')
@@ -255,8 +258,22 @@ const TopNav: FC<Props> = ({ user, locales, resolveLocaleSwitch }) => {
animationsOff
? undefined
: (reducedAnimations
? 'transition-[transform,width] duration-75 ease-linear'
: 'transition-[transform,width] duration-200 ease-out')
? 'transition-[transform,width,height] duration-75 ease-linear'
: 'transition-[transform,width,height] duration-200 ease-out')
const submenuItemMotion = animationsOff ? { } : {
initial: { x: verticalPreview ? -40 : 0,
y: verticalPreview ? 0 : -40, opacity: 0 },
animate: { x: 0, y: 0, opacity: 1 },
exit: { x: verticalPreview ? -40 : 0,
y: verticalPreview ? 0 : -40, opacity: 0 } }
const submenuVariants = {
enter: (direction: -1 | 1) => ({
x: verticalPreview ? direction * 24 : 0,
y: verticalPreview ? 0 : direction * 24, opacity: 0 }),
centre: { x: 0, y: 0, opacity: 1 },
exit: (direction: -1 | 1) => ({
x: verticalPreview ? (-direction) * 24 : 0,
y: verticalPreview ? 0 : (-direction) * 24, opacity: 0 }) }
const prevActiveIdxRef = useRef<number> (activeIdx)
@@ -287,6 +304,14 @@ const TopNav: FC<Props> = ({ user, locales, resolveLocaleSwitch }) => {
return
}
if (verticalPreview)
{
setDesktop (true)
setDensity ('auto')
measure (moreVsbl ? -1 : activeIdx)
return
}
// Measure the same desktop elements while the mobile layout is active.
layout.setAttribute ('data-measuring', '')
const previousDensity = layout.getAttribute ('data-density') ?? 'auto'
@@ -351,7 +376,8 @@ const TopNav: FC<Props> = ({ user, locales, resolveLocaleSwitch }) => {
removeEventListener ('resize', schedule)
document.fonts?.removeEventListener ('loadingdone', schedule)
}
}, [activeIdx, moreVsbl, desktop, density, user?.name, locale, locales, menuLabels])
}, [activeIdx, moreVsbl, desktop, density, user?.name, locale, locales,
menuLabels, verticalPreview])
useEffect (() => {
if (desktop == null || layoutReady)
@@ -378,7 +404,8 @@ const TopNav: FC<Props> = ({ user, locales, resolveLocaleSwitch }) => {
measure (moreVsbl ? -1 : activeIdx)
if (!(desktop))
setMoreVsbl (false)
}, [activeIdx, moreVsbl, desktop, density, locale, animationMode, menuLabels])
}, [activeIdx, moreVsbl, desktop, density, locale, animationMode,
menuLabels, verticalPreview])
useEffect (() => {
const unsubscribe = WikiIdBus.subscribe (setWikiId)
@@ -392,6 +419,7 @@ const TopNav: FC<Props> = ({ user, locales, resolveLocaleSwitch }) => {
const update = () => {
setSubmenuHeight (Math.ceil (content.getBoundingClientRect ().height))
setSubmenuWidth (Math.ceil (content.getBoundingClientRect ().width))
}
const observer =
typeof ResizeObserver === 'undefined' ? null : new ResizeObserver (update)
@@ -403,7 +431,7 @@ const TopNav: FC<Props> = ({ user, locales, resolveLocaleSwitch }) => {
observer?.disconnect ()
removeEventListener ('resize', update)
}
}, [activeIdx, moreVsbl, desktop, density, locale])
}, [activeIdx, moreVsbl, desktop, density, locale, verticalPreview])
useEffect (() => {
setMenuOpen (false)
@@ -479,8 +507,11 @@ const TopNav: FC<Props> = ({ user, locales, resolveLocaleSwitch }) => {
className={cn ('absolute inset-y-0 h-12',
'top-nav-highlight',
highlightTransitionClass)}
style={{ width: hl.width,
transform: `translateX(${ hl.left }px)`,
style={{ width: verticalPreview ? '100%' : hl.width,
height: verticalPreview ? hl.width : undefined,
transform: verticalPreview
? `translateY(${ hl.left }px)`
: `translateX(${ hl.left }px)`,
opacity: hl.visible ? 1 : 0 }}/>
{visibleMenu.map ((item, i) => (
@@ -529,10 +560,18 @@ const TopNav: FC<Props> = ({ user, locales, resolveLocaleSwitch }) => {
<div className="top-nav-account ml-auto flex min-w-0 items-center gap-2">
<TopNavUser user={user}/>
{localeSelector}
{verticalPreview && (
<span className="vertical-preview-sample" lang="mn-Mong">
ᠮᠣᠩᠭᠣᠯ
</span>)}
</div>
<div className="top-nav-mobile-controls ml-auto flex shrink-0 items-center gap-2">
{menuOpen && localeSelector}
{verticalPreview && (
<span className="vertical-preview-sample" lang="mn-Mong">
ᠮᠣᠩᠭᠣᠯ
</span>)}
<button
type="button"
className="top-nav-mobile-toggle shrink-0 whitespace-nowrap
@@ -554,7 +593,9 @@ const TopNav: FC<Props> = ({ user, locales, resolveLocaleSwitch }) => {
layout
className="top-nav-desktop-submenu relative z-20 shrink-0
overflow-hidden top-nav-submenu"
animate={{ height: desktop == null ? 'auto' : submenuHeight }}
animate={verticalPreview && desktop
? { width: submenuWidth, height: '100%' }
: { width: 'auto', height: desktop == null ? 'auto' : submenuHeight }}
onMouseLeave={() => {
if (moreVsbl)
setMoreVsbl (false)
@@ -566,7 +607,8 @@ const TopNav: FC<Props> = ({ user, locales, resolveLocaleSwitch }) => {
onLayoutAnimationComplete={() => {
measure (highlightTargetRef.current)
}}>
<div ref={submenuContentRef} className={cn (moreVsbl && 'top-nav-more-menu')}>
<div ref={submenuContentRef}
className={cn ('top-nav-submenu-content', moreVsbl && 'top-nav-more-menu')}>
{moreVsbl
? (
moreMenu.map ((item, i) => (
@@ -576,9 +618,11 @@ const TopNav: FC<Props> = ({ user, locales, resolveLocaleSwitch }) => {
{...((item.visible ?? true)
? { layoutId: `menu-${ menu.indexOf (item) }` }
: (animationsOff ? { }
: { initial: { x: 40, y: -40, opacity: 0 },
: { initial: {
x: 40, y: verticalPreview ? 0 : -40, opacity: 0 },
animate: { x: 0, y: 0, opacity: 1 },
exit: { x: 40, y: -40, opacity: 0 } }))}
exit: {
x: 40, y: verticalPreview ? 0 : -40, opacity: 0 } }))}
onLayoutAnimationComplete={() => measure (highlightTargetRef.current)}
className="z-10 min-w-0 px-3 py-2 font-bold">
<h2>
@@ -599,10 +643,7 @@ const TopNav: FC<Props> = ({ user, locales, resolveLocaleSwitch }) => {
transition={topNavTransition}
{...(visibleMenu[activeIdx] === item
? { layoutId: `submenu-${ menu.indexOf (item) }-${ j }` }
: (animationsOff ? { }
: { initial: { y: -40, opacity: 0 },
animate: { y: 0, opacity: 1 },
exit: { y: -40, opacity: 0 } }))}>
: submenuItemMotion)}>
{subItem.component}
</motion.div>)
: (
@@ -612,10 +653,7 @@ const TopNav: FC<Props> = ({ user, locales, resolveLocaleSwitch }) => {
transition={topNavTransition}
{...(visibleMenu[activeIdx] === item
? { layoutId: `submenu-${ menu.indexOf (item) }-${ j }` }
: (animationsOff ? { }
: { initial: { y: -40, opacity: 0 },
animate: { y: 0, opacity: 1 },
exit: { y: -40, opacity: 0 } }))}>
: submenuItemMotion)}>
<PrefetchLink
to={subItem.to}
target={subItem.to.slice (0, 2) === '//' ? '_blank' : undefined}
@@ -633,7 +671,8 @@ const TopNav: FC<Props> = ({ user, locales, resolveLocaleSwitch }) => {
<div className="relative min-h-[40px]">
{animationsOff
? (
<div className="flex min-h-[40px] flex-wrap items-center px-3">
<div className="top-nav-submenu-items flex min-h-[40px]
flex-wrap items-center px-3">
{(visibleMenu[activeIdx]?.subMenu ?? [])
.filter (item => item.visible ?? true)
.map ((item, i) => (
@@ -662,10 +701,9 @@ const TopNav: FC<Props> = ({ user, locales, resolveLocaleSwitch }) => {
<motion.div
key={activeIdx}
custom={dir}
variants={{ enter: (d: -1 | 1) => ({ y: d * 24, opacity: 0 }),
centre: { y: 0, opacity: 1 },
exit: (d: -1 | 1) => ({ y: (-d) * 24, opacity: 0 }) }}
className="relative flex min-h-[40px] flex-wrap items-center px-3"
variants={submenuVariants}
className="top-nav-submenu-items relative flex min-h-[40px]
flex-wrap items-center px-3"
initial="enter"
animate="centre"
exit="exit"
+58 -5
ファイルの表示
@@ -1,7 +1,10 @@
import { motion } from 'framer-motion'
import { useEffect, useRef } from 'react'
import { useLocation } from 'react-router-dom'
import { clientAnimationTransition } from '@/lib/clientAnimation'
import { useClientBehaviourSettings } from '@/lib/useClientBehaviourSettings'
import { useVerticalPreview } from '@/lib/useLocale'
import { cn } from '@/lib/utils'
import type { FC, ReactNode } from 'react'
@@ -9,21 +12,71 @@ import type { FC, ReactNode } from 'react'
type Props = {
children: ReactNode
className?: string
animateLayout?: boolean }
animateLayout?: boolean
verticalContent?: boolean }
const MainArea: FC<Props> = ({ children, className, animateLayout = true }) => {
const MainArea: FC<Props> = (
{ children, className, animateLayout = true, verticalContent = false },
) => {
const behaviourSettings = useClientBehaviourSettings ()
const verticalPreview = useVerticalPreview ()
const horizontal = verticalPreview && verticalContent
const { pathname, search } = useLocation ()
const rootRef = useRef<HTMLElement | null> (null)
const animationMode = behaviourSettings.animation ?? 'normal'
const layoutTransition = clientAnimationTransition (
animationMode,
{ normal: { duration: .2, ease: 'easeOut' as const } },
)
{ normal: { duration: .2, ease: 'easeOut' as const } })
useEffect (() => {
if (horizontal && rootRef.current != null)
rootRef.current.scrollLeft = 0
}, [horizontal, pathname, search])
useEffect (() => {
const root = rootRef.current
if (!horizontal || root == null)
return
const handleWheel = (event: WheelEvent) => {
if (event.ctrlKey || event.metaKey || event.shiftKey
|| event.deltaX !== 0 || event.deltaY === 0 || event.defaultPrevented)
return
// Nested tables, forms and code blocks retain their own scrolling.
let node = event.target instanceof HTMLElement ? event.target : null
while (node != null && node !== root)
{
if (node.matches ('input, select, textarea, [contenteditable="true"]'))
return
const style = getComputedStyle (node)
if ((/auto|scroll/.test (style.overflowY) && node.scrollHeight > node.clientHeight)
|| (/auto|scroll/.test (style.overflowX) && node.scrollWidth > node.clientWidth))
return
node = node.parentElement
}
const unit = event.deltaMode === 1 ? 16 : (event.deltaMode === 2 ? root.clientWidth : 1)
const delta = event.deltaY * unit
if ((delta < 0 && root.scrollLeft <= 0)
|| (delta > 0 && root.scrollLeft + root.clientWidth >= root.scrollWidth))
return
event.preventDefault ()
root.scrollLeft += delta
}
root.addEventListener ('wheel', handleWheel, { passive: false })
return () => root.removeEventListener ('wheel', handleWheel)
}, [horizontal])
return (
<motion.main
ref={rootRef}
data-vertical-content={horizontal || undefined}
tabIndex={horizontal ? 0 : undefined}
transition={{ layout: layoutTransition }}
className={cn ('flex-1 overflow-y-auto p-4', className)}
className={cn ('main-area flex-1 overflow-y-auto p-4', className)}
layout={(animationMode === 'off' || !animateLayout) ? false : 'position'}>
{children}
</motion.main>)
+126 -103
ファイルの表示
@@ -1,13 +1,14 @@
import { motion } from 'framer-motion'
import { useCallback, useEffect, useRef, useState } from 'react'
import { Helmet } from 'react-helmet-async'
import { clientAnimationTransition } from '@/lib/clientAnimation'
import { useClientBehaviourSettings } from '@/lib/useClientBehaviourSettings'
import { cn } from '@/lib/utils'
import { useUITranslation } from '@/lib/useUITranslation'
import { useVerticalPreview } from '@/lib/useLocale'
import type { CSSProperties, FC, MouseEvent, PointerEvent, ReactNode } from 'react'
import { useCallback, useEffect, useRef, useState } from 'react'
type SidebarSide = 'left' | 'right'
@@ -23,12 +24,12 @@ type Props = {
type ResizeState = {
pointerId: number
startWidth: number
startSize: number
startX: number
startY: number
verticalPreview: boolean
side: SidebarSide }
type PendingPressState = ResizeState & { startY: number }
const DEFAULT_SIDEBAR_WIDTH = 256
const MIN_SIDEBAR_WIDTH = 192
const MIN_MAIN_WIDTH = 360
@@ -37,84 +38,97 @@ const LONG_PRESS_DELAY_MS = 350
const TOUCH_MOVE_THRESHOLD_PX = 8
const storageKeyForSidebar = (sidebarKey: string, side: SidebarSide): string =>
`sidebar:${ sidebarKey }:${ side }:width`
const storageKeyForSidebar = (
sidebarKey: string,
side: SidebarSide,
verticalPreview = false,
): string =>
`sidebar:${ sidebarKey }:${ side }:${ verticalPreview ? 'height' : 'width' }`
const getSidebarMaxWidth = (containerWidth: number): number => {
const getSidebarMaxWidth = (containerSize: number): number => {
const viewportWidth =
typeof window === 'undefined' ? containerWidth : window.innerWidth
typeof window === 'undefined' ? containerSize : window.innerWidth
const viewportMaxWidth = Math.floor (viewportWidth * MAX_VIEWPORT_WIDTH_RATIO)
const containerMaxWidth = Math.floor (containerWidth - MIN_MAIN_WIDTH)
const containerMaxWidth = Math.floor (containerSize - MIN_MAIN_WIDTH)
const dynamicMaxWidth = Math.min (viewportMaxWidth, containerMaxWidth)
return Math.max (MIN_SIDEBAR_WIDTH, dynamicMaxWidth)
}
const readStoredSidebarWidth = (
const readStoredSidebarSize = (
sidebarKey: string,
side: SidebarSide): number => {
side: SidebarSide,
verticalPreview: boolean,
): number => {
if (typeof window === 'undefined')
return DEFAULT_SIDEBAR_WIDTH
const value = window.localStorage.getItem (storageKeyForSidebar (sidebarKey, side))
const width = value == null ? Number.NaN : Number (value)
return Number.isFinite (width) ? width : DEFAULT_SIDEBAR_WIDTH
try
{
const value = window.localStorage.getItem (
storageKeyForSidebar (sidebarKey, side, verticalPreview))
const size = value == null ? Number.NaN : Number (value)
return Number.isFinite (size) ? size : DEFAULT_SIDEBAR_WIDTH
}
catch
{
return DEFAULT_SIDEBAR_WIDTH
}
}
const SidebarComponent: FC<Props> = ({
children,
className,
maxWidth,
maxWidthRatio,
animateLayout = true,
onWidthChange,
sidebarKey,
side = 'left',
}) => {
const SidebarComponent: FC<Props> = (
{ children, className, maxWidth, maxWidthRatio, animateLayout = true,
onWidthChange, sidebarKey, side = 'left' },
) => {
const { message } = useUITranslation ('shared')
const behaviourSettings = useClientBehaviourSettings ()
const verticalPreview = useVerticalPreview ()
const animationMode = behaviourSettings.animation ?? 'normal'
const layoutTransition = clientAnimationTransition (
animationMode,
{ normal: { duration: .2, ease: 'easeOut' as const } },
)
{ normal: { duration: .2, ease: 'easeOut' as const } })
const rootRef = useRef<HTMLDivElement | null> (null)
const maxWidthRef = useRef (maxWidth)
const maxWidthRatioRef = useRef (maxWidthRatio)
const getContainerWidth = useCallback (() => {
const containerWidth = rootRef.current
?.closest ('[data-sidebar-container]')
?.clientWidth
?? window.innerWidth
return containerWidth
}, [])
const getClampedWidth = useCallback ((
nextWidth: number,
containerWidth: number): number => {
const getContainerSize = useCallback (() => {
const container = rootRef.current?.closest ('[data-sidebar-container]')
return verticalPreview
? container?.clientHeight ?? window.innerHeight
: container?.clientWidth ?? window.innerWidth
}, [verticalPreview])
const getClampedSize = useCallback ((
nextSize: number,
containerSize: number,
): number => {
if (verticalPreview)
{
const maximum = Math.max (64, Math.min (
window.innerHeight * .6, containerSize - 160))
return Math.min (maximum, Math.max (64, nextSize))
}
const viewportWidth =
typeof window === 'undefined' ? containerWidth : window.innerWidth
typeof window === 'undefined' ? containerSize : window.innerWidth
const viewportMaxWidth = Math.floor (viewportWidth * MAX_VIEWPORT_WIDTH_RATIO)
const ratioMaxWidth = maxWidthRatioRef.current == null
? Infinity : Math.floor (containerWidth * maxWidthRatioRef.current)
? Infinity : Math.floor (containerSize * maxWidthRatioRef.current)
const configuredMaxWidth = maxWidthRef.current == null
? getSidebarMaxWidth (containerWidth)
? getSidebarMaxWidth (containerSize)
: Math.max (MIN_SIDEBAR_WIDTH,
Math.min (viewportMaxWidth, maxWidthRef.current))
const maxAllowedWidth = Math.max (MIN_SIDEBAR_WIDTH,
Math.min (ratioMaxWidth, configuredMaxWidth))
return Math.min (maxAllowedWidth, Math.max (MIN_SIDEBAR_WIDTH, nextWidth))
}, [])
return Math.min (maxAllowedWidth, Math.max (MIN_SIDEBAR_WIDTH, nextSize))
}, [verticalPreview])
const [width, setWidth] = useState (DEFAULT_SIDEBAR_WIDTH)
const [size, setSize] = useState (DEFAULT_SIDEBAR_WIDTH)
const [resizing, setResizing] = useState (false)
const widthRef = useRef (width)
const sizeRef = useRef (size)
const longPressTimerRef = useRef<ReturnType<typeof setTimeout> | null> (null)
const pendingPressRef = useRef<PendingPressState | null> (null)
const pendingPressRef = useRef<ResizeState | null> (null)
const resizeRef = useRef<ResizeState | null> (null)
const bodyStyleRef = useRef<{ cursor: string, userSelect: string } | null> (null)
@@ -148,7 +162,7 @@ const SidebarComponent: FC<Props> = ({
userSelect: document.body.style.userSelect }
}
document.body.style.cursor = 'col-resize'
document.body.style.cursor = state.verticalPreview ? 'row-resize' : 'col-resize'
document.body.style.userSelect = 'none'
}, [clearLongPressTimer])
@@ -159,43 +173,50 @@ const SidebarComponent: FC<Props> = ({
const state = resizeRef.current
resizeRef.current = null
if (state != null)
window.localStorage.setItem (
storageKeyForSidebar (sidebarKey, state.side),
String (widthRef.current))
try
{
if (state != null)
window.localStorage.setItem (
storageKeyForSidebar (sidebarKey, state.side, state.verticalPreview),
String (sizeRef.current))
}
catch
{
// Resizing remains usable when browser storage is unavailable.
}
setResizing (false)
restoreBodyStyle ()
}, [clearLongPressTimer, restoreBodyStyle, sidebarKey])
const applyWidth = useCallback ((nextWidth: number) => {
if (nextWidth === widthRef.current)
const applySize = useCallback ((nextSize: number) => {
if (nextSize === sizeRef.current)
return
widthRef.current = nextWidth
setWidth (nextWidth)
sizeRef.current = nextSize
setSize (nextSize)
}, [])
useEffect (() => {
maxWidthRef.current = maxWidth
maxWidthRatioRef.current = maxWidthRatio
const containerWidth = getContainerWidth ()
const nextWidth = getClampedWidth (widthRef.current, containerWidth)
applyWidth (nextWidth)
}, [applyWidth, getClampedWidth, getContainerWidth, maxWidth, maxWidthRatio])
const containerSize = getContainerSize ()
const nextSize = getClampedSize (sizeRef.current, containerSize)
applySize (nextSize)
}, [applySize, getClampedSize, getContainerSize, maxWidth, maxWidthRatio])
const updateWidth = useCallback ((clientX: number) => {
const updateSize = useCallback ((clientX: number, clientY: number) => {
const state = resizeRef.current
if (state == null)
return
const containerWidth = getContainerWidth ()
const delta = state.side === 'left'
? clientX - state.startX
: state.startX - clientX
const nextWidth = getClampedWidth (state.startWidth + delta, containerWidth)
applyWidth (nextWidth)
}, [applyWidth, getClampedWidth, getContainerWidth])
const containerSize = getContainerSize ()
const delta = state.verticalPreview
? clientY - state.startY
: (state.side === 'left' ? clientX - state.startX : state.startX - clientX)
const nextSize = getClampedSize (state.startSize + delta, containerSize)
applySize (nextSize)
}, [applySize, getClampedSize, getContainerSize])
const handlePointerDown = (ev: PointerEvent<HTMLDivElement>) => {
if (ev.pointerType !== 'mouse' && ev.pointerType !== 'touch')
@@ -211,8 +232,10 @@ const SidebarComponent: FC<Props> = ({
const state = {
pointerId: ev.pointerId,
side,
startWidth: widthRef.current,
startX: ev.clientX }
startSize: sizeRef.current,
startX: ev.clientX,
startY: ev.clientY,
verticalPreview }
if (ev.pointerType === 'mouse')
{
@@ -220,7 +243,7 @@ const SidebarComponent: FC<Props> = ({
return
}
pendingPressRef.current = { ...state, startY: ev.clientY }
pendingPressRef.current = state
clearLongPressTimer ()
longPressTimerRef.current = setTimeout (() => {
if (pendingPressRef.current != null)
@@ -234,7 +257,7 @@ const SidebarComponent: FC<Props> = ({
if (resizeRef.current != null)
{
ev.preventDefault ()
updateWidth (ev.clientX)
updateSize (ev.clientX, ev.clientY)
return
}
@@ -262,58 +285,58 @@ const SidebarComponent: FC<Props> = ({
finishResize ()
}
const resetWidth = useCallback (() => {
const containerWidth = getContainerWidth ()
const nextWidth = getClampedWidth (DEFAULT_SIDEBAR_WIDTH, containerWidth)
applyWidth (nextWidth)
window.localStorage.removeItem (storageKeyForSidebar (sidebarKey, side))
}, [applyWidth, getClampedWidth, getContainerWidth, sidebarKey, side])
const resetSize = useCallback (() => {
const containerSize = getContainerSize ()
const nextSize = getClampedSize (DEFAULT_SIDEBAR_WIDTH, containerSize)
applySize (nextSize)
window.localStorage.removeItem (
storageKeyForSidebar (sidebarKey, side, verticalPreview))
}, [applySize, getClampedSize, getContainerSize, sidebarKey, side, verticalPreview])
const handleDoubleClick = (ev: MouseEvent<HTMLDivElement>) => {
ev.preventDefault ()
ev.stopPropagation ()
resetWidth ()
resetSize ()
}
useEffect (() => {
const containerWidth = getContainerWidth ()
const nextWidth = getClampedWidth (
readStoredSidebarWidth (sidebarKey, side),
containerWidth)
applyWidth (nextWidth)
}, [applyWidth, getClampedWidth, getContainerWidth, sidebarKey, side])
const containerSize = getContainerSize ()
const nextSize = getClampedSize (
readStoredSidebarSize (sidebarKey, side, verticalPreview),
containerSize)
applySize (nextSize)
}, [applySize, getClampedSize, getContainerSize, sidebarKey, side, verticalPreview])
useEffect (() => {
const containerWidth = getContainerWidth ()
const nextWidth = getClampedWidth (widthRef.current, containerWidth)
applyWidth (nextWidth)
}, [applyWidth, getClampedWidth, getContainerWidth])
const containerSize = getContainerSize ()
const nextSize = getClampedSize (sizeRef.current, containerSize)
applySize (nextSize)
}, [applySize, getClampedSize, getContainerSize])
useEffect (() => {
if (typeof window === 'undefined')
return
const handleResize = () => {
const containerWidth = getContainerWidth ()
const nextWidth = getClampedWidth (widthRef.current, containerWidth)
applyWidth (nextWidth)
const containerSize = getContainerSize ()
const nextSize = getClampedSize (sizeRef.current, containerSize)
applySize (nextSize)
}
window.addEventListener ('resize', handleResize)
return () => window.removeEventListener ('resize', handleResize)
}, [applyWidth, getClampedWidth, getContainerWidth])
}, [applySize, getClampedSize, getContainerSize])
useEffect (() => {
onWidthChange?.(width)
}, [onWidthChange, width])
if (!verticalPreview)
onWidthChange?.(size)
}, [onWidthChange, size, verticalPreview])
useEffect (() => () => {
clearLongPressTimer ()
restoreBodyStyle ()
}, [clearLongPressTimer, restoreBodyStyle])
useEffect (() => () => finishResize (), [finishResize, verticalPreview])
const style = {
'--sidebar-width': `${ width }px` } as CSSProperties
'--sidebar-width': `${ size }px`,
'--sidebar-height': `${ size }px` } as CSSProperties
return (
<motion.div
@@ -322,7 +345,7 @@ const SidebarComponent: FC<Props> = ({
transition={{ layout: layoutTransition }}
style={style}
className={cn (
'relative w-full md:w-[var(--sidebar-width)] md:shrink-0 md:h-full',
'sidebar-panel relative w-full md:w-[var(--sidebar-width)] md:shrink-0 md:h-full',
className)}>
<Helmet>
<style>
@@ -345,16 +368,16 @@ const SidebarComponent: FC<Props> = ({
<div
role="separator"
aria-orientation="vertical"
aria-label={message ('sidebarWidth').text}
lang={message ('sidebarWidth').language}
aria-orientation={verticalPreview ? 'horizontal' : 'vertical'}
aria-label={message (verticalPreview ? 'labels.menu' : 'sidebarWidth').text}
lang={message (verticalPreview ? 'labels.menu' : 'sidebarWidth').language}
onPointerDown={handlePointerDown}
onPointerMove={handlePointerMove}
onPointerUp={handlePointerEnd}
onPointerCancel={handlePointerEnd}
onDoubleClick={handleDoubleClick}
className={cn (
'hidden md:block absolute top-0 bottom-0 z-20 w-4 cursor-col-resize',
'sidebar-resize hidden md:block absolute top-0 bottom-0 z-20 w-4 cursor-col-resize',
'touch-none select-none',
side === 'left' ? '-right-2' : '-left-2',
'before:absolute before:top-0 before:bottom-0 before:w-px',