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

@ -19,20 +19,9 @@ const linkClass =
'text-sm text-sky-400/90 transition-colors hover:text-sky-300'; 'text-sm text-sky-400/90 transition-colors hover:text-sky-300';
--- ---
<footer class="relative z-[1] border-t border-nx-border px-4 py-12 sm:px-6"> <footer id="site-footer" class="relative z-[1] border-t border-nx-border px-4 py-12 sm:px-6">
<div class="mx-auto max-w-6xl"> <div class="mx-auto max-w-6xl">
<div class="border-b border-nx-border pb-10"> <!-- Griglia link; brand + slogan spostati in fondo a fianco al copyright -->
<a
href="/"
class="inline-block text-base font-semibold tracking-tight text-nx-fg hover:text-white sm:text-lg"
>{footer.brandName}</a
>
<p class="mt-2 max-w-xl text-sm leading-relaxed text-nx-muted">
{footer.tagline}
</p>
</div>
<!-- Quattro colonne omogenee (solo link / elenchi); preferenze cookie sotto, staccate -->
<div class="border-b border-nx-border py-10"> <div class="border-b border-nx-border py-10">
<div <div
class="grid grid-cols-1 gap-10 sm:grid-cols-2 lg:grid-cols-4 lg:items-start lg:gap-x-10 xl:gap-x-12" class="grid grid-cols-1 gap-10 sm:grid-cols-2 lg:grid-cols-4 lg:items-start lg:gap-x-10 xl:gap-x-12"
@ -210,8 +199,17 @@ const linkClass =
} }
</div> </div>
<p class="pt-8 text-xs leading-relaxed text-nx-muted sm:text-sm"> <div
© {new Date().getFullYear()} NexStudio. {footer.legalLine} class="mt-0 flex flex-col gap-3 border-t border-nx-border pt-8 sm:flex-row sm:items-end sm:justify-between sm:gap-6"
</p> >
<p class="min-w-0 text-xs leading-relaxed text-nx-muted sm:text-sm">
© {new Date().getFullYear()} NexStudio. {footer.legalLine}
</p>
<p
class="max-w-md shrink-0 text-xs leading-relaxed text-nx-muted sm:max-w-lg sm:text-right sm:text-sm"
>
{footer.tagline}
</p>
</div>
</div> </div>
</footer> </footer>

View File

