Files
zeekayandClaude Opus 5.5 a5ce56a558 Every home section clips its backdrop to the page
The responsive band runs 1000px past its container, so once that
section mounted the document was 1390px wide under a 390px screen again.
Clipping only the hero left every other backdrop free to do the same.
The clip moves into TintSection: each section is full-bleed, so its
edge is the page edge, and nothing on screen changes.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
2026-09-24 10:35:50 -07:00

177 lines
4.3 KiB
TypeScript

import { NEUTRAL, getTints, useTint } from '@hanzogui/logo'
import { useEffect, useMemo, useRef } from 'react'
import type { LayoutRectangle } from 'react-native'
import type { GetProps } from '@hanzo/gui'
import { XStack, YStack, styled } from '@hanzo/gui'
import { useOnIntersecting } from '~/hooks/useOnIntersecting'
type Props = SectionProps & { index: number }
const numIntersectingAtSection = getTints().tints.map((_) => 0)
export const tintSectionDimensions: Record<number, LayoutRectangle> = {}
export const TintSection = ({ children, index, z, ...props }: Props) => {
const top = useRef<HTMLElement>(null)
const bottom = useRef<HTMLElement>(null)
const mid = useRef<HTMLElement>(null)
const { tints, setTintIndex } = useTint()
useOnIntersecting(
useMemo(() => [top, mid, bottom], []),
(entries) => {
const count = entries.reduce((a, b) => a + (b?.isIntersecting ? 1 : 0), 0)
numIntersectingAtSection[index] = count
if (count < 1) {
return
}
let topIndex = -1
let topStr = -1
numIntersectingAtSection.forEach((str, index) => {
if (str >= topStr) {
topIndex = index
topStr = str
}
})
if (topIndex === index && topIndex !== current) {
const tintIndex = topIndex <= 1 ? NEUTRAL : topIndex % tints.length
setTintIndex(tintIndex)
current = index
listeners.forEach((cb) => cb(topIndex, count))
}
},
{
threshold: 0.2,
}
)
// A section is full-bleed, and its backdrops are not: the hero's grid runs
// 500px past its container and the responsive band 1000px. The section clips
// them at the page edge, so a backdrop never widens the page.
return (
<YStack
onLayout={(e) => (tintSectionDimensions[index] = e.nativeEvent.layout)}
z={z}
position="relative"
overflowX="clip"
>
{useMemo(() => {
return (
<>
<XStack
ref={top as any}
position="absolute"
t="10%"
l={0}
r={0}
height={10}
opacity={0}
pointerEvents="none"
/>
<XStack
ref={mid as any}
position="absolute"
t="50%"
l={0}
r={0}
height={10}
opacity={0}
pointerEvents="none"
/>
<XStack
ref={bottom as any}
position="absolute"
b="10%"
l={0}
r={0}
height={10}
opacity={0}
pointerEvents="none"
/>
</>
)
}, [top, mid, bottom])}
<HomeSection {...props}>{children}</HomeSection>
</YStack>
)
}
let current = 0
const listeners = new Set<Function>()
export const useTintSectionIndex = (cb: (index: number, str: number) => void) => {
useEffect(() => {
listeners.add(cb)
return () => {
listeners.delete(cb)
}
}, [])
}
export const HomeSection = styled(YStack, {
name: 'Section',
position: 'relative',
py: '$14',
z: 2,
variants: {
below: {
true: {
z: 1,
},
},
} as const,
})
type SectionProps = GetProps<typeof HomeSection>
// A raised section. Its surface and its edges are the neutral scale's, like the
// page ground: a tint is an accent, and an accent spread over a whole section is
// how yellow became olive and orange became brown.
export const SectionTinted = ({
children,
gradient,
extraPad,
bubble,
noBorderTop,
...props
}: any) => {
const childrenMemo = useMemo(() => children, [children])
return (
<YStack
z={2}
contain="paint"
position="relative"
py="$14"
elevation="$2"
{...(bubble && {
maxW: 1400,
rounded: '$6',
borderWidth: 1,
borderColor: '$borderColor',
self: 'center',
width: '100%',
})}
{...props}
>
<YStack
fullscreen
className="all ease-in ms1000"
z={-1}
opacity={0.4}
bg={gradient ? '$color2' : null}
{...(!bubble && {
borderTopWidth: noBorderTop ? 0 : 1,
borderBottomWidth: 1,
borderColor: '$borderColor',
})}
/>
{childrenMemo}
</YStack>
)
}