mirror of
https://github.com/hanzo-apps/hanzo.ai.git
synced 2026-10-30 14:58:08 +00:00
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
75 lines
3.5 KiB
TypeScript
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>
|
|
)
|
|
}
|