Files
supabase/apps/www/components/Products/DatabaseVisual.tsx
Pamela Chia 6dfae09b6d fix(www): postgres card art overlaps text column (#47734)
Before 
<img width="588" height="439" alt="Screenshot 2026-07-08 at 7 29 07 PM"
src="https://github.com/user-attachments/assets/8a3380d3-571f-49cd-8f32-7b38650dd0e5"
/>

After 
<img width="578" height="436" alt="Screenshot 2026-07-08 at 7 29 55 PM"
src="https://github.com/user-attachments/assets/09e74c31-e131-41d5-87da-b4518514cfb5"
/>


## Summary

On the homepage, the Postgres Database product card's elephant artwork
renders over the card's description text at every viewport ≥1280px. I
traced it to #47226 (merged June 24): standardizing the marketing
container to `section-container` (`max-w-7xl` + `xl:px-24`) capped
products-grid content at 1088px, shrinking the card to ~538px while its
fixed geometry (250px text column + 398px right-anchored square artwork
box) needs ~625px. Large monitors regressed hardest: before June 24 they
got ~676px cards and no overlap. The app-router homepage move (#47228),
the color system PR (#47288), and the artwork PNGs are all unrelated
(verified against production DOM and full diffs).

Two changes to the artwork span in `DatabaseVisual.tsx`:

1. Cap the box width at `calc(100% - 280px)` from `md` up, where 280
covers the text column's `md:max-w-[250px]` cap (in `ProductCard.tsx`'s
`isDatabase` branch) plus card padding and breathing room. The art
scales down through its existing `object-contain`. At md/lg the card is
full-width (`md:col-span-12`), so the clamp never binds and rendering is
unchanged there.
2. Remove the `xl:-right-12` bleed (base `right-0` stands, matching how
2xl already rendered). The bleed used to clip only the art's transparent
canvas margin; with the clamped box the art fills its full width, so the
48px offset was cropping the elephant itself at 1280-1535px.

The hover line-art SVG scales with the box and stays aligned with the
PNG (identical aspect ratios: viewBox 390:430, PNG 585x645).

## Changes
- Add `md:max-w-[calc(100%-280px)]` to the artwork span in
`DatabaseVisual.tsx` so the Postgres card art can never cross its text
column
- Drop `xl:-right-12 2xl:right-0` from the same span so the smaller art
isn't clipped at the card's right edge

## Testing

Round 1 on the Vercel preview (commit e1ca671, measured via
getBoundingClientRect + computed styles):
- [x] Art clear of the text column at 1280/1440/1600/1920 (gap 54px at
1280/1440, 6px at 1600/1920); computed `max-width: calc(100% - 280px)`
applies
- [x] Hover at 1440 — SVG and PNG rects identical (pixel-exact
alignment)
- [x] 768/1024 — full-width card unchanged, span still a 398px square
(clamp resolves but doesn't bind)
- [x] Light theme at 1440 — same geometry, no overlap

Round 1 also surfaced that the `xl:-right-12` bleed now cropped the
elephant at 1280-1535px widths → second commit removes it. Round 2
(commit e28b408):
- [x] 1280/1440/1512/1600/1920 — art fully visible (span right edge
flush at the card's inner edge, e.g. 713 vs 714 at 1440), still clear of
the text column (span left 457 vs text right 451)
- [x] 768/1024 — unchanged (span still a right-anchored 398px square)
- [x] Hover alignment still exact (PNG and SVG rects identical:
457,452,256,398)

## Linear
- fixes GROWTH-971
2026-07-08 19:56:12 +08:00

149 lines
7.4 KiB
TypeScript

import { detectBrowser, isBrowser } from 'common'
import Image from 'next/image'
import React, { useEffect, useRef, useState } from 'react'
import { cn } from 'ui'
interface Props {
className?: string
hasGlow?: boolean
}
const DatabaseVisual: React.FC<Props> = ({ className, hasGlow = true }) => {
const containerRef = useRef(null)
const ref = useRef(null)
const [gradientPos, setGradientPos] = useState({ x: 0, y: 0 })
const isSafari = isBrowser && detectBrowser() === 'Safari'
const handleGlow = (event: any) => {
if (!ref.current || !containerRef.current || !hasGlow) return null
const containerRefElement = containerRef.current as HTMLDivElement
const {
x: contX,
y: contY,
width: containerWidth,
height: containerHeight,
} = containerRefElement.getBoundingClientRect()
const xCont = event.clientX - contX
const yCont = event.clientY - contY
const isContainerHovered =
xCont > -3 && xCont < containerWidth + 3 && yCont > -3 && yCont < containerHeight + 3
if (!isContainerHovered) return
const svgElement = ref.current as SVGElement
const { x: svgX, y: svgY } = svgElement.getBoundingClientRect()
const x = event.clientX - svgX
const y = event.clientY - svgY
setGradientPos({ x, y })
}
useEffect(() => {
if (!isBrowser || isSafari) return
window.addEventListener('mousemove', handleGlow)
return () => {
window.removeEventListener('mousemove', handleGlow)
}
}, [])
const gradientTransform = isSafari
? `translate(150 150) rotate(45) scale(166 180)`
: `translate(${gradientPos?.x} ${gradientPos?.y}) rotate(45) scale(166 180)`
return (
<figure
className={cn('absolute inset-0 z-0', className)}
ref={containerRef}
role="img"
aria-label="Supabase Postgres database visual composition"
>
<span className="absolute group w-full md:w-auto h-full md:aspect-square md:max-w-[calc(100%-280px)] flex items-end md:items-center justify-center md:justify-end right-0 left-0 md:left-auto top-12 md:top-0 md:bottom-0 my-auto">
<Image
src="/images/index/products/database-dark.png"
alt="Supabase Postgres database"
fill
priority
quality={100}
sizes="100%"
draggable={false}
className="hidden dark:block absolute antialiased inset-0 object-contain object-center z-0 w-full md:w-auto h-full transition-opacity group-hover:opacity-80"
/>
<Image
src="/images/index/products/database-light.png"
alt="Supabase Postgres database"
fill
priority
quality={100}
draggable={false}
sizes="100%"
className="dark:hidden absolute antialiased inset-0 object-contain object-center z-0 w-full md:w-auto h-full transition-opacity group-hover:opacity-80"
/>
<svg
ref={ref}
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 390 430"
className="absolute w-full h-full z-10 m-auto will-change-transform opacity-0 transition-opacity group-hover:opacity-100"
>
<g stroke="var(--foreground-lighter)" filter="url(#filter5_bd_467_4905)">
<path d="M192.144 125.816h-53.465c-8.506 0-16.159 5.17-19.334 13.061L99.0045 189.43c-3.0613 7.608-1.3448 16.306 4.3775 22.181l10.232 10.506c4.792 4.919 7.474 11.516 7.474 18.384l-.001 14.473c0 20.197 16.373 36.569 36.569 36.569 6.16 0 11.154-4.993 11.154-11.153l.001-86.241c0-18.629 7.441-36.486 20.668-49.602 2.746-2.723 7.178-2.704 9.9.041 2.722 2.745 2.703 7.178-.042 9.9-10.577 10.488-16.526 24.766-16.526 39.661l-.001 86.241c0 13.892-11.262 25.153-25.154 25.153-27.928 0-50.569-22.64-50.569-50.569l.001-14.474c0-3.218-1.257-6.309-3.503-8.615L93.353 221.38c-9.5904-9.847-12.4673-24.424-7.3366-37.176l20.3406-50.553c5.308-13.192 18.101-21.835 32.322-21.835h55.729v.084h10.339c49.104 0 88.91 39.806 88.91 88.91v50.842c0 3.866-3.134 7-7 7s-7-3.134-7-7V200.81c0-41.372-33.538-74.91-74.91-74.91H193.23c-.37 0-.732-.029-1.086-.084Z" />
<path d="M210.03 283.94c0-3.866-3.134-7-7-7s-7 3.134-7 7v3.113c0 26.959 21.854 48.814 48.813 48.814 26.351 0 47.825-20.879 48.781-46.996h24.614c3.866 0 7-3.134 7-7s-3.134-7-7-7h-26.841c-30.744 0-60.256-12.083-82.173-33.643-2.756-2.711-7.188-2.675-9.899.081-2.711 2.756-2.675 7.188.081 9.9 21.725 21.371 50.116 34.423 80.228 37.134-.679 18.629-15.995 33.524-34.791 33.524-19.227 0-34.813-15.587-34.813-34.814v-3.113ZM238.03 202.145c0 4.792 3.885 8.677 8.677 8.677s8.676-3.885 8.676-8.677-3.884-8.676-8.676-8.676-8.677 3.884-8.677 8.676Z" />
</g>
<path
stroke="url(#a)"
d="M192.144 125.816h-53.465c-8.506 0-16.159 5.17-19.334 13.061L99.0045 189.43c-3.0613 7.608-1.3448 16.306 4.3775 22.181l10.232 10.506c4.792 4.919 7.474 11.516 7.474 18.384l-.001 14.473c0 20.197 16.373 36.569 36.569 36.569 6.16 0 11.154-4.993 11.154-11.153l.001-86.241c0-18.629 7.441-36.486 20.668-49.602 2.746-2.723 7.178-2.704 9.9.041 2.722 2.745 2.703 7.178-.042 9.9-10.577 10.488-16.526 24.766-16.526 39.661l-.001 86.241c0 13.892-11.262 25.153-25.154 25.153-27.928 0-50.569-22.64-50.569-50.569l.001-14.474c0-3.218-1.257-6.309-3.503-8.615L93.353 221.38c-9.5904-9.847-12.4673-24.424-7.3366-37.176l20.3406-50.553c5.308-13.192 18.101-21.835 32.322-21.835h55.729v.084h10.339c49.104 0 88.91 39.806 88.91 88.91v50.842c0 3.866-3.134 7-7 7s-7-3.134-7-7V200.81c0-41.372-33.538-74.91-74.91-74.91H193.23c-.37 0-.732-.029-1.086-.084Z"
/>
<path
stroke="url(#b)"
d="M210.03 283.94c0-3.866-3.134-7-7-7s-7 3.134-7 7v3.113c0 26.959 21.854 48.814 48.813 48.814 26.351 0 47.825-20.879 48.781-46.996h24.614c3.866 0 7-3.134 7-7s-3.134-7-7-7h-26.841c-30.744 0-60.256-12.083-82.173-33.643-2.756-2.711-7.188-2.675-9.899.081-2.711 2.756-2.675 7.188.081 9.9 21.725 21.371 50.116 34.423 80.228 37.134-.679 18.629-15.995 33.524-34.791 33.524-19.227 0-34.813-15.587-34.813-34.814v-3.113Z"
/>
<path
stroke="url(#c)"
d="M238.03 202.145c0 4.792 3.885 8.677 8.677 8.677s8.676-3.885 8.676-8.677-3.884-8.676-8.676-8.676-8.677 3.884-8.677 8.676Z"
/>
<defs>
<radialGradient
id="a"
cx="0"
cy="0"
r={isSafari ? '10' : '1'}
gradientUnits="userSpaceOnUse"
gradientTransform={gradientTransform}
>
<stop stopColor="hsl(var(--brand-default))" />
<stop offset="1" stopColor="var(--foreground-lighter)" />
</radialGradient>
<radialGradient
id="b"
cx="0"
cy="0"
r={isSafari ? '3' : '1'}
gradientUnits="userSpaceOnUse"
gradientTransform={gradientTransform}
>
<stop stopColor="hsl(var(--brand-default))" />
<stop offset="1" stopColor="var(--foreground-lighter)" />
</radialGradient>
<radialGradient
id="c"
cx="0"
cy="0"
r={isSafari ? '3' : '1'}
gradientUnits="userSpaceOnUse"
gradientTransform={gradientTransform}
>
<stop stopColor="hsl(var(--brand-default))" />
<stop offset="1" stopColor="var(--foreground-lighter)" />
</radialGradient>
</defs>
</svg>
</span>
</figure>
)
}
export default DatabaseVisual