mirror of
https://github.com/hanzo-apps/hanzo.ai.git
synced 2026-10-29 03:30:33 +00:00
Every import, the catalog pin and the docs name @hanzo/app and github.com/hanzoai/app. lib/rooms.ts is lib/app.ts and components/rooms.tsx is components/app.tsx.
281 lines
9.7 KiB
TypeScript
281 lines
9.7 KiB
TypeScript
import type React from 'react'
|
|
import { Check } from 'lucide-react'
|
|
import { Github } from '@/components/brand'
|
|
import { AgentPrompt } from '@/components/product/AgentPrompt'
|
|
import { Related } from '@/components/product/Related'
|
|
import { Surface } from '@/components/product/Surface'
|
|
import { type CloudCategory, type Primitive } from '@/lib/data/cloud-primitives'
|
|
import { POSITIONING } from '@/lib/layers'
|
|
import { product } from '@/lib/product'
|
|
import { Box, Text, View, XStack } from '@/components/ui'
|
|
import { Action, Display, Lede, Line } from '@hanzo/ui/marketing'
|
|
import { tap } from '@hanzo/app/lib/tap'
|
|
import { Grid } from '@hanzo/ui/grid'
|
|
|
|
const STATUS_LABEL: Record<NonNullable<Primitive['status']>, string> = {
|
|
ga: 'Generally available',
|
|
beta: 'Beta',
|
|
}
|
|
|
|
/**
|
|
* Canonical overview page for any cloud primitive that doesn't have a bespoke
|
|
* marketing page yet. Data-driven, monochrome, and self-contained so every
|
|
* mega-menu leaf resolves to a real, unique page — never a 404, never an empty
|
|
* stub. One component renders them all (DRY).
|
|
*
|
|
* The FACTS are what keep the second half of that promise. The taxonomy is
|
|
* hydrated from the commerce catalog now, so a product can reach this page with
|
|
* no marketing copy written for it yet — and a page holding a name and two CTAs
|
|
* is the empty stub this component exists to avoid. What the catalog does state
|
|
* about every product is specific and true: the operation that answers for it,
|
|
* and the Google Cloud service a reader may already know it by. Those go above
|
|
* the copy, so a page without prose still says what the thing is and where its
|
|
* API is, rather than pretending to say more.
|
|
*
|
|
* The two blocks a written product page ends with are here too — the agent
|
|
* prompt under the hero, `Related` last — off the same `lib/product`
|
|
* value, so a generated page offers what a written one offers and says the
|
|
* command and the docs link the audit verified, or says neither.
|
|
*/
|
|
/**
|
|
* The two strengths of the foreground this page reads in: 80% for what the
|
|
* product says, 70% for the category in the crumb. Theme tokens with an
|
|
* opacity, so both follow the ink when the theme flips.
|
|
*/
|
|
const INK_80 = '$ink/80'
|
|
const INK_70 = '$ink/70'
|
|
|
|
/** A word in an outline: the product's status and its two chips. */
|
|
const PILL = {
|
|
display: 'inline-flex',
|
|
items: 'center',
|
|
gap: '$2',
|
|
borderWidth: 1,
|
|
borderColor: '$edge',
|
|
rounded: 'var(--radius-full)',
|
|
bg: 'var(--glass)',
|
|
fontSize: '$1',
|
|
lineHeight: '$1',
|
|
color: 'var(--text-secondary)',
|
|
} as const
|
|
|
|
/** One fact of a definition list: its term in a fixed column, then the value. */
|
|
function Fact({ term, mono, children }: { term: string; mono?: boolean; children: React.ReactNode }) {
|
|
return (
|
|
<XStack gap="$4">
|
|
<Text render="dt" width={96} shrink={0} color="var(--muted-foreground)">
|
|
{term}
|
|
</Text>
|
|
<Text render="dd" m={0} minW={0} fontFamily={mono ? '$mono' : undefined} color={INK_80}>
|
|
{children}
|
|
</Text>
|
|
</XStack>
|
|
)
|
|
}
|
|
|
|
export function CloudPrimitiveOverview({
|
|
primitive,
|
|
category,
|
|
}: {
|
|
primitive: Primitive
|
|
category?: CloudCategory
|
|
}) {
|
|
const Icon = primitive.icon
|
|
// The same value the written product pages hand those two blocks, so a
|
|
// generated page says what one of them says, in the same words.
|
|
const about = product(primitive.slug)
|
|
|
|
return (
|
|
<Box minH="100vh" $platform-web={{ color: 'var(--foreground)' }}>
|
|
<Box
|
|
render="section"
|
|
width="100%"
|
|
maxW={1024}
|
|
mx="auto"
|
|
px="$5"
|
|
pt={112}
|
|
pb="$12"
|
|
>
|
|
{/* Breadcrumb — reads like a cloud console path */}
|
|
<Text render="nav" display="flex" mb="$7" items="center" gap="$2" fontSize="$2" lineHeight="1.15rem" color="var(--muted-foreground)">
|
|
<Line href="/products" {...tap} color="var(--text-primary)">
|
|
Cloud
|
|
</Line>
|
|
{category && (
|
|
<>
|
|
<Text aria-hidden color="inherit">
|
|
/
|
|
</Text>
|
|
<Text color={INK_70}>{category.title}</Text>
|
|
</>
|
|
)}
|
|
</Text>
|
|
|
|
{/* Hero */}
|
|
<XStack items="flex-start" gap={20}>
|
|
<Text
|
|
display="flex"
|
|
width={56}
|
|
height={56}
|
|
shrink={0}
|
|
items="center"
|
|
justify="center"
|
|
rounded="var(--radius-2xl)"
|
|
borderWidth={1}
|
|
borderColor="$edge"
|
|
bg="var(--neutral-900)"
|
|
color="$ink"
|
|
>
|
|
{/* The catalog's icon takes `size` and a class and nothing else, so
|
|
its ink comes from `currentColor` on the tile around it. */}
|
|
<Icon size={28} />
|
|
</Text>
|
|
<Box minW={0}>
|
|
{primitive.status && (
|
|
<Text {...PILL} mb={12} py={2} px={10} fontWeight="500">
|
|
{STATUS_LABEL[primitive.status]}
|
|
</Text>
|
|
)}
|
|
<Display>{primitive.title}</Display>
|
|
{primitive.tagline && (
|
|
<Lede mt={12}>
|
|
{primitive.tagline}
|
|
</Lede>
|
|
)}
|
|
</Box>
|
|
</XStack>
|
|
|
|
{/* Open-source + on-chain chips */}
|
|
<XStack mt="$5" flexWrap="wrap" items="center" gap="$2">
|
|
<Text {...PILL} py={4} px={10}>
|
|
Open source
|
|
</Text>
|
|
<Text {...PILL} py={4} px={10}>
|
|
On-chain settlement
|
|
</Text>
|
|
</XStack>
|
|
|
|
{/* What the catalog states — the route that answers for this product,
|
|
and the Google Cloud service it stands in for. */}
|
|
{/* Two facts side by side above ~520px of measure, stacked under it. */}
|
|
<Text
|
|
render="dl"
|
|
display="grid"
|
|
gridTemplateColumns="repeat(auto-fit, minmax(min(240px, 100%), 1fr))"
|
|
columnGap={40}
|
|
rowGap={12}
|
|
m={0}
|
|
mt={32}
|
|
fontSize="$2"
|
|
lineHeight="$2"
|
|
>
|
|
{primitive.api && (
|
|
<Fact term="API" mono>
|
|
{primitive.api}
|
|
</Fact>
|
|
)}
|
|
{primitive.gcp && <Fact term="Google Cloud">{primitive.gcp}</Fact>}
|
|
</Text>
|
|
</Box>
|
|
|
|
{/* The prompt a reader hands to an agent, between what the product IS and
|
|
the product running. */}
|
|
{about && <AgentPrompt product={about} />}
|
|
|
|
<Box
|
|
render="section"
|
|
width="100%"
|
|
maxW={1024}
|
|
mx="auto"
|
|
px="$5"
|
|
pt="$12"
|
|
pb="$12"
|
|
>
|
|
{/* The product's own surface, drawn — its corner of the console, the
|
|
route it answers on, and one example project running in it. It is
|
|
composed from the same catalog this page is routed on, so a product
|
|
added to the catalog arrives here already drawn and there is no
|
|
second list to keep in step. */}
|
|
{category && (
|
|
<Surface
|
|
name={primitive.title}
|
|
api={primitive.api}
|
|
rail={category.id}
|
|
here={primitive.slug}
|
|
/>
|
|
)}
|
|
|
|
{/* Description */}
|
|
{primitive.description && (
|
|
<Text render="p" display="block" mt={32} maxW={768} fontSize="$3" lineHeight="1.4219rem" color={INK_80}>
|
|
{primitive.description}
|
|
</Text>
|
|
)}
|
|
|
|
{/* Features */}
|
|
{primitive.features && primitive.features.length > 0 && (
|
|
<Grid columns={{ min: 280, max: 2 }} gap={12} mt={40}>
|
|
{primitive.features.map((feature) => (
|
|
<XStack
|
|
key={feature}
|
|
items="flex-start"
|
|
gap="$3"
|
|
p="$4"
|
|
rounded="var(--radius-xl)"
|
|
borderWidth={1}
|
|
borderColor="$edge"
|
|
bg="$panel"
|
|
>
|
|
<View mt={2} shrink={0}>
|
|
<Check size={16} color="var(--foreground)" />
|
|
</View>
|
|
<Text fontSize="$2" lineHeight="1.15rem" color={INK_80}>
|
|
{feature}
|
|
</Text>
|
|
</XStack>
|
|
))}
|
|
</Grid>
|
|
)}
|
|
|
|
{/* Positioning line */}
|
|
<Text render="p" display="block" mt={56} fontSize="$2" lineHeight="1.15rem" color="var(--muted-foreground)">
|
|
{POSITIONING}
|
|
</Text>
|
|
|
|
{/* CTAs — anchors, not `Button asChild`.
|
|
`Button` is Tamagui, and a sub-theme makes it wrap its child in
|
|
`<span class="t_sub_theme" style="display:contents">`. That wrapper
|
|
needs the Tamagui provider context, which a SERVER component does not
|
|
carry: the server wrote a bare `<a role="button">` and the client
|
|
rendered the span, so every one of these pages failed hydration and
|
|
regenerated its whole tree. The sibling heroes never hit it because
|
|
both are 'use client'.
|
|
|
|
An anchor with the page's own tokens is what CloudLanding already
|
|
uses for exactly this row, so the shape is the house one — and it
|
|
drops a runtime from a page that only needed a few links.
|
|
|
|
The way in is the closing call's and the docs link is `Related`'s
|
|
below, which is why neither is here: one start action to a page, and
|
|
one docs link, from the map that says which product docs publishes. */}
|
|
<XStack mt="$5" flexWrap="wrap" gap="$3">
|
|
<Action href="/contact-sales">
|
|
Talk to us
|
|
</Action>
|
|
{primitive.github && (
|
|
<Action href={primitive.github} target="_blank" rel="noopener noreferrer">
|
|
<Github size={16} />
|
|
Source
|
|
</Action>
|
|
)}
|
|
</XStack>
|
|
</Box>
|
|
|
|
{/* The docs, and what sits beside this product. */}
|
|
{about && <Related product={about} />}
|
|
</Box>
|
|
)
|
|
}
|
|
|
|
export default CloudPrimitiveOverview
|