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:
@ -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>
|
||||||
|
|||||||
@ -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.',
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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 l’altezza header hanno stesso valore. Lo spy usa +4px
|
||||||
|
* (SPY_VIEWPORT_FUDGE_PX) così il collegamento attivo segue l’inizio 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 l’inizio 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: l’inizio sezione attraverso
|
||||||
|
// l’orizzonte “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
|
||||||
|
// l’ingombro 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') {
|
||||||
|
|||||||
@ -17,6 +17,8 @@
|
|||||||
@layer base {
|
@layer base {
|
||||||
html {
|
html {
|
||||||
scroll-behavior: smooth;
|
scroll-behavior: smooth;
|
||||||
|
/* Fallback: la homepage alza il valore misurando l’header 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;
|
||||||
|
|||||||
Reference in New Issue
Block a user