Files
zeekayandGitHub 3df66c3990 i18n: /m* through /r* and the product kits draw their words through t (#70)
Pages from /machines to /risk, the model pages, and the kits they are made of
(page-kit, ProductLanding, LayerPage, Stack, the models kit, band's Head) mark
authored copy where it is written and draw it in the reader's language. A server
page marks a literal phrase('…') or wraps it in <T>; a client page calls t. The
kits translate a string prop through useSay (components/marketing/say), so a
page passes phrase('…') and markup passes as written. An arrow that points
turns over right to left (data-i18n-mirror).

Rebased on main: the fold-based heroes (Fold, PageHero, MachinesHero, the
product and layer pages) keep main's structure and copy, with their label,
title and lede marked phrase. /platform is main's CloudLanding; the deleted
/platform/layers and the old referral components stay deleted.

Claude-Session: https://claude.ai/code/session_015XYKLjZtDTJEnMo4eK3AFH
2026-10-30 00:18:44 -07:00

75 lines
3.5 KiB
TypeScript

/**
* The last block on a product page, above the site's one closing call: this
* product's docs, and what sits beside it in the catalog.
*
* It carries no call of its own. It used to open on "Start building with X"
* and a filled "Get your API key", which put two closing calls a screen apart
* on every product page — this one, then the layout's "Build what's next."
* with Try Hanzo — in two different button shapes. The way in is the closing
* call's; what only this block knows is which docs page is the product's and
* which products are its neighbours.
*
* `docs` IS A LINK OR IT IS NOTHING. `lib/constants/products-metadata` answers
* null for every product docs does not publish a page for, and this block drops
* the link on null rather than aiming at the index. A near-miss page is worse
* than no link, because it looks like it worked.
*
* The related rows are read, never typed. Membership comes from the commerce
* catalog (`lib/data/catalog.json`, through `cloud-primitives`), visibility from
* `lib/publish`, and the descriptor from the same copy deck the mega-menu sets
* under a leaf. With no docs page and no neighbours the block does not render.
*/
import { XStack, YStack } from '@/components/ui'
import { Eyebrow, Leaf, Lift, Line, More } from '@hanzo/ui/marketing'
import { Band } from '@/components/marketing/band'
import { cloudCategories, getPrimitiveCategory } from '@/lib/data/cloud-primitives'
import type { Product } from '@/lib/product'
import { T } from '@hanzo/i18n/react'
/** The rest of this product's corner of the catalog, at most three. */
function near(slug: string) {
const home = getPrimitiveCategory(slug)
const shown = home && cloudCategories.find((c) => c.id === home.id)
return (shown?.items ?? []).filter((i) => i.slug !== slug).slice(0, 3)
}
/** A small grey line, on the leading its utility paired with `text-sm`. */
const SMALL = { fontSize: '$2', color: 'var(--muted-foreground)', lineHeight: '1.4286' } as const
/** A row, as `Leaf` — the role whose reason to exist is the hover. `height`
* is auto here: the role stretches to a grid cell, and these are a column. */
const ROW = { py: 14, px: 20, borderColor: "var(--border)", display: "flex", flexWrap: "wrap", items: "baseline", gap: 12, height: "auto", rounded: "var(--radius-xl)", bg: "var(--card)", hoverStyle: { borderColor: "var(--border-selected)" } } as const
/** The name states no ink: it rests in the row's and lifts with the card. */
const NAME = { fontSize: "$2", lineHeight: "1.4286", fontWeight: "600" } as const
export function Related({ product }: { product: Product }) {
const rows = near(product.slug)
if (rows.length === 0 && !product.docs) return null
return (
<Band measure={896}>
<XStack flexWrap="wrap" justify="space-between" items="baseline" gap={12} mb={16}>
<Eyebrow>{rows.length > 0 ? <T>Related capabilities</T> : product.name}</Eyebrow>
{product.docs ? (
<More href={product.docs}>
<T values={{ name: product.name }}>{'{name} docs →'}</T>
</More>
) : null}
</XStack>
{rows.length > 0 ? (
<YStack gap={8}>
{rows.map((i) => (
<Leaf href={i.href} key={i.slug} {...ROW} hoverStyle={{ borderColor: "var(--border-selected)" }}>
<Lift {...NAME}>
{i.title}
</Lift>
{i.desc ? <Line {...SMALL}><T>{i.desc}</T></Line> : null}
</Leaf>
))}
</YStack>
) : null}
</Band>
)
}