Home: navigazione, footer, CTA, servizi (Breaking Glass), scroll-spy

- Allinea scroll a #contatti, misura header e scroll-padding, spy con documentTopY
  e piccolo SPY_FUDGE; hashchange/ResizeObserver; footer visibile azzera Contattaci
- Footer: id site-footer, griglia senza blocco brand sopra, tagline a destra
  del copyright; global.css fallback scroll-padding
- CTA: eyebrow Contatti, bordo/sezione allineate alle FAQ, #contatti sulla section
- Servizi: callout Breaking Glass con look glossy (gradient, highlight, luce)

Made-with: Cursor
This commit is contained in:
Javaxman
2026-04-22 13:42:05 +02:00
parent 16c380dfda
commit 9d96253888
5 changed files with 140 additions and 51 deletions

View File

@ -1,16 +1,38 @@
/**
* Home: evidenzia in bianco il link del menu che corrisponde alla sezione in vista.
* `scrollPaddingTop` e laltezza header hanno stesso valore. Lo spy usa +4px
* (SPY_VIEWPORT_FUDGE_PX) così il collegamento attivo segue linizio sezione
* a schermo, senza dover forzare altro scroll a mano.
*/
const SECTION_IDS = ['prodotti', 'servizi', 'stack', 'faq', 'contatti'] as const;
const HEADER_OFFSET = 88;
let headerOffset = 80;
/** Soglia leggermente sotto linizio reale sezione, così il link si accende in linea con lo sguardo. */
const SPY_VIEWPORT_FUDGE_PX = 4;
function documentTopY(el: Element): number {
const { top } = el.getBoundingClientRect();
return top + window.scrollY;
}
function getActiveSectionId(): string | null {
const y = window.scrollY + HEADER_OFFSET;
// y = scrollY + (header reale) + leggera tolleranza: linizio sezione attraverso
// lorizzonte “link attivo” un poco prima del minimo in px.
const y = window.scrollY + headerOffset + SPY_VIEWPORT_FUDGE_PX;
let active: string | null = null;
for (const id of SECTION_IDS) {
const el = document.getElementById(id);
if (!el) continue;
if (el.offsetTop <= y) active = id;
if (documentTopY(el) <= y) active = id;
}
// Fuori focus dalla CTA: quando entra in vista il footer, non evidenziare Contattaci.
if (active === 'contatti') {
const siteFooter = document.getElementById('site-footer');
if (siteFooter) {
const { top: footerTop } = siteFooter.getBoundingClientRect();
if (footerTop < window.innerHeight) {
active = null;
}
}
}
return active;
}
@ -36,15 +58,45 @@ function onScroll(): void {
});
}
function measureStickyBarHeight(): number {
const header = document.querySelector<HTMLElement>('header');
if (!header) return headerOffset;
// Riga fissa o, se il menu mobile è aperto, pannello incluso: deve coincidere con
// lingombro reale sotto al quale il main può iniziare.
return Math.round(header.getBoundingClientRect().height);
}
function measureHeaderAndSync(): void {
headerOffset = measureStickyBarHeight();
if (headerOffset > 0) {
document.documentElement.style.scrollPaddingTop = `${headerOffset}px`;
}
applyActive();
}
function bind(): void {
const path = window.location.pathname;
if (path !== '/' && path !== '/index.html') return;
applyActive();
const header = document.querySelector('header');
measureHeaderAndSync();
const reflow = () => {
requestAnimationFrame(() => {
measureHeaderAndSync();
});
};
reflow();
if (header && 'ResizeObserver' in window) {
new ResizeObserver(() => {
reflow();
}).observe(header);
}
window.addEventListener('scroll', onScroll, { passive: true });
window.addEventListener('resize', onScroll, { passive: true });
window.addEventListener('hashchange', applyActive);
window.addEventListener('load', applyActive);
window.addEventListener('resize', reflow, { passive: true });
window.addEventListener('hashchange', () => {
reflow();
});
window.addEventListener('load', reflow, { passive: true });
}
if (typeof document !== 'undefined') {