Files
hanzo.ai/components/realtime/Features.tsx

140 lines
5.1 KiB
TypeScript

'use client'
import React from "react";
import { motion } from "@/components/motion";
import { Activity, Zap, Globe, Users, Clock, Lock, Workflow, Shield } from "lucide-react";
import { View, YStack, XStack } from '@hanzo/ui'
import { Grid } from '@hanzo/ui/grid'
import { Line } from '@/components/type'
import { Lede, Title } from '@hanzo/ui/marketing'
interface FeatureCardProps {
icon: React.ElementType;
title: string;
description: string;
delay: number;
}
/**
* The tile a feature sits in.
*
* A COLUMN, not a block: gui's `Text` is `display: inline`, so the title and the
* paragraph under it would share a line unless each is a flex item. `stretch` is
* the default and is what a paragraph wants; the disc holds its own width.
*/
const CARD = {
p: 24,
rounded: 'var(--radius-xl)',
borderWidth: 1,
borderColor: 'var(--neutral-800)',
background: 'linear-gradient(to bottom right, var(--neutral-900), var(--background))',
} as const
const FeatureCard: React.FC<FeatureCardProps> = ({ icon: Icon, title, description, delay }) => {
return (
<YStack
render={<motion.div initial={{ opacity: 0, y: 20 }} whileInView={{ opacity: 1, y: 0 }} viewport={{ once: true }} transition={{ duration: 0.5, delay }} />}
{...CARD}
>
<XStack
width={48}
height={48}
items="center"
justify="center"
mb={16}
rounded="var(--radius-lg)"
bg="$raised"
>
<Icon width={24} height={24} color="var(--foreground)" />
</XStack>
<Line render="h3" size="xl" weight="600" marginBottom={8}>{title}</Line>
<Line render="p" tone="muted">{description}</Line>
</YStack>
);
};
const Features = () => {
const features = [
{
icon: Activity,
title: "One stream, server-sent",
description: "A single long-lived HTTP response carries every update, so nothing special has to sit in front of it. The browser notices a dropped network and reconnects on its own."
},
{
icon: Workflow,
title: "A collection, or one record",
description: "Watch a whole collection and hear about everything that lands in it, or watch one record and hear only about that one. Change what you are watching without dropping the stream."
},
{
icon: Globe,
title: "One broker per tenant",
description: "An org's subscribers attach to that org's own broker, reading that org's own data. Two tenants using the same collection name are not sharing a topic, so there is no cross-tenant fan-out to get wrong."
},
{
icon: Users,
title: "Deletes are checked too",
description: "A delete is announced only to the subscribers whose rule would have let them read the record. Something you were never allowed to see does not reveal itself on the way out."
},
{
icon: Clock,
title: "The same data, not a copy of it",
description: "The stream reads from the store the API reads. There is no separate event bus to keep in step, and nothing that can be current in one place and stale in the other."
},
{
icon: Zap,
title: "The rule is the filter",
description: "What reaches a subscriber is decided by the collection's access rule, evaluated against the identity that opened the stream. It is the same predicate that guards the API, not a second one written for the stream."
},
{
icon: Lock,
title: "A grant, not your token",
description: "A browser cannot put a header on the request that opens a stream. So you mint a short-lived grant on an ordinary authenticated call and spend it once, within thirty seconds — rather than putting a token that opens every service into a URL that every proxy and access log will keep."
},
{
icon: Shield,
title: "Limits belong to the deployment",
description: "How many streams and how many requests are settings on the deployment and counted once for the process, not copied onto each tenant — so a limit of two means two."
}
];
return (
<YStack
render="section"
py={80}
px={16}
$sm={{ px: 24 }}
$lg={{ px: 32 }}
>
<YStack width="100%" maxW={1280} mx="auto">
<View
render={<motion.div initial={{ opacity: 0, y: 20 }} whileInView={{ opacity: 1, y: 0 }} viewport={{ once: true }} transition={{ duration: 0.5 }} />}
display="block"
mb={64}
$platform-web={{ textAlign: 'center' }}
>
<Title quiet mb={16}>
What the stream gives you
</Title>
<Lede maxW={768} mx="auto" mt="$3.5">
One connection, and the rules you already wrote deciding what goes down it
</Lede>
</View>
<Grid columns={{ min: 260, max: 4 }} gap={24}>
{features.map((feature, index) => (
<FeatureCard
key={index}
icon={feature.icon}
title={feature.title}
description={feature.description}
delay={index * 0.1}
/>
))}
</Grid>
</YStack>
</YStack>
);
};
export default Features;