Backend / staging cleanup, performance substrate, and create-flow polish #60
@@ -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 = {
|
||||
|
||||
@@ -45,19 +45,22 @@ function CaseStudyView({
|
||||
return (
|
||||
<div
|
||||
data-figma-node="21993-32352"
|
||||
className={`relative flex h-[305px] w-[305px] shrink-0 overflow-hidden ${CASE_TILE_RADIUS_CLASS} ${SURFACE_CLASS[surface]} ${className}`.trim()}
|
||||
className={`relative h-[305px] w-[305px] shrink-0 overflow-hidden ${CASE_TILE_RADIUS_CLASS} ${SURFACE_CLASS[surface]} ${className}`.trim()}
|
||||
>
|
||||
{visual ? (
|
||||
<div className="flex size-full items-center justify-center p-2">{visual}</div>
|
||||
) : (
|
||||
<Art
|
||||
role="img"
|
||||
aria-label={imageAlt}
|
||||
data-case-study-art={SURFACE_ART_DATA_KEY[surface]}
|
||||
width={305}
|
||||
height={305}
|
||||
className="pointer-events-none size-full select-none object-contain object-center"
|
||||
/>
|
||||
<div className="absolute inset-0">
|
||||
<Art
|
||||
role="img"
|
||||
aria-label={imageAlt}
|
||||
data-case-study-art={SURFACE_ART_DATA_KEY[surface]}
|
||||
width="100%"
|
||||
height="100%"
|
||||
className="pointer-events-none block select-none"
|
||||
preserveAspectRatio="xMidYMid meet"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -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<HeroBannerProps>(
|
||||
({ title, subtitle, description, ctaText, ctaHref }) => {
|
||||
const t = useTranslation();
|
||||
const imageAlt = t("heroBanner.imageAlt");
|
||||
|
||||
({
|
||||
title,
|
||||
subtitle,
|
||||
description,
|
||||
ctaText,
|
||||
ctaHref,
|
||||
imageAlt = "Hero illustration",
|
||||
}) => {
|
||||
return (
|
||||
<section className="bg-transparent px-[var(--spacing-scale-008)] sm:px-[var(--spacing-scale-010)] md:px-[var(--spacing-scale-016)] lg:px-[var(--spacing-scale-024)] xl:px-[var(--spacing-scale-048)]">
|
||||
<div className="flex flex-col gap-[var(--spacing-scale-010)]">
|
||||
@@ -58,7 +60,7 @@ const HeroBanner = memo<HeroBannerProps>(
|
||||
</div>
|
||||
|
||||
{/* Hero Image Container */}
|
||||
<div className="w-full h-full md:flex-1 rounded-[8px] overflow-hidden relative z-10 flex items-center justify-center">
|
||||
<div className="relative z-10 flex w-full items-center justify-center overflow-hidden rounded-[8px] aspect-[16/10] md:flex-1">
|
||||
<Image
|
||||
src={getAssetPath(ASSETS.HERO_IMAGE)}
|
||||
alt={imageAlt}
|
||||
@@ -66,7 +68,7 @@ const HeroBanner = memo<HeroBannerProps>(
|
||||
height={HERO_IMAGE_HEIGHT}
|
||||
priority
|
||||
sizes="(min-width: 768px) 50vw, 100vw"
|
||||
className="w-full h-auto"
|
||||
className="size-full object-contain"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,12 +1,23 @@
|
||||
"use client";
|
||||
|
||||
import { memo } from "react";
|
||||
import { memo, useEffect, useState } from "react";
|
||||
|
||||
interface HeroDecorProps {
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const HeroDecor = memo<HeroDecorProps>(({ 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 (
|
||||
<svg
|
||||
className={`text-[var(--color-surface-default-brand-lighter-accent)] opacity-50 ${className}`}
|
||||
@@ -59,7 +70,7 @@ const HeroDecor = memo<HeroDecorProps>(({ className = "" }) => {
|
||||
</defs>
|
||||
|
||||
{/* apply filter only to the decoration paths */}
|
||||
<g fill="currentColor" filter="url(#grain)">
|
||||
<g fill="currentColor" filter={grainEnabled ? "url(#grain)" : undefined}>
|
||||
<path d="M1441.54 226.758C1495.92 226.758 1540 320.385 1540 435.879C1540 551.373 1495.92 645 1441.54 645C1387.16 645 1343.08 551.373 1343.08 435.879C1343.08 320.385 1387.16 226.758 1441.54 226.758Z" />
|
||||
<path d="M1441.54 226.758C1495.92 226.758 1540 320.385 1540 435.879C1540 551.373 1495.92 645 1441.54 645C1387.16 645 1343.08 551.373 1343.08 435.879C1343.08 320.385 1387.16 226.758 1441.54 226.758Z" />
|
||||
<path d="M674.066 209.121C728.443 209.121 772.525 302.748 772.525 418.242C772.525 533.737 728.443 627.363 674.066 627.363C619.688 627.363 575.607 533.737 575.607 418.242C575.607 302.748 619.688 209.121 674.066 209.121Z" />
|
||||
|
||||
+24
-2
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user