From 09982965e4b31b7ec78d7b03dd8819c1d5de881a Mon Sep 17 00:00:00 2001 From: Sofi Bel <91890402+sofi-bel@users.noreply.github.com> Date: Thu, 16 Apr 2026 14:55:48 +0300 Subject: [PATCH] feat(PG): add scroll-to-top button Refs ENG-104 --- client/src/sites/pg/PgLayout.tsx | 3 + .../src/sites/pg/components/PgScrollToTop.tsx | 59 +++++++++++++++++++ 2 files changed, 62 insertions(+) create mode 100644 client/src/sites/pg/components/PgScrollToTop.tsx diff --git a/client/src/sites/pg/PgLayout.tsx b/client/src/sites/pg/PgLayout.tsx index 9361d009..2117cb56 100644 --- a/client/src/sites/pg/PgLayout.tsx +++ b/client/src/sites/pg/PgLayout.tsx @@ -30,6 +30,7 @@ import { FooterLinkIcon, FooterSectionKind } from "~/graphql/enums"; import { useSnapshot } from "valtio"; import { siteConfigState, type FooterSection } from "@/sites/pg/siteConfigState"; import { PgHeroHeader } from "@/sites/pg/components/PgHeader"; +import { PgScrollToTop } from "@/sites/pg/components/PgScrollToTop"; import { layout } from "@/sites/pg/PgLayoutConfig"; const style = layout.style.container; @@ -67,6 +68,8 @@ export default function PgLayout() { + + ); diff --git a/client/src/sites/pg/components/PgScrollToTop.tsx b/client/src/sites/pg/components/PgScrollToTop.tsx new file mode 100644 index 00000000..1ebd23be --- /dev/null +++ b/client/src/sites/pg/components/PgScrollToTop.tsx @@ -0,0 +1,59 @@ +import { useEffect, useRef } from "react"; +import { Box, IconButton } from "@chakra-ui/react"; +import { LuArrowUp } from "react-icons/lu"; +import { useStateValtio } from "@neuronhub/shared/utils/useStateValtio"; + +const containerHalfWithGap = 551; + +const style = { + button: { + pos: "fixed", + zIndex: "sticky", + bottom: { base: "gap.sm", lg: "gap.md" }, + right: { base: "gap.sm", md: "6", xl: `calc(50% - ${containerHalfWithGap}px)` }, + w: "10", + h: "10", + borderWidth: "1px", + borderColor: "subtle", + transition: "opacity 0.2s, border-color 0.3s", + bg: "bg.card", + borderRadius: "full", + cursor: "pointer", + color: "fg", + _hover: { borderColor: "brand.black" }, + }, +} as const; + +export function PgScrollToTop() { + const sentinelRef = useRef(null); + + const state = useStateValtio({ isVisible: false }); + + useEffect(() => { + const sentinel = sentinelRef.current; + if (!sentinel) { + return; + } + const observer = new IntersectionObserver(([entry]) => { + state.mutable.isVisible = !entry!.isIntersecting; + }); + observer.observe(sentinel); + return () => observer.disconnect(); + }, []); + + return ( + <> + + window.scrollTo({ top: 0, behavior: "smooth" })} + variant="plain" + {...style.button} + opacity={state.snap.isVisible ? 1 : 0} + pointerEvents={state.snap.isVisible ? "auto" : "none"} + > + + + + ); +}