diff --git a/app/(marketing)/page.tsx b/app/(marketing)/page.tsx index 2a15724..4be91a2 100644 --- a/app/(marketing)/page.tsx +++ b/app/(marketing)/page.tsx @@ -49,6 +49,7 @@ export default function Page() { description: t("pages.home.heroBanner.description"), ctaText: t("pages.home.heroBanner.ctaText"), ctaHref: t("pages.home.heroBanner.ctaHref"), + imageAlt: t("heroBanner.imageAlt"), }; const cardStepsData = { diff --git a/app/components/cards/CaseStudy/CaseStudy.view.tsx b/app/components/cards/CaseStudy/CaseStudy.view.tsx index f90db1f..f9ea5b7 100644 --- a/app/components/cards/CaseStudy/CaseStudy.view.tsx +++ b/app/components/cards/CaseStudy/CaseStudy.view.tsx @@ -45,19 +45,22 @@ function CaseStudyView({ return (
{visual ? (
{visual}
) : ( - +
+ +
)}
); diff --git a/app/components/sections/HeroBanner/HeroBanner.tsx b/app/components/sections/HeroBanner/HeroBanner.tsx index d8c8779..5d5d47f 100644 --- a/app/components/sections/HeroBanner/HeroBanner.tsx +++ b/app/components/sections/HeroBanner/HeroBanner.tsx @@ -1,12 +1,9 @@ -"use client"; - /** * Figma: "Sections / Hero" (see registry) */ import { memo } from "react"; import Image from "next/image"; -import { useTranslation } from "../../../contexts/MessagesContext"; import ContentLockup from "../../type/ContentLockup"; import HeroDecor from "./HeroDecor"; import { ASSETS, getAssetPath } from "../../../../lib/assetUtils"; @@ -25,13 +22,18 @@ interface HeroBannerProps { description?: string; ctaText?: string; ctaHref?: string; + imageAlt?: string; } const HeroBanner = memo( - ({ title, subtitle, description, ctaText, ctaHref }) => { - const t = useTranslation(); - const imageAlt = t("heroBanner.imageAlt"); - + ({ + title, + subtitle, + description, + ctaText, + ctaHref, + imageAlt = "Hero illustration", + }) => { return (
@@ -58,7 +60,7 @@ const HeroBanner = memo(
{/* Hero Image Container */} -
+
{imageAlt}( height={HERO_IMAGE_HEIGHT} priority sizes="(min-width: 768px) 50vw, 100vw" - className="w-full h-auto" + className="size-full object-contain" />
diff --git a/app/components/sections/HeroBanner/HeroDecor.tsx b/app/components/sections/HeroBanner/HeroDecor.tsx index 1481772..075ff4a 100644 --- a/app/components/sections/HeroBanner/HeroDecor.tsx +++ b/app/components/sections/HeroBanner/HeroDecor.tsx @@ -1,12 +1,23 @@ "use client"; -import { memo } from "react"; +import { memo, useEffect, useState } from "react"; interface HeroDecorProps { className?: string; } const HeroDecor = memo(({ className = "" }) => { + const [grainEnabled, setGrainEnabled] = useState(false); + + useEffect(() => { + // feTurbulence forces tiled rasterization that reads as top-down segments on + // first paint. Flat shapes render immediately; grain applies after paint. + const frame = requestAnimationFrame(() => { + setGrainEnabled(true); + }); + return () => cancelAnimationFrame(frame); + }, []); + return ( (({ className = "" }) => { {/* apply filter only to the decoration paths */} - + diff --git a/next.config.mjs b/next.config.mjs index a0a113f..510c153 100644 --- a/next.config.mjs +++ b/next.config.mjs @@ -1,6 +1,23 @@ import createMDX from "@next/mdx"; /* eslint-env node */ + +/** Keep viewBox so inline SVGR art can scale/center like `object-contain`. */ +const svgrLoaderOptions = { + svgoConfig: { + plugins: [ + { + name: "preset-default", + params: { + overrides: { + removeViewBox: false, + }, + }, + }, + ], + }, +}; + /** @type {import('next').NextConfig} */ const nextConfig = { output: "standalone", @@ -22,7 +39,12 @@ const nextConfig = { rules: { "*.svg": { condition: { not: "foreign" }, - loaders: ["@svgr/webpack"], + loaders: [ + { + loader: "@svgr/webpack", + options: svgrLoaderOptions, + }, + ], as: "*.js", }, }, @@ -104,7 +126,7 @@ const nextConfig = { config.module.rules.push({ test: /\.svg$/, issuer: /\.[jt]sx?$/, - use: ["@svgr/webpack"], + use: [{ loader: "@svgr/webpack", options: svgrLoaderOptions }], }); // Bundle analysis - only in production builds