4 Commits

Author SHA1 Message Date
539437b824 Use New York style display headings 2026-05-16 22:15:29 +01:00
6549b03d9a Use SF Pro style font stack 2026-05-16 21:52:38 +01:00
0ac37261a8 Correct French 2004 wording 2026-05-16 15:11:33 +01:00
ad070a21a5 Add responsive nav link stretch 2026-05-16 15:08:06 +01:00
6 changed files with 64 additions and 14 deletions

View File

@@ -6,9 +6,13 @@ All notable changes to the New Optic website will be documented in this file.
### 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.
- 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".
## [1.0.0] - 2026-05-16

View File

@@ -6,6 +6,8 @@
color-scheme: light;
--bg: #f6f5f2;
--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,
linear-gradient(180deg, #fbfaf8 0%, #f6f5f2 42%, #ffffff 100%);
color: var(--ink);
font-family: var(--font-sans), system-ui, sans-serif;
font-family: var(--font-sans);
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
}
@@ -67,6 +69,10 @@ body[dir="rtl"] {
font-family: var(--font-arabic), system-ui, sans-serif;
}
body:not([dir="rtl"]) :where(h1, h2) {
font-family: var(--font-display);
}
a {
color: inherit;
text-decoration: none;

View File

@@ -1,10 +1,9 @@
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 { business } from "@/config/business";
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" });
export const metadata: Metadata = {
@@ -28,7 +27,7 @@ export const metadata: Metadata = {
export default function RootLayout({ children }: Readonly<{ children: React.ReactNode }>) {
return (
<html lang="fr" className={`${inter.variable} ${arabic.variable} scroll-smooth`}>
<html lang="fr" className={`${arabic.variable} scroll-smooth`}>
<body>
{children}
</body>

View File

@@ -1,14 +1,17 @@
"use client";
import { AnimatePresence, motion } from "framer-motion";
import { AnimatePresence, motion, useMotionValue, useSpring, useTransform } from "framer-motion";
import { Menu, X } from "lucide-react";
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 type { Messages } from "@/messages";
import LanguageSwitcher from "./LanguageSwitcher";
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 }) {
const [open, setOpen] = useState(false);
@@ -34,22 +37,20 @@ export default function Navbar({ locale, onLocaleChange, t, whatsappUrl }: { loc
<div className="relative z-[3] hidden items-center gap-7 lg:flex">
{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">
{link.label}
</a>
<StretchNavLink key={link.href} href={link.href} label={link.label} onClick={(event) => scrollToSection(event, link.href)} />
))}
</div>
<div className="relative z-[3] hidden items-center gap-3 md:flex">
<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}
</PhysicsButton>
</div>
<div className="relative z-[3] flex items-center gap-2 md:hidden">
<LanguageSwitcher locale={locale} onLocaleChange={onLocaleChange} className="bg-white/80 backdrop-blur-none" buttonClassName="px-2 sm:px-3" />
<PhysicsButton href={whatsappUrl} external className="rounded-full bg-ink px-3 py-2.5 text-xs font-semibold text-white shadow-soft transition-colors hover:bg-optical max-[374px]:hidden sm:px-5 sm:text-sm">
<PhysicsButton href={whatsappUrl} external className={`${navCtaClassName} px-3 py-2.5 text-xs max-[374px]:hidden sm:px-5 sm:text-sm`}>
{t.nav.cta}
</PhysicsButton>
<motion.button
@@ -97,3 +98,42 @@ export default function Navbar({ locale, onLocaleChange, t, whatsappUrl }: { loc
</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>
);
}

View File

@@ -26,7 +26,7 @@ export const fr = {
about: {
eyebrow: "Une adresse locale reconnue",
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"]
},
services: {
@@ -68,7 +68,7 @@ export const fr = {
eyebrow: "Reputation",
title: "Un opticien de quartier avec une exigence durable.",
stats: [
{ value: "2004", label: "Présence locale depuis autour de" },
{ value: "2004", label: "Présence locale depuis" },
{ value: "100%", label: "Montures originales" },
{ value: "360°", label: "Conseil, verres, montures et suivi" }
],

View File

@@ -11,7 +11,8 @@ const config: Config = {
optical: "#315f8f"
},
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"]
},
boxShadow: {