mirror of
https://github.com/hanzoai/gui.git
synced 2026-10-23 02:31:31 +00:00
Hold the page to one gutter at every width
The responsive section measured itself against a second container. Its breakpoint ruler rode Container — the docs page's measure and gutter — nested in the ContainerLarge every other section stands on, so a marker labelled 500 stood 522px from the mock it measures, and below sm a hand-fitted translate put the mock 25px left of its own heading, off the page. Both take the section's own box now. The preview strip scrolled to an arithmetic copy of its padding and gap that had drifted from them by one gap per stop, so the ninth colour sat a whole card off centre. It asks the layout instead. A stop reaches the swatch, the demo, the ink and the stored accent, never the ground, and a lamp carried past the hero is the ground by another route — the wash still under the player row. The glow stays at the hero. Two footer columns had no basis to size from once stacked and drew over the next, nine swatches could not fit 390 in one row, and a title glued to its last word was split mid-word instead. Co-authored-by: Hanzo Dev <[email protected]>
This commit is contained in:
@@ -3,7 +3,7 @@ import { useLoader } from 'one'
|
||||
import { H2, H3, Paragraph, Text, XStack, YStack, styled } from '@hanzo/gui'
|
||||
import { HeadInfo } from '~/components/HeadInfo'
|
||||
import { Link } from '~/components/Link'
|
||||
import { SubTitle, nbspLastWord } from '~/components/SubTitle'
|
||||
import { SubTitle } from '~/components/SubTitle'
|
||||
import { DocsPageFrame } from '~/features/docs/DocsPageFrame'
|
||||
import type { Entry } from '~/api'
|
||||
import { id, named, packages } from '~/features/docs/packages'
|
||||
@@ -50,7 +50,7 @@ export default function ApiPackagePage() {
|
||||
/>
|
||||
|
||||
<HomeH1>{pack.name}</HomeH1>
|
||||
<SubTitle>{nbspLastWord(pack.description ?? '')}</SubTitle>
|
||||
<SubTitle>{pack.description ?? ''}</SubTitle>
|
||||
|
||||
<Mono color="$color11">
|
||||
{`v${pack.version} · ${pack.entry} · ${values} values · ${pack.entries.length - values} types`}
|
||||
|
||||
@@ -3,7 +3,7 @@ import { getMDXComponent } from '@vxrn/mdx-rust/client'
|
||||
import { useMemo } from 'react'
|
||||
import { useLoader } from 'one'
|
||||
import { HeadInfo } from '~/components/HeadInfo'
|
||||
import { SubTitle, nbspLastWord } from '~/components/SubTitle'
|
||||
import { SubTitle } from '~/components/SubTitle'
|
||||
import { DocsPageFrame } from '~/features/docs/DocsPageFrame'
|
||||
import { useDocsMenu } from '~/features/docs/useDocsMenu'
|
||||
import { components } from '~/features/mdx/MDXComponents'
|
||||
@@ -59,7 +59,7 @@ export default function DocCorePage() {
|
||||
],
|
||||
}}
|
||||
/>
|
||||
<HomeH1>{nbspLastWord(frontmatter.title)}</HomeH1>
|
||||
<HomeH1>{frontmatter.title}</HomeH1>
|
||||
<SubTitle>{frontmatter.description || ''}</SubTitle>
|
||||
<ThemeTint>
|
||||
<Component components={components as any} />
|
||||
|
||||
@@ -3,7 +3,7 @@ import { getMDXComponent } from '@vxrn/mdx-rust/client'
|
||||
import { useMemo } from 'react'
|
||||
import { useLoader } from 'one'
|
||||
import { HeadInfo } from '~/components/HeadInfo'
|
||||
import { SubTitle, nbspLastWord } from '~/components/SubTitle'
|
||||
import { SubTitle } from '~/components/SubTitle'
|
||||
import { DocsPageFrame } from '~/features/docs/DocsPageFrame'
|
||||
import { useDocsMenu } from '~/features/docs/useDocsMenu'
|
||||
import { components } from '~/features/mdx/MDXComponents'
|
||||
@@ -59,8 +59,8 @@ export default function DocGuidesPage() {
|
||||
],
|
||||
}}
|
||||
/>
|
||||
<HomeH1>{nbspLastWord(frontmatter.title)}</HomeH1>
|
||||
<SubTitle>{nbspLastWord(frontmatter.description || '')}</SubTitle>
|
||||
<HomeH1>{frontmatter.title}</HomeH1>
|
||||
<SubTitle>{frontmatter.description || ''}</SubTitle>
|
||||
<ThemeTint>
|
||||
<Component components={components as any} />
|
||||
</ThemeTint>
|
||||
|
||||
@@ -3,7 +3,7 @@ import { getMDXComponent } from '@vxrn/mdx-rust/client'
|
||||
import { useLoader } from 'one'
|
||||
import { useMemo } from 'react'
|
||||
import { HeadInfo } from '~/components/HeadInfo'
|
||||
import { SubTitle, nbspLastWord } from '~/components/SubTitle'
|
||||
import { SubTitle } from '~/components/SubTitle'
|
||||
import { GuiExamples } from '~/components/GuiExamples'
|
||||
import { DocsPageFrame } from '~/features/docs/DocsPageFrame'
|
||||
import { useDocsMenu } from '~/features/docs/useDocsMenu'
|
||||
@@ -68,8 +68,8 @@ export default function DocIntroPage() {
|
||||
],
|
||||
}}
|
||||
/>
|
||||
<HomeH1>{nbspLastWord(frontmatter.title)}</HomeH1>
|
||||
<SubTitle>{nbspLastWord(frontmatter.description || '')}</SubTitle>
|
||||
<HomeH1>{frontmatter.title}</HomeH1>
|
||||
<SubTitle>{frontmatter.description || ''}</SubTitle>
|
||||
<ThemeTint>
|
||||
<GuiExamples.Provider value={examples}>
|
||||
<Component components={components as any} />
|
||||
|
||||
@@ -12,7 +12,7 @@ import { useDocsMenu } from '~/features/docs/useDocsMenu'
|
||||
import { useIsDocsTinted } from '~/features/docs/docsTint'
|
||||
import { components } from '~/features/mdx/MDXComponents'
|
||||
import { getOgUrl } from '~/features/site/getOgUrl'
|
||||
import { nbspLastWord, SubTitle } from '../../../../components/SubTitle'
|
||||
import { SubTitle } from '../../../../components/SubTitle'
|
||||
import { HomeH1 } from '../../../../features/site/home/HomeHeaders'
|
||||
|
||||
export async function generateStaticParams() {
|
||||
@@ -104,9 +104,9 @@ export function DocComponentsPage() {
|
||||
}}
|
||||
/>
|
||||
|
||||
<HomeH1>{nbspLastWord(frontmatter.title)}</HomeH1>
|
||||
<HomeH1>{frontmatter.title}</HomeH1>
|
||||
|
||||
<SubTitle>{nbspLastWord(frontmatter.description || '')}</SubTitle>
|
||||
<SubTitle>{frontmatter.description || ''}</SubTitle>
|
||||
|
||||
<MDXProvider frontmatter={frontmatter}>
|
||||
<DocsThemeTint>
|
||||
|
||||
@@ -1,23 +1,5 @@
|
||||
import { Fragment } from 'react'
|
||||
import { H3 } from '@hanzo/gui'
|
||||
|
||||
export const nbspLastWord = (sentence: string) => {
|
||||
if (typeof sentence !== 'string') {
|
||||
return sentence
|
||||
}
|
||||
const titleWords = sentence.split(' ')
|
||||
if (titleWords.length === 1) {
|
||||
return sentence
|
||||
}
|
||||
return titleWords.map((word, i) => {
|
||||
return i === titleWords.length - 1 ? (
|
||||
<Fragment key={i}> {word}</Fragment>
|
||||
) : (
|
||||
<Fragment key={i}> {word}</Fragment>
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
export const SubTitle = ({ children, ...props }) => {
|
||||
if (!children) {
|
||||
return null
|
||||
@@ -48,7 +30,7 @@ export const SubTitle = ({ children, ...props }) => {
|
||||
}}
|
||||
{...props}
|
||||
>
|
||||
{nbspLastWord(childText)}
|
||||
{childText}
|
||||
</H3>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ export const Footer = memo(() => {
|
||||
$sm={{ items: 'center' }}
|
||||
py="$5"
|
||||
flex={2}
|
||||
flexBasis="auto"
|
||||
mb="$2"
|
||||
px="$6"
|
||||
gap="$4"
|
||||
@@ -75,6 +76,7 @@ export const Footer = memo(() => {
|
||||
px="$4"
|
||||
py="$5"
|
||||
flex={1.5}
|
||||
flexBasis="auto"
|
||||
gap="$3"
|
||||
>
|
||||
<H4 mb="$3" fontFamily="$mono" size="$4" letterSpacing={0.5} opacity={0.5}>
|
||||
|
||||
@@ -152,6 +152,10 @@ export function Header(props: HeaderProps) {
|
||||
|
||||
const tooltipDelay = { open: 0, close: 150 }
|
||||
|
||||
// A finger wants 44px, and under sm the pointer is one. The menu button already
|
||||
// stands that tall; the toggle, the search and the octocat are given it here.
|
||||
const touchTarget = { $sm: { minW: 44, minH: 44 } } as const
|
||||
|
||||
export const HeaderContents = React.memo((props: HeaderProps) => {
|
||||
const pathname = usePathname()
|
||||
const isHome = pathname === '/'
|
||||
@@ -181,23 +185,25 @@ export const HeaderContents = React.memo((props: HeaderProps) => {
|
||||
</Link>
|
||||
|
||||
<TooltipGroup delay={tooltipDelay}>
|
||||
<XGroup maxH={32} bg="transparent" items="center" size="$4">
|
||||
<XGroup bg="transparent" items="center" size="$4">
|
||||
<XGroup.Item>
|
||||
<ThemeToggle borderWidth={0} chromeless />
|
||||
<ThemeToggle borderWidth={0} chromeless {...touchTarget} />
|
||||
</XGroup.Item>
|
||||
</XGroup>
|
||||
</TooltipGroup>
|
||||
|
||||
<SearchButton size="$2" rounded="$10" elevation="$0.5" />
|
||||
<SearchButton size="$2" rounded="$10" elevation="$0.5" {...touchTarget} />
|
||||
|
||||
<Link target="_blank" href="https://github.com/hanzoai/gui">
|
||||
<XStack group containerType="normal">
|
||||
<XStack
|
||||
items="center"
|
||||
justify="center"
|
||||
gap="$2"
|
||||
p="$2"
|
||||
opacity={0.9}
|
||||
hoverStyle={{ opacity: 1 }}
|
||||
{...touchTarget}
|
||||
>
|
||||
<GithubIcon width={22} />
|
||||
<>
|
||||
|
||||
@@ -227,6 +227,10 @@ const CodeExamples = memo(({ examples = [], title }: any) => {
|
||||
flexBasis="auto"
|
||||
height={325}
|
||||
maxH={325}
|
||||
// 325 is what makes the two panes match when they stand side by
|
||||
// side. Stacked, they no longer face each other, and a short sample
|
||||
// was holding a tall one's room open under it.
|
||||
$sm={{ height: 'auto' }}
|
||||
flex={1}
|
||||
language={example.language}
|
||||
source={example.code}
|
||||
|
||||
@@ -4,97 +4,42 @@ import { AnimatePresence, YStack, isClient, useMedia } from '@hanzo/gui'
|
||||
|
||||
import { useTintSectionIndex } from './TintSection'
|
||||
|
||||
const positions = [
|
||||
[-100 * 2, 420 * 1.5, 100],
|
||||
[-230 * 2, 64 * 1.5, 0],
|
||||
[212 * 2, 127 * 1.5, 0],
|
||||
[-135 * 2, 11 * 1.5, 0],
|
||||
[268 * 2, 61 * 1.5, 0],
|
||||
[-20 * 2, 145 * 1.5, 0],
|
||||
[336 * 2, 104 * 1.5, 0],
|
||||
[-141 * 2, 30 * 1.5, 0],
|
||||
[369 * 2, 98 * 1.5, 0],
|
||||
[-403 * 2, 1 * 1.5, 0],
|
||||
[339 * 2, 138 * 1.5, 0],
|
||||
[-42 * 2, 106 * 1.5, 0],
|
||||
[404 * 2, 86 * 1.5, 0],
|
||||
[-490 * 2, 60 * 1.5, 0],
|
||||
[155 * 2, 9 * 1.5, 0],
|
||||
]
|
||||
// Three lamps behind the hero, in the ramp's next three stops.
|
||||
//
|
||||
// They stop at the hero. A stop reaches the swatch, the demo, the ink and the
|
||||
// stored accent — never the ground — and a lamp carried down the page is the
|
||||
// ground by another route: it is what put a wash under the player row and left
|
||||
// the section brown at orange and olive at yellow. Below the hero the sections
|
||||
// stand on the neutral scale, which is where they were published.
|
||||
const scales = [0.94, 0.51, 1.06]
|
||||
|
||||
const scales = [
|
||||
[0.91, 1.05, 1.08],
|
||||
[0.92, 1.03, 1.07],
|
||||
[0.93, 1.04, 1.09],
|
||||
[0.94, 1.02, 1.06],
|
||||
[0.95, 1.01, 1.1],
|
||||
[0.96, 1.0, 1.05],
|
||||
[0.97, 1.06, 1.04],
|
||||
[0.98, 1.07, 1.03],
|
||||
[0.99, 1.08, 1.02],
|
||||
[1.0, 1.09, 1.01],
|
||||
[1.01, 1.1, 0.99],
|
||||
[1.02, 1.05, 0.98],
|
||||
[1.03, 1.04, 0.97],
|
||||
[1.04, 1.03, 0.96],
|
||||
[1.05, 1.02, 0.95],
|
||||
]
|
||||
export const HomeGlow = memo(() => {
|
||||
const { tints, tint, tintAlt, tintIndex } = useTint()
|
||||
const [sectionIndex, setSectionIndex] = useState(0)
|
||||
const isOnHeroBelow = sectionIndex <= 1
|
||||
const [scrollTop, setScrollTop] = useState(0)
|
||||
const xs = 400
|
||||
const scale = isOnHeroBelow ? 2 : 3
|
||||
const [atHero, setAtHero] = useState(true)
|
||||
const { reduceMotion } = useMedia()
|
||||
|
||||
if (isClient) {
|
||||
useTintSectionIndex((index) => {
|
||||
setSectionIndex(index)
|
||||
// const dims = tintSectionDimensions[index]
|
||||
// console.log('index', index, dims)
|
||||
const sy = document.documentElement?.scrollTop ?? 0
|
||||
setScrollTop(sy + 100)
|
||||
setAtHero(index <= 1)
|
||||
})
|
||||
}
|
||||
|
||||
const glows = useMemo(() => {
|
||||
return [
|
||||
tints[(tintIndex - 0) % tints.length],
|
||||
tints[(tintIndex + 1) % tints.length],
|
||||
tints[(tintIndex + 2) % tints.length],
|
||||
].map((curTint, i) => {
|
||||
if (!atHero) return null
|
||||
|
||||
return scales.map((scale, i) => {
|
||||
const curTint = tints[(tintIndex + i) % tints.length]
|
||||
if (!curTint) {
|
||||
// TODO this is being hit on quick move to T
|
||||
return null
|
||||
}
|
||||
|
||||
const isOpposing = tintIndex % 2 === 0
|
||||
const isAlt = i === 1
|
||||
|
||||
const xRand = isOnHeroBelow
|
||||
? 1
|
||||
: positions[(isOpposing ? 2 - i : i) % positions.length][0]
|
||||
const yRand = isOnHeroBelow
|
||||
? 1
|
||||
: positions[(isOpposing ? 2 - i : i) % positions.length][1]
|
||||
const heroBelowShift = tintIndex === 2 ? -100 : tintIndex === 4 ? 100 : 0
|
||||
|
||||
const x =
|
||||
xRand +
|
||||
(isOnHeroBelow ? heroBelowShift + (isAlt ? -250 : 250) : isAlt ? -300 : 300)
|
||||
|
||||
return (
|
||||
<YStack
|
||||
key={`${i}${tint}${tintAlt}`}
|
||||
transition={reduceMotion ? null : 'superLazy'}
|
||||
enterStyle={{
|
||||
opacity: isOnHeroBelow ? 0.5 : 0,
|
||||
}}
|
||||
exitStyle={{
|
||||
opacity: 0,
|
||||
}}
|
||||
opacity={isOnHeroBelow ? 0.5 : 0.8}
|
||||
enterStyle={{ opacity: 0.5 }}
|
||||
exitStyle={{ opacity: 0 }}
|
||||
opacity={0.5}
|
||||
overflow="hidden"
|
||||
height="100vh"
|
||||
maxH={650}
|
||||
@@ -102,10 +47,9 @@ export const HomeGlow = memo(() => {
|
||||
position="absolute"
|
||||
t={0}
|
||||
l={`calc(50vw - 500px)`}
|
||||
x={x}
|
||||
y={isOnHeroBelow ? 350 : yRand + 250}
|
||||
scale={scale * (isAlt ? 0.5 : 1) * scales[tintIndex][i]}
|
||||
scaleX={isOpposing ? 1 : 1}
|
||||
x={i === 1 ? -249 : 251}
|
||||
y={350}
|
||||
scale={2 * scale}
|
||||
>
|
||||
<YStack
|
||||
fullscreen
|
||||
@@ -121,7 +65,7 @@ export const HomeGlow = memo(() => {
|
||||
</YStack>
|
||||
)
|
||||
})
|
||||
}, [scale, tint, tints, reduceMotion])
|
||||
}, [atHero, tint, tintAlt, tintIndex, tints, reduceMotion])
|
||||
|
||||
return (
|
||||
<YStack
|
||||
@@ -133,16 +77,11 @@ export const HomeGlow = memo(() => {
|
||||
key={0}
|
||||
z={0}
|
||||
x={0}
|
||||
y={scrollTop}
|
||||
opacity={0.3}
|
||||
y={-100}
|
||||
opacity={0.24}
|
||||
// keep the whole glow group on its own compositor layer so page scroll
|
||||
// moves a cached texture rather than repainting the gradients
|
||||
style={{ willChange: 'transform' }}
|
||||
{...(isOnHeroBelow && {
|
||||
x: sectionIndex === 2 ? -xs : sectionIndex === 4 ? xs : 0,
|
||||
y: -100,
|
||||
opacity: 0.24,
|
||||
})}
|
||||
>
|
||||
<AnimatePresence>{glows}</AnimatePresence>
|
||||
</YStack>
|
||||
|
||||
@@ -3,6 +3,10 @@ import { H1, H2, H3, styled } from '@hanzo/gui'
|
||||
export const HomeH1 = styled(H1, {
|
||||
fontFamily: '$mono',
|
||||
className: 'word-break-keep-all',
|
||||
// Balance is how a title avoids a lone last word. Gluing the last two with a
|
||||
// non-breaking space is the other answer, and on a narrow page it made a pair
|
||||
// too long for the measure and the browser split it mid-word instead.
|
||||
'$platform-web': { textWrap: 'balance' },
|
||||
size: '$9',
|
||||
mb: '$2',
|
||||
color: '$color11',
|
||||
|
||||
@@ -31,7 +31,7 @@ import {
|
||||
useIsomorphicLayoutEffect,
|
||||
useMedia,
|
||||
} from '@hanzo/gui'
|
||||
import { Container, ContainerLarge } from '~/components/Containers'
|
||||
import { ContainerLarge } from '~/components/Containers'
|
||||
import { useIsIntersecting } from '~/hooks/useOnIntersecting'
|
||||
import { useTransitionState } from '~/hooks/useTransitionState'
|
||||
import favicon from '~/public/favicon.svg'
|
||||
@@ -131,6 +131,7 @@ export const HomeResponsive = memo(() => {
|
||||
const media = useMedia()
|
||||
|
||||
const [smIndex, setSmIndex] = useState(0)
|
||||
const [gutter, setGutter] = useState(0)
|
||||
const [width, setWidth] = useState(initialWidth)
|
||||
const isSmall = initialWidth + Math.max(0, move) < 680
|
||||
|
||||
@@ -149,26 +150,27 @@ export const HomeResponsive = memo(() => {
|
||||
prevMove.current = 0
|
||||
}, [])
|
||||
|
||||
const scale = 0.7 - smIndex * 0.05
|
||||
// The mock is a browser of a fixed width, so under sm it is scaled to the
|
||||
// gutter box it stands in — from that box's left edge, which is where the
|
||||
// heading starts.
|
||||
const fit = media.sm && gutter ? Math.min(1, gutter / width) : 1
|
||||
|
||||
return (
|
||||
<YStack ref={ref as any} y={0} mt={-80} position="relative">
|
||||
<ContainerLarge position="relative">
|
||||
<ResponsiveHeader />
|
||||
<Spacer size="$6" $sm={{ size: '$0' }} />
|
||||
<YStack height={browserHeight + 80} />
|
||||
<YStack
|
||||
height={(browserHeight + 80) * fit}
|
||||
onLayout={(e) => setGutter(e.nativeEvent.layout.width)}
|
||||
/>
|
||||
<XStack
|
||||
b={-20}
|
||||
position="absolute"
|
||||
z={1}
|
||||
flex={1}
|
||||
gap="$1"
|
||||
// mostly keeping this to make sure we get a good ACID test of useMedia().sm
|
||||
{...(media.sm && {
|
||||
scale,
|
||||
x: 150 - width / 2 - (smIndex ? (0.68 - scale) * 920 : 0),
|
||||
y: -40,
|
||||
})}
|
||||
{...(fit !== 1 && { scale: fit, transformOrigin: 'left bottom' })}
|
||||
>
|
||||
<YStack
|
||||
z={2}
|
||||
@@ -187,21 +189,21 @@ export const HomeResponsive = memo(() => {
|
||||
<Safari shouldLoad={hasInteracted} isSmall={isSmall} />
|
||||
</YStack>
|
||||
|
||||
<Container z={1} position="absolute">
|
||||
<XStack x={-10} $sm={{ display: 'none' }}>
|
||||
{breakpoints.map((bp, i) => {
|
||||
return (
|
||||
<Marker
|
||||
key={i}
|
||||
onPress={handleMarkerPress}
|
||||
active={i === 0 ? true : sizeI > i}
|
||||
name={breakpoints[i].name}
|
||||
l={breakpoints[i].at}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
</XStack>
|
||||
</Container>
|
||||
{/* The ruler shares the mock's own box, so a marker at 500 stands
|
||||
500px from the edge the mock starts at. */}
|
||||
<XStack position="absolute" t={0} l={0} z={1} $sm={{ display: 'none' }}>
|
||||
{breakpoints.map((bp, i) => {
|
||||
return (
|
||||
<Marker
|
||||
key={i}
|
||||
onPress={handleMarkerPress}
|
||||
active={i === 0 ? true : sizeI > i}
|
||||
name={bp.name}
|
||||
l={bp.at}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
</XStack>
|
||||
</XStack>
|
||||
|
||||
<YStack
|
||||
@@ -304,7 +306,6 @@ const SafariFrame = ({ children, ...props }: YStackProps) => {
|
||||
rounded="$4"
|
||||
borderColor="$borderColor"
|
||||
borderWidth={1}
|
||||
width="99%"
|
||||
{...props}
|
||||
>
|
||||
{useMemo(() => children, [children])}
|
||||
|
||||
@@ -31,6 +31,7 @@ const Row = ({ children, ...props }: { children: any; 'aria-label': string }) =>
|
||||
<XStack
|
||||
role="group"
|
||||
gap="$2"
|
||||
$xs={{ gap: '$1' }}
|
||||
p="$2"
|
||||
self="center"
|
||||
rounded="$10"
|
||||
@@ -129,6 +130,14 @@ export const HomeThemes = memo(function HomeThemes() {
|
||||
const width = 110
|
||||
const scale = 0.38
|
||||
|
||||
// Where the row stands for a preview to sit under the demo. Asked of the
|
||||
// layout: an arithmetic copy of the padding and the gap drifts from them, and
|
||||
// it did — by one gap per stop, so the ninth sat a whole card off centre.
|
||||
const stopAt = (node: HTMLElement, index: number) => {
|
||||
const item = node.firstElementChild?.children[index] as HTMLElement | undefined
|
||||
return item ? item.offsetLeft + (item.offsetWidth - node.clientWidth) / 2 : null
|
||||
}
|
||||
|
||||
const scrollToIndex = useEvent((index: number, force = false) => {
|
||||
const node = scrollView.current
|
||||
const lock = getLock()
|
||||
@@ -136,8 +145,8 @@ export const HomeThemes = memo(function HomeThemes() {
|
||||
const isForced = force && (isReadyToAnimate || lock === null)
|
||||
const shouldPrevent = !isReadyToAnimate && !isForced
|
||||
if (!node || shouldPrevent) return
|
||||
const left = width * index + width / 2
|
||||
if (node.scrollLeft === left) return
|
||||
const left = stopAt(node, index)
|
||||
if (left === null || Math.abs(node.scrollLeft - left) < 1) return
|
||||
node.scrollTo({ left, top: 0, behavior: 'smooth' })
|
||||
})
|
||||
|
||||
@@ -193,52 +202,50 @@ export const HomeThemes = memo(function HomeThemes() {
|
||||
}, [])}
|
||||
|
||||
<YStack my="$8" items="center" justify="center">
|
||||
<XStack className="scroll-horizontal no-scrollbar">
|
||||
<XStack px="$4" gap="$2">
|
||||
<Row aria-label="Color scheme">
|
||||
{(['light', 'dark'] as const).map((name) => {
|
||||
const Icon = name === 'dark' ? Moon : Sun
|
||||
return (
|
||||
<ActiveCircle
|
||||
key={name}
|
||||
aria-label={name}
|
||||
isActive={userScheme.value === name}
|
||||
onPress={() => userScheme.set(name)}
|
||||
>
|
||||
<Icon size={15} color="$color" />
|
||||
</ActiveCircle>
|
||||
)
|
||||
})}
|
||||
</Row>
|
||||
|
||||
<Row aria-label="Accent">
|
||||
{themes[0].map((color, i) => (
|
||||
<XStack px="$4" gap="$2" width="100%" justify="center" flexWrap="wrap">
|
||||
<Row aria-label="Color scheme">
|
||||
{(['light', 'dark'] as const).map((name) => {
|
||||
const Icon = name === 'dark' ? Moon : Sun
|
||||
return (
|
||||
<ActiveCircle
|
||||
key={`${String(color)}${i}`}
|
||||
aria-label={String(color)}
|
||||
isActive={curColorI === i}
|
||||
onPress={() => {
|
||||
updateActiveI([i, curShadeI])
|
||||
setAccent(tints, i)
|
||||
}}
|
||||
key={name}
|
||||
aria-label={name}
|
||||
isActive={userScheme.value === name}
|
||||
onPress={() => userScheme.set(name)}
|
||||
>
|
||||
{/* Only the dot wears the stop. Its rings stay the page's own,
|
||||
<Icon size={15} color="$color" />
|
||||
</ActiveCircle>
|
||||
)
|
||||
})}
|
||||
</Row>
|
||||
|
||||
<Row aria-label="Accent">
|
||||
{themes[0].map((color, i) => (
|
||||
<ActiveCircle
|
||||
key={`${String(color)}${i}`}
|
||||
aria-label={String(color)}
|
||||
isActive={curColorI === i}
|
||||
onPress={() => {
|
||||
updateActiveI([i, curShadeI])
|
||||
setAccent(tints, i)
|
||||
}}
|
||||
>
|
||||
{/* Only the dot wears the stop. Its rings stay the page's own,
|
||||
because white and black carry the ground's own ink, and a
|
||||
ring in it says nothing on the scheme they belong to. */}
|
||||
<Theme name={color}>
|
||||
<Circle
|
||||
size={16}
|
||||
bg={
|
||||
`$color${STOPS[String(color)]?.[userScheme.value] ?? 9}` as ColorTokens
|
||||
}
|
||||
borderWidth={1}
|
||||
borderColor="$color"
|
||||
/>
|
||||
</Theme>
|
||||
</ActiveCircle>
|
||||
))}
|
||||
</Row>
|
||||
</XStack>
|
||||
<Theme name={color}>
|
||||
<Circle
|
||||
size={16}
|
||||
bg={
|
||||
`$color${STOPS[String(color)]?.[userScheme.value] ?? 9}` as ColorTokens
|
||||
}
|
||||
borderWidth={1}
|
||||
borderColor="$color"
|
||||
/>
|
||||
</Theme>
|
||||
</ActiveCircle>
|
||||
))}
|
||||
</Row>
|
||||
</XStack>
|
||||
|
||||
<YStack
|
||||
@@ -257,14 +264,14 @@ export const HomeThemes = memo(function HomeThemes() {
|
||||
if (scrollLock === 'animate' || scrollLock === 'shouldAnimate') {
|
||||
return
|
||||
}
|
||||
const scrollX = Math.max(0, e.target.scrollLeft)
|
||||
// Match the formula in scrollToIndex: left = width * index + width / 2
|
||||
const itemI = Math.max(
|
||||
0,
|
||||
Math.min(
|
||||
Math.round((scrollX - width / 2) / width),
|
||||
themeCombos.length - 1
|
||||
)
|
||||
const node = e.target as HTMLElement
|
||||
const items = [...node.firstElementChild!.children] as HTMLElement[]
|
||||
const mid = node.scrollLeft + node.clientWidth / 2
|
||||
const off = (el: HTMLElement) =>
|
||||
Math.abs(el.offsetLeft + el.offsetWidth / 2 - mid)
|
||||
const itemI = items.reduce(
|
||||
(best, el, i) => (off(el) < off(items[best]) ? i : best),
|
||||
0
|
||||
)
|
||||
const [n1, n2] = flatToSplit(itemI)
|
||||
const [c1, c2] = activeI
|
||||
@@ -279,8 +286,7 @@ export const HomeThemes = memo(function HomeThemes() {
|
||||
justify="center"
|
||||
gap="$5"
|
||||
position="relative"
|
||||
px={`calc(50vw + 30px)`}
|
||||
x={-45 - 30}
|
||||
px="50%"
|
||||
>
|
||||
{useMemo(() => {
|
||||
return themeCombos.map((name, i) => {
|
||||
|
||||
Reference in New Issue
Block a user