feat(ui): editorial off-grid restyling — landing + tutte le pagine app
Direzione visiva "asimmetrico/off-grid" applicata in modo coerente:
titoli mega che sbordano dal viewport (bleed-left), chapter markers
giganti in stroke trasparente come art di sfondo, eyebrow verticale
sul bordo destro, mesh-blob fluttuanti, cursor-glow che segue il mouse.
- Landing marketing rifatta da zero: hero cinema con mockup ruotato in
absolute, marquee infinito di keyword, stats sparse a quote diverse,
bento di feature ruotate, mockup browser 3D che sborda a destra,
CTA finale dark con "?" gigante.
- Dashboard loggata: greeting bleed-left, hero card "/plan" ruotata,
nav asimmetrica con chapter markers per ogni voce.
- /plan, /onboarding, /profile, /tracking, /fasting, /shopping: stesso
trattamento chrome (titolo bleed, chapter marker, atmosfera mesh-blob),
contenuti funzionali invariati.
Componenti nuovi:
- CursorGlow (client) — alone radiale che segue il mouse, easing morbido
- EditorialHeader (server) — header riusabile con chapter+eyebrow+title
- EditorialAtmosphere (server) — preset di mesh-blob per pagina
CSS utilities: text-mega, text-chapter, text-stroke, bleed-left/right,
btn-gradient, glow-ring, tilt-3d, animate-marquee, animate-float-{x,y,z},
animate-ring-trace. Tutte rispettano prefers-reduced-motion.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
d17c5ef869
commit
d467e212cb
12 files changed
+1671
-132
No files matched your search
@@ -1,9 +1,12 @@
|
|||||||
{
|
{
|
||||||
"Home": {
|
"Home": {
|
||||||
"title": "Una keto italiana, sostenibile",
|
"title": "Una keto italiana, sostenibile",
|
||||||
|
"titleAccent": "sostenibile",
|
||||||
"tagline": "Il sentiero che ti accompagna dalla prima settimana di adattamento al mantenimento — settimana dopo settimana, piatto dopo piatto.",
|
"tagline": "Il sentiero che ti accompagna dalla prima settimana di adattamento al mantenimento — settimana dopo settimana, piatto dopo piatto.",
|
||||||
"ctaPrimary": "Iscriviti",
|
"ctaPrimary": "Iscriviti",
|
||||||
"ctaSecondary": "Accedi",
|
"ctaSecondary": "Accedi",
|
||||||
|
"ctaTryFree": "Inizia gratis",
|
||||||
|
"ctaTryFreeHint": "Niente carta, niente spam. Disinstalla quando vuoi.",
|
||||||
"disclaimer": "KetoPath è uno strumento di stile di vita: suggerisce, non prescrive. Le indicazioni nutrizionali non sostituiscono il parere del tuo medico.",
|
"disclaimer": "KetoPath è uno strumento di stile di vita: suggerisce, non prescrive. Le indicazioni nutrizionali non sostituiscono il parere del tuo medico.",
|
||||||
"signedInAs": "Accesso effettuato come {name}",
|
"signedInAs": "Accesso effettuato come {name}",
|
||||||
"signOut": "Esci",
|
"signOut": "Esci",
|
||||||
@@ -17,7 +20,42 @@
|
|||||||
"greeting": "Buongiorno, <name>{name}</name>.",
|
"greeting": "Buongiorno, <name>{name}</name>.",
|
||||||
"principle1": "Personalizzazione totale: ciò che vedi è solo ciò che ti riguarda.",
|
"principle1": "Personalizzazione totale: ciò che vedi è solo ciò che ti riguarda.",
|
||||||
"principle2": "Cucina italiana e mediterranea: ingredienti veri, di stagione, reperibili.",
|
"principle2": "Cucina italiana e mediterranea: ingredienti veri, di stagione, reperibili.",
|
||||||
"principle3": "Progressività: dalla fase intensiva al mantenimento, senza strappi."
|
"principle3": "Progressività: dalla fase intensiva al mantenimento, senza strappi.",
|
||||||
|
"stat1Label": "Fasi del percorso",
|
||||||
|
"stat1Value": "3",
|
||||||
|
"stat1Hint": "Intensiva · Transizione · Mantenimento",
|
||||||
|
"stat2Label": "Ricette mediterranee",
|
||||||
|
"stat2Value": "96+",
|
||||||
|
"stat2Hint": "Tutte stagionali, italiane",
|
||||||
|
"stat3Label": "Costo del piano",
|
||||||
|
"stat3Value": "0€",
|
||||||
|
"stat3Hint": "Gratis durante la beta",
|
||||||
|
"featureSectionEyebrow": "Cosa fa per te",
|
||||||
|
"featureSectionTitle": "Tre strumenti, un solo ritmo.",
|
||||||
|
"feature1Eyebrow": "Cucina",
|
||||||
|
"feature1Title": "Piano settimanale che si adatta a te",
|
||||||
|
"feature1Body": "Ogni domenica un menù di sette giorni costruito sui tuoi macros, le tue esclusioni e il tempo che hai in cucina. La lista della spesa si genera da sola.",
|
||||||
|
"feature2Eyebrow": "Digiuno",
|
||||||
|
"feature2Title": "Digiuno intermittente, senza ansia",
|
||||||
|
"feature2Body": "Sei protocolli, dal 14:10 al 5:2. Il timer ti accompagna con micro-promemoria di idratazione, elettroliti, e una proposta morbida per rompere il digiuno.",
|
||||||
|
"feature3Eyebrow": "Tracking",
|
||||||
|
"feature3Title": "Numeri che parlano, non urlano",
|
||||||
|
"feature3Body": "Una pesata a settimana, qualche misura quando vuoi, una foto se ti va. Trend lineare, soglia d'allarme, esportazione PDF per il medico.",
|
||||||
|
"previewSectionEyebrow": "Sotto il cofano",
|
||||||
|
"previewSectionTitle": "L'app, in tre schermate.",
|
||||||
|
"previewSectionBody": "Niente dashboard infinite. Tre superfici essenziali: il piano di oggi, la sessione di digiuno, la pesata della settimana.",
|
||||||
|
"previewPlanLabel": "Mercoledì · giorno III",
|
||||||
|
"previewPlanRecipe1": "Frittata di spinaci",
|
||||||
|
"previewPlanRecipe2": "Tagliata con rucola",
|
||||||
|
"previewPlanRecipe3": "Branzino al cartoccio",
|
||||||
|
"previewFastLabel": "16:8 · in corso",
|
||||||
|
"previewFastTime": "08:42",
|
||||||
|
"previewFastHint": "Ancora 7h 18m alla finestra alimentare.",
|
||||||
|
"previewWeightLabel": "−4,2 kg in 8 settimane",
|
||||||
|
"ctaSectionTitle": "Pronti a partire?",
|
||||||
|
"ctaSectionBody": "Sette passaggi, dieci minuti, e il tuo primo piano è pronto. Senza carta di credito.",
|
||||||
|
"principlesEyebrow": "Tre principi non negoziabili",
|
||||||
|
"footerCopyright": "KetoPath — anno I, Italia."
|
||||||
},
|
},
|
||||||
"Welcome": {
|
"Welcome": {
|
||||||
"title": "Prima di iniziare",
|
"title": "Prima di iniziare",
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { redirect } from 'next/navigation';
|
|||||||
import { useTranslations } from 'next-intl';
|
import { useTranslations } from 'next-intl';
|
||||||
import { setRequestLocale } from 'next-intl/server';
|
import { setRequestLocale } from 'next-intl/server';
|
||||||
|
|
||||||
|
import { CursorGlow } from '@/components/cursor-glow';
|
||||||
import { Masthead } from '@/components/masthead';
|
import { Masthead } from '@/components/masthead';
|
||||||
import { getServerSession } from '@/lib/auth';
|
import { getServerSession } from '@/lib/auth';
|
||||||
|
|
||||||
@@ -41,20 +42,47 @@ function FastingPageContent({
|
|||||||
const t = useTranslations('Fasting');
|
const t = useTranslations('Fasting');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mx-auto min-h-screen max-w-5xl px-6 sm:px-10">
|
<div className="relative">
|
||||||
|
<div className="mx-auto min-h-screen max-w-7xl px-6 sm:px-10">
|
||||||
<Masthead issueLabel="N. 07 — Digiuno" />
|
<Masthead issueLabel="N. 07 — Digiuno" />
|
||||||
<main className="py-12 sm:py-16">
|
<main className="relative overflow-hidden pb-24 pt-10 sm:pt-12">
|
||||||
<p className="editorial-eyebrow animate-fade-up">Capitolo VII</p>
|
<CursorGlow color="hsl(var(--pomodoro))" size={520} />
|
||||||
<h1 className="font-display text-display text-ink animate-fade-up mt-3 font-medium leading-[0.95] tracking-tight [animation-delay:120ms]">
|
<div
|
||||||
|
aria-hidden
|
||||||
|
className="mesh-blob mesh-blob--pomodoro animate-float-y -right-32 top-12 h-[28rem] w-[28rem] opacity-50"
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
aria-hidden
|
||||||
|
className="mesh-blob mesh-blob--oro animate-float-x -left-24 top-72 h-72 w-72 opacity-40"
|
||||||
|
/>
|
||||||
|
<span
|
||||||
|
aria-hidden
|
||||||
|
className="font-display text-stroke-thin text-chapter pointer-events-none absolute -right-4 -top-12 select-none italic"
|
||||||
|
>
|
||||||
|
VII
|
||||||
|
</span>
|
||||||
|
<div
|
||||||
|
aria-hidden
|
||||||
|
className="pointer-events-none absolute right-0 top-32 hidden md:block"
|
||||||
|
style={{ writingMode: 'vertical-rl' }}
|
||||||
|
>
|
||||||
|
<p className="text-ink-soft font-mono text-xs uppercase tracking-[0.4em]">
|
||||||
|
Capitolo VII — Digiuno
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<h1 className="font-display text-mega bleed-left animate-fade-up relative mt-8 font-medium [animation-delay:120ms]">
|
||||||
|
<span className="text-ink block">
|
||||||
{t('title')}
|
{t('title')}
|
||||||
<span className="text-pomodoro">.</span>
|
<span className="text-pomodoro">.</span>
|
||||||
|
</span>
|
||||||
</h1>
|
</h1>
|
||||||
<p className="font-display text-ink-soft animate-fade-up mt-6 max-w-2xl text-xl italic leading-snug [animation-delay:240ms]">
|
<p className="font-display text-ink-soft animate-fade-up relative mt-10 max-w-2xl text-2xl italic leading-snug [animation-delay:240ms] md:ml-[16.66%]">
|
||||||
{t('subtitle')}
|
{t('subtitle')}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div className="rule animate-rule-in my-10 [animation-delay:360ms]" />
|
<div className="rule animate-rule-in relative my-12 [animation-delay:360ms]" />
|
||||||
|
|
||||||
|
<div className="relative">
|
||||||
<FastingPauseToggle initial={pauseStatus} />
|
<FastingPauseToggle initial={pauseStatus} />
|
||||||
|
|
||||||
<FastTimer active={active} />
|
<FastTimer active={active} />
|
||||||
@@ -63,7 +91,9 @@ function FastingPageContent({
|
|||||||
<p className="editorial-eyebrow mb-6">{t('historyTitle')}</p>
|
<p className="editorial-eyebrow mb-6">{t('historyTitle')}</p>
|
||||||
<FastHistory events={history} />
|
<FastHistory events={history} />
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -3,6 +3,7 @@ import { redirect } from 'next/navigation';
|
|||||||
import { useTranslations } from 'next-intl';
|
import { useTranslations } from 'next-intl';
|
||||||
import { setRequestLocale } from 'next-intl/server';
|
import { setRequestLocale } from 'next-intl/server';
|
||||||
|
|
||||||
|
import { CursorGlow } from '@/components/cursor-glow';
|
||||||
import { Masthead } from '@/components/masthead';
|
import { Masthead } from '@/components/masthead';
|
||||||
import { getServerSession } from '@/lib/auth';
|
import { getServerSession } from '@/lib/auth';
|
||||||
|
|
||||||
@@ -47,24 +48,57 @@ function OnboardingPageContent({
|
|||||||
const t = useTranslations('Onboarding');
|
const t = useTranslations('Onboarding');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mx-auto min-h-screen max-w-5xl px-6 sm:px-10">
|
<div className="relative">
|
||||||
|
<div className="mx-auto min-h-screen max-w-7xl px-6 sm:px-10">
|
||||||
<Masthead issueLabel="N. 03 — Inizio" />
|
<Masthead issueLabel="N. 03 — Inizio" />
|
||||||
<main className="py-12 sm:py-16">
|
<main className="relative overflow-hidden pb-24 pt-10 sm:pt-12">
|
||||||
<p className="editorial-eyebrow animate-fade-up">Capitolo III</p>
|
<CursorGlow color="hsl(var(--oro))" size={520} />
|
||||||
<h1 className="font-display text-display text-ink animate-fade-up mt-3 font-medium leading-[0.95] tracking-tight [animation-delay:120ms]">
|
<div
|
||||||
{t('title')}
|
aria-hidden
|
||||||
<span className="text-pomodoro">.</span>
|
className="mesh-blob mesh-blob--oro animate-float-y -right-24 top-12 h-[28rem] w-[28rem] opacity-50"
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
aria-hidden
|
||||||
|
className="mesh-blob mesh-blob--pomodoro animate-float-x -left-32 top-72 h-80 w-80 opacity-40"
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Chapter "III" gigante in stroke */}
|
||||||
|
<span
|
||||||
|
aria-hidden
|
||||||
|
className="font-display text-stroke-thin text-chapter pointer-events-none absolute -right-4 -top-12 select-none italic"
|
||||||
|
>
|
||||||
|
III
|
||||||
|
</span>
|
||||||
|
|
||||||
|
{/* Eyebrow verticale destra */}
|
||||||
|
<div
|
||||||
|
aria-hidden
|
||||||
|
className="pointer-events-none absolute right-0 top-32 hidden md:block"
|
||||||
|
style={{ writingMode: 'vertical-rl' }}
|
||||||
|
>
|
||||||
|
<p className="text-ink-soft font-mono text-xs uppercase tracking-[0.4em]">
|
||||||
|
Capitolo III — Inizio
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Titolo bleed-left */}
|
||||||
|
<h1 className="font-display text-mega bleed-left animate-fade-up relative mt-8 font-medium [animation-delay:120ms]">
|
||||||
|
<span className="text-ink block">{t('title')}</span>
|
||||||
</h1>
|
</h1>
|
||||||
<p className="font-display text-ink-soft animate-fade-up mt-6 max-w-2xl text-xl italic leading-snug [animation-delay:240ms]">
|
|
||||||
|
<p className="font-display text-ink-soft animate-fade-up relative mt-10 max-w-2xl text-2xl italic leading-snug [animation-delay:240ms] md:ml-[16.66%]">
|
||||||
{t('subtitle')}
|
{t('subtitle')}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div className="rule animate-rule-in my-10 [animation-delay:360ms]" />
|
<div className="rule animate-rule-in my-12 [animation-delay:360ms]" />
|
||||||
|
|
||||||
|
<div className="relative">
|
||||||
<OnboardingFlow initialProfile={profile} initialPreferences={preferences} />
|
<OnboardingFlow initialProfile={profile} initialPreferences={preferences} />
|
||||||
|
</div>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<div className="rule-thick mt-16" />
|
<div className="rule-thick" />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
File diff suppressed because it is too large.
Load diff
@@ -4,6 +4,7 @@ import { redirect } from 'next/navigation';
|
|||||||
import { useTranslations } from 'next-intl';
|
import { useTranslations } from 'next-intl';
|
||||||
import { setRequestLocale } from 'next-intl/server';
|
import { setRequestLocale } from 'next-intl/server';
|
||||||
|
|
||||||
|
import { CursorGlow } from '@/components/cursor-glow';
|
||||||
import { Masthead } from '@/components/masthead';
|
import { Masthead } from '@/components/masthead';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { getServerSession } from '@/lib/auth';
|
import { getServerSession } from '@/lib/auth';
|
||||||
@@ -55,41 +56,74 @@ function PlanPageContent({
|
|||||||
const t = useTranslations('Plan');
|
const t = useTranslations('Plan');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mx-auto min-h-screen max-w-6xl px-6 sm:px-10">
|
<div className="relative">
|
||||||
|
<div className="mx-auto min-h-screen max-w-7xl px-6 sm:px-10">
|
||||||
<Masthead issueLabel="N. 05 — Cucina" />
|
<Masthead issueLabel="N. 05 — Cucina" />
|
||||||
<main className="py-12 sm:py-16">
|
<main className="relative overflow-hidden py-10 sm:py-12">
|
||||||
<div className="grid items-end gap-6 md:grid-cols-12">
|
<CursorGlow color="hsl(var(--pomodoro))" size={520} />
|
||||||
<div className="md:col-span-8">
|
<div
|
||||||
<p className="editorial-eyebrow animate-fade-up">Capitolo V</p>
|
aria-hidden
|
||||||
<h1 className="font-display text-display text-ink animate-fade-up mt-3 font-medium leading-[0.95] tracking-tight [animation-delay:120ms]">
|
className="mesh-blob mesh-blob--pomodoro animate-float-x -left-32 top-12 h-[28rem] w-[28rem] opacity-50"
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
aria-hidden
|
||||||
|
className="mesh-blob mesh-blob--oliva animate-float-y -right-32 top-64 h-80 w-80 opacity-40"
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Chapter "V" gigante in stroke */}
|
||||||
|
<span
|
||||||
|
aria-hidden
|
||||||
|
className="font-display text-stroke-thin text-chapter pointer-events-none absolute -right-4 -top-12 select-none italic"
|
||||||
|
>
|
||||||
|
V
|
||||||
|
</span>
|
||||||
|
|
||||||
|
{/* Eyebrow verticale destra */}
|
||||||
|
<div
|
||||||
|
aria-hidden
|
||||||
|
className="pointer-events-none absolute right-0 top-32 hidden md:block"
|
||||||
|
style={{ writingMode: 'vertical-rl' }}
|
||||||
|
>
|
||||||
|
<p className="text-ink-soft font-mono text-xs uppercase tracking-[0.4em]">
|
||||||
|
Capitolo V — Cucina
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Titolo che sborda */}
|
||||||
|
<h1 className="font-display text-mega bleed-left animate-fade-up relative mt-12 font-medium [animation-delay:120ms]">
|
||||||
|
<span className="text-ink block italic">
|
||||||
{t('title')}
|
{t('title')}
|
||||||
<span className="text-pomodoro">.</span>
|
<span className="text-pomodoro">.</span>
|
||||||
|
</span>
|
||||||
</h1>
|
</h1>
|
||||||
<p className="font-display text-ink-soft animate-fade-up mt-5 max-w-2xl text-xl italic leading-snug [animation-delay:240ms]">
|
|
||||||
|
<div className="relative mt-12 grid grid-cols-12 items-end gap-6">
|
||||||
|
<div className="col-span-12 md:col-span-7 md:col-start-2">
|
||||||
|
<p className="font-display text-ink-soft animate-fade-up text-2xl italic leading-snug [animation-delay:240ms]">
|
||||||
{plan ? formatWeek(plan.weekStart) : t('noPlan')}
|
{plan ? formatWeek(plan.weekStart) : t('noPlan')}
|
||||||
</p>
|
</p>
|
||||||
|
<div className="animate-fade-up mt-5 flex flex-wrap items-baseline gap-x-6 gap-y-2 [animation-delay:300ms]">
|
||||||
{plan?.fastingProtocol ? (
|
{plan?.fastingProtocol ? (
|
||||||
<p className="animate-fade-up mt-4 [animation-delay:300ms]">
|
<span className="flex items-baseline gap-2">
|
||||||
<span className="editorial-eyebrow">{t('window')}</span>{' '}
|
<span className="editorial-eyebrow">{t('window')}</span>
|
||||||
<span className="font-display text-pomodoro ml-2 text-base italic">
|
<span className="font-display text-pomodoro text-base italic">
|
||||||
{t(`protocolLabel.${plan.fastingProtocol}`)}
|
{t(`protocolLabel.${plan.fastingProtocol}`)}
|
||||||
</span>
|
</span>
|
||||||
</p>
|
</span>
|
||||||
) : null}
|
) : null}
|
||||||
{plan?.currentPhase === 'TRANSITION' && plan.phase2Week ? (
|
{plan?.currentPhase === 'TRANSITION' && plan.phase2Week ? (
|
||||||
<p className="animate-fade-up mt-3 [animation-delay:330ms]">
|
<span className="flex items-baseline gap-2">
|
||||||
<span className="editorial-eyebrow">{t('phase2Eyebrow')}</span>{' '}
|
<span className="editorial-eyebrow">{t('phase2Eyebrow')}</span>
|
||||||
<span className="font-display text-oliva ml-2 text-base italic">
|
<span className="font-display text-oliva text-base italic">
|
||||||
{t('phase2Week', { n: plan.phase2Week })}
|
{t('phase2Week', { n: plan.phase2Week })}
|
||||||
</span>
|
</span>
|
||||||
</p>
|
</span>
|
||||||
) : plan?.currentPhase === 'MAINTENANCE' ? (
|
) : plan?.currentPhase === 'MAINTENANCE' ? (
|
||||||
<p className="animate-fade-up mt-3 [animation-delay:330ms]">
|
|
||||||
<span className="editorial-eyebrow">{t('phase3Eyebrow')}</span>
|
<span className="editorial-eyebrow">{t('phase3Eyebrow')}</span>
|
||||||
</p>
|
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
<div className="animate-fade-up flex flex-col items-stretch gap-3 [animation-delay:360ms] md:col-span-4 md:items-end">
|
</div>
|
||||||
|
<div className="animate-fade-up col-span-12 flex flex-col items-stretch gap-3 [animation-delay:360ms] md:col-span-4 md:col-start-9 md:items-end">
|
||||||
<form action={regeneratePlan}>
|
<form action={regeneratePlan}>
|
||||||
<Button type="submit" variant={plan ? 'outline' : 'default'} size="lg">
|
<Button type="submit" variant={plan ? 'outline' : 'default'} size="lg">
|
||||||
{plan ? t('regenerate') : t('generate')}
|
{plan ? t('regenerate') : t('generate')}
|
||||||
@@ -116,14 +150,15 @@ function PlanPageContent({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="rule animate-rule-in my-10 [animation-delay:420ms]" />
|
<div className="rule animate-rule-in my-12 [animation-delay:420ms]" />
|
||||||
|
|
||||||
{plan ? (
|
{plan ? (
|
||||||
<div className="animate-fade-up [animation-delay:480ms]">
|
<div className="animate-fade-up relative [animation-delay:480ms]">
|
||||||
<PlanWeek plan={plan} dailyTarget={dailyTarget} />
|
<PlanWeek plan={plan} dailyTarget={dailyTarget} />
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -3,6 +3,7 @@ import { redirect } from 'next/navigation';
|
|||||||
import { useTranslations } from 'next-intl';
|
import { useTranslations } from 'next-intl';
|
||||||
import { setRequestLocale } from 'next-intl/server';
|
import { setRequestLocale } from 'next-intl/server';
|
||||||
|
|
||||||
|
import { CursorGlow } from '@/components/cursor-glow';
|
||||||
import { Masthead } from '@/components/masthead';
|
import { Masthead } from '@/components/masthead';
|
||||||
import { getServerSession } from '@/lib/auth';
|
import { getServerSession } from '@/lib/auth';
|
||||||
|
|
||||||
@@ -56,19 +57,45 @@ function ProfilePageContent({
|
|||||||
const t = useTranslations('Profile');
|
const t = useTranslations('Profile');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mx-auto min-h-screen max-w-4xl px-6 sm:px-10">
|
<div className="relative">
|
||||||
|
<div className="mx-auto min-h-screen max-w-7xl px-6 sm:px-10">
|
||||||
<Masthead issueLabel="N. 04 — Profilo" />
|
<Masthead issueLabel="N. 04 — Profilo" />
|
||||||
<main className="py-12 sm:py-16">
|
<main className="relative overflow-hidden pb-24 pt-10 sm:pt-12">
|
||||||
<p className="editorial-eyebrow animate-fade-up">Capitolo IV</p>
|
<CursorGlow color="hsl(var(--oliva))" size={520} />
|
||||||
<h1 className="font-display text-display text-ink animate-fade-up mt-3 font-medium leading-[0.95] tracking-tight [animation-delay:120ms]">
|
<div
|
||||||
|
aria-hidden
|
||||||
|
className="mesh-blob mesh-blob--oliva animate-float-y -right-32 top-12 h-[28rem] w-[28rem] opacity-50"
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
aria-hidden
|
||||||
|
className="mesh-blob mesh-blob--oro animate-float-x -left-24 top-72 h-72 w-72 opacity-35"
|
||||||
|
/>
|
||||||
|
<span
|
||||||
|
aria-hidden
|
||||||
|
className="font-display text-stroke-thin text-chapter pointer-events-none absolute -right-4 -top-12 select-none italic"
|
||||||
|
>
|
||||||
|
IV
|
||||||
|
</span>
|
||||||
|
<div
|
||||||
|
aria-hidden
|
||||||
|
className="pointer-events-none absolute right-0 top-32 hidden md:block"
|
||||||
|
style={{ writingMode: 'vertical-rl' }}
|
||||||
|
>
|
||||||
|
<p className="text-ink-soft font-mono text-xs uppercase tracking-[0.4em]">
|
||||||
|
Capitolo IV — Profilo
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<h1 className="font-display text-mega bleed-left animate-fade-up relative mt-8 font-medium [animation-delay:120ms]">
|
||||||
|
<span className="text-ink block">
|
||||||
{t('title')}
|
{t('title')}
|
||||||
<span className="text-pomodoro">.</span>
|
<span className="text-pomodoro">.</span>
|
||||||
|
</span>
|
||||||
</h1>
|
</h1>
|
||||||
<p className="font-display text-ink-soft animate-fade-up mt-6 max-w-2xl text-xl italic leading-snug [animation-delay:240ms]">
|
<p className="font-display text-ink-soft animate-fade-up relative mt-10 max-w-2xl text-2xl italic leading-snug [animation-delay:240ms] md:ml-[16.66%]">
|
||||||
{t('subtitle')}
|
{t('subtitle')}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div className="rule animate-rule-in my-10 [animation-delay:360ms]" />
|
<div className="rule animate-rule-in my-12 [animation-delay:360ms]" />
|
||||||
|
|
||||||
<div className="animate-fade-up [animation-delay:300ms]">
|
<div className="animate-fade-up [animation-delay:300ms]">
|
||||||
<ProfileForm initial={initial} />
|
<ProfileForm initial={initial} />
|
||||||
@@ -99,5 +126,6 @@ function ProfilePageContent({
|
|||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -4,6 +4,7 @@ import { redirect } from 'next/navigation';
|
|||||||
import { useTranslations } from 'next-intl';
|
import { useTranslations } from 'next-intl';
|
||||||
import { setRequestLocale } from 'next-intl/server';
|
import { setRequestLocale } from 'next-intl/server';
|
||||||
|
|
||||||
|
import { CursorGlow } from '@/components/cursor-glow';
|
||||||
import { Masthead } from '@/components/masthead';
|
import { Masthead } from '@/components/masthead';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { getServerSession } from '@/lib/auth';
|
import { getServerSession } from '@/lib/auth';
|
||||||
@@ -51,22 +52,47 @@ function ShoppingPageContent({ list }: { list: ShoppingList | null }) {
|
|||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mx-auto min-h-screen max-w-5xl px-6 sm:px-10">
|
<div className="relative">
|
||||||
|
<div className="mx-auto min-h-screen max-w-7xl px-6 sm:px-10">
|
||||||
<Masthead issueLabel="N. 09 — La spesa" />
|
<Masthead issueLabel="N. 09 — La spesa" />
|
||||||
<main className="py-12 sm:py-16">
|
<main className="relative overflow-hidden pb-24 pt-10 sm:pt-12">
|
||||||
<div className="grid items-end gap-6 md:grid-cols-12">
|
<CursorGlow color="hsl(var(--oliva))" size={520} />
|
||||||
<div className="md:col-span-8">
|
<div
|
||||||
<p className="editorial-eyebrow animate-fade-up">Capitolo IX</p>
|
aria-hidden
|
||||||
<h1 className="font-display text-display text-ink animate-fade-up mt-3 font-medium leading-[0.95] tracking-tight [animation-delay:120ms]">
|
className="mesh-blob mesh-blob--oliva animate-float-y -right-32 top-12 h-[28rem] w-[28rem] opacity-50"
|
||||||
{t('title')}
|
/>
|
||||||
<span className="text-pomodoro">.</span>
|
<div
|
||||||
</h1>
|
aria-hidden
|
||||||
<p className="font-display text-ink-soft animate-fade-up mt-5 max-w-2xl text-xl italic leading-snug [animation-delay:240ms]">
|
className="mesh-blob mesh-blob--pomodoro animate-float-x -left-24 top-72 h-72 w-72 opacity-35"
|
||||||
{list ? formatWeek(list.plan.weekStart) : t('noPlan')}
|
/>
|
||||||
|
<span
|
||||||
|
aria-hidden
|
||||||
|
className="font-display text-stroke-thin text-chapter pointer-events-none absolute -right-4 -top-12 select-none italic"
|
||||||
|
>
|
||||||
|
IX
|
||||||
|
</span>
|
||||||
|
<div
|
||||||
|
aria-hidden
|
||||||
|
className="pointer-events-none absolute right-0 top-32 hidden md:block"
|
||||||
|
style={{ writingMode: 'vertical-rl' }}
|
||||||
|
>
|
||||||
|
<p className="text-ink-soft font-mono text-xs uppercase tracking-[0.4em]">
|
||||||
|
Capitolo IX — La spesa
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
<h1 className="font-display text-mega bleed-left animate-fade-up relative mt-8 font-medium [animation-delay:120ms]">
|
||||||
|
<span className="text-ink block">
|
||||||
|
{t('title')}
|
||||||
|
<span className="text-pomodoro">.</span>
|
||||||
|
</span>
|
||||||
|
</h1>
|
||||||
|
|
||||||
|
<div className="relative mt-12 grid grid-cols-12 items-end gap-6">
|
||||||
|
<p className="font-display text-ink-soft animate-fade-up col-span-12 max-w-2xl text-2xl italic leading-snug [animation-delay:240ms] md:col-span-7 md:col-start-2">
|
||||||
|
{list ? formatWeek(list.plan.weekStart) : t('noPlan')}
|
||||||
|
</p>
|
||||||
{list ? (
|
{list ? (
|
||||||
<aside className="animate-fade-up [animation-delay:360ms] md:col-span-4 md:text-right">
|
<aside className="animate-fade-up col-span-12 [animation-delay:360ms] md:col-span-3 md:col-start-10 md:text-right">
|
||||||
<p className="editorial-eyebrow">{t('totalLabel')}</p>
|
<p className="editorial-eyebrow">{t('totalLabel')}</p>
|
||||||
<p className="font-display text-ink mt-2 font-mono text-3xl tabular-nums">
|
<p className="font-display text-ink mt-2 font-mono text-3xl tabular-nums">
|
||||||
{totalItems}
|
{totalItems}
|
||||||
@@ -79,7 +105,7 @@ function ShoppingPageContent({ list }: { list: ShoppingList | null }) {
|
|||||||
) : null}
|
) : null}
|
||||||
</aside>
|
</aside>
|
||||||
) : (
|
) : (
|
||||||
<div className="animate-fade-up [animation-delay:360ms] md:col-span-4 md:text-right">
|
<div className="animate-fade-up col-span-12 [animation-delay:360ms] md:col-span-3 md:col-start-10 md:text-right">
|
||||||
<Button asChild size="lg">
|
<Button asChild size="lg">
|
||||||
<Link href="/plan">{t('viewPlan')}</Link>
|
<Link href="/plan">{t('viewPlan')}</Link>
|
||||||
</Button>
|
</Button>
|
||||||
@@ -87,16 +113,17 @@ function ShoppingPageContent({ list }: { list: ShoppingList | null }) {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="rule animate-rule-in my-10 [animation-delay:420ms]" />
|
<div className="rule animate-rule-in relative my-12 [animation-delay:420ms]" />
|
||||||
|
|
||||||
{list ? (
|
{list ? (
|
||||||
<div className="animate-fade-up [animation-delay:480ms]">
|
<div className="animate-fade-up relative [animation-delay:480ms]">
|
||||||
<ShoppingChecklist list={list} />
|
<ShoppingChecklist list={list} />
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<div className="rule-thick mt-16" />
|
<div className="rule-thick" />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { redirect } from 'next/navigation';
|
|||||||
import { useTranslations } from 'next-intl';
|
import { useTranslations } from 'next-intl';
|
||||||
import { setRequestLocale } from 'next-intl/server';
|
import { setRequestLocale } from 'next-intl/server';
|
||||||
|
|
||||||
|
import { CursorGlow } from '@/components/cursor-glow';
|
||||||
import { Masthead } from '@/components/masthead';
|
import { Masthead } from '@/components/masthead';
|
||||||
import { getServerSession } from '@/lib/auth';
|
import { getServerSession } from '@/lib/auth';
|
||||||
|
|
||||||
@@ -63,19 +64,45 @@ function TrackingPageContent({
|
|||||||
const alertActive = alertKg != null && latest != null && latest.weightKg >= alertKg;
|
const alertActive = alertKg != null && latest != null && latest.weightKg >= alertKg;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mx-auto min-h-screen max-w-5xl px-6 sm:px-10">
|
<div className="relative">
|
||||||
|
<div className="mx-auto min-h-screen max-w-7xl px-6 sm:px-10">
|
||||||
<Masthead issueLabel="N. 06 — Tracking" />
|
<Masthead issueLabel="N. 06 — Tracking" />
|
||||||
<main className="py-12 sm:py-16">
|
<main className="relative overflow-hidden pb-24 pt-10 sm:pt-12">
|
||||||
<p className="editorial-eyebrow animate-fade-up">Capitolo VI</p>
|
<CursorGlow color="hsl(var(--oro))" size={520} />
|
||||||
<h1 className="font-display text-display text-ink animate-fade-up mt-3 font-medium leading-[0.95] tracking-tight [animation-delay:120ms]">
|
<div
|
||||||
|
aria-hidden
|
||||||
|
className="mesh-blob mesh-blob--oro animate-float-y -right-32 top-12 h-[28rem] w-[28rem] opacity-50"
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
aria-hidden
|
||||||
|
className="mesh-blob mesh-blob--oliva animate-float-x -left-24 top-72 h-72 w-72 opacity-40"
|
||||||
|
/>
|
||||||
|
<span
|
||||||
|
aria-hidden
|
||||||
|
className="font-display text-stroke-thin text-chapter pointer-events-none absolute -right-4 -top-12 select-none italic"
|
||||||
|
>
|
||||||
|
VI
|
||||||
|
</span>
|
||||||
|
<div
|
||||||
|
aria-hidden
|
||||||
|
className="pointer-events-none absolute right-0 top-32 hidden md:block"
|
||||||
|
style={{ writingMode: 'vertical-rl' }}
|
||||||
|
>
|
||||||
|
<p className="text-ink-soft font-mono text-xs uppercase tracking-[0.4em]">
|
||||||
|
Capitolo VI — Tracking
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<h1 className="font-display text-mega bleed-left animate-fade-up relative mt-8 font-medium [animation-delay:120ms]">
|
||||||
|
<span className="text-ink block">
|
||||||
{t('title')}
|
{t('title')}
|
||||||
<span className="text-pomodoro">.</span>
|
<span className="text-pomodoro">.</span>
|
||||||
|
</span>
|
||||||
</h1>
|
</h1>
|
||||||
<p className="font-display text-ink-soft animate-fade-up mt-6 max-w-2xl text-xl italic leading-snug [animation-delay:240ms]">
|
<p className="font-display text-ink-soft animate-fade-up relative mt-10 max-w-2xl text-2xl italic leading-snug [animation-delay:240ms] md:ml-[16.66%]">
|
||||||
{t('subtitle')}
|
{t('subtitle')}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p className="animate-fade-up mt-4 [animation-delay:300ms]">
|
<p className="animate-fade-up relative mt-6 [animation-delay:300ms] md:ml-[16.66%]">
|
||||||
<a
|
<a
|
||||||
href="/api/tracking-export"
|
href="/api/tracking-export"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
@@ -86,7 +113,7 @@ function TrackingPageContent({
|
|||||||
</a>
|
</a>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div className="rule animate-rule-in my-10 [animation-delay:360ms]" />
|
<div className="rule animate-rule-in relative my-12 [animation-delay:360ms]" />
|
||||||
|
|
||||||
<DailyCheckIn initial={todayCheckIn} />
|
<DailyCheckIn initial={todayCheckIn} />
|
||||||
|
|
||||||
@@ -105,7 +132,7 @@ function TrackingPageContent({
|
|||||||
</aside>
|
</aside>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
<div className="grid gap-12 md:grid-cols-12">
|
<div className="relative grid gap-12 md:grid-cols-12">
|
||||||
<section className="animate-fade-up [animation-delay:300ms] md:col-span-7">
|
<section className="animate-fade-up [animation-delay:300ms] md:col-span-7">
|
||||||
<p className="editorial-eyebrow mb-6">{t('newEntry')}</p>
|
<p className="editorial-eyebrow mb-6">{t('newEntry')}</p>
|
||||||
<WeightEntryForm latest={latest} />
|
<WeightEntryForm latest={latest} />
|
||||||
@@ -117,5 +144,6 @@ function TrackingPageContent({
|
|||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useEffect, useRef } from 'react';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cursor-tracking glow — un alone radiale che segue il puntatore.
|
||||||
|
* Si appiccica al parent posizionato relative/absolute. Rispetta
|
||||||
|
* `prefers-reduced-motion` (in quel caso resta centrato).
|
||||||
|
*/
|
||||||
|
export function CursorGlow({
|
||||||
|
className = '',
|
||||||
|
color = 'hsl(var(--pomodoro))',
|
||||||
|
size = 520,
|
||||||
|
}: {
|
||||||
|
className?: string;
|
||||||
|
color?: string;
|
||||||
|
size?: number;
|
||||||
|
}) {
|
||||||
|
const ref = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const el = ref.current;
|
||||||
|
if (!el) return;
|
||||||
|
const reduce = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
|
||||||
|
if (reduce) return;
|
||||||
|
|
||||||
|
const parent = el.parentElement;
|
||||||
|
if (!parent) return;
|
||||||
|
|
||||||
|
let raf = 0;
|
||||||
|
let targetX = parent.clientWidth / 2;
|
||||||
|
let targetY = parent.clientHeight / 3;
|
||||||
|
let currentX = targetX;
|
||||||
|
let currentY = targetY;
|
||||||
|
|
||||||
|
function onMove(e: PointerEvent): void {
|
||||||
|
const rect = parent!.getBoundingClientRect();
|
||||||
|
targetX = e.clientX - rect.left;
|
||||||
|
targetY = e.clientY - rect.top;
|
||||||
|
}
|
||||||
|
|
||||||
|
function loop(): void {
|
||||||
|
// Easing: avvicino la posizione corrente al target con un delta morbido.
|
||||||
|
currentX += (targetX - currentX) * 0.12;
|
||||||
|
currentY += (targetY - currentY) * 0.12;
|
||||||
|
if (el) {
|
||||||
|
el.style.transform = `translate3d(${currentX - size / 2}px, ${currentY - size / 2}px, 0)`;
|
||||||
|
}
|
||||||
|
raf = requestAnimationFrame(loop);
|
||||||
|
}
|
||||||
|
|
||||||
|
parent.addEventListener('pointermove', onMove);
|
||||||
|
raf = requestAnimationFrame(loop);
|
||||||
|
return () => {
|
||||||
|
cancelAnimationFrame(raf);
|
||||||
|
parent.removeEventListener('pointermove', onMove);
|
||||||
|
};
|
||||||
|
}, [size]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
ref={ref}
|
||||||
|
aria-hidden
|
||||||
|
className={`pointer-events-none absolute left-0 top-0 rounded-full mix-blend-multiply ${className}`}
|
||||||
|
style={{
|
||||||
|
width: size,
|
||||||
|
height: size,
|
||||||
|
background: `radial-gradient(closest-side, ${color}, transparent 70%)`,
|
||||||
|
opacity: 0.55,
|
||||||
|
filter: 'blur(48px)',
|
||||||
|
transform: `translate3d(${-size}px, ${-size}px, 0)`,
|
||||||
|
willChange: 'transform',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,91 @@
|
|||||||
|
import { CursorGlow } from './cursor-glow';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Atmosfera di pagina riusabile: 2 mesh-blob fluttuanti + cursor glow
|
||||||
|
* opzionale. Va inserita come primo figlio di un `relative overflow-hidden`,
|
||||||
|
* sotto al contenuto principale.
|
||||||
|
*/
|
||||||
|
export function EditorialAtmosphere({
|
||||||
|
variant = 'default',
|
||||||
|
withCursor = false,
|
||||||
|
}: {
|
||||||
|
variant?: 'default' | 'pomodoro' | 'oliva' | 'oro';
|
||||||
|
withCursor?: boolean;
|
||||||
|
}) {
|
||||||
|
// Combinazione di blob per ogni variante.
|
||||||
|
const blobs =
|
||||||
|
variant === 'pomodoro'
|
||||||
|
? [
|
||||||
|
{
|
||||||
|
kind: 'pomodoro',
|
||||||
|
pos: '-left-32 top-12',
|
||||||
|
size: 'h-[28rem] w-[28rem]',
|
||||||
|
anim: 'animate-float-x',
|
||||||
|
},
|
||||||
|
{ kind: 'oro', pos: '-right-24 top-64', size: 'h-80 w-80', anim: 'animate-float-y' },
|
||||||
|
]
|
||||||
|
: variant === 'oliva'
|
||||||
|
? [
|
||||||
|
{
|
||||||
|
kind: 'oliva',
|
||||||
|
pos: '-right-32 top-12',
|
||||||
|
size: 'h-[28rem] w-[28rem]',
|
||||||
|
anim: 'animate-float-y',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
kind: 'pomodoro',
|
||||||
|
pos: '-left-24 top-64',
|
||||||
|
size: 'h-72 w-72',
|
||||||
|
anim: 'animate-float-x',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
: variant === 'oro'
|
||||||
|
? [
|
||||||
|
{
|
||||||
|
kind: 'oro',
|
||||||
|
pos: 'right-1/4 top-12',
|
||||||
|
size: 'h-[24rem] w-[24rem]',
|
||||||
|
anim: 'animate-float-z',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
kind: 'oliva',
|
||||||
|
pos: '-left-24 bottom-12',
|
||||||
|
size: 'h-72 w-72',
|
||||||
|
anim: 'animate-float-y',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
: [
|
||||||
|
{
|
||||||
|
kind: 'oliva',
|
||||||
|
pos: '-right-32 top-12',
|
||||||
|
size: 'h-80 w-80',
|
||||||
|
anim: 'animate-float-y',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
kind: 'pomodoro',
|
||||||
|
pos: '-left-24 top-64',
|
||||||
|
size: 'h-72 w-72',
|
||||||
|
anim: 'animate-float-x',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const cursorColor =
|
||||||
|
variant === 'pomodoro'
|
||||||
|
? 'hsl(var(--pomodoro))'
|
||||||
|
: variant === 'oro'
|
||||||
|
? 'hsl(var(--oro))'
|
||||||
|
: 'hsl(var(--oliva))';
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{withCursor ? <CursorGlow color={cursorColor} size={520} /> : null}
|
||||||
|
{blobs.map((b, i) => (
|
||||||
|
<div
|
||||||
|
key={i}
|
||||||
|
aria-hidden
|
||||||
|
className={`mesh-blob mesh-blob--${b.kind} ${b.anim} ${b.pos} ${b.size} opacity-40`}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
/**
|
||||||
|
* Header tipografico riusabile in stile "asimmetrico off-grid".
|
||||||
|
* Chapter marker gigante (numero romano) come art di sfondo,
|
||||||
|
* eyebrow defilata, titolo che può sbordare dal bordo sinistro,
|
||||||
|
* sottotitolo indentato in colonna stretta.
|
||||||
|
*
|
||||||
|
* Va wrappato in un padre `relative overflow-hidden` (la pagina lo è
|
||||||
|
* già di norma). Il chapter marker è in stroke trasparente, decorativo.
|
||||||
|
*/
|
||||||
|
export function EditorialHeader({
|
||||||
|
chapter,
|
||||||
|
eyebrow,
|
||||||
|
title,
|
||||||
|
accent,
|
||||||
|
subtitle,
|
||||||
|
rightSlot,
|
||||||
|
}: {
|
||||||
|
/** Numero romano grande (es. "II", "IV", "VII") usato come art di sfondo. */
|
||||||
|
chapter: string;
|
||||||
|
/** Eyebrow in alto. */
|
||||||
|
eyebrow: string;
|
||||||
|
/** Titolo principale. */
|
||||||
|
title: string;
|
||||||
|
/** Parola/parole da evidenziare in gradient italico (sostituisce nel titolo). */
|
||||||
|
accent?: string;
|
||||||
|
/** Sottotitolo italic, indentato. */
|
||||||
|
subtitle?: string;
|
||||||
|
/** Slot opzionale a destra del titolo (es. CTA "rigenera", date pickers). */
|
||||||
|
rightSlot?: React.ReactNode;
|
||||||
|
}) {
|
||||||
|
let titleNode: React.ReactNode = title;
|
||||||
|
if (accent) {
|
||||||
|
const idx = title.toLowerCase().indexOf(accent.toLowerCase());
|
||||||
|
if (idx >= 0) {
|
||||||
|
titleNode = (
|
||||||
|
<>
|
||||||
|
{title.slice(0, idx)}
|
||||||
|
<span className="text-gradient italic">{title.slice(idx, idx + accent.length)}</span>
|
||||||
|
{title.slice(idx + accent.length)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<header className="relative pb-6 pt-10 sm:pt-16">
|
||||||
|
{/* Chapter marker decorativo */}
|
||||||
|
<span
|
||||||
|
aria-hidden
|
||||||
|
className="font-display text-stroke-thin text-chapter pointer-events-none absolute -right-2 -top-4 select-none italic sm:-top-12"
|
||||||
|
>
|
||||||
|
{chapter}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<div className="relative grid grid-cols-12 items-end gap-6">
|
||||||
|
<div className="col-span-12 md:col-span-9">
|
||||||
|
<p className="editorial-eyebrow animate-fade-up">{eyebrow}</p>
|
||||||
|
<h1 className="font-display text-display text-ink animate-fade-up mt-4 font-medium leading-[0.95] tracking-tight [animation-delay:120ms]">
|
||||||
|
{titleNode}
|
||||||
|
<span className="text-pomodoro">.</span>
|
||||||
|
</h1>
|
||||||
|
{subtitle ? (
|
||||||
|
<p className="font-display text-ink-soft animate-fade-up mt-5 max-w-2xl text-xl italic leading-snug [animation-delay:240ms]">
|
||||||
|
{subtitle}
|
||||||
|
</p>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
{rightSlot ? (
|
||||||
|
<div className="animate-fade-up col-span-12 [animation-delay:360ms] md:col-span-3 md:flex md:justify-end">
|
||||||
|
{rightSlot}
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -160,4 +160,258 @@
|
|||||||
.monogram {
|
.monogram {
|
||||||
@apply font-display text-2xl italic leading-none tracking-tight;
|
@apply font-display text-2xl italic leading-none tracking-tight;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ─────────────────────────────────────────────────────────────────────
|
||||||
|
* Atmosfera moderna: blob, gradient, motion utilities.
|
||||||
|
* Usate dalla landing rifatta — pensate per essere applicate con
|
||||||
|
* parsimonia (tutta la palette è "warm cream" di base).
|
||||||
|
* ─────────────────────────────────────────────────────────────────── */
|
||||||
|
|
||||||
|
.mesh-blob {
|
||||||
|
position: absolute;
|
||||||
|
border-radius: 9999px;
|
||||||
|
filter: blur(64px);
|
||||||
|
opacity: 0.55;
|
||||||
|
pointer-events: none;
|
||||||
|
will-change: transform;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mesh-blob--oliva {
|
||||||
|
background: radial-gradient(closest-side, hsl(var(--oliva) / 0.7), transparent);
|
||||||
|
}
|
||||||
|
.mesh-blob--pomodoro {
|
||||||
|
background: radial-gradient(closest-side, hsl(var(--pomodoro) / 0.6), transparent);
|
||||||
|
}
|
||||||
|
.mesh-blob--oro {
|
||||||
|
background: radial-gradient(closest-side, hsl(var(--oro) / 0.55), transparent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-gradient {
|
||||||
|
background: linear-gradient(
|
||||||
|
120deg,
|
||||||
|
hsl(var(--pomodoro)) 0%,
|
||||||
|
hsl(var(--oro)) 45%,
|
||||||
|
hsl(var(--oliva)) 100%
|
||||||
|
);
|
||||||
|
background-size: 200% 200%;
|
||||||
|
-webkit-background-clip: text;
|
||||||
|
background-clip: text;
|
||||||
|
color: transparent;
|
||||||
|
animation: gradient-pan 9s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hover-lift {
|
||||||
|
transition:
|
||||||
|
transform 320ms cubic-bezier(0.2, 0.7, 0.2, 1),
|
||||||
|
box-shadow 320ms cubic-bezier(0.2, 0.7, 0.2, 1),
|
||||||
|
border-color 320ms ease;
|
||||||
|
}
|
||||||
|
.hover-lift:hover {
|
||||||
|
transform: translateY(-4px);
|
||||||
|
box-shadow:
|
||||||
|
0 14px 30px -18px hsl(var(--ink) / 0.25),
|
||||||
|
0 2px 4px -2px hsl(var(--ink) / 0.06);
|
||||||
|
}
|
||||||
|
|
||||||
|
.glow-ring {
|
||||||
|
box-shadow:
|
||||||
|
0 0 0 1px hsl(var(--ink) / 0.06),
|
||||||
|
0 18px 40px -22px hsl(var(--pomodoro) / 0.45),
|
||||||
|
0 6px 18px -10px hsl(var(--oliva) / 0.25);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-gradient {
|
||||||
|
background: linear-gradient(
|
||||||
|
120deg,
|
||||||
|
hsl(var(--pomodoro)) 0%,
|
||||||
|
hsl(var(--oro)) 50%,
|
||||||
|
hsl(var(--oliva)) 100%
|
||||||
|
);
|
||||||
|
background-size: 180% 180%;
|
||||||
|
color: hsl(var(--carta-light));
|
||||||
|
transition:
|
||||||
|
background-position 600ms ease,
|
||||||
|
transform 200ms ease,
|
||||||
|
box-shadow 320ms ease;
|
||||||
|
box-shadow:
|
||||||
|
0 12px 30px -14px hsl(var(--pomodoro) / 0.55),
|
||||||
|
0 4px 10px -4px hsl(var(--oliva) / 0.3);
|
||||||
|
}
|
||||||
|
.btn-gradient:hover {
|
||||||
|
background-position: 100% 50%;
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
.btn-gradient:active {
|
||||||
|
transform: translateY(1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Display tipografico WOW: stroke outline (text-only border). Funziona
|
||||||
|
* con -webkit-text-stroke che ha buon supporto su tutti i browser
|
||||||
|
* moderni; il fill resta trasparente così si vede solo il contorno. */
|
||||||
|
.text-stroke {
|
||||||
|
-webkit-text-stroke: 2px hsl(var(--ink));
|
||||||
|
color: transparent;
|
||||||
|
}
|
||||||
|
.text-stroke-thick {
|
||||||
|
-webkit-text-stroke: 3px hsl(var(--ink));
|
||||||
|
color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Sezione dark per il contrasto col cream — usata nel CTA finale.
|
||||||
|
* Niente paper grain qui, palette inverted. */
|
||||||
|
.section-dark {
|
||||||
|
background: linear-gradient(135deg, hsl(28 18% 8%) 0%, hsl(8 30% 14%) 100%);
|
||||||
|
color: hsl(var(--carta));
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Mockup card 3D — leggera rotazione X+Y per dare profondità.
|
||||||
|
* Il container va settato perspective-1000 per attivare la prospettiva. */
|
||||||
|
.perspective-1000 {
|
||||||
|
perspective: 1400px;
|
||||||
|
}
|
||||||
|
.tilt-3d {
|
||||||
|
transform: rotateX(8deg) rotateY(-12deg) rotateZ(0.5deg);
|
||||||
|
transform-style: preserve-3d;
|
||||||
|
transition: transform 600ms cubic-bezier(0.2, 0.7, 0.2, 1);
|
||||||
|
}
|
||||||
|
.tilt-3d:hover {
|
||||||
|
transform: rotateX(2deg) rotateY(-3deg) rotateZ(0deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@layer utilities {
|
||||||
|
/* Marquee: scroll orizzontale infinito per keyword strip.
|
||||||
|
* Il contenuto deve essere duplicato due volte per evitare il salto. */
|
||||||
|
@keyframes marquee {
|
||||||
|
from {
|
||||||
|
transform: translateX(0);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
transform: translateX(-50%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.animate-marquee {
|
||||||
|
animation: marquee 38s linear infinite;
|
||||||
|
}
|
||||||
|
.animate-marquee--fast {
|
||||||
|
animation: marquee 22s linear infinite;
|
||||||
|
}
|
||||||
|
.animate-marquee--reverse {
|
||||||
|
animation: marquee 38s linear infinite reverse;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Display gigante WOW per il titolo hero — fino a 16vw. */
|
||||||
|
.text-mega {
|
||||||
|
font-size: clamp(3.5rem, 16vw, 12rem);
|
||||||
|
line-height: 0.85;
|
||||||
|
letter-spacing: -0.045em;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Chapter marker gigante usato come art di sfondo per le sezioni. */
|
||||||
|
.text-chapter {
|
||||||
|
font-size: clamp(8rem, 28vw, 22rem);
|
||||||
|
line-height: 0.8;
|
||||||
|
letter-spacing: -0.06em;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Stroke fine, per chapter markers di sfondo. */
|
||||||
|
.text-stroke-thin {
|
||||||
|
-webkit-text-stroke: 1.5px hsl(var(--ink) / 0.18);
|
||||||
|
color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Bleed: rompe il container per attaccarsi al bordo del viewport.
|
||||||
|
* Usato sui titoli mega che devono "uscire dalla pagina". */
|
||||||
|
.bleed-left {
|
||||||
|
margin-left: calc(50% - 50vw);
|
||||||
|
padding-left: max(1.5rem, calc(50vw - 50%));
|
||||||
|
}
|
||||||
|
.bleed-right {
|
||||||
|
margin-right: calc(50% - 50vw);
|
||||||
|
padding-right: max(1.5rem, calc(50vw - 50%));
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-reduced-motion: reduce) {
|
||||||
|
.animate-marquee,
|
||||||
|
.animate-marquee--fast,
|
||||||
|
.animate-marquee--reverse {
|
||||||
|
animation: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@layer utilities {
|
||||||
|
@keyframes gradient-pan {
|
||||||
|
0%,
|
||||||
|
100% {
|
||||||
|
background-position: 0% 50%;
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
background-position: 100% 50%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes float-x {
|
||||||
|
0%,
|
||||||
|
100% {
|
||||||
|
transform: translate3d(0, 0, 0);
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
transform: translate3d(2.5rem, -1.5rem, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes float-y {
|
||||||
|
0%,
|
||||||
|
100% {
|
||||||
|
transform: translate3d(0, 0, 0);
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
transform: translate3d(-1.5rem, 2.5rem, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes float-z {
|
||||||
|
0%,
|
||||||
|
100% {
|
||||||
|
transform: translate3d(0, 0, 0) scale(1);
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
transform: translate3d(1.5rem, 1.5rem, 0) scale(1.08);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.animate-float-x {
|
||||||
|
animation: float-x 16s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
.animate-float-y {
|
||||||
|
animation: float-y 20s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
.animate-float-z {
|
||||||
|
animation: float-z 24s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes ring-trace {
|
||||||
|
0% {
|
||||||
|
stroke-dashoffset: 360;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
stroke-dashoffset: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.animate-ring-trace {
|
||||||
|
animation: ring-trace 2.6s cubic-bezier(0.7, 0, 0.3, 1) both;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Reduce motion: stoppa i drift in sottofondo. */
|
||||||
|
@media (prefers-reduced-motion: reduce) {
|
||||||
|
.animate-float-x,
|
||||||
|
.animate-float-y,
|
||||||
|
.animate-float-z,
|
||||||
|
.text-gradient,
|
||||||
|
.animate-ring-trace {
|
||||||
|
animation: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Reference in new issue
Block a user