Files
zeekay f0602c82bf Send analytics from hanzo.help, build again, and publish on push
The page mounts the docs.hanzo.ai wiring: @hanzo/event sends the pageview
to api.hanzo.ai/v1/event with project hanzo-help's publishable key,
startTags loads the site's GA4 and Meta tags from cloud once the visitor
consents, and @hanzo/ui/consent is the banner.

main did not build: the page imported a Section that no published
@hanzo/ui exports, and the layout read GeistSans from @hanzo/font, which
was not a dependency and exports Zen. The page now lays its rows out
with a local Band, the layout binds Zen and ZenMono, and @hanzo/font is
declared.

@hanzo/ui 8.29.12 registers its gui config, so stacks type against the v5
shorthands, and the page moves to them. Its stylesheet is imported in the
layout. Overrides keep one gui and one event client in the tree.

hanzo.yml and the hanzoai/ci caller type-check, build out/ and publish it
as Sites project hanzo-help on every push to main.
2026-10-25 19:21:09 -07:00

158 lines
4.4 KiB
TypeScript

import type { ComponentProps } from "react";
import type { LucideIcon } from "lucide-react";
import { ArrowRight, ExternalLink } from "lucide-react";
import { Anchor, Text, XStack, YStack } from "@hanzo/ui";
/**
* Every link on the page is an `Anchor`. The three below are the three shapes
* it wears here — running text, a rounded frame, a card — and each was written
* out by hand at every call site before this file existed: the frame nine
* times, the text five, the card six.
*/
/**
* Opening in a new tab and disowning the opener are ONE decision. A `target`
* without the `rel` leaves the new page holding a live `window.opener` back to
* this one, so the pair travels together or not at all.
*/
export const blank = { target: "_blank", rel: "noopener noreferrer" } as const;
type Props = ComponentProps<typeof Anchor>;
/** A link in running text. Quiet until pointed at. */
export const Link = (props: Props) => (
<Anchor
fontSize="$3"
color="$soft"
textDecorationLine="none"
hoverStyle={{ color: "$color" }}
{...props}
/>
);
/** A link wearing a button. `solid` is the one that asks to be pressed. */
export const Pill = ({ solid, ...props }: Props & { solid?: boolean }) => (
<Anchor
display="flex"
flexDirection="row"
items="center"
gap="$1.5"
px="$3"
py="$1.5"
rounded="$10"
borderWidth={1}
fontSize="$3"
fontWeight="500"
textDecorationLine="none"
{...(solid
? {
bg: "$accentBackground",
borderColor: "$colorTransparent",
color: "$accentColor",
hoverStyle: { opacity: 0.9 },
}
: {
bg: "$panel",
borderColor: "$borderColor",
color: "$soft",
hoverStyle: {
bg: "$hover",
borderColor: "$borderColorHover",
color: "$color",
},
})}
{...props}
/>
);
/**
* A category, as a card. `featured` is a SCALE, not a second component: the two
* at the top get the full frame and an outbound mark, the four below the
* compact row, and every other decision about them is the same one.
*
* The icon sits inside a `Text` because a lucide glyph strokes with
* `currentColor` — colour the box and the glyph follows, with nothing to keep
* in step.
*/
export function Topic({
icon: Icon,
name,
description,
href,
featured,
}: {
icon: LucideIcon;
name: string;
description: string;
href: string;
featured?: boolean;
}) {
return (
<Anchor
href={href}
{...blank}
group
display="flex"
grow={1}
flexDirection={featured ? "column" : "row"}
items={featured ? "stretch" : "flex-start"}
gap={featured ? "$4" : "$3"}
p={featured ? "$5" : "$4"}
rounded="$5"
borderWidth={1}
borderColor="$borderColor"
bg="$panel"
textDecorationLine="none"
hoverStyle={{ bg: "$hover", borderColor: "$borderColorHover" }}
>
<XStack items="center" justify="space-between" shrink={0}>
<Text
display="flex"
items="center"
justify="center"
width={featured ? 40 : 32}
height={featured ? 40 : 32}
rounded="$3"
bg="$raised"
color="$soft"
$group-hover={{ color: "$color" }}
>
<Icon size={featured ? 20 : 16} />
</Text>
{featured && (
<Text color="$bound" $group-hover={{ color: "$soft" }}>
<ExternalLink size={16} />
</Text>
)}
</XStack>
{/* Shrinkable, or the compact row's text runs straight out of the card:
a flex item's automatic minimum is its content, so it refuses to
narrow and the words are clipped rather than wrapped. */}
<YStack gap={featured ? "$1" : "$0.5"} shrink={1} minW={0}>
<Text fontSize={featured ? "$6" : "$3"} fontWeight="600" color="$color">
{name}
</Text>
<Text fontSize={featured ? "$3" : "$1"} lineHeight={featured ? "$5" : "$2"} color="$soft">
{description}
</Text>
</YStack>
{featured && (
<Text
display="flex"
flexDirection="row"
items="center"
gap="$1.5"
mt="auto"
fontSize="$3"
fontWeight="500"
color="$soft"
$group-hover={{ color: "$color" }}
>
Explore
<ArrowRight size={14} />
</Text>
)}
</Anchor>
);
}