@ -1,5 +1,8 @@
/** Call-to-action (blocco contatti / chiusura pagina) */ /** Call-to-action (blocco contatti / chiusura pagina) */
export const cta = { export const cta = {
/** Riga sezione (stesso ruolo di FAQ, Stack, …) */
eyebrow: 'Contatti',
/** Titolo CTA su due righe (dopo il punto) */ /** Titolo CTA su due righe (dopo il punto) */
titleLine1: 'Automatizza il complesso.', titleLine1: 'Automatizza il complesso.',
titleLine2: 'Velocizza il processo.', titleLine2: 'Velocizza il processo.',

View File

@ -339,13 +339,29 @@ const techSliderItems = stackSection.techSlider.items;
{card.body} {card.body}
</p> </p>
{'callout' in card && card.callout && ( {'callout' in card && card.callout && (
<div class="mt-4 rounded-xl border border-sky-500/25 bg-sky-950/20 p-4"> <div
<p class="text-sm font-semibold text-sky-200/95"> class="group relative mt-4 overflow-hidden rounded-xl border border-sky-400/35 bg-gradient-to-br from-sky-500/20 via-slate-900/70 to-violet-950/50 p-4 shadow-[inset_0_1px_0_0_rgba(255,255,255,0.16),0_8px_32px_-6px_rgba(14,165,233,0.18)]"
{card.callout.title} >
</p> <div
<p class="mt-2 text-sm leading-relaxed text-nx-muted"> class="pointer-events-none absolute inset-0 bg-gradient-to-b from-white/[0.14] via-white/[0.02] to-transparent"
{card.callout.body} aria-hidden="true"
</p> />
<div
class="pointer-events-none absolute -right-6 -top-12 h-28 w-36 rotate-12 rounded-full bg-gradient-to-br from-cyan-200/30 via-sky-400/15 to-transparent blur-2xl"
aria-hidden="true"
/>
<div
class="pointer-events-none absolute -bottom-8 -left-4 h-20 w-32 rotate-[-8deg] rounded-full bg-gradient-to-tr from-violet-500/10 to-transparent blur-xl"
aria-hidden="true"
/>
<div class="relative z-10">
<p class="text-sm font-semibold text-sky-100/95">
{card.callout.title}
</p>
<p class="mt-2 text-sm leading-relaxed text-nx-muted">
{card.callout.body}
</p>
</div>
</div> </div>
)} )}
{'bullets' in card && card.bullets && ( {'bullets' in card && card.bullets && (
@ -581,29 +597,47 @@ const techSliderItems = stackSection.techSlider.items;
</div> </div>
</section> </section>
<section id="contatti" class="relative z-10 px-4 pb-16 pt-4 sm:px-6 sm:pb-20"> <section
<div id="contatti"
id="nx-cta-card" class="relative z-10 border-t border-nx-border bg-nx-surface/20 px-4 pt-14 pb-16 sm:px-6 sm:pt-20 sm:pb-20"
class="relative mx-auto max-w-6xl overflow-hidden rounded-3xl border border-nx-border bg-gradient-to-br from-sky-500/15 via-nx-elevated to-violet-600/15 px-5 py-5 sm:px-6 sm:py-6" >
> <div class="mx-auto max-w-6xl">
<div <h2
class="relative z-10 flex flex-col gap-6 lg:flex-row lg:items-start lg:gap-10" class="text-sm font-semibold uppercase tracking-wider text-sky-400/90"
> >
<div class="min-w-0 flex-1 lg:max-w-[min(100%,28rem)] xl:max-w-[min(100%,32rem)]"> {cta.eyebrow}
<h2 </h2>
class="text-2xl font-semibold leading-snug tracking-tight sm:text-3xl" <p
> class="mt-2 text-3xl font-semibold leading-snug tracking-tight text-nx-fg sm:text-4xl"
<span class="block text-nx-fg">{cta.titleLine1}</span> >
<span class="mt-0.5 block text-sky-200 sm:text-[#bae6fd]">{cta.titleLine2}</span> <span class="block">{cta.titleLine1}</span>
</h2> <span class="mt-1 block text-sky-200 sm:text-sky-100/95"
<p class="mt-3 max-w-2xl text-nx-muted"> >{cta.titleLine2}</span
{cta.body}
</p>
</div>
<div
class="w-full min-w-0 shrink-0 lg:max-w-md xl:max-w-lg"
> >
<ContactFormInline /> </p>
<div
class="mt-8 sm:mt-10"
>
<div
class="nx-cta-card relative mx-auto max-w-6xl overflow-hidden rounded-3xl border border-nx-border bg-gradient-to-br from-sky-500/15 via-nx-elevated to-violet-600/15 px-5 py-5 sm:px-6 sm:py-6"
>
<div
class="relative z-10 flex flex-col gap-6 lg:flex-row lg:items-start lg:gap-10"
>
<div
class="min-w-0 flex-1 lg:max-w-[min(100%,28rem)] xl:max-w-[min(100%,32rem)]"
>
<p class="max-w-2xl text-base leading-relaxed text-nx-muted sm:text-lg">
{cta.body}
</p>
</div>
<div
class="w-full min-w-0 shrink-0 lg:max-w-md xl:max-w-lg"
>
<ContactFormInline />
</div>
</div>
</div> </div>
</div> </div>
</div> </div>

View File

@ -1,16 +1,38 @@
/** /**
* Home: evidenzia in bianco il link del menu che corrisponde alla sezione in vista. * 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 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 { 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; let active: string | null = null;
for (const id of SECTION_IDS) { for (const id of SECTION_IDS) {
const el = document.getElementById(id); const el = document.getElementById(id);
if (!el) continue; 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; 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 { function bind(): void {
const path = window.location.pathname; const path = window.location.pathname;
if (path !== '/' && path !== '/index.html') return; 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('scroll', onScroll, { passive: true });
window.addEventListener('resize', onScroll, { passive: true }); window.addEventListener('resize', reflow, { passive: true });
window.addEventListener('hashchange', applyActive); window.addEventListener('hashchange', () => {
window.addEventListener('load', applyActive); reflow();
});
window.addEventListener('load', reflow, { passive: true });
} }
if (typeof document !== 'undefined') { if (typeof document !== 'undefined') {

View File

@ -17,6 +17,8 @@
@layer base { @layer base {
html { html {
scroll-behavior: smooth; scroll-behavior: smooth;
/* Fallback: la homepage alza il valore misurando lheader in nx-nav-spy.ts */
scroll-padding-top: 5.5rem;
} }
body { body {
@apply bg-nx-bg text-nx-fg antialiased; @apply bg-nx-bg text-nx-fg antialiased;