Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 44 additions & 0 deletions docs/hero-offering-debug-log.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# Hero Offering Debug Log

## Context
We are tracking ongoing attempts to prevent the HeroOffering section from stretching edge-to-edge on narrow devices while maintaining SSR/CSR markup parity and respecting the landing page container system.

## Attempt History

### Attempt 1 — Remove `useIsMobile` hook (Commit: remove mobile hook)
- **Change:** Replaced the runtime `useIsMobile` check with Tailwind responsive spacing classes.
- **Result:** Hydration mismatch resolved, but hero still expanded to the full viewport width on mobile.

### Attempt 2 — Responsive padding refinements
- **Change:** Added explicit responsive padding/min-height Tailwind utilities to align SSR and CSR markup.
- **Result:** Layout remained stretched on narrow screens; padding adjustments alone were insufficient.

### Attempt 3 — Constrain wrapper width with responsive `max-w-*`
- **Change:** Introduced responsive `max-w` utilities on the hero wrapper to mirror other sections.
- **Result:** Desktop layout matched expectations, but mobile devices still rendered the hero at full viewport width.

### Attempt 4 — Instrument layout metrics
- **Change:** Added refs to the container/media/image wrappers and development-only `console.debug` output capturing widths, heights, and viewport dimensions. Updated Jest coverage accordingly.
- **Result:** Metrics confirm the container respects `max-w-screen-xl`, yet on devices <=360px the image wrapper still scales to the viewport. Further investigation needed into surrounding layout context (e.g., parent flex/grid constraints).

### Attempt 5 — Trace parent chain & computed styles
- **Change:** Expanded the debug payload with computed style snapshots (box sizing, margins, padding, `max-width`) and a parent chain audit so each ancestor's width and padding can be compared against the hero container at runtime. Timestamped log entries for easier correlation with screenshots.
- **Result:** Early console output still shows 100% width propagation from upstream layout nodes. Awaiting fresh mobile captures to isolate which parent is forcing the stretch.

### Attempt 6 — Narrow base container max-width
- **Change:** Reduced the hero container's base `max-width` to `max-w-[18rem]` while keeping wider breakpoints for tablets/desktops so the section centers on small screens without stretching edge-to-edge.
- **Result:** Mobile devices still rendered the hero full-bleed despite the tighter `max-width`, indicating upstream flex alignment was overriding the constraint.

### Attempt 7 — Adjust flex alignment to prevent stretching
- **Change:** Removed the default `w-full` stretch by switching the wrapper to `w-auto` on mobile, adding `self-center`, and only re-introducing `md:w-full` so the hero can shrink to its intrinsic width on narrow screens while filling space on larger breakpoints.
- **Result:** Mobile capture still shows the hero card stretched to the viewport edges. The parent flex column appears to enforce a 100% width despite the wrapper's intrinsic sizing.

### Attempt 8 — Introduce `w-fit` / `max-w-full` sizing strategy (Current change)
- **Change:** Reworked the hero container, media wrapper, and image wrappers to use `w-fit` + `max-w-full` so they shrink-wrap their contents on mobile, while promoting to `md:w-full` and `md:max-w-none` for tablet/desktop. Image sizing now relies on `w-auto` with progressive `max-w-*` caps.
- **Result:** Pending—requires fresh device validation to confirm the inline-fit approach finally prevents the full-width stretch.

## Next Steps
- Capture live screenshots (mobile & tablet) to correlate visual output with logged metrics.
- Audit parent layout styles to determine whether upstream containers force full-width stretching.
- Monitor updated layout to confirm the narrower base `max-width` resolves the stretching; if issues persist, continue auditing ancestor containers.

259 changes: 212 additions & 47 deletions src/components/home/HeroOffering.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,9 @@

