Files
zeekayandHanzo Dev f11062f432 fix(analytics): theme-aware hero ground and surfaces to fix light-mode contrast
The /analytics landing page hardcoded theme-independent colors
(var(--pure-black), var(--pure-white), --neutral-*) into the hero ground,
dashboard mock, code windows and section washes. In light mode that meant
near-black ink on a pure-black hero and white chart lines on light
surfaces — unreadable and harsh.

Swap them for @hanzo/design's theme-aware tokens (--background, --muted,
--secondary, --primary, --muted-foreground). Dark rendering is unchanged;
light mode now resolves correctly.

Verified with headless Chromium (hero bg/ink before vs after) and clean
tsc --noEmit / design lint.


Co-authored-by: Hanzo Dev <[email protected]>
2026-10-25 09:02:26 -07:00

234 lines
9.1 KiB
TypeScript

'use client'
import React, { useState, useEffect, useRef } from "react";
import { motion } from "@/components/motion";
import { ExternalLink, Users, BookOpen } from "lucide-react"
import { Github } from "@/components/brand"
import { Band, Head } from '@/components/marketing/band'
import { Action } from '@hanzo/ui/marketing'
import { Body, Disc, Lattice, Tile, ink } from './kit'
import { Box, Text, XStack, YStack } from '@hanzo/ui'
import { Grid } from '@hanzo/ui/grid'
import { DOCS } from '@/components/home/nav-data'
import { phrase } from '@hanzo/i18n'
import { useI18n } from '@hanzo/i18n/react'
const GROUND = 'linear-gradient(to bottom, var(--background), color-mix(in oklab, var(--muted) 30%, transparent))'
const statsItems = [
{ icon: <Github size={24} />, value: 17, suffix: "M+", label: phrase("Downloads"), countUpDuration: 2 },
{ icon: <Github size={24} />, value: 25, suffix: "K+", label: phrase("GitHub Stars"), countUpDuration: 2.2 },
{ icon: <Users size={24} />, value: 280, suffix: "+", label: phrase("Contributors"), countUpDuration: 1.8 },
];
const RESOURCES = [
{ title: phrase("Documentation"), description: phrase("Comprehensive guides and API references"), link: `${DOCS}/docs/analytics` },
{ title: phrase("Community Forum"), description: phrase("Join discussions with developers and AI experts"), link: "https://discord.gg/XthHQQj" },
{ title: phrase("Examples"), description: phrase("Sample projects showcasing Hanzo Analytics"), link: "https://github.com/hanzoai/insights" },
{ title: phrase("Support"), description: phrase("Get help from our team and community"), link: "/support" },
]
/** A card heading in this section, the 2xl rung. */
function Heading({ children }: { children: React.ReactNode }) {
return (
<Text render="h3" display="block" fontSize="$7" lineHeight="1.75rem" fontWeight="700" color="inherit">
{children}
</Text>
)
}
/** A placeholder row in the contributor mock. */
function Bar({ width, mt }: { width: '75%' | '50%'; mt?: '$2' }) {
return <Box height={8} rounded={4} bg="var(--neutral-700)" width={width} mt={mt} />
}
/**
* The observer here COUNTS, it does not conceal.
*
* It used to do both: one `IntersectionObserver` started the tallies AND drove
* an `animate={controls}` that held both card grids at `opacity: 0` until it
* fired — so a reader the observer missed got the stats section as a blank
* 872px. Starting a count when the number comes into view is a real thing to
* watch for; deciding whether the copy exists is not, and the two had no
* business on the same trigger. The reveal is `whileInView` now, like every
* other on the site, which cannot hide anything (`components/motion.tsx`).
*/
const Community = () => {
const { t } = useI18n();
const [isInView, setIsInView] = useState(false);
const ref = useRef<HTMLDivElement>(null);
useEffect(() => {
const el = ref.current;
if (!el) return;
const observer = new IntersectionObserver(
([entry]) => {
if (entry.isIntersecting) setIsInView(true);
},
{ threshold: 0.2 }
);
observer.observe(el);
return () => observer.unobserve(el);
}, []);
const containerVariants = {
visible: {
opacity: 1,
transition: {
staggerChildren: 0.2
}
}
};
const itemVariants = {
visible: { opacity: 1, y: 0 }
};
return (
<Band rule={false} pad="$14" measure={1280} ground={GROUND}>
<Lattice size={20} opacity={0.02} />
<Box position="relative" z="var(--z-raised)">
<Head
loud
title={t('Powered by Community, Improved by Collaboration')}
lede={t('Join thousands of developers building the future of analytics together.')}
/>
<div ref={ref}>
<motion.div variants={containerVariants} whileInView="visible" viewport={{ once: true }}>
<Grid columns={{ min: 300, max: 3 }} gap={32} mb={64}>
{statsItems.map((item) => (
<motion.div key={item.label} variants={itemVariants}>
<Tile height="100%" p="$6" bg="$panel" $platform-web={{ textAlign: 'center' }}>
<XStack justify="center" mb="$4">
<Disc edge="var(--border)">{item.icon}</Disc>
</XStack>
<Text display="block" mb={8} fontSize="$10" lineHeight="2.2222rem" fontWeight="700" color="inherit">
{isInView ? <CountUp end={item.value} duration={item.countUpDuration} /> : 0}
{item.suffix}
</Text>
<Text display="block" fontSize="$3" lineHeight="1.25rem" color="var(--muted-foreground)">
{t(item.label)}
</Text>
</Tile>
</motion.div>
))}
</Grid>
</motion.div>
<motion.div variants={containerVariants} whileInView="visible" viewport={{ once: true }}>
<Grid columns={{ min: 420, max: 2 }} gap={32}>
<motion.div variants={itemVariants}>
<Tile height="100%" p="$6" bg="$panel">
<XStack items="center" mb="$5" gap="$3">
<Github size={24} color="var(--foreground)" />
<Heading>{t('Open Source')}</Heading>
</XStack>
<Body mb={24}>
{t('Hanzo Analytics is fully open source, with a vibrant community of developers contributing from around the world. Join us on GitHub to collaborate on the future of analytics.')}
</Body>
<YStack gap="$4" mb="$6">
{[0, 1, 2].map((i) => (
<XStack key={i} items="center" gap="$3">
<Box width={32} height={32} rounded="var(--radius-full)" bg="var(--neutral-700)" />
<Box flex={1}>
<Bar width="75%" />
<Bar width="50%" mt="$2" />
</Box>
</XStack>
))}
</YStack>
<Action href="https://github.com/hanzoai/insights">
<Github size={16} />
{t('GitHub Repo')}
<ExternalLink size={12} />
</Action>
</Tile>
</motion.div>
<motion.div variants={itemVariants}>
<Tile height="100%" p="$6" bg="$panel">
<XStack items="center" mb="$5" gap="$3">
<BookOpen size={24} color={ink(70)} />
<Heading>{t('Resources')}</Heading>
</XStack>
<Body mb={24}>
{t('Access comprehensive documentation, tutorials, and examples to help you make the most of Hanzo Analytics.')}
</Body>
<Grid columns={{ min: 200, max: 2 }} gap={16} mb={32}>
{RESOURCES.map((resource) => (
// A card that is a link: its edge brightens on approach.
<Box
key={resource.title}
render={<a href={resource.link} />}
display="block"
height="100%"
p={16}
rounded="var(--radius-xl)"
borderWidth={1}
borderColor="var(--neutral-700)"
hoverStyle={{ borderColor: 'var(--neutral-500)' }}
$platform-web={{ backgroundColor: 'color-mix(in oklab, var(--neutral-800) 50%, transparent)', textDecoration: 'none' }}
>
<Text render="h4" display="block" mb={4} fontSize="$3" lineHeight="1.25rem" fontWeight="500" color="var(--foreground)">
{t(resource.title)}
</Text>
<Text render="p" display="block" fontSize="$2" lineHeight="1.15rem" color="var(--muted-foreground)">
{t(resource.description)}
</Text>
</Box>
))}
</Grid>
<Action href={`${DOCS}/docs/analytics`}>
{t('Browse All Resources')}
<ExternalLink size={12} />
</Action>
</Tile>
</motion.div>
</Grid>
</motion.div>
</div>
</Box>
</Band>
);
};
// Simple CountUp component
const CountUp = ({ end, duration = 2 }: { end: number; duration?: number }) => {
const [count, setCount] = useState(0);
useEffect(() => {
let startTime: number;
let animationFrame: number;
const updateCount = (timestamp: number) => {
if (!startTime) startTime = timestamp;
const progress = Math.min((timestamp - startTime) / (duration * 1000), 1);
setCount(Math.floor(progress * end));
if (progress < 1) {
animationFrame = requestAnimationFrame(updateCount);
}
};
animationFrame = requestAnimationFrame(updateCount);
return () => {
cancelAnimationFrame(animationFrame);
};
}, [end, duration]);
return <>{count}</>;
};
export default Community;