diff --git a/src/app/tenders/[details]/constants.ts b/src/app/tenders/[details]/constants.ts index 427ab49..e5b8e57 100644 --- a/src/app/tenders/[details]/constants.ts +++ b/src/app/tenders/[details]/constants.ts @@ -13,7 +13,11 @@ export const TENDER_SECTION_IDS = { aiSummary: "tender-section-ai-summary", } as const; -export const HEADER_SCROLL_MARGIN = "scroll-mt-[5.5rem]"; +/** Sticky app header clearance — keep in sync with `getTenderSectionScrollOffset`. */ +export const TENDER_SECTION_SCROLL_OFFSET_PX = 96; + +/** For hash / native scroll-into-view (matches offset above). */ +export const HEADER_SCROLL_MARGIN = "scroll-mt-24"; /** Body area only: center content in the card; ShowcaseSection `title` is unchanged */ export const SHOWCASE_CENTERED_BODY = { diff --git a/src/app/tenders/[details]/hooks/use-tender-section-scroll-spy.ts b/src/app/tenders/[details]/hooks/use-tender-section-scroll-spy.ts index 32f5a6e..a3cab79 100644 --- a/src/app/tenders/[details]/hooks/use-tender-section-scroll-spy.ts +++ b/src/app/tenders/[details]/hooks/use-tender-section-scroll-spy.ts @@ -1,4 +1,5 @@ import { useEffect, useState } from "react"; +import { getTenderSectionScrollOffset } from "../scroll-to-section"; export function useTenderSectionScrollSpy(sectionIds: string[]) { const [activeId, setActiveId] = useState(sectionIds[0] ?? ""); @@ -17,7 +18,7 @@ export function useTenderSectionScrollSpy(sectionIds: string[]) { if (sectionIds.length === 0) return; const pickActive = () => { - const offset = 96; + const offset = getTenderSectionScrollOffset(); const scrollY = window.scrollY; const viewportHeight = window.innerHeight; const documentHeight = Math.max( diff --git a/src/app/tenders/[details]/scroll-to-section.ts b/src/app/tenders/[details]/scroll-to-section.ts index 1fc1c34..5dddd4a 100644 --- a/src/app/tenders/[details]/scroll-to-section.ts +++ b/src/app/tenders/[details]/scroll-to-section.ts @@ -1,11 +1,34 @@ +import { TENDER_SECTION_SCROLL_OFFSET_PX } from "./constants"; + +const SECTION_SCROLL_GAP_PX = 8; + +/** Pixels to leave above the section top (sticky header + small gap). */ +export function getTenderSectionScrollOffset(): number { + if (typeof document === "undefined") { + return TENDER_SECTION_SCROLL_OFFSET_PX + SECTION_SCROLL_GAP_PX; + } + const header = document.querySelector("header"); + const headerHeight = header?.getBoundingClientRect().height ?? 0; + return ( + Math.max(TENDER_SECTION_SCROLL_OFFSET_PX, headerHeight) + + SECTION_SCROLL_GAP_PX + ); +} + export function scrollToTenderSection(id: string) { const el = document.getElementById(id); if (!el) return; + const reduceMotion = typeof window !== "undefined" && window.matchMedia("(prefers-reduced-motion: reduce)").matches; - el.scrollIntoView({ + + const offset = getTenderSectionScrollOffset(); + const top = + el.getBoundingClientRect().top + window.scrollY - offset; + + window.scrollTo({ + top: Math.max(0, top), behavior: reduceMotion ? "auto" : "smooth", - block: "start", }); }