Files
hanzo.ai/components/products/Aside.tsx

89 lines
3.3 KiB
TypeScript

import type { ReactNode } from 'react'
import { Text, YStack, XStack } from '@/components/ui'
import { Action } from '@hanzo/ui/marketing'
import { Line } from '@/components/type'
import type { CTA } from '@/components/product/ProductLanding'
/**
* A bordered note: a heading, a paragraph, and the links that go with it.
*
* Two different sections on four product pages turn out to be this same shape —
* the "Built on Open Source" panel naming the upstream project, and the
* "Need a Custom Solution?" panel under the calls to action. They were written
* twice per page and four times over, which is eight copies of a box with a
* heading in it.
*
* The body is a node because both of them put a link inside the sentence, and a
* sentence with a link in it is markup rather than a string. It is a `Text` with
* its white space put back to `normal`: gui's `Text` is `pre-wrap`, so a sentence
* a caller authored across three source lines would ship with three hard breaks
* in it. A heading takes one line and one string, so it is type the site already
* sets.
*
* The links are `Action`, this site's one button shape — the hover is the
* one thing neither a style attribute nor a gui prop can say.
*
* The name lost its `hz-` prefix when this document's rules did, and this call
* site did not follow. `.hz-action` is declared nowhere, so these links were
* rendering as bare text; `Start.tsx` carries the measurement.
*/
/**
* The link INSIDE that sentence, brighter than the grey around it.
*
* It belongs here rather than beside each caller because it is a fact about
* this panel's body: three product pages wrote the same pair of utilities on
* the same link in the same paragraph. Not `More` or `Action` — both are
* standalone controls with their own box, and stretching one inside a sentence
* tears the line, which is the rule `app/globals.css` already states for prose.
*/
export const LINK = { color: 'var(--foreground)', textDecoration: 'none' } as const
export function Aside({ title, children, links }: { title: string; children: ReactNode; links?: CTA[] }) {
return (
<YStack
render="section"
py={64}
px={16}
$sm={{ px: 24 }}
$lg={{ px: 32 }}
>
<YStack
width="100%"
maxW={768}
mx="auto"
p={32}
borderWidth={1}
borderColor="var(--edge, var(--white-08))"
rounded="var(--radius-2xl)"
bg="$panel"
>
<Line render="h3" size="xl" weight="600" marginBottom={12} color="var(--foreground)">
{title}
</Line>
<Line render="p" display="block" fontSize="$2" color="var(--muted-foreground)" whiteSpace="normal" lineHeight="var(--leading-relaxed)">
{children}
</Line>
{links && links.length > 0 && (
<XStack mt={24} flexWrap="wrap" gap={12}>
{links.map((l) => {
const Icon = l.icon
return (
<Action
key={l.label}
href={l.href}
target="_blank"
rel="noopener noreferrer"
>
{Icon && <Icon width={16} height={16} />} {l.label}
</Action>
)
})}
</XStack>
)}
</YStack>
</YStack>
)
}
export default Aside