mirror of
https://github.com/hanzo-apps/hanzo.ai.git
synced 2026-10-29 07:20:43 +00:00
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]>
156 lines
6.9 KiB
TypeScript
156 lines
6.9 KiB
TypeScript
'use client'
|
|
|
|
import React from "react";
|
|
import { motion } from "@/components/motion";
|
|
import { ShieldCheck, Lock, CheckCircle } from "lucide-react";
|
|
import { Band, Head, INK } from '@/components/marketing/band'
|
|
import { More } from '@hanzo/ui/marketing'
|
|
import { Body, Disc, Sub, Title, ink } from './kit'
|
|
import { Box, Text, XStack, YStack } from '@hanzo/ui'
|
|
import { Grid } from '@hanzo/ui/grid'
|
|
import { phrase } from '@hanzo/i18n'
|
|
import { useI18n } from '@hanzo/i18n/react'
|
|
|
|
// A framework name under a heading reading "Certifications" IS a claim to
|
|
// hold one, whatever the line beneath it says -- and the qualifier here sat in
|
|
// `details`, a second line nobody reads before they read the name. That is the
|
|
// exact shape the /security badge row had. What each of these is instead: a
|
|
// control we run, named by the control.
|
|
const assurances = [
|
|
{ name: phrase("Continual internal audits"), details: phrase("Report on request.") },
|
|
{ name: phrase("Data protection"), details: phrase("EU data handling, deletion, and export on request.") },
|
|
{ name: phrase("Privacy by design"), details: phrase("Collected because it is used, not because it can be.") }
|
|
];
|
|
|
|
const SecurityFeatures = [
|
|
phrase("End-to-end encryption"),
|
|
phrase("Data residency controls"),
|
|
phrase("Custom data retention policies"),
|
|
phrase("Role-based access control"),
|
|
phrase("Regular security audits"),
|
|
phrase("Privacy-focused data collection"),
|
|
phrase("Automated compliance monitoring")
|
|
];
|
|
|
|
/** The two panels' shared ground — neutral falling to half of itself. */
|
|
const PANEL = { background: 'linear-gradient(to bottom right, var(--muted), color-mix(in oklab, var(--muted) 50%, transparent))' } as const
|
|
|
|
/** One of the two panels. */
|
|
function Panel({ children }: { children: React.ReactNode }) {
|
|
return (
|
|
<Box height="100%" rounded="var(--radius-xl)" borderWidth={1} borderColor="var(--neutral-800)" p="$5" $platform-web={PANEL}>
|
|
{children}
|
|
</Box>
|
|
)
|
|
}
|
|
|
|
const Security = () => {
|
|
const { t } = useI18n()
|
|
return (
|
|
<Band rule={false} pad="$14" measure={1152} ground={INK}>
|
|
<Box position="relative" z="var(--z-raised)">
|
|
<XStack mb="$4" justify="center">
|
|
<ShieldCheck size={40} color="var(--foreground)" />
|
|
</XStack>
|
|
<Head
|
|
loud
|
|
title={t('Enterprise-Grade Security & Compliance')}
|
|
lede={t('Secure by design, ensuring your data meets stringent compliance standards.')}
|
|
/>
|
|
|
|
{/* One column on a phone, three from `$lg` — a fixed `columns={3}` never
|
|
collapses and pushes the wide panel off the side. */}
|
|
<Box display="grid" gridTemplateColumns="minmax(0,1fr)" gap="$6" mb="$10" $lg={{ gridTemplateColumns: 'repeat(3, minmax(0,1fr))' }}>
|
|
<motion.div initial={{ opacity: 0, y: 20 }} whileInView={{ opacity: 1, y: 0 }} viewport={{ once: true }} transition={{ duration: 0.5 }}>
|
|
<Panel>
|
|
<XStack justify="center" mb="$5">
|
|
<Disc pct={20}>
|
|
<Lock size={32} />
|
|
</Disc>
|
|
</XStack>
|
|
|
|
<Title mb={24} text="center">
|
|
{t('Security Features')}
|
|
</Title>
|
|
|
|
<YStack render="ul" gap="$3">
|
|
{SecurityFeatures.map((feature, index) => (
|
|
<XStack key={feature} render="li">
|
|
<motion.div initial={{ opacity: 0, x: -10 }} whileInView={{ opacity: 1, x: 0 }} viewport={{ once: true }} transition={{ duration: 0.3, delay: 0.1 * index }}>
|
|
<XStack items="center">
|
|
<XStack mr={12} shrink={0}>
|
|
<CheckCircle size={20} color="var(--foreground)" />
|
|
</XStack>
|
|
<Text fontSize="$3" lineHeight="1.25rem" $platform-web={{ color: ink(80) }}>
|
|
{t(feature)}
|
|
</Text>
|
|
</XStack>
|
|
</motion.div>
|
|
</XStack>
|
|
))}
|
|
</YStack>
|
|
</Panel>
|
|
</motion.div>
|
|
|
|
{/* A one-cell grid, so the panel inside stretches to the row the
|
|
narrow panel beside it sets. */}
|
|
<Box display="grid" $lg={{ gridColumn: 'span 2' }} minW={0}>
|
|
<motion.div initial={{ opacity: 0, y: 20 }} whileInView={{ opacity: 1, y: 0 }} viewport={{ once: true }} transition={{ duration: 0.5, delay: 0.2 }}>
|
|
<Panel>
|
|
<Title mb={24}>{t('Security and assurance')}</Title>
|
|
|
|
<Grid columns={{ min: 260, max: 2 }} gap={24}>
|
|
{assurances.map((cert, index) => (
|
|
<motion.div
|
|
key={cert.name}
|
|
initial={{ opacity: 0, y: 20 }}
|
|
whileInView={{ opacity: 1, y: 0 }}
|
|
viewport={{ once: true }}
|
|
transition={{ duration: 0.5, delay: 0.1 * index }}
|
|
>
|
|
<Box height="100%" rounded="var(--radius-lg)" borderWidth={1} borderColor="var(--neutral-700)" bg="$panel" p={20}>
|
|
<XStack items="center">
|
|
<Box mr={12}>
|
|
<Disc pct={30} pad={8} edge="none">
|
|
<ShieldCheck size={24} />
|
|
</Disc>
|
|
</Box>
|
|
<Sub>{t(cert.name)}</Sub>
|
|
</XStack>
|
|
<Body mt={12}>{t(cert.details)}</Body>
|
|
</Box>
|
|
</motion.div>
|
|
))}
|
|
</Grid>
|
|
|
|
<Box mt="$6" rounded="var(--radius-lg)" borderWidth={1} borderColor="$edge" bg="$raised" p={20}>
|
|
<XStack>
|
|
<Box mr={16} self="flex-start">
|
|
<Disc pct={20} pad={8} edge="none">
|
|
<svg width={24} height={24} fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
|
</svg>
|
|
</Disc>
|
|
</Box>
|
|
<Box>
|
|
<Sub mb={8}>{t('Data Privacy Commitment')}</Sub>
|
|
<Body>
|
|
{t('Hanzo Analytics collects what a report uses and nothing else. You choose where the data lives, how long it is kept, and who on your team can read it, and you can export or delete it on request.')}
|
|
</Body>
|
|
<More href="/legal/privacy" mt={12} color="var(--foreground)">
|
|
{t('Read our privacy policy →')}
|
|
</More>
|
|
</Box>
|
|
</XStack>
|
|
</Box>
|
|
</Panel>
|
|
</motion.div>
|
|
</Box>
|
|
</Box>
|
|
</Box>
|
|
</Band>
|
|
);
|
|
};
|
|
|
|
export default Security;
|