import SafeMotionDiv from "@/components/ui/SafeMotionDiv";
import SplineModel from "@/components/ui/spline-model";
import { useIsMobile } from "@/hooks/use-mobile";
import { cn } from "@/lib/utils";
import Image, { type StaticImageData } from "next/image";
import type React from "react";
import * as React from "react";

export interface HeroOfferingProps {
image?: string | StaticImageData | React.ReactNode;
Expand All @@ -24,51 +23,217 @@ export const HeroOffering: React.FC<HeroOfferingProps> = ({
imageAlt,
className,
}) => {
const isMobile = useIsMobile();

const isImageSrc = (img: unknown): img is string | StaticImageData =>
typeof img === "string" ||
(typeof img === "object" && img !== null && "src" in img);

return (
<div
className={cn(
"flex w-full items-center justify-center",
isMobile
? "py-6"
: "min-h-[340px] sm:min-h-[400px] md:min-h-[460px] lg:min-h-[520px]",
className,
)}
>
<div className="relative mx-auto flex h-full w-full max-w-[18rem] items-center justify-center sm:max-w-md md:max-w-lg lg:max-w-xl xl:max-w-2xl 2xl:max-w-3xl">
{image ? (
isImageSrc(image) ? (
<SafeMotionDiv
initial={{ opacity: 0, scale: 0.8, y: 40 }}
animate={{ opacity: 1, scale: 1, y: 0 }}
transition={{
type: "spring",
stiffness: 80,
damping: 16,
duration: 0.8,
}}
>
<div className="flex w-full max-w-sm items-center justify-center p-0 sm:max-w-md md:max-w-lg">
<Image
src={image}
alt={imageAlt || "Hero Offering"}
className="h-auto w-full max-w-xs rounded-xl object-contain sm:max-w-md md:max-w-lg lg:max-w-xl xl:max-w-2xl 2xl:max-w-3xl"
style={{
filter: "drop-shadow(0 8px 32px rgba(80, 0, 255, 0.18))",
}}
width={800}
height={800}
priority
/>
</div>
</SafeMotionDiv>
) : (
<SafeMotionDiv
const containerClassName = React.useMemo(
() =>
cn(
"mx-auto flex flex-col items-center justify-center self-center px-4 py-6 sm:min-h-[400px] sm:px-6 md:min-h-[460px] md:flex-row md:py-0 md:w-full lg:min-h-[520px] lg:px-8",
"w-fit max-w-full md:max-w-none",
"max-w-[18rem] sm:max-w-3xl lg:max-w-5xl xl:max-w-6xl",
className,
),
[className],
);

const mediaWrapperClassName = React.useMemo(
() =>
cn(
"relative mx-auto flex h-full w-fit max-w-full flex-col items-center justify-center",
"max-w-[18rem] sm:max-w-md md:w-full md:max-w-lg lg:max-w-xl xl:max-w-2xl 2xl:max-w-3xl",
),
[],
);

const imageWrapperClassName = React.useMemo(
() =>
cn(
"flex w-fit max-w-full items-center justify-center p-0",
"max-w-[16rem] sm:max-w-md md:w-full md:max-w-lg",
),
[],
);

const imageClassName = React.useMemo(
() =>
cn(
"h-auto w-auto max-w-[14rem] rounded-xl object-contain",
"sm:max-w-md md:max-w-lg lg:max-w-xl xl:max-w-2xl 2xl:max-w-3xl",
),
[],
);

const containerRef = React.useRef<HTMLDivElement | null>(null);
const mediaWrapperRef = React.useRef<HTMLDivElement | null>(null);
const imageWrapperRef = React.useRef<HTMLDivElement | null>(null);
const imageElementRef = React.useRef<HTMLImageElement | null>(null);

React.useEffect(() => {
if (process.env.NODE_ENV === "production") {
return undefined;
}

const element = containerRef.current;

if (!element) {
return undefined;
}

const logBounds = () => {
const getMetrics = (node: HTMLElement | null) => {
if (!node) {
return undefined;
}

const rect = node.getBoundingClientRect();
const computedStyles =
typeof window === "undefined"
? undefined
: window.getComputedStyle(node);

return {
className: node.className,
height: Math.round(rect.height),
width: Math.round(rect.width),
offsetWidth: Math.round(node.offsetWidth),
scrollWidth: Math.round(node.scrollWidth),
computed: computedStyles
? {
boxSizing: computedStyles.boxSizing,
display: computedStyles.display,
marginInline: `${computedStyles.marginLeft} ${computedStyles.marginRight}`,
maxWidth: computedStyles.maxWidth,
minWidth: computedStyles.minWidth,
paddingInline: `${computedStyles.paddingLeft} ${computedStyles.paddingRight}`,
position: computedStyles.position,
}
: undefined,
};
};

const viewportWidth =
typeof window === "undefined"
? undefined
: Math.round(window.innerWidth);

const parentChain = (() => {
if (typeof window === "undefined") {
return undefined;
}

const parents: Array<{
className: string;
nodeName: string;
width: number;
maxWidth: string | undefined;
paddingInline: string | undefined;
}> = [];

let current: HTMLElement | null = element.parentElement;

while (current) {
const rect = current.getBoundingClientRect();
const computedStyles = window.getComputedStyle(current);

parents.push({
className: current.className,
nodeName: current.nodeName,
width: Math.round(rect.width),
maxWidth: computedStyles.maxWidth || undefined,
paddingInline: computedStyles
? `${computedStyles.paddingLeft} ${computedStyles.paddingRight}`
: undefined,
});

if (current.nodeName === "BODY") {
break;
}

current = current.parentElement;
}

return parents;
})();

console.debug("[HeroOffering] layout metrics", {
container: getMetrics(element),
image: getMetrics(imageElementRef.current),
imageWrapper: getMetrics(imageWrapperRef.current),
mediaWrapper: getMetrics(mediaWrapperRef.current),
parentChain,
timestamp: new Date().toISOString(),
viewportWidth,
});
};

logBounds();

if (typeof window === "undefined") {
return undefined;
}

const ResizeObserverCtor = window.ResizeObserver;

if (typeof ResizeObserverCtor === "function") {
const observer = new ResizeObserverCtor(() => {
logBounds();
});

observer.observe(element);

return () => {
observer.disconnect();
};
}

const handleResize = () => {
logBounds();
};

console.debug("[HeroOffering] ResizeObserver unavailable; using window resize listener");

window.addEventListener("resize", handleResize);

return () => {
window.removeEventListener("resize", handleResize);
};
}, [containerClassName]);

return (
<div ref={containerRef} className={containerClassName}>
<div ref={mediaWrapperRef} className={mediaWrapperClassName}>
{image ? (
isImageSrc(image) ? (
<SafeMotionDiv
initial={{ opacity: 0, scale: 0.8, y: 40 }}
animate={{ opacity: 1, scale: 1, y: 0 }}
transition={{
type: "spring",
stiffness: 80,
damping: 16,
duration: 0.8,
}}
>
<div ref={imageWrapperRef} className={imageWrapperClassName}>
<Image
src={image}
alt={imageAlt || "Hero Offering"}
className={imageClassName}
style={{
filter: "drop-shadow(0 8px 32px rgba(80, 0, 255, 0.18))",
}}
width={800}
height={800}
priority
ref={imageElementRef}
/>
</div>
</SafeMotionDiv>
) : (
<SafeMotionDiv
initial={{ opacity: 0, scale: 0.8, y: 40 }}
animate={{ opacity: 1, scale: 1, y: 0 }}
transition={{
Expand All @@ -82,9 +247,9 @@ export const HeroOffering: React.FC<HeroOfferingProps> = ({
</SafeMotionDiv>
)
) : (
<SplineModel />
)}
</div>
</div>
);
<SplineModel />
)}
</div>
</div>
);
};
Loading