mirror of
https://github.com/hanzoai/gui.git
synced 2026-10-30 18:12:27 +00:00
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]>
177 lines
4.3 KiB
TypeScript
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>
|
|
)
|
|
}
|