mirror of
https://github.com/hanzo-apps/hanzo.ai.git
synced 2026-10-30 14:58:08 +00:00
The product pages from /s3 to /zap, /university and its course data, /trust, /security, /sdks, /support, /tasks, /startups, /sms-opt-in, the buying specialist, Zen, Kai's guide, Studio, the settings pages, and the shared sales, solutions, zap and ui pieces. Client code draws with t(), server components with <T>, copy held as data is marked with phrase() and drawn through t() where it lands. A link or a figure inside a sentence is a value, so a translator reorders the whole sentence. Rebased on main: every page main moved onto Fold or Steps keeps main's layout, with its words wrapped (label, title, lede, points). Steps now draws a step's body through t, as it already drew the title. Settings keeps main's usage panel, refresh, rewards wallet and sign-in wallet rows, each label wrapped. Left English: the skills page's install line (a command and two URLs), and the solution eyebrow and sibling heading built from a kind label. Claude-Session: https://claude.ai/code/session_015XYKLjZtDTJEnMo4eK3AFH
583 lines
21 KiB
TypeScript
583 lines
21 KiB
TypeScript
"use client"
|
|
|
|
/**
|
|
* /tasks — durable workflow execution, and what it costs.
|
|
*
|
|
* NO TAILWIND. The six 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`, `Chip`, `Action`, `Leaf`), 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. A plan card keeps `flexBasis="0%"` on its middle pane rather than
|
|
* gui's `flex={1}`, which writes a definite `0px` basis — the same `flex: 1 1 0%`
|
|
* the utility meant, said where it cannot be mistaken for a height.
|
|
*/
|
|
|
|
import type { ReactNode } from "react"
|
|
import { motion } from "@/components/motion"
|
|
import { ProductFooter } from "@/components/products/ProductFooter"
|
|
import {
|
|
ListChecks,
|
|
ArrowRight,
|
|
RotateCcw,
|
|
Shield,
|
|
Clock,
|
|
GitBranch,
|
|
Radio,
|
|
History,
|
|
Lock,
|
|
Users,
|
|
Bot,
|
|
ShoppingCart,
|
|
Workflow,
|
|
CalendarClock,
|
|
UserCheck,
|
|
Check,
|
|
} from "lucide-react"
|
|
import { Box, 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 { Related } from '@/components/product/Related'
|
|
import { product } from '@/lib/product'
|
|
import { Action, Chip, Claim, Display, Lede, Leaf, Line, type LineProps } from '@hanzo/ui/marketing'
|
|
import { phrase } from '@hanzo/i18n'
|
|
import { useI18n } from '@hanzo/i18n/react'
|
|
|
|
/** What the two shared blocks say about this product: the audited command, the
|
|
* route the catalog proves it by, and the docs page or null. One table answers
|
|
* for every product page, so this one cannot say something its neighbours do
|
|
* not. */
|
|
const PRODUCT = product('tasks')
|
|
|
|
/** How narrow a column may get before a grid drops one, per shape. */
|
|
const DECK = { min: 304, max: 3 } as const
|
|
const DUO = { min: 320, max: 2 } as const
|
|
const PLANS = { min: 240, max: 4 } 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)'
|
|
const TILE_SOFT = 'color-mix(in oklab, var(--secondary) 30%, transparent)'
|
|
|
|
/** The card whose edge lifts on approach — `Leaf`, at this page's two washes. */
|
|
const LEAF = { p: 24, borderColor: 'var(--border)', hoverStyle: { borderColor: 'var(--neutral-600)' }, bg: TILE } as const
|
|
|
|
const LEAF_SOFT = { ...LEAF, bg: TILE_SOFT } as const
|
|
|
|
/** The hairline tile a stat and a plan sit on, and the plan that is lit. 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
|
|
const LIT = {
|
|
rounded: 'var(--radius-xl)',
|
|
borderWidth: 2,
|
|
'$platform-web': { backgroundColor: WASH, borderColor: 'color-mix(in oklab, var(--foreground) 20%, transparent)' },
|
|
} 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 }: { size: number; children: ReactNode }) => (
|
|
<XStack items="center" justify="center" mb={16} width={size} height={size} shrink={0} rounded="var(--radius-lg)" $platform-web={{ backgroundColor: WASH }}>
|
|
{children}
|
|
</XStack>
|
|
)
|
|
|
|
/** The source 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>
|
|
)
|
|
}
|
|
|
|
/** The code inside a frame. `whiteSpace="pre"` because a gui Text wraps by
|
|
* default and a code line scrolls; `marginBlock` is the `1em` a `<pre>` keeps,
|
|
* which gui's Text would zero. */
|
|
const Source = ({ children, ...p }: LineProps) => (
|
|
<Line render="pre" display="block" marginBlock="1em" p={16} overflowX="auto" whiteSpace="pre" fontSize="$2" lineHeight="1.4286" color="inherit" {...p}>
|
|
<Line render="code" color={SOFT}>
|
|
{children}
|
|
</Line>
|
|
</Line>
|
|
)
|
|
|
|
/** The filled control this page draws, everywhere it draws one. */
|
|
const FILL = { px: 32 } as const
|
|
|
|
const heroStats = [
|
|
{ value: phrase("Durable"), label: phrase("Execution") },
|
|
{ value: phrase("4 SDKs"), label: "Go / Py / TS / Java" },
|
|
{ value: phrase("Sagas"), label: phrase("Built-in") },
|
|
{ value: phrase("Free"), label: phrase("25K actions/mo") },
|
|
]
|
|
|
|
|
|
const architecture = `
|
|
+------------------+ +-------------------+
|
|
| Your Code | | Hanzo Tasks |
|
|
| | | Server |
|
|
| Workflow Def ---------> Scheduler |
|
|
| Activity Impl --------> Event History |
|
|
| Worker Pool ----------> State Store |
|
|
| | | |
|
|
+------------------+ +--------+----------+
|
|
|
|
|
+---------------+---------------+
|
|
| | |
|
|
+-----------+ +-----------+ +-----------+
|
|
| Hanzo SQL | | Hanzo KV | | Hanzo KMS |
|
|
| (History) | | (Queues) | | (Secrets) |
|
|
+-----------+ +-----------+ +-----------+
|
|
`
|
|
|
|
const pricingTiers = [
|
|
{
|
|
name: phrase("Free"),
|
|
price: "$0",
|
|
period: phrase("/mo"),
|
|
description: phrase("For prototyping and small projects"),
|
|
actions: phrase("25K actions/mo"),
|
|
namespaces: phrase("1 namespace"),
|
|
features: [
|
|
phrase("Durable execution"),
|
|
phrase("Automatic retries"),
|
|
phrase("Web console"),
|
|
phrase("Community support"),
|
|
],
|
|
cta: phrase("Try Hanzo"),
|
|
ctaHref: "/login",
|
|
highlight: false,
|
|
},
|
|
{
|
|
name: phrase("Pro"),
|
|
price: "$0.035",
|
|
period: phrase("/1K actions"),
|
|
description: phrase("1M actions included per month"),
|
|
actions: phrase("1M actions included"),
|
|
namespaces: phrase("10 namespaces"),
|
|
features: [
|
|
phrase("Everything in Free"),
|
|
phrase("Scheduled workflows"),
|
|
phrase("Signals & queries"),
|
|
phrase("Email support"),
|
|
],
|
|
cta: phrase("Start Pro"),
|
|
ctaHref: "https://platform.hanzo.ai",
|
|
highlight: true,
|
|
},
|
|
{
|
|
name: phrase("Team"),
|
|
price: "$0.025",
|
|
period: phrase("/1K actions"),
|
|
description: phrase("10M actions included per month"),
|
|
actions: phrase("10M actions included"),
|
|
namespaces: phrase("Unlimited"),
|
|
features: [
|
|
phrase("Everything in Pro"),
|
|
phrase("Multi-tenant isolation"),
|
|
phrase("IAM integration"),
|
|
phrase("Priority support"),
|
|
],
|
|
cta: phrase("Start Team"),
|
|
ctaHref: "https://platform.hanzo.ai",
|
|
highlight: false,
|
|
},
|
|
{
|
|
name: phrase("Enterprise"),
|
|
price: phrase("Custom"),
|
|
period: "",
|
|
description: phrase("Dedicated resources & SLAs"),
|
|
actions: phrase("Unlimited actions"),
|
|
namespaces: phrase("Unlimited"),
|
|
features: [
|
|
phrase("Everything in Team"),
|
|
phrase("Dedicated cluster"),
|
|
phrase("Custom SLA"),
|
|
phrase("24/7 support"),
|
|
],
|
|
cta: phrase("Contact Sales"),
|
|
ctaHref: "/contact-sales",
|
|
highlight: false,
|
|
},
|
|
]
|
|
|
|
const features = [
|
|
{
|
|
icon: RotateCcw,
|
|
title: phrase("Durable Execution"),
|
|
description:
|
|
phrase("Workflows survive process crashes, server restarts, and network partitions. State is persisted automatically."),
|
|
},
|
|
{
|
|
icon: GitBranch,
|
|
title: phrase("Saga Compensation"),
|
|
description:
|
|
phrase("Define compensating actions for every step. Automatic rollback on failure. Distributed transactions done right."),
|
|
},
|
|
{
|
|
icon: Clock,
|
|
title: phrase("Scheduled Workflows"),
|
|
description:
|
|
phrase("Cron expressions, delayed starts, and recurring schedules. Timezone-aware. No external scheduler needed."),
|
|
},
|
|
{
|
|
icon: Radio,
|
|
title: phrase("Signals & Queries"),
|
|
description:
|
|
phrase("Send signals to running workflows. Query workflow state at any point. Real-time coordination between workflows."),
|
|
},
|
|
{
|
|
icon: History,
|
|
title: phrase("Full History & Replay"),
|
|
description:
|
|
phrase("Complete event history for every workflow execution. Deterministic replay for debugging. Audit trail built in."),
|
|
},
|
|
{
|
|
icon: Lock,
|
|
title: phrase("KMS Secrets"),
|
|
description:
|
|
phrase("Inject secrets from Hanzo KMS into workflow activities. Rotated automatically. Never stored in workflow history."),
|
|
},
|
|
{
|
|
icon: Shield,
|
|
title: phrase("IAM Integration"),
|
|
description:
|
|
phrase("Hanzo IAM for authentication and authorization. Namespace-level RBAC. Org-scoped data isolation."),
|
|
},
|
|
{
|
|
icon: Users,
|
|
title: phrase("Multi-Tenant"),
|
|
description:
|
|
phrase("Namespace isolation per team or customer. Resource quotas. Independent scaling. Shared nothing architecture."),
|
|
},
|
|
{
|
|
icon: Workflow,
|
|
title: phrase("Child Workflows"),
|
|
description:
|
|
phrase("Spawn child workflows from parents. Fan-out/fan-in patterns. Hierarchical execution with independent lifecycles."),
|
|
},
|
|
]
|
|
|
|
const useCases = [
|
|
{
|
|
icon: Bot,
|
|
title: phrase("AI Agent Orchestration"),
|
|
description:
|
|
phrase("Coordinate multi-step agent workflows with tool calls, human approval gates, and automatic retry on LLM failures. Durable state means agents can run for hours or days."),
|
|
},
|
|
{
|
|
icon: ShoppingCart,
|
|
title: phrase("Commerce Sagas"),
|
|
description:
|
|
phrase("Payment capture, inventory reservation, shipping, and notification as a single durable saga. Automatic compensation if any step fails. No lost orders."),
|
|
},
|
|
{
|
|
icon: Workflow,
|
|
title: phrase("CI/CD Pipelines"),
|
|
description:
|
|
phrase("Build, test, deploy, and verify as a durable workflow. Retry flaky tests. Wait for manual approval. Roll back on failure. Full execution history."),
|
|
},
|
|
{
|
|
icon: CalendarClock,
|
|
title: phrase("Scheduled Jobs"),
|
|
description:
|
|
phrase("Replace fragile cron with durable scheduled workflows. Timezone-aware. Exactly-once execution guarantees. Built-in monitoring and alerting."),
|
|
},
|
|
{
|
|
icon: UserCheck,
|
|
title: phrase("Human-in-the-Loop"),
|
|
description:
|
|
phrase("Pause workflows waiting for human input. Approval gates, review steps, and escalation timeouts. Resume exactly where you left off."),
|
|
},
|
|
]
|
|
|
|
/** A workflow in one SDK, its code highlighted by the server page. */
|
|
export interface Sample {
|
|
file: string
|
|
delay: number
|
|
code: ReactNode
|
|
}
|
|
|
|
export default function TasksPage({ samples }: { samples: Sample[] }) {
|
|
const { t } = useI18n()
|
|
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)"
|
|
>
|
|
<ListChecks width={16} height={16} color="var(--foreground)" />
|
|
<Line fontWeight="500" color={SOFT}>
|
|
tasks.hanzo.ai
|
|
</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 Tasks
|
|
</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"
|
|
>
|
|
{t('Durable Workflow Execution')}
|
|
</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"
|
|
>
|
|
{t('Build invincible applications. Workflows that survive crashes, retries that never give up, and sagas that always compensate. Your code runs to completion or rolls back cleanly. Every time.')}
|
|
</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)">
|
|
{t(stat.value)}
|
|
</Line>
|
|
<CardBody render="div">{t(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}>
|
|
<Action
|
|
href="/login"
|
|
fill
|
|
{...FILL}
|
|
>
|
|
{t('Try Hanzo')}
|
|
<ArrowRight width={16} height={16} data-i18n-mirror />
|
|
</Action>
|
|
</XStack>
|
|
</motion.div>
|
|
</Box>
|
|
</XStack>
|
|
|
|
{PRODUCT && <AgentPrompt product={PRODUCT} />}
|
|
|
|
{/* Features Grid */}
|
|
<Band pad="$11" measure={1280}>
|
|
<Head
|
|
title={t('Workflows That Never Fail')}
|
|
lede={t('Every primitive you need for building reliable distributed systems.')}
|
|
/>
|
|
|
|
<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}
|
|
>
|
|
<Plate size={48}>
|
|
<feature.icon width={24} height={24} color="var(--foreground)" />
|
|
</Plate>
|
|
<CardTitle>{t(feature.title)}</CardTitle>
|
|
<CardBody>{t(feature.description)}</CardBody>
|
|
</Leaf>
|
|
))}
|
|
</Grid>
|
|
</Band>
|
|
|
|
{/* Use Cases */}
|
|
<Band pad="$11" measure={1280}>
|
|
<Head
|
|
title={t('Built for Real Workloads')}
|
|
lede={t('From AI agents to payment sagas, Tasks handles the hard parts.')}
|
|
/>
|
|
|
|
<Grid columns={DECK} gap={24}>
|
|
{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_SOFT}
|
|
>
|
|
<Plate size={40}>
|
|
<useCase.icon width={20} height={20} color="var(--foreground)" />
|
|
</Plate>
|
|
<CardTitle>{t(useCase.title)}</CardTitle>
|
|
<CardBody>{t(useCase.description)}</CardBody>
|
|
</Leaf>
|
|
))}
|
|
</Grid>
|
|
</Band>
|
|
|
|
{/* SDK Code Examples */}
|
|
<Band pad="$11" measure={896}>
|
|
<Head
|
|
title={t('Native SDKs for Every Stack')}
|
|
lede={t('Write workflows in Go, Python, TypeScript, or Java. Same durable guarantees everywhere.')}
|
|
/>
|
|
|
|
<Grid columns={DUO} gap={24}>
|
|
{samples.map((sample) => (
|
|
<Box
|
|
key={sample.file}
|
|
render={<motion.div initial={{ opacity: 0, y: 20 }} whileInView={{ opacity: 1, y: 0 }} viewport={{ once: true }} transition={{ duration: 0.5, delay: sample.delay }} />}
|
|
{...FRAME}
|
|
>
|
|
<FrameBar file={sample.file} />
|
|
{sample.code}
|
|
</Box>
|
|
))}
|
|
</Grid>
|
|
</Band>
|
|
|
|
{/* Architecture */}
|
|
<Band pad="$11" measure={896}>
|
|
<Head
|
|
title={t('How It Works')}
|
|
lede={t('Durable execution powered by event sourcing.')}
|
|
/>
|
|
|
|
<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="architecture" />
|
|
<Source p={24}>
|
|
{architecture}
|
|
</Source>
|
|
</Box>
|
|
</Band>
|
|
|
|
{/* Pricing */}
|
|
<Band pad="$11" measure={1280}>
|
|
<Head
|
|
title={t('Simple, Predictable Pricing')}
|
|
lede={t('Pay per action. An action is a workflow start, activity execution, signal, or timer fire.')}
|
|
/>
|
|
|
|
<Grid columns={PLANS} gap={24}>
|
|
{pricingTiers.map((tier, index) => (
|
|
<YStack
|
|
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 }} />}
|
|
p={24}
|
|
{...(tier.highlight ? LIT : TILE_BOX)}
|
|
>
|
|
<Box mb={24}>
|
|
<CardTitle mb={4}>{t(tier.name)}</CardTitle>
|
|
<XStack items="baseline" gap={4}>
|
|
<Line fontSize="$8" lineHeight="1.2" fontWeight="700" color="var(--foreground)">
|
|
{t(tier.price)}
|
|
</Line>
|
|
<CardBody render="span" display="inline">
|
|
{tier.period ? t(tier.period) : tier.period}
|
|
</CardBody>
|
|
</XStack>
|
|
<CardBody mt={8}>{t(tier.description)}</CardBody>
|
|
</Box>
|
|
|
|
<YStack gap={12} mb={24} grow={1} shrink={1} flexBasis="0%">
|
|
<CardBody render="div" fontWeight="500" color="var(--foreground)">
|
|
{t(tier.actions)}
|
|
</CardBody>
|
|
<CardBody render="div">{t(tier.namespaces)}</CardBody>
|
|
<Box borderTopWidth={1} borderColor="var(--border)" pt={12} mt={12}>
|
|
{tier.features.map((feature) => (
|
|
<XStack key={feature} items="center" gap={8} py={4}>
|
|
<Check width={14} height={14} color="color-mix(in oklab, var(--foreground) 60%, transparent)" />
|
|
<CardBody render="span" display="inline">
|
|
{t(feature)}
|
|
</CardBody>
|
|
</XStack>
|
|
))}
|
|
</Box>
|
|
</YStack>
|
|
|
|
<Action
|
|
href={tier.ctaHref}
|
|
fill={tier.highlight}
|
|
justify="center"
|
|
px={24}
|
|
{...(tier.highlight ? null : { borderColor: 'var(--border)' })}
|
|
>
|
|
{t(tier.cta)}
|
|
</Action>
|
|
</YStack>
|
|
))}
|
|
</Grid>
|
|
</Band>
|
|
|
|
{PRODUCT && <Related product={PRODUCT} />}
|
|
|
|
<ProductFooter slug="tasks" name="Tasks" />
|
|
</>
|
|
)
|
|
}
|