Files

120 lines
4.4 KiB
TypeScript

'use client'
/**
* NO TAILWIND. The section frame is `components/marketing/band`'s `Band` and
* `Head` — the rule, the rhythm, the gutter, the measure and the centred head
* this file used to write out by hand — and the cards are gui props over
* @hanzo/design's tokens.
*
* `Grid` here is lucide's ICON of that name, so gui's grid comes in as
* `Columns`. A `Fit` is one floor and a cap rather than a pair of breakpoints:
* 224 lands the widest step (3-up) on the 768 the source asked for.
*/
import React from "react";
import { motion } from "@/components/motion";
import { Zap, Scale, Shield, Grid, DatabaseIcon, Activity } from "lucide-react";
import { View, XStack } from '@hanzo/ui'
import { Grid as Columns } from '@hanzo/ui/grid'
import { Band, Head, Words } from '@/components/marketing/band'
const FeatureCard = ({
icon: Icon,
title,
description,
delay
}: {
icon: React.ElementType;
title: string;
description: string;
delay: number;
}) => (
<View
render={<motion.div initial={{ opacity: 0, y: 20 }} whileInView={{ opacity: 1, y: 0 }} viewport={{ once: true }} transition={{ duration: 0.4, delay }} />}
display="block"
rounded="var(--radius-xl)"
borderWidth={1}
borderColor="var(--neutral-800)"
background="color-mix(in oklab, var(--neutral-900) 20%, transparent)"
p={32}
>
<XStack
width={48}
height={48}
items="center"
justify="center"
mb={24}
rounded="var(--radius-lg)"
bg="$raised"
>
<Icon width={24} height={24} color="color-mix(in oklab, var(--foreground) 70%, transparent)" />
</XStack>
{/* A card's own heading and its paragraph — not a section head, so not a role. */}
<Words render="h3" mb={16} fontSize="$6" lineHeight="1.4" weight="700" color="var(--text-primary)">
{title}
</Words>
<Words render="p" color="var(--muted-foreground)">
{description}
</Words>
</View>
);
const KeyFeatures = () => {
const features = [
{
icon: Zap,
title: "Columns, not rows",
description: "A table is stored one column at a time, so a query reads only the columns it names. Each column gets a codec suited to what it holds — deltas for a rising timestamp, dictionaries for a repeated label, general compression over the result."
},
{
icon: Scale,
title: "Shard it and it keeps up",
description: "Tables are partitioned and spread across nodes; add nodes and both the storage and the scan spread with them. Parts merge in the background, so ingest never stops for a compaction window."
},
{
icon: Shield,
title: "Replicas that agree",
description: "Replicated tables coordinate through a Raft quorum running inside the server itself — there is no separate coordination service to stand up beside the database. A replica that falls behind catches up part by part."
},
{
icon: Grid,
title: "Local disks, or object storage",
description: "Run a shared-nothing cluster on local NVMe, or put the table's data on object storage and scale query nodes on their own. Old partitions expire on a TTL you write into the schema."
},
{
icon: DatabaseIcon,
title: "Events, metrics and logs are one shape",
description: "Append-heavy, partitioned by time, queried by range — the same engine serves all three. Materialized views keep rollups current as rows land, so the summary exists before anyone asks for it."
},
{
icon: Activity,
title: "SQL, on several protocols",
description: "Query over HTTP on 8123, on the native protocol on 9000, or through the MySQL and PostgreSQL wire ports — which is how BI tools and drivers that know nothing about it still connect. A CSV, a Parquet file or a JSON stream loads without a conversion step first."
}
];
return (
<Band id="features" rule={false} pad="$14" measure={1152} ground="var(--black)">
<Head
title="What it does"
lede="Scan a great many rows. Return a small answer. Do it while the rows are still arriving."
measure={768}
/>
<Columns columns={{ min: 224, max: 3 }} gap={32}>
{features.map((feature, index) => (
<FeatureCard
key={feature.title}
icon={feature.icon}
title={feature.title}
description={feature.description}
delay={0.1 * (index + 1)}
/>
))}
</Columns>
</Band>
);
};
export default KeyFeatures;