Compare commits
21 Commits
5a91d8ebc1
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| ec8812c6e2 | |||
| cdd6f1b103 | |||
| ec9bc7552f | |||
| c6b649ef33 | |||
| 5fab9c95ac | |||
| e168a646a1 | |||
| d93f862cba | |||
| ebec21f1ac | |||
| 539437b824 | |||
| 6549b03d9a | |||
| b77883a16c | |||
| e69e20f753 | |||
| 9186d90aac | |||
| 3bed358990 | |||
| c6143d7d13 | |||
| 4ccfaf7b92 | |||
| 24beb5aa5e | |||
| 0ac37261a8 | |||
| ad070a21a5 | |||
| a5d9eed843 | |||
| d64bb10fa8 |
13
CHANGELOG.md
13
CHANGELOG.md
@@ -6,8 +6,19 @@ All notable changes to the New Optic website will be documented in this file.
|
|||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|
||||||
|
- Added a New York-style display font stack for large latin headings while keeping SF-style UI text.
|
||||||
|
- Switched the latin typography to an SF Pro-style system font stack for a more Apple-like feel.
|
||||||
- Removed the desktop liquid glass WebGL layer from navbar and CTA controls so text remains readable and glass surfaces do not show dark rendering artifacts while scrolling.
|
- Removed the desktop liquid glass WebGL layer from navbar and CTA controls so text remains readable and glass surfaces do not show dark rendering artifacts while scrolling.
|
||||||
- Made the mobile hamburger menu open with springy jelly motion and added a draggable corner grip that stretches the menu before snapping back.
|
- Replaced the mobile hamburger dropdown with a fast, solid menu animation without bounce or stretch effects.
|
||||||
|
- Kept the language switcher visible in the mobile top navigation bar.
|
||||||
|
- Added cursor-reactive stretch motion to desktop nav links and matched the glossy dark WhatsApp CTA style on mobile.
|
||||||
|
- Corrected the French 2004 trust wording to use "depuis" without "autour de".
|
||||||
|
- Tightened mobile sizing for the WhatsApp nav pill and hero trust badge.
|
||||||
|
- Balanced mobile navbar controls so the language switcher, WhatsApp CTA, and menu do not bunch up on the right.
|
||||||
|
- Reduced the mobile gap between the reputation section and contact panel.
|
||||||
|
- Let the mobile WhatsApp nav CTA stretch into available navbar space.
|
||||||
|
- Restored mobile glass blur and premium highlight layers instead of flattening glass surfaces.
|
||||||
|
- Added a dedicated stronger blur treatment for the mobile top navigation bar.
|
||||||
|
|
||||||
## [1.0.0] - 2026-05-16
|
## [1.0.0] - 2026-05-16
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,8 @@
|
|||||||
color-scheme: light;
|
color-scheme: light;
|
||||||
--bg: #f6f5f2;
|
--bg: #f6f5f2;
|
||||||
--ink: #111317;
|
--ink: #111317;
|
||||||
|
--font-sans: "SF Pro Text", "SF Pro Display", -apple-system, BlinkMacSystemFont, "Inter", "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
||||||
|
--font-display: "New York Large", "New York", "NewYork", ui-serif, Georgia, Cambria, "Times New Roman", serif;
|
||||||
}
|
}
|
||||||
|
|
||||||
* {
|
* {
|
||||||
@@ -24,7 +26,7 @@ body {
|
|||||||
url('/assets/New-optic-BG-mobile.webp') center top / cover scroll,
|
url('/assets/New-optic-BG-mobile.webp') center top / cover scroll,
|
||||||
linear-gradient(180deg, #fbfaf8 0%, #f6f5f2 42%, #ffffff 100%);
|
linear-gradient(180deg, #fbfaf8 0%, #f6f5f2 42%, #ffffff 100%);
|
||||||
color: var(--ink);
|
color: var(--ink);
|
||||||
font-family: var(--font-sans), system-ui, sans-serif;
|
font-family: var(--font-sans);
|
||||||
text-rendering: optimizeLegibility;
|
text-rendering: optimizeLegibility;
|
||||||
-webkit-font-smoothing: antialiased;
|
-webkit-font-smoothing: antialiased;
|
||||||
}
|
}
|
||||||
@@ -67,6 +69,10 @@ body[dir="rtl"] {
|
|||||||
font-family: var(--font-arabic), system-ui, sans-serif;
|
font-family: var(--font-arabic), system-ui, sans-serif;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
body:not([dir="rtl"]) :where(h1, h2) {
|
||||||
|
font-family: var(--font-display);
|
||||||
|
}
|
||||||
|
|
||||||
a {
|
a {
|
||||||
color: inherit;
|
color: inherit;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
@@ -80,7 +86,8 @@ a {
|
|||||||
background: rgba(255, 255, 255, 0.68);
|
background: rgba(255, 255, 255, 0.68);
|
||||||
border: 1px solid rgba(255, 255, 255, 0.72);
|
border: 1px solid rgba(255, 255, 255, 0.72);
|
||||||
box-shadow: 0 18px 60px rgba(17, 19, 23, 0.08);
|
box-shadow: 0 18px 60px rgba(17, 19, 23, 0.08);
|
||||||
backdrop-filter: blur(24px);
|
backdrop-filter: blur(24px) saturate(1.22);
|
||||||
|
-webkit-backdrop-filter: blur(24px) saturate(1.22);
|
||||||
}
|
}
|
||||||
|
|
||||||
.premium-glass::before {
|
.premium-glass::before {
|
||||||
@@ -109,6 +116,12 @@ a {
|
|||||||
inset 18px 0 34px rgba(255, 255, 255, 0.12);
|
inset 18px 0 34px rgba(255, 255, 255, 0.12);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.nav-glass {
|
||||||
|
background: rgba(255, 255, 255, 0.62);
|
||||||
|
backdrop-filter: blur(30px) saturate(1.32);
|
||||||
|
-webkit-backdrop-filter: blur(30px) saturate(1.32);
|
||||||
|
}
|
||||||
|
|
||||||
.hairline {
|
.hairline {
|
||||||
background: linear-gradient(90deg, transparent, rgba(17, 19, 23, 0.15), transparent);
|
background: linear-gradient(90deg, transparent, rgba(17, 19, 23, 0.15), transparent);
|
||||||
height: 1px;
|
height: 1px;
|
||||||
@@ -166,25 +179,21 @@ a {
|
|||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.floating-sheen {
|
|
||||||
animation: none;
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.glass,
|
|
||||||
[class*="backdrop-blur"] {
|
|
||||||
backdrop-filter: none !important;
|
|
||||||
-webkit-backdrop-filter: none !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.glass {
|
.glass {
|
||||||
background: rgba(255, 255, 255, 0.86);
|
background: rgba(255, 255, 255, 0.72);
|
||||||
box-shadow: 0 10px 30px rgba(17, 19, 23, 0.07);
|
box-shadow: 0 14px 34px rgba(17, 19, 23, 0.08);
|
||||||
|
backdrop-filter: blur(18px) saturate(1.24);
|
||||||
|
-webkit-backdrop-filter: blur(18px) saturate(1.24);
|
||||||
}
|
}
|
||||||
|
|
||||||
.premium-glass::before,
|
.nav-glass {
|
||||||
.premium-glass::after {
|
background: rgba(255, 255, 255, 0.58);
|
||||||
display: none;
|
border-color: rgba(255, 255, 255, 0.86);
|
||||||
|
box-shadow:
|
||||||
|
0 14px 36px rgba(17, 19, 23, 0.1),
|
||||||
|
inset 0 1px 0 rgba(255, 255, 255, 0.78);
|
||||||
|
backdrop-filter: blur(26px) saturate(1.38);
|
||||||
|
-webkit-backdrop-filter: blur(26px) saturate(1.38);
|
||||||
}
|
}
|
||||||
|
|
||||||
main > section {
|
main > section {
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
import type { Metadata } from "next";
|
import type { Metadata } from "next";
|
||||||
import { Inter, Noto_Kufi_Arabic } from "next/font/google";
|
import { Noto_Kufi_Arabic } from "next/font/google";
|
||||||
import "./globals.css";
|
import "./globals.css";
|
||||||
import { business } from "@/config/business";
|
import { business } from "@/config/business";
|
||||||
import { fr } from "@/messages/fr";
|
import { fr } from "@/messages/fr";
|
||||||
|
|
||||||
const inter = Inter({ subsets: ["latin"], variable: "--font-sans", display: "swap" });
|
|
||||||
const arabic = Noto_Kufi_Arabic({ subsets: ["arabic"], variable: "--font-arabic", display: "swap" });
|
const arabic = Noto_Kufi_Arabic({ subsets: ["arabic"], variable: "--font-arabic", display: "swap" });
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
@@ -28,7 +27,7 @@ export const metadata: Metadata = {
|
|||||||
|
|
||||||
export default function RootLayout({ children }: Readonly<{ children: React.ReactNode }>) {
|
export default function RootLayout({ children }: Readonly<{ children: React.ReactNode }>) {
|
||||||
return (
|
return (
|
||||||
<html lang="fr" className={`${inter.variable} ${arabic.variable} scroll-smooth`}>
|
<html lang="fr" className={`${arabic.variable} scroll-smooth`}>
|
||||||
<body>
|
<body>
|
||||||
{children}
|
{children}
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import PhysicsButton from "./PhysicsButton";
|
|||||||
|
|
||||||
export default function ContactSection({ t, whatsappUrl }: { t: Messages; whatsappUrl: string }) {
|
export default function ContactSection({ t, whatsappUrl }: { t: Messages; whatsappUrl: string }) {
|
||||||
return (
|
return (
|
||||||
<AnimatedSection id="contact" className="px-4 py-20 sm:px-6">
|
<AnimatedSection id="contact" className="px-4 pb-16 pt-6 sm:px-6 sm:py-20">
|
||||||
<div className="mx-auto grid max-w-7xl gap-6 lg:grid-cols-[1.08fr_0.92fr]">
|
<div className="mx-auto grid max-w-7xl gap-6 lg:grid-cols-[1.08fr_0.92fr]">
|
||||||
<div className="rounded-[3rem] bg-white p-8 shadow-glass sm:p-12">
|
<div className="rounded-[3rem] bg-white p-8 shadow-glass sm:p-12">
|
||||||
<p className="mb-4 text-xs font-semibold uppercase tracking-[0.28em] text-optical/75">{t.contact.eyebrow}</p>
|
<p className="mb-4 text-xs font-semibold uppercase tracking-[0.28em] text-optical/75">{t.contact.eyebrow}</p>
|
||||||
|
|||||||
@@ -29,9 +29,9 @@ export default function HeroSection({ t, whatsappUrl }: { t: Messages; whatsappU
|
|||||||
<motion.div className="absolute left-1/2 top-24 hidden h-72 w-72 -translate-x-1/2 rounded-full bg-optical/10 blur-3xl lg:block" animate={canAnimateIntro ? { scale: [1, 1.16, 1], opacity: [0.55, 0.88, 0.55] } : undefined} transition={{ duration: 8, repeat: Infinity, ease: "easeInOut" }} />
|
<motion.div className="absolute left-1/2 top-24 hidden h-72 w-72 -translate-x-1/2 rounded-full bg-optical/10 blur-3xl lg:block" animate={canAnimateIntro ? { scale: [1, 1.16, 1], opacity: [0.55, 0.88, 0.55] } : undefined} transition={{ duration: 8, repeat: Infinity, ease: "easeInOut" }} />
|
||||||
<div className="mx-auto grid max-w-7xl items-center gap-12 lg:grid-cols-[1fr_0.92fr]">
|
<div className="mx-auto grid max-w-7xl items-center gap-12 lg:grid-cols-[1fr_0.92fr]">
|
||||||
<motion.div initial={canAnimateIntro ? { opacity: 0, y: 24 } : false} animate={{ opacity: 1, y: 0 }} transition={{ duration: 0.8, ease: [0.22, 1, 0.36, 1] }} className="relative z-10 text-center lg:text-start">
|
<motion.div initial={canAnimateIntro ? { opacity: 0, y: 24 } : false} animate={{ opacity: 1, y: 0 }} transition={{ duration: 0.8, ease: [0.22, 1, 0.36, 1] }} className="relative z-10 text-center lg:text-start">
|
||||||
<div className="mx-auto mb-6 inline-flex items-center gap-2 rounded-full border border-ink/10 bg-white/65 px-4 py-2 text-xs font-semibold uppercase tracking-[0.18em] text-ink/60 shadow-sm backdrop-blur lg:mx-0">
|
<div className="mx-auto mb-5 inline-flex max-w-full items-center gap-1.5 rounded-full border border-ink/10 bg-white/65 px-3 py-1.5 text-[10px] font-semibold uppercase tracking-[0.12em] text-ink/60 shadow-sm backdrop-blur max-[374px]:px-2.5 max-[374px]:text-[9px] max-[374px]:tracking-[0.08em] sm:mb-6 sm:gap-2 sm:px-4 sm:py-2 sm:text-xs sm:tracking-[0.18em] lg:mx-0">
|
||||||
<ShieldCheck size={15} className="text-optical" />
|
<ShieldCheck className="size-3.5 shrink-0 text-optical sm:size-[15px]" />
|
||||||
{t.hero.eyebrow}
|
<span className="whitespace-nowrap leading-4">{t.hero.eyebrow}</span>
|
||||||
</div>
|
</div>
|
||||||
<h1 className="mx-auto max-w-4xl text-5xl font-semibold tracking-[-0.075em] text-ink sm:text-7xl lg:mx-0 lg:text-8xl">{t.hero.title}</h1>
|
<h1 className="mx-auto max-w-4xl text-5xl font-semibold tracking-[-0.075em] text-ink sm:text-7xl lg:mx-0 lg:text-8xl">{t.hero.title}</h1>
|
||||||
<p className="mx-auto mt-6 max-w-2xl text-lg leading-8 text-ink/64 sm:text-xl lg:mx-0">{t.hero.subtitle}</p>
|
<p className="mx-auto mt-6 max-w-2xl text-lg leading-8 text-ink/64 sm:text-xl lg:mx-0">{t.hero.subtitle}</p>
|
||||||
|
|||||||
@@ -3,9 +3,9 @@
|
|||||||
import { languages, type Locale } from "@/config/business";
|
import { languages, type Locale } from "@/config/business";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
export default function LanguageSwitcher({ locale, onLocaleChange }: { locale: Locale; onLocaleChange: (locale: Locale) => void }) {
|
export default function LanguageSwitcher({ locale, onLocaleChange, className, buttonClassName }: { locale: Locale; onLocaleChange: (locale: Locale) => void; className?: string; buttonClassName?: string }) {
|
||||||
return (
|
return (
|
||||||
<div className="flex rounded-full border border-ink/10 bg-white/65 p-1 shadow-sm backdrop-blur-xl" aria-label="Language switcher">
|
<div className={cn("flex rounded-full border border-ink/10 bg-white/65 p-1 shadow-sm backdrop-blur-xl", className)} aria-label="Language switcher">
|
||||||
{languages.map((language) => (
|
{languages.map((language) => (
|
||||||
<button
|
<button
|
||||||
key={language.code}
|
key={language.code}
|
||||||
@@ -13,6 +13,7 @@ export default function LanguageSwitcher({ locale, onLocaleChange }: { locale: L
|
|||||||
onClick={() => onLocaleChange(language.code)}
|
onClick={() => onLocaleChange(language.code)}
|
||||||
className={cn(
|
className={cn(
|
||||||
"rounded-full px-3 py-1.5 text-xs font-semibold transition-all duration-300",
|
"rounded-full px-3 py-1.5 text-xs font-semibold transition-all duration-300",
|
||||||
|
buttonClassName,
|
||||||
locale === language.code ? "bg-ink text-white shadow-sm" : "text-ink/55 hover:text-ink"
|
locale === language.code ? "bg-ink text-white shadow-sm" : "text-ink/55 hover:text-ink"
|
||||||
)}
|
)}
|
||||||
aria-pressed={locale === language.code}
|
aria-pressed={locale === language.code}
|
||||||
|
|||||||
@@ -1,32 +1,19 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { AnimatePresence, motion, useMotionValue, useSpring, useTransform } from "framer-motion";
|
import { AnimatePresence, motion, useMotionValue, useSpring, useTransform } from "framer-motion";
|
||||||
import { Grip, Menu, X } from "lucide-react";
|
import { Menu, X } from "lucide-react";
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
import { type MouseEvent, useState } from "react";
|
import { type MouseEvent, type PointerEvent, useState } from "react";
|
||||||
import { business, type Locale } from "@/config/business";
|
import { business, type Locale } from "@/config/business";
|
||||||
import type { Messages } from "@/messages";
|
import type { Messages } from "@/messages";
|
||||||
import LanguageSwitcher from "./LanguageSwitcher";
|
import LanguageSwitcher from "./LanguageSwitcher";
|
||||||
import PhysicsButton from "./PhysicsButton";
|
import PhysicsButton from "./PhysicsButton";
|
||||||
|
|
||||||
|
const navCtaClassName =
|
||||||
|
"rounded-full border border-white/45 bg-[linear-gradient(180deg,#565b63_0%,#20242a_48%,#111317_100%)] font-semibold text-white shadow-[inset_0_1px_0_rgba(255,255,255,0.38),inset_0_-10px_18px_rgba(0,0,0,0.18),0_14px_32px_rgba(17,19,23,0.18)] transition-colors hover:bg-[linear-gradient(180deg,#476a95_0%,#264b73_54%,#173655_100%)]";
|
||||||
|
|
||||||
export default function Navbar({ locale, onLocaleChange, t, whatsappUrl }: { locale: Locale; onLocaleChange: (locale: Locale) => void; t: Messages; whatsappUrl: string }) {
|
export default function Navbar({ locale, onLocaleChange, t, whatsappUrl }: { locale: Locale; onLocaleChange: (locale: Locale) => void; t: Messages; whatsappUrl: string }) {
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
const stretchX = useMotionValue(0);
|
|
||||||
const stretchY = useMotionValue(0);
|
|
||||||
const springX = useSpring(stretchX, { stiffness: 260, damping: 13, mass: 0.34 });
|
|
||||||
const springY = useSpring(stretchY, { stiffness: 260, damping: 13, mass: 0.34 });
|
|
||||||
const menuScaleX = useTransform(springX, [-70, 0, 96], [0.9, 1, 1.22]);
|
|
||||||
const menuScaleY = useTransform(springY, [-54, 0, 104], [0.88, 1, 1.18]);
|
|
||||||
const menuSkewX = useTransform(springX, [-70, 0, 96], [-5.5, 0, 7]);
|
|
||||||
const menuRadius = useTransform(springY, [-54, 0, 104], ["2.65rem", "2rem", "2.9rem"]);
|
|
||||||
const sheenX = useTransform(springX, [-70, 0, 96], ["-18%", "0%", "20%"]);
|
|
||||||
const cornerX = useSpring(stretchX, { stiffness: 520, damping: 20, mass: 0.24 });
|
|
||||||
const cornerY = useSpring(stretchY, { stiffness: 520, damping: 20, mass: 0.24 });
|
|
||||||
|
|
||||||
function releaseMenuStretch() {
|
|
||||||
stretchX.set(0);
|
|
||||||
stretchY.set(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
function scrollToSection(event: MouseEvent<HTMLAnchorElement>, href: string) {
|
function scrollToSection(event: MouseEvent<HTMLAnchorElement>, href: string) {
|
||||||
if (!href.startsWith("#")) return;
|
if (!href.startsWith("#")) return;
|
||||||
@@ -39,10 +26,10 @@ export default function Navbar({ locale, onLocaleChange, t, whatsappUrl }: { loc
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<header className="fixed inset-x-0 top-0 z-50 px-4 pt-4 sm:px-6">
|
<header className="fixed inset-x-0 top-0 z-50 px-4 pt-4 max-[374px]:px-2.5 sm:px-6">
|
||||||
<nav className="glass premium-glass mx-auto flex max-w-7xl items-center justify-between gap-2 rounded-full px-3 py-3 sm:px-5" aria-label="Main navigation">
|
<nav className="glass premium-glass nav-glass relative mx-auto flex max-w-7xl items-center justify-start gap-2 overflow-hidden rounded-full px-3 py-3 max-[374px]:px-2 max-[374px]:py-2.5 md:justify-between sm:px-5" aria-label="Main navigation">
|
||||||
<a href="#home" onClick={(event) => scrollToSection(event, "#home")} className="relative z-[3] flex items-center gap-3" aria-label="New Optic home">
|
<a href="#home" onClick={(event) => scrollToSection(event, "#home")} className="relative z-[3] flex shrink-0 items-center gap-3" aria-label="New Optic home">
|
||||||
<span className="relative grid size-10 place-items-center overflow-hidden rounded-full bg-white shadow-sm">
|
<span className="relative grid size-10 place-items-center overflow-hidden rounded-full bg-white shadow-sm max-[374px]:size-9">
|
||||||
<Image src={business.assets.logo} alt="New Optic logo" fill sizes="40px" className="object-contain p-1" priority />
|
<Image src={business.assets.logo} alt="New Optic logo" fill sizes="40px" className="object-contain p-1" priority />
|
||||||
</span>
|
</span>
|
||||||
<span className="hidden text-sm font-semibold tracking-[-0.02em] xs:inline sm:inline">{business.name}</span>
|
<span className="hidden text-sm font-semibold tracking-[-0.02em] xs:inline sm:inline">{business.name}</span>
|
||||||
@@ -50,29 +37,28 @@ export default function Navbar({ locale, onLocaleChange, t, whatsappUrl }: { loc
|
|||||||
|
|
||||||
<div className="relative z-[3] hidden items-center gap-7 lg:flex">
|
<div className="relative z-[3] hidden items-center gap-7 lg:flex">
|
||||||
{t.nav.links.map((link) => (
|
{t.nav.links.map((link) => (
|
||||||
<a key={link.href} href={link.href} onClick={(event) => scrollToSection(event, link.href)} className="text-sm font-medium text-ink/58 transition hover:text-ink">
|
<StretchNavLink key={link.href} href={link.href} label={link.label} onClick={(event) => scrollToSection(event, link.href)} />
|
||||||
{link.label}
|
|
||||||
</a>
|
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="relative z-[3] hidden items-center gap-3 md:flex">
|
<div className="relative z-[3] hidden items-center gap-3 md:flex">
|
||||||
<LanguageSwitcher locale={locale} onLocaleChange={onLocaleChange} />
|
<LanguageSwitcher locale={locale} onLocaleChange={onLocaleChange} />
|
||||||
<PhysicsButton href={whatsappUrl} external className="rounded-full bg-ink px-5 py-2.5 text-sm font-semibold text-white shadow-soft transition-colors hover:bg-optical">
|
<PhysicsButton href={whatsappUrl} external className={`${navCtaClassName} px-5 py-2.5`}>
|
||||||
{t.nav.cta}
|
{t.nav.cta}
|
||||||
</PhysicsButton>
|
</PhysicsButton>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="relative z-[3] flex items-center gap-2 md:hidden">
|
<div className="relative z-[3] flex min-w-0 flex-1 items-center gap-1.5 max-[374px]:gap-0.5 sm:gap-2 md:hidden">
|
||||||
<PhysicsButton href={whatsappUrl} external className="rounded-full bg-ink px-4 py-2.5 text-xs font-semibold text-white shadow-soft transition-colors hover:bg-optical sm:px-5 sm:text-sm">
|
<LanguageSwitcher locale={locale} onLocaleChange={onLocaleChange} className="w-[132px] shrink-0 bg-white/72 backdrop-blur-xl max-[374px]:w-[106px] max-[374px]:p-0.5" buttonClassName="flex-1 px-0.5 max-[374px]:px-0 max-[374px]:py-1.5 sm:px-3" />
|
||||||
|
<PhysicsButton href={whatsappUrl} external className={`${navCtaClassName} min-w-0 flex-1 px-2.5 py-2 text-[11px] max-[374px]:px-1.5 max-[374px]:py-1.5 max-[374px]:text-[9px] sm:px-5 sm:py-2.5 sm:text-sm`}>
|
||||||
{t.nav.cta}
|
{t.nav.cta}
|
||||||
</PhysicsButton>
|
</PhysicsButton>
|
||||||
<motion.button
|
<motion.button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => setOpen((value) => !value)}
|
onClick={() => setOpen((value) => !value)}
|
||||||
whileTap={{ scaleX: 1.22, scaleY: 0.82 }}
|
whileTap={{ scale: 0.94 }}
|
||||||
transition={{ type: "spring", stiffness: 520, damping: 14, mass: 0.32 }}
|
transition={{ duration: 0.08 }}
|
||||||
className="grid size-11 place-items-center rounded-full bg-white/70 text-ink outline-none transition focus-visible:ring-2 focus-visible:ring-optical/45 focus-visible:ring-offset-2"
|
className="grid size-11 shrink-0 place-items-center rounded-full bg-white/70 text-ink outline-none transition max-[374px]:size-9 focus-visible:ring-2 focus-visible:ring-optical/45 focus-visible:ring-offset-2"
|
||||||
aria-label={t.nav.menu}
|
aria-label={t.nav.menu}
|
||||||
aria-expanded={open}
|
aria-expanded={open}
|
||||||
>
|
>
|
||||||
@@ -84,67 +70,70 @@ export default function Navbar({ locale, onLocaleChange, t, whatsappUrl }: { loc
|
|||||||
<AnimatePresence>
|
<AnimatePresence>
|
||||||
{open ? (
|
{open ? (
|
||||||
<motion.div
|
<motion.div
|
||||||
initial={{ opacity: 0, y: -28, scaleX: 0.18, scaleY: 0.08, skewX: locale === "ar" ? -10 : 10, borderRadius: "999px" }}
|
initial={{ opacity: 0, y: -8, scale: 0.985 }}
|
||||||
animate={{ opacity: 1, y: 0, scaleX: [0.18, 1.16, 0.96, 1], scaleY: [0.08, 1.2, 0.94, 1], skewX: [locale === "ar" ? -10 : 10, locale === "ar" ? 5 : -5, 0], borderRadius: ["999px", "2.85rem", "1.65rem", "2rem"] }}
|
animate={{ opacity: 1, y: 0, scale: 1 }}
|
||||||
exit={{ opacity: 0, y: -18, scaleX: 0.38, scaleY: 0.14, skewX: locale === "ar" ? -7 : 7, borderRadius: "999px" }}
|
exit={{ opacity: 0, y: -6, scale: 0.99 }}
|
||||||
transition={{ duration: 0.42, ease: [0.16, 1, 0.3, 1] }}
|
transition={{ duration: 0.13, ease: "easeOut" }}
|
||||||
style={{ transformOrigin: locale === "ar" ? "top left" : "top right" }}
|
className="relative mx-auto mt-3 max-w-7xl overflow-hidden rounded-[2rem] border border-ink/8 bg-white p-4 shadow-glass md:hidden"
|
||||||
className="mx-auto mt-3 max-w-7xl md:hidden"
|
|
||||||
>
|
>
|
||||||
<motion.div
|
<div className="grid gap-2">
|
||||||
style={{ scaleX: menuScaleX, scaleY: menuScaleY, skewX: menuSkewX, borderRadius: menuRadius, transformOrigin: locale === "ar" ? "bottom left" : "bottom right" }}
|
|
||||||
onPointerLeave={releaseMenuStretch}
|
|
||||||
className="relative w-full overflow-hidden border border-ink/8 bg-white p-4 pb-10 shadow-glass"
|
|
||||||
>
|
|
||||||
<motion.div
|
|
||||||
aria-hidden
|
|
||||||
className="pointer-events-none absolute -inset-x-10 top-1 h-10 rounded-full bg-silver/35 blur-xl"
|
|
||||||
initial={{ opacity: 0, scaleX: 0.4 }}
|
|
||||||
animate={{ opacity: [0.22, 0.9, 0.38], scaleX: [0.3, 1.28, 0.88], x: ["-20%", "12%", "0%"] }}
|
|
||||||
transition={{ duration: 0.38, ease: [0.22, 1, 0.36, 1] }}
|
|
||||||
/>
|
|
||||||
<motion.div aria-hidden className="pointer-events-none absolute bottom-0 right-0 h-28 w-28 rounded-full bg-optical/8 blur-2xl rtl:left-0 rtl:right-auto" style={{ x: sheenX, y: springY }} />
|
|
||||||
<div className="relative z-[2] grid gap-2">
|
|
||||||
{t.nav.links.map((link) => (
|
{t.nav.links.map((link) => (
|
||||||
<motion.a
|
<motion.a
|
||||||
key={link.href}
|
key={link.href}
|
||||||
href={link.href}
|
href={link.href}
|
||||||
onClick={(event) => scrollToSection(event, link.href)}
|
onClick={(event) => scrollToSection(event, link.href)}
|
||||||
initial={{ opacity: 0, x: locale === "ar" ? -28 : 28, scaleX: 0.86 }}
|
initial={{ opacity: 0, x: locale === "ar" ? -8 : 8 }}
|
||||||
animate={{ opacity: 1, x: 0, scaleX: 1 }}
|
animate={{ opacity: 1, x: 0 }}
|
||||||
transition={{ type: "spring", stiffness: 680, damping: 22, mass: 0.26 }}
|
transition={{ duration: 0.1, ease: "easeOut" }}
|
||||||
whileTap={{ scaleX: 1.12, scaleY: 0.86 }}
|
whileTap={{ scale: 0.98 }}
|
||||||
className="rounded-2xl px-4 py-3 text-sm font-semibold text-ink/72 hover:bg-white"
|
className="rounded-2xl px-4 py-3 text-sm font-semibold text-ink/72 hover:bg-smoke"
|
||||||
>
|
>
|
||||||
{link.label}
|
{link.label}
|
||||||
</motion.a>
|
</motion.a>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
<div className="relative z-[2] mt-4 flex items-center justify-between gap-3">
|
|
||||||
<LanguageSwitcher locale={locale} onLocaleChange={onLocaleChange} />
|
|
||||||
</div>
|
|
||||||
<motion.button
|
|
||||||
type="button"
|
|
||||||
aria-label={`${t.nav.menu} grip`}
|
|
||||||
drag
|
|
||||||
dragSnapToOrigin
|
|
||||||
dragConstraints={{ left: -56, right: 72, top: -46, bottom: 82 }}
|
|
||||||
dragElastic={0.94}
|
|
||||||
onDrag={(_, info) => {
|
|
||||||
stretchX.set(Math.max(-70, Math.min(96, info.offset.x)));
|
|
||||||
stretchY.set(Math.max(-54, Math.min(104, info.offset.y)));
|
|
||||||
}}
|
|
||||||
onDragEnd={releaseMenuStretch}
|
|
||||||
onPointerUp={releaseMenuStretch}
|
|
||||||
style={{ x: cornerX, y: cornerY }}
|
|
||||||
className="absolute bottom-3 right-3 z-[3] grid size-9 touch-none cursor-grab place-items-center rounded-full border border-ink/10 bg-white/80 text-ink/55 shadow-sm outline-none transition focus-visible:ring-2 focus-visible:ring-optical/35 active:cursor-grabbing rtl:left-3 rtl:right-auto"
|
|
||||||
>
|
|
||||||
<Grip size={15} />
|
|
||||||
</motion.button>
|
|
||||||
</motion.div>
|
|
||||||
</motion.div>
|
</motion.div>
|
||||||
) : null}
|
) : null}
|
||||||
</AnimatePresence>
|
</AnimatePresence>
|
||||||
</header>
|
</header>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function StretchNavLink({ href, label, onClick }: { href: string; label: string; onClick: (event: MouseEvent<HTMLAnchorElement>) => void }) {
|
||||||
|
const pullX = useMotionValue(0);
|
||||||
|
const pullY = useMotionValue(0);
|
||||||
|
const x = useSpring(pullX, { stiffness: 260, damping: 16, mass: 0.3 });
|
||||||
|
const y = useSpring(pullY, { stiffness: 260, damping: 16, mass: 0.3 });
|
||||||
|
const scaleX = useTransform(x, [-18, 0, 18], [1.12, 1, 1.12]);
|
||||||
|
const scaleY = useTransform(y, [-12, 0, 12], [0.94, 1, 0.94]);
|
||||||
|
|
||||||
|
function handlePointerMove(event: PointerEvent<HTMLAnchorElement>) {
|
||||||
|
if (event.pointerType === "touch") return;
|
||||||
|
|
||||||
|
const bounds = event.currentTarget.getBoundingClientRect();
|
||||||
|
const localX = event.clientX - bounds.left - bounds.width / 2;
|
||||||
|
const localY = event.clientY - bounds.top - bounds.height / 2;
|
||||||
|
|
||||||
|
pullX.set(Math.max(-18, Math.min(18, localX * 0.42)));
|
||||||
|
pullY.set(Math.max(-12, Math.min(12, localY * 0.38)));
|
||||||
|
}
|
||||||
|
|
||||||
|
function release() {
|
||||||
|
pullX.set(0);
|
||||||
|
pullY.set(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<motion.a
|
||||||
|
href={href}
|
||||||
|
onClick={onClick}
|
||||||
|
onPointerMove={handlePointerMove}
|
||||||
|
onPointerLeave={release}
|
||||||
|
onPointerCancel={release}
|
||||||
|
style={{ x, y, scaleX, scaleY }}
|
||||||
|
className="inline-flex min-h-9 items-center rounded-full px-1 text-sm font-medium text-ink/58 will-change-transform transition-colors hover:text-ink"
|
||||||
|
>
|
||||||
|
{label}
|
||||||
|
</motion.a>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import SectionHeader from "./SectionHeader";
|
|||||||
|
|
||||||
export default function TrustSection({ t }: { t: Messages }) {
|
export default function TrustSection({ t }: { t: Messages }) {
|
||||||
return (
|
return (
|
||||||
<AnimatedSection id="trust" className="px-4 py-20 sm:px-6">
|
<AnimatedSection id="trust" className="px-4 pb-8 pt-16 sm:px-6 sm:py-20">
|
||||||
<div className="mx-auto max-w-7xl overflow-hidden rounded-[3rem] bg-ink p-8 text-white shadow-glass sm:p-12 lg:p-16">
|
<div className="mx-auto max-w-7xl overflow-hidden rounded-[3rem] bg-ink p-8 text-white shadow-glass sm:p-12 lg:p-16">
|
||||||
<SectionHeader eyebrow={t.trust.eyebrow} title={t.trust.title} body={t.trust.body} tone="dark" />
|
<SectionHeader eyebrow={t.trust.eyebrow} title={t.trust.title} body={t.trust.body} tone="dark" />
|
||||||
<motion.div className="mt-12 grid gap-4 md:grid-cols-3" initial={false} whileInView="show" viewport={{ once: true, amount: 0.08 }} variants={{ show: { transition: { staggerChildren: 0.12 } } }}>
|
<motion.div className="mt-12 grid gap-4 md:grid-cols-3" initial={false} whileInView="show" viewport={{ once: true, amount: 0.08 }} variants={{ show: { transition: { staggerChildren: 0.12 } } }}>
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ export const fr = {
|
|||||||
about: {
|
about: {
|
||||||
eyebrow: "Une adresse locale reconnue",
|
eyebrow: "Une adresse locale reconnue",
|
||||||
title: "Un service optique simple, professionnel et fiable.",
|
title: "Un service optique simple, professionnel et fiable.",
|
||||||
body: "Depuis autour de 2004, New Optic accompagne les clients de Temara avec une approche claire: bien conseiller, proposer des montures authentiques, garder des prix justes et assurer le suivi apres l'achat.",
|
body: "Depuis 2004, New Optic accompagne les clients de Temara avec une approche claire: bien conseiller, proposer des montures authentiques, garder des prix justes et assurer le suivi apres l'achat.",
|
||||||
cards: ["Reputation locale solide", "Montures originales uniquement", "Conseil de la monture a la garantie"]
|
cards: ["Reputation locale solide", "Montures originales uniquement", "Conseil de la monture a la garantie"]
|
||||||
},
|
},
|
||||||
services: {
|
services: {
|
||||||
@@ -68,7 +68,7 @@ export const fr = {
|
|||||||
eyebrow: "Reputation",
|
eyebrow: "Reputation",
|
||||||
title: "Un opticien de quartier avec une exigence durable.",
|
title: "Un opticien de quartier avec une exigence durable.",
|
||||||
stats: [
|
stats: [
|
||||||
{ value: "2004", label: "Présence locale depuis autour de" },
|
{ value: "2004", label: "Présence locale depuis" },
|
||||||
{ value: "100%", label: "Montures originales" },
|
{ value: "100%", label: "Montures originales" },
|
||||||
{ value: "360°", label: "Conseil, verres, montures et suivi" }
|
{ value: "360°", label: "Conseil, verres, montures et suivi" }
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -11,7 +11,8 @@ const config: Config = {
|
|||||||
optical: "#315f8f"
|
optical: "#315f8f"
|
||||||
},
|
},
|
||||||
fontFamily: {
|
fontFamily: {
|
||||||
sans: ["var(--font-sans)", "Inter", "system-ui", "sans-serif"],
|
sans: ["var(--font-sans)"],
|
||||||
|
display: ["var(--font-display)"],
|
||||||
arabic: ["var(--font-arabic)", "system-ui", "sans-serif"]
|
arabic: ["var(--font-arabic)", "system-ui", "sans-serif"]
|
||||||
},
|
},
|
||||||
boxShadow: {
|
boxShadow: {
|
||||||
|
|||||||
Reference in New Issue
Block a user