mirror of
https://github.com/hanzo-apps/hanzo.ai.git
synced 2026-10-30 14:58:08 +00:00
Code on hanzo.ai was plain monospace: 285 blocks across 200 pages, one of them hand-inked in four greys. components/code now renders each sample at build with shiki and @hanzo/ui's codeTheme (Dracula dark, GitHub Light light), every colour written as light-dark() so the page's color-scheme picks it, on the theme's own ground. The page ships the coloured spans and no highlighter; e2e/gates/code.spec.ts holds both. A client page that shows code is split into a server page.tsx that owns the sample and a client view.tsx that takes the highlighted node: ProductLanding's code.source, the blockchain pages' codeExamples, the models kit Sample, the guest-key card, realtime, analytics, payments, sdks and the rest. Transcripts, agent prompts and ASCII diagrams are not code and keep their markup. Takes @hanzo/ui 8.27.83 for codeTheme and @hanzo/design 0.5.35 with it, regenerates app/gui.css for the new theme keys, and lowers the raw-colour allowance to 58: the samples' literal inks are gone.
605 lines
21 KiB
TypeScript
605 lines
21 KiB
TypeScript
"use client"
|
|
|
|
/**
|
|
* /dns — one control plane over every DNS provider.
|
|
*
|
|
* NO TAILWIND. The five section frames are `components/marketing/band`'s `Band`
|
|
* and `Head` — the rule, the rhythm, the gutter, the measure and the centred
|
|
* head this page wrote out by hand at each of them — the roles carry the type
|
|
* and the controls (`Display`, `Claim`, `Lede`, `Title`, `Chip`, `Action`,
|
|
* `Leaf`, `Line`), and everything left is gui props over @hanzo/design's
|
|
* tokens.
|
|
*
|
|
* Three things the swap moves, each deliberate:
|
|
*
|
|
* - the rules take `Band`'s `--neutral-900` hairline rather than this page's
|
|
* `--border`, which is `rgb(255 255 255 / .10)` — the same line to within a
|
|
* value of grey, and now the one line the whole site draws.
|
|
* - `Claim` and `Lede` step at 640 where `md:` stepped at 768, and each sits
|
|
* on its role's rung rather than the one this page typed.
|
|
* - `Action` is the one button shape at its two weights, and `Leaf` is the
|
|
* card whose edge lifts, its hover a prop beside its resting edge.
|
|
*
|
|
* The hero is NOT a Band: it is a 90vh centred stage rather than a section of
|
|
* rhythm, and `Band` has neither the height nor the centring to say so.
|
|
*/
|
|
|
|
import type { ReactNode } from "react"
|
|
import { motion } from "@/components/motion"
|
|
import { ProductFooter } from "@/components/products/ProductFooter"
|
|
import {
|
|
Globe,
|
|
ArrowRight,
|
|
Layers,
|
|
Zap,
|
|
Terminal,
|
|
Code2,
|
|
Shield,
|
|
Server,
|
|
Lock,
|
|
Cloud,
|
|
Network,
|
|
Rocket,
|
|
Check,
|
|
} from "lucide-react"
|
|
import { Box, View, XStack, YStack } from '@hanzo/ui'
|
|
import { Grid } from '@hanzo/ui/grid'
|
|
import { Band, Head } from '@/components/marketing/band'
|
|
import { AgentPrompt } from '@/components/product/AgentPrompt'
|
|
import { product } from '@/lib/product'
|
|
import { Action, Chip, Claim, Display, Lede, Title, Leaf, Line, type LineProps } from '@hanzo/ui/marketing'
|
|
|
|
/** The prompt's product, off the one table that holds a product's name, route,
|
|
* audited command and docs page. `ProductFooter` closes the page. */
|
|
const PRODUCT = product('dns')
|
|
|
|
/** How narrow a column may get before a grid drops one, per shape. */
|
|
const DECK = { min: 304, max: 3 } as const
|
|
const PAIR = { min: 400, max: 2 } as const
|
|
const TIERS = { min: 280, max: 3 } as const
|
|
const STATS = { min: 150, max: 4 } as const
|
|
|
|
/** The inks this page draws body copy and marks in. */
|
|
const SOFT = 'color-mix(in oklab, var(--foreground) 80%, transparent)'
|
|
const WASH = 'color-mix(in oklab, var(--primary) 10%, transparent)'
|
|
const TILE = 'color-mix(in oklab, var(--secondary) 50%, transparent)'
|
|
|
|
/** The card whose edge lifts on approach — `Leaf`, at this page's two ends. */
|
|
const LEAF = { p: 24, borderColor: "var(--border)", bg: TILE, hoverStyle: { borderColor: "var(--neutral-600)" } } as const
|
|
|
|
/** The hairline tile a stat and a plan sit on. A `color-mix()` ink is not a
|
|
* value gui's colour props type, so it rides `$platform-web`, which gui still
|
|
* writes as an atomic class. */
|
|
const TILE_BOX = { rounded: 'var(--radius-xl)', borderWidth: 1, borderColor: 'var(--border)', '$platform-web': { backgroundColor: TILE } } as const
|
|
|
|
/** A card's heading and the paragraph under it. */
|
|
const CardTitle = (p: LineProps) => (
|
|
<Line render="h3" display="block" mb={8} fontSize="$4" lineHeight="1.5556" fontWeight="600" color="var(--foreground)" {...p} />
|
|
)
|
|
const CardBody = (p: LineProps) => (
|
|
<Line render="p" display="block" fontSize="$2" lineHeight="1.4286" color="var(--muted-foreground)" {...p} />
|
|
)
|
|
|
|
/** The mark's plate, at the two sizes this page draws it. */
|
|
const Plate = ({ size, children, ...p }: { size: number; children: ReactNode; mb?: number }) => (
|
|
<XStack items="center" justify="center" width={size} height={size} shrink={0} rounded="var(--radius-lg)" {...p} $platform-web={{ backgroundColor: WASH }}>
|
|
{children}
|
|
</XStack>
|
|
)
|
|
|
|
/** The terminal frame: a card whose top bar carries three lamps and a file name. */
|
|
const FRAME = { overflow: 'hidden', rounded: 'var(--radius-xl)', borderWidth: 1, borderColor: 'var(--border)', bg: 'var(--secondary)' } as const
|
|
|
|
function FrameBar({ file }: { file: string }) {
|
|
return (
|
|
<XStack items="center" gap={8} px={16} py={8} borderBottomWidth={1} borderColor="var(--border)">
|
|
<XStack gap={6}>
|
|
{[0, 1, 2].map((i) => (
|
|
<Box key={i} width={12} height={12} rounded="var(--radius-full)" bg="var(--neutral-700)" />
|
|
))}
|
|
</XStack>
|
|
<Line ml={8} fontSize="$1" lineHeight="1.3333" color="var(--muted-foreground)">
|
|
{file}
|
|
</Line>
|
|
</XStack>
|
|
)
|
|
}
|
|
|
|
/** A named box in the architecture diagram, its heading, and a cell inside it. */
|
|
const GROUP = { p: 16, rounded: 'var(--radius-lg)', borderWidth: 1, borderColor: 'var(--border)', bg: 'var(--secondary)' } as const
|
|
const GROUP_LIT = {
|
|
...GROUP,
|
|
'$platform-web': {
|
|
borderColor: 'color-mix(in oklab, var(--primary) 20%, transparent)',
|
|
backgroundColor: 'color-mix(in oklab, var(--primary) 5%, transparent)',
|
|
},
|
|
} as const
|
|
const CHIP = {
|
|
display: 'block',
|
|
p: 8,
|
|
rounded: 'var(--radius)',
|
|
borderWidth: 1,
|
|
borderColor: 'var(--border)',
|
|
bg: 'color-mix(in oklab, var(--background) 50%, transparent)',
|
|
fontSize: '$1',
|
|
lineHeight: '1.3333',
|
|
text: 'center',
|
|
color: 'var(--muted-foreground)',
|
|
} as const
|
|
|
|
/** The two controls this page draws, everywhere it draws them. */
|
|
const FILL = { px: 32 } as const
|
|
const OUTLINE = { px: 32, borderColor: "var(--border)" } as const
|
|
|
|
const heroStats = [
|
|
{ value: "5+", label: "Providers" },
|
|
{ value: "CLI", label: "Native" },
|
|
{ value: "DNSSEC", label: "Built-in" },
|
|
{ value: "Edge", label: "CoreDNS" },
|
|
]
|
|
|
|
const controlPlane = [
|
|
{ title: "Your Applications", columns: 3, accent: false, cells: ["CI/CD", "CLI", "REST API"] },
|
|
{ title: "Hanzo DNS Control Plane", columns: 2, accent: true, cells: ["Record Manager", "DNSSEC Engine", "Parallel Query", "Audit Log"] },
|
|
{ title: "DNS Providers", columns: 3, accent: false, cells: ["Cloudflare", "Route53", "CoreDNS", "GoDaddy", "DigitalOcean", "+ More"] },
|
|
] as const
|
|
|
|
const guarantees = [
|
|
{ icon: Server, text: "Provider-agnostic record management with automatic conflict resolution" },
|
|
{ icon: Lock, text: "Audit logging for every DNS change with rollback support" },
|
|
{ icon: Zap, text: "Sub-second propagation checks across global resolvers" },
|
|
{ icon: Shield, text: "DNSSEC key management with automated rotation schedules" },
|
|
] as const
|
|
|
|
const features = [
|
|
{
|
|
icon: Layers,
|
|
title: "Multi-Provider",
|
|
description:
|
|
"Unified API across Cloudflare, CoreDNS, Route53, GoDaddy, and DigitalOcean. Manage all your DNS from one interface.",
|
|
},
|
|
{
|
|
icon: Zap,
|
|
title: "Parallel Queries",
|
|
description:
|
|
"Query all providers simultaneously and merge results. Get a unified view of your DNS records across every provider.",
|
|
},
|
|
{
|
|
icon: Terminal,
|
|
title: "CLI Native",
|
|
description:
|
|
"Full CLI control with hanzo dns zones list, hanzo dns zones records create, and more. Script and automate your DNS workflows natively.",
|
|
},
|
|
{
|
|
icon: Code2,
|
|
title: "API First",
|
|
description:
|
|
"REST API for programmatic DNS management. Integrate DNS operations into your CI/CD pipelines and infrastructure code.",
|
|
},
|
|
{
|
|
icon: Shield,
|
|
title: "DNSSEC",
|
|
description:
|
|
"Built-in DNSSEC signing and automatic key rotation. Protect your domains from spoofing and cache poisoning attacks.",
|
|
},
|
|
{
|
|
icon: Globe,
|
|
title: "Edge DNS",
|
|
description:
|
|
"CoreDNS at the edge with Hanzo plugins. Low-latency resolution with custom middleware for advanced routing logic.",
|
|
},
|
|
]
|
|
|
|
const useCases = [
|
|
{
|
|
icon: Rocket,
|
|
title: "DevOps Automation",
|
|
description:
|
|
"Automate DNS record management as part of your deployment pipeline. Create, update, and clean up records programmatically.",
|
|
},
|
|
{
|
|
icon: Cloud,
|
|
title: "Multi-Cloud DNS",
|
|
description:
|
|
"Manage DNS across AWS, Cloudflare, and DigitalOcean from a single API. No more switching consoles.",
|
|
},
|
|
{
|
|
icon: Network,
|
|
title: "Domain Portfolio Management",
|
|
description:
|
|
"Manage hundreds of domains across multiple registrars and DNS providers from one unified interface.",
|
|
},
|
|
]
|
|
|
|
// PRICING IS CUSTOM HERE, deliberately.
|
|
//
|
|
// These tiers used to publish $20 Pro, $25 Team, $200 Enterprise and "$0.10 per
|
|
// 10K queries". Commerce — GET /v1/billing/plans, category `dns`, the rows that
|
|
// actually charge — carries three rows at $0 / $5 / $25, no Team row at any
|
|
// price, and no per-query rate anywhere. So Pro was advertised at 4x what it
|
|
// bills, Enterprise at 8x, and one tier did not exist.
|
|
//
|
|
// Rather than republish the charged numbers, the owner decision is that the only
|
|
// prices Hanzo publishes are personal 20/100/200 and team $25/seat. Everything
|
|
// else is a conversation, because an enterprise deal can be shaped many ways and
|
|
// a number on a page forecloses that. The feature comparison stays — it is what
|
|
// a reader is actually here for.
|
|
const pricingTiers = [
|
|
{
|
|
name: "Starter",
|
|
price: "Custom",
|
|
period: "",
|
|
description: "Usage-based pricing for getting started",
|
|
features: [
|
|
"Up to 5 zones",
|
|
"2 DNS providers",
|
|
"CLI and API access",
|
|
"Volume-based query pricing",
|
|
],
|
|
cta: "Talk to us",
|
|
ctaHref: "/contact-sales",
|
|
highlight: false,
|
|
},
|
|
{
|
|
name: "Pro",
|
|
price: "Custom",
|
|
period: "/month",
|
|
description: "For teams managing production DNS",
|
|
features: [
|
|
"Unlimited zones",
|
|
"All DNS providers",
|
|
"DNSSEC management",
|
|
"Audit logging",
|
|
"Priority support",
|
|
],
|
|
cta: "Talk to us",
|
|
ctaHref: "/contact-sales",
|
|
highlight: true,
|
|
},
|
|
{
|
|
name: "Team",
|
|
price: "Custom",
|
|
period: "/month",
|
|
description: "For teams with shared DNS management",
|
|
features: [
|
|
"Everything in Pro",
|
|
"Team collaboration",
|
|
"Role-based access",
|
|
"Shared audit log",
|
|
"Priority support",
|
|
],
|
|
cta: "Talk to us",
|
|
ctaHref: "/contact-sales",
|
|
highlight: false,
|
|
},
|
|
{
|
|
name: "Enterprise",
|
|
price: "Custom",
|
|
period: "/month",
|
|
description: "For organizations with advanced needs",
|
|
features: [
|
|
"Everything in Team",
|
|
"Edge DNS (CoreDNS)",
|
|
"Custom plugins",
|
|
"SLA guarantee",
|
|
"Dedicated support",
|
|
],
|
|
cta: "Contact Sales",
|
|
ctaHref: "/contact-sales",
|
|
highlight: false,
|
|
},
|
|
]
|
|
|
|
export default function DNSPage({ cli }: { cli: ReactNode }) {
|
|
return (
|
|
<>
|
|
{/* Hero Section */}
|
|
<XStack
|
|
render="section"
|
|
position="relative"
|
|
minH="90vh"
|
|
items="center"
|
|
justify="center"
|
|
overflow="hidden"
|
|
py={80}
|
|
px={16}
|
|
$sm={{ px: 24 }}
|
|
$lg={{ px: 32 }}
|
|
>
|
|
|
|
<Box position="relative" z="var(--z-raised)" width="100%" maxW={1024} mx="auto" $platform-web={{ textAlign: 'center' }}>
|
|
<Chip
|
|
render={<motion.div initial={{ opacity: 0, y: 20 }} animate={{ opacity: 1, y: 0 }} transition={{ duration: 0.5 }} />}
|
|
mb={32}
|
|
px={16}
|
|
py={8}
|
|
borderColor="var(--border)"
|
|
bg="color-mix(in oklab, var(--primary) 5%, transparent)"
|
|
>
|
|
<Globe width={16} height={16} color="var(--foreground)" />
|
|
<Line fontWeight="500" color={SOFT}>
|
|
Programmable DNS Infrastructure
|
|
</Line>
|
|
</Chip>
|
|
|
|
<Display
|
|
render={<motion.h1 initial={{ opacity: 0, y: 20 }} animate={{ opacity: 1, y: 0 }} transition={{ duration: 0.5, delay: 0.1 }} />}
|
|
mb={24}
|
|
>
|
|
Hanzo DNS
|
|
</Display>
|
|
|
|
<Claim
|
|
render={<motion.p initial={{ opacity: 0, y: 20 }} animate={{ opacity: 1, y: 0 }} transition={{ duration: 0.5, delay: 0.15 }} />}
|
|
mb={16}
|
|
fontWeight="500"
|
|
>
|
|
Multi-provider DNS management
|
|
</Claim>
|
|
|
|
<Lede
|
|
render={<motion.p initial={{ opacity: 0, y: 20 }} animate={{ opacity: 1, y: 0 }} transition={{ duration: 0.5, delay: 0.2 }} />}
|
|
mb={32}
|
|
width="100%"
|
|
maxW={768}
|
|
mx="auto"
|
|
>
|
|
One API for Cloudflare, CoreDNS, Route53, and more. Parallel queries,
|
|
DNSSEC, audit logging, and CLI-native workflows for infrastructure teams.
|
|
</Lede>
|
|
|
|
<Box
|
|
render={<motion.div initial={{ opacity: 0, y: 20 }} animate={{ opacity: 1, y: 0 }} transition={{ duration: 0.5, delay: 0.25 }} />}
|
|
mb={48}
|
|
width="100%"
|
|
maxW={768}
|
|
mx="auto"
|
|
>
|
|
<Grid columns={STATS} gap={16}>
|
|
{heroStats.map((stat) => (
|
|
<Box key={stat.label} p={16} {...TILE_BOX}>
|
|
<Line render="div" display="block" fontSize="$7" lineHeight="1.3333" fontWeight="700" color="var(--foreground)">
|
|
{stat.value}
|
|
</Line>
|
|
<CardBody render="div">{stat.label}</CardBody>
|
|
</Box>
|
|
))}
|
|
</Grid>
|
|
</Box>
|
|
|
|
<motion.div
|
|
initial={{ opacity: 0, y: 20 }}
|
|
animate={{ opacity: 1, y: 0 }}
|
|
transition={{ duration: 0.5, delay: 0.3 }}
|
|
>
|
|
<XStack flexWrap="wrap" justify="center" gap={16}>
|
|
{/* docs publishes no DNS page: `/docs/services/dns` is a 404 and
|
|
`/docs/dns` is a `Moved` stub whose canonical 404s too. The way
|
|
in is the key, which is what every other product page offers. */}
|
|
<Action
|
|
href="/login"
|
|
fill
|
|
{...FILL}
|
|
>
|
|
Try Hanzo
|
|
<ArrowRight width={16} height={16} />
|
|
</Action>
|
|
<Action
|
|
href="https://github.com/hanzoai"
|
|
{...OUTLINE}
|
|
>
|
|
GitHub
|
|
</Action>
|
|
</XStack>
|
|
</motion.div>
|
|
</Box>
|
|
</XStack>
|
|
|
|
{PRODUCT && <AgentPrompt product={PRODUCT} />}
|
|
|
|
{/* Features Grid */}
|
|
<Band pad="$11" measure={1280}>
|
|
<Head
|
|
title="One API. Every Provider."
|
|
lede="Stop juggling provider consoles. Every zone you own, in one place."
|
|
/>
|
|
|
|
<Grid columns={DECK} gap={24}>
|
|
{features.map((feature, index) => (
|
|
<Leaf
|
|
render={<motion.div initial={{ opacity: 0, y: 20 }} whileInView={{ opacity: 1, y: 0 }} viewport={{ once: true }} transition={{ duration: 0.5, delay: index * 0.05 }} />}
|
|
key={feature.title}
|
|
{...LEAF}
|
|
hoverStyle={{ borderColor: "var(--neutral-600)" }}
|
|
>
|
|
<Plate size={48} mb={16}>
|
|
<feature.icon width={24} height={24} color="var(--foreground)" />
|
|
</Plate>
|
|
<CardTitle>{feature.title}</CardTitle>
|
|
<CardBody>{feature.description}</CardBody>
|
|
</Leaf>
|
|
))}
|
|
</Grid>
|
|
</Band>
|
|
|
|
{/* Architecture Section */}
|
|
<Band pad="$11" measure={1280}>
|
|
<Grid columns={PAIR} gap={48} items="center">
|
|
<motion.div
|
|
initial={{ opacity: 0, x: -20 }}
|
|
whileInView={{ opacity: 1, x: 0 }}
|
|
viewport={{ once: true }}
|
|
transition={{ duration: 0.5 }}
|
|
>
|
|
<Title quiet mb={24}>
|
|
Built for Infrastructure Teams
|
|
</Title>
|
|
<Lede mb={24} mt="$3.5">
|
|
Hanzo DNS sits between your applications and DNS providers,
|
|
giving you a single control plane for all DNS operations.
|
|
</Lede>
|
|
<YStack render="ul" gap={16}>
|
|
{guarantees.map(({ icon: Icon, text }) => (
|
|
<XStack key={text} render="li" items="flex-start" gap={12}>
|
|
<View mt={2}>
|
|
<Icon width={20} height={20} color="var(--foreground)" />
|
|
</View>
|
|
<Line color="var(--muted-foreground)">{text}</Line>
|
|
</XStack>
|
|
))}
|
|
</YStack>
|
|
</motion.div>
|
|
|
|
{/* Architecture Diagram */}
|
|
<Box
|
|
render={<motion.div initial={{ opacity: 0, x: 20 }} whileInView={{ opacity: 1, x: 0 }} viewport={{ once: true }} transition={{ duration: 0.5, delay: 0.1 }} />}
|
|
p={32}
|
|
{...TILE_BOX}
|
|
>
|
|
<YStack gap={16}>
|
|
{controlPlane.map((group, index) => (
|
|
<YStack key={group.title} gap={16}>
|
|
{index > 0 ? (
|
|
<XStack justify="center">
|
|
<Box width={1} height={24} bg="var(--border)" />
|
|
</XStack>
|
|
) : null}
|
|
<Box {...(group.accent ? GROUP_LIT : GROUP)}>
|
|
<Line render="h4" display="block" mb={8} fontSize="$2" lineHeight="1.4286" fontWeight="600" color="var(--foreground)" text={group.accent ? 'center' : undefined}>
|
|
{group.title}
|
|
</Line>
|
|
<Grid columns={group.columns} gap={8}>
|
|
{group.cells.map((cell) => (
|
|
<Line key={cell} render="div" {...CHIP}>
|
|
{cell}
|
|
</Line>
|
|
))}
|
|
</Grid>
|
|
</Box>
|
|
</YStack>
|
|
))}
|
|
</YStack>
|
|
</Box>
|
|
</Grid>
|
|
</Band>
|
|
|
|
{/* CLI / Quickstart Code Example */}
|
|
<Band pad="$11" measure={896}>
|
|
<Head
|
|
title="Get Started in Minutes"
|
|
lede="Three steps, and every zone is in one place"
|
|
/>
|
|
|
|
<Box
|
|
render={<motion.div initial={{ opacity: 0, y: 20 }} whileInView={{ opacity: 1, y: 0 }} viewport={{ once: true }} transition={{ duration: 0.5, delay: 0.1 }} />}
|
|
{...FRAME}
|
|
>
|
|
<FrameBar file="terminal" />
|
|
{cli}
|
|
</Box>
|
|
</Band>
|
|
|
|
{/* Use Cases */}
|
|
<Band pad="$11" measure={896}>
|
|
<Head
|
|
title="Use Cases"
|
|
lede="From startups to enterprise, Hanzo DNS scales with your infrastructure"
|
|
/>
|
|
|
|
<YStack gap={16}>
|
|
{useCases.map((useCase, index) => (
|
|
<Leaf
|
|
render={<motion.div initial={{ opacity: 0, y: 20 }} whileInView={{ opacity: 1, y: 0 }} viewport={{ once: true }} transition={{ duration: 0.5, delay: index * 0.05 }} />}
|
|
key={useCase.title}
|
|
{...LEAF}
|
|
hoverStyle={{ borderColor: "var(--neutral-600)" }}
|
|
>
|
|
<XStack items="flex-start" gap={16}>
|
|
<Plate size={40}>
|
|
<useCase.icon width={20} height={20} color="var(--foreground)" />
|
|
</Plate>
|
|
<Box>
|
|
<CardTitle>{useCase.title}</CardTitle>
|
|
<CardBody>{useCase.description}</CardBody>
|
|
</Box>
|
|
</XStack>
|
|
</Leaf>
|
|
))}
|
|
</YStack>
|
|
</Band>
|
|
|
|
{/* Pricing */}
|
|
<Band pad="$11" measure={1024}>
|
|
<Head
|
|
title="Pricing"
|
|
lede="Start free. Scale as you grow."
|
|
/>
|
|
|
|
<Grid columns={TIERS} gap={24}>
|
|
{pricingTiers.map((tier, index) => (
|
|
<Box
|
|
key={tier.name}
|
|
render={<motion.div initial={{ opacity: 0, y: 20 }} whileInView={{ opacity: 1, y: 0 }} viewport={{ once: true }} transition={{ duration: 0.5, delay: index * 0.05 }} />}
|
|
position="relative"
|
|
p={24}
|
|
{...TILE_BOX}
|
|
borderColor={tier.highlight ? 'var(--primary)' : 'var(--border)'}
|
|
>
|
|
{tier.highlight && (
|
|
<Line
|
|
render="div"
|
|
display="block"
|
|
position="absolute"
|
|
t={-12}
|
|
l="50%"
|
|
x="-50%"
|
|
px={12}
|
|
py={2}
|
|
rounded="var(--radius-full)"
|
|
bg="var(--primary)"
|
|
color="var(--primary-foreground)"
|
|
fontSize="$1"
|
|
lineHeight="1.3333"
|
|
fontWeight="500"
|
|
>
|
|
Popular
|
|
</Line>
|
|
)}
|
|
<CardTitle>{tier.name}</CardTitle>
|
|
<Line render="p" display="block" mb={4} fontSize="$8" lineHeight="1.2" fontWeight="700" color="var(--foreground)">
|
|
{tier.price}
|
|
<Line fontSize="$2" lineHeight="1.4286" fontWeight="400" color="var(--muted-foreground)">
|
|
{tier.period}
|
|
</Line>
|
|
</Line>
|
|
<CardBody mb={24}>{tier.description}</CardBody>
|
|
<YStack render="ul" gap={8} mb={24}>
|
|
{tier.features.map((feature) => (
|
|
<XStack key={feature} render="li" items="center" gap={8}>
|
|
<Check width={16} height={16} color="var(--foreground)" />
|
|
<CardBody render="span" display="inline">
|
|
{feature}
|
|
</CardBody>
|
|
</XStack>
|
|
))}
|
|
</YStack>
|
|
<Action
|
|
href={tier.ctaHref}
|
|
fill={tier.highlight}
|
|
width="100%"
|
|
justify="center"
|
|
px={24}
|
|
{...(tier.highlight ? null : { borderColor: 'var(--border)' })}
|
|
>
|
|
{tier.cta}
|
|
</Action>
|
|
</Box>
|
|
))}
|
|
</Grid>
|
|
</Band>
|
|
|
|
<ProductFooter slug="dns" name="DNS" />
|
|
</>
|
|
)
|
|
}
|