From f6a67877ef3b435dd38d8bd9dbd6a206b6553918 Mon Sep 17 00:00:00 2001 From: manager Date: Wed, 6 May 2026 17:18:38 +0000 Subject: [PATCH] fix(mobile): logo navigates home; AI Atlas no longer hover-locks on Android MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Burger-open: tapping the centered KeepSimple logo closes the sheet and navigates to "/" (was a no-op on small screens). - AI Atlas: gate Mouse*-driven hover state behind a (hover: hover) media query and neutralize sticky :hover styles on touch — Android Chrome fires mouseenter on tap but never mouseleave, locking nodes. Co-Authored-By: Claude Opus 4.7 --- src/components/Header/Header.tsx | 9 ++++++-- src/pages/ai-atlas.tsx | 35 ++++++++++++++++++++++++++------ src/styles/ai-atlas.css | 30 +++++++++++++++++++++++++++ 3 files changed, 66 insertions(+), 8 deletions(-) diff --git a/src/components/Header/Header.tsx b/src/components/Header/Header.tsx index 39e2089..697734e 100644 --- a/src/components/Header/Header.tsx +++ b/src/components/Header/Header.tsx @@ -123,9 +123,14 @@ const Header: FC = () => { { - if (router.pathname !== '/') { - !isSmallScreen && handleClick(e, '/'); + const goingToLanding = router.pathname !== '/'; + if (isSmallScreen) { + e.preventDefault(); + if (isOpenedSidebar) toggleSidebar(); + if (goingToLanding) router.push('/'); + return; } + if (goingToLanding) handleClick(e, '/'); }} src={ isDarkTheme diff --git a/src/pages/ai-atlas.tsx b/src/pages/ai-atlas.tsx index 78d8e8d..451ce2d 100644 --- a/src/pages/ai-atlas.tsx +++ b/src/pages/ai-atlas.tsx @@ -20,6 +20,21 @@ const POL = (r: number, theta: number) => ({ y: Math.sin(RAD(theta)) * r * HALF, }); +/* On touch devices Mouse* events fire synthetically on tap but never + get a leave — without this, hover state would lock on Android. */ +function useHasHover() { + const [hasHover, setHasHover] = useState(false); + useEffect(() => { + if (typeof window === 'undefined' || !window.matchMedia) return; + const mq = window.matchMedia('(hover: hover) and (pointer: fine)'); + const update = () => setHasHover(mq.matches); + update(); + mq.addEventListener?.('change', update); + return () => mq.removeEventListener?.('change', update); + }, []); + return hasHover; +} + type Lang = 'en' | 'ru'; const pickLang = (locale: string | undefined): Lang => locale === 'ru' ? 'ru' : 'en'; @@ -1328,8 +1343,11 @@ function SecurityCallout({ ); } -function SecurityView({ t }: { t: T }) { - const [hoveredLayer, setHoveredLayer] = useState(null); +function SecurityView({ t, hasHover }: { t: T; hasHover: boolean }) { + const [hoveredLayer, setHoveredLayerRaw] = useState(null); + const setHoveredLayer = (n: number | null) => { + if (hasHover) setHoveredLayerRaw(n); + }; const leftLayers = t.securityLayers.filter(l => l.side === 'left'); const rightLayers = t.securityLayers.filter(l => l.side === 'right'); const calloutTops = ['4%', '38%', '72%']; @@ -1448,6 +1466,7 @@ function AiAtlasApp() { const [hoverNode, setHoverNode] = useState(null); const [linkHoverNode, setLinkHoverNode] = useState(null); const [viewMode, setViewMode] = useState('environment'); + const hasHover = useHasHover(); /* mark while AI Atlas is mounted so the global navbar can match the page's paper background (light mode only for now). @@ -1651,9 +1670,11 @@ function AiAtlasApp() { const pinnedSolo = !!focusedNode && !hoverNode && !linkHoverNode; const onSelect = (id: string | null, mode?: string) => { - if (mode === 'hover') setHoverNode(id); - else if (mode === 'link-hover') setLinkHoverNode(id); - else { + if (mode === 'hover') { + if (hasHover) setHoverNode(id); + } else if (mode === 'link-hover') { + if (hasHover) setLinkHoverNode(id); + } else { setLinkHoverNode(null); setFocusedNode(id === focusedNode ? null : id); } @@ -2126,7 +2147,9 @@ function AiAtlasApp() { )} - {viewMode === 'security' && } + {viewMode === 'security' && ( + + )}