mirror of
https://github.com/hanzo-apps/hanzo.ai.git
synced 2026-10-29 07:20:43 +00:00
204 lines
7.4 KiB
TypeScript
204 lines
7.4 KiB
TypeScript
'use client'
|
|
|
|
// Presentational hexagram pieces, shared across all three explorer views.
|
|
// A reader sees only: the Unicode glyph, the number, and an English label.
|
|
// Chinese (nameZh / namePinyin) lives in the data module and is never rendered.
|
|
//
|
|
// NO STYLESHEET. The card is a native `<details>`: its summary is a flex row,
|
|
// which is what takes the UA's triangle away, and the chevron's half turn as
|
|
// the card opens is the one piece of state, read from the element's own
|
|
// `toggle` — the same construction as @hanzo/ui's Disclosure. With no script
|
|
// the card still opens; the chevron just keeps pointing down.
|
|
//
|
|
// `TrigramLines` lost its `className` — its one call site never passed one —
|
|
// and `HexagramHead` takes a font SIZE off the ramp rather than a class, so the
|
|
// glyph steps between rungs without a second type vocabulary.
|
|
|
|
import React, { useState, type ReactNode } from "react";
|
|
import {
|
|
type Hexagram,
|
|
TRIGRAMS,
|
|
LENSES,
|
|
englishLabel,
|
|
glyphAria,
|
|
displayNum,
|
|
} from "./hexagrams";
|
|
import { Box, Text, View, XStack, YStack } from '@hanzo/ui'
|
|
import { Chip } from '@hanzo/ui/marketing'
|
|
|
|
/** A share of the full or the muted ink, for the strengths this card dims by degree. */
|
|
const ink = (pct: number) => `color-mix(in oklab, var(--foreground) ${pct}%, transparent)`
|
|
const muted = (pct: number) => `color-mix(in oklab, var(--muted-foreground) ${pct}%, transparent)`
|
|
const white = (pct: number) => `color-mix(in oklab, var(--pure-white) ${pct}%, transparent)`
|
|
|
|
/** One bar of a trigram, solid or half of a broken one. */
|
|
function Bar({ width }: { width: number }) {
|
|
return <View render="span" display="block" width={width} height={3} rounded={1} bg="var(--foreground)" />
|
|
}
|
|
|
|
// A single trigram drawn as three stacked lines (top → bottom).
|
|
// Yang = solid bar, Yin = broken bar. Purely decorative (aria-hidden).
|
|
export function TrigramLines({
|
|
lines,
|
|
}: {
|
|
lines: readonly [number, number, number];
|
|
}) {
|
|
return (
|
|
<YStack render="span" aria-hidden display="inline-flex" items="center" justify="center" gap={3}>
|
|
{[2, 1, 0].map((i) =>
|
|
lines[i] === 1 ? (
|
|
<Bar key={i} width={20} />
|
|
) : (
|
|
<XStack key={i} render="span" width={20} justify="space-between">
|
|
<Bar width={8} />
|
|
<Bar width={8} />
|
|
</XStack>
|
|
)
|
|
)}
|
|
</YStack>
|
|
);
|
|
}
|
|
|
|
function LensTags({ lenses }: { lenses: Hexagram["lenses"] }) {
|
|
if (lenses.length === 0) return null;
|
|
return (
|
|
<XStack flexWrap="wrap" gap={6}>
|
|
{lenses.map((k) => {
|
|
const lens = LENSES.find((l) => l.key === k)!;
|
|
return (
|
|
// A word in an outline at this card's own size and ink.
|
|
<Chip key={k} title={lens.definition} px={8} py={2} borderColor="var(--border)" bg={white(5)} fontSize={11} fontWeight="500" color={ink(80)}>
|
|
{lens.name}
|
|
</Chip>
|
|
);
|
|
})}
|
|
</XStack>
|
|
);
|
|
}
|
|
|
|
/** The label over each definition. */
|
|
function Term({ mb, children }: { mb?: number; children: ReactNode }) {
|
|
return (
|
|
<Text render="dt" display="block" mb={mb} fontFamily="$mono" fontSize={11} lineHeight="1.25rem" letterSpacing={0.55} textTransform="uppercase" $platform-web={{ color: muted(50) }}>
|
|
{children}
|
|
</Text>
|
|
)
|
|
}
|
|
|
|
/** A definition, indented the browser's 40px under its term. */
|
|
function Def({ children, ...p }: { children: ReactNode } & React.ComponentProps<typeof Text>) {
|
|
return (
|
|
<Text render="dd" display="block" ml={40} {...p}>
|
|
{children}
|
|
</Text>
|
|
)
|
|
}
|
|
|
|
// The expanded content for a hexagram, in canonical order:
|
|
// condition → principle → operational rule → diagnostic question → lenses.
|
|
export function HexagramDetail({ h }: { h: Hexagram }) {
|
|
const upper = TRIGRAMS[h.upper];
|
|
const lower = TRIGRAMS[h.lower];
|
|
const over = { fontSize: 11, lineHeight: '1.25rem', fontWeight: '500', letterSpacing: 0.55, textTransform: 'uppercase' } as const
|
|
return (
|
|
<YStack gap={16} $platform-web={{ textAlign: 'left' }}>
|
|
<XStack render="p" items="center" gap={8}>
|
|
<Text {...over} $platform-web={{ color: ink(70) }}>
|
|
{upper.name}
|
|
</Text>
|
|
<Text {...over} $platform-web={{ color: muted(40) }}>
|
|
over
|
|
</Text>
|
|
<Text {...over} $platform-web={{ color: ink(70) }}>
|
|
{lower.name}
|
|
</Text>
|
|
</XStack>
|
|
<YStack render="dl" gap={12} my={14}>
|
|
<Box>
|
|
<Term>Principle</Term>
|
|
<Def fontSize="$4" lineHeight="1.25rem" fontWeight="500" color="var(--foreground)">
|
|
{h.statement}
|
|
</Def>
|
|
</Box>
|
|
<Box>
|
|
<Term>In practice</Term>
|
|
<Def fontSize="$2" lineHeight="1.3203rem" color="var(--muted-foreground)">
|
|
{h.rule}
|
|
</Def>
|
|
</Box>
|
|
<Box>
|
|
<Term>Ask</Term>
|
|
<Def fontSize="$2" lineHeight="1.1607rem" fontStyle="italic" $platform-web={{ color: ink(90) }}>
|
|
{h.question}
|
|
</Def>
|
|
</Box>
|
|
{h.lenses.length > 0 && (
|
|
<Box>
|
|
<Term mb={4}>Lenses</Term>
|
|
<Def>
|
|
<LensTags lenses={h.lenses} />
|
|
</Def>
|
|
</Box>
|
|
)}
|
|
</YStack>
|
|
</YStack>
|
|
);
|
|
}
|
|
|
|
// The always-visible header: glyph + number + English label.
|
|
export function HexagramHead({
|
|
h,
|
|
glyph = '$8',
|
|
}: {
|
|
h: Hexagram;
|
|
/** A rung of the ramp — `$8` is --text-3xl, `$10` --text-4xl — so the two sizes this is drawn at stay on one scale. */
|
|
glyph?: '$8' | '$10';
|
|
}) {
|
|
return (
|
|
<>
|
|
<Text role="img" aria-label={glyphAria(h)} shrink={0} fontSize={glyph} lineHeight={glyph === '$10' ? '2rem' : '1.625rem'} color="var(--foreground)">
|
|
{h.glyph}
|
|
</Text>
|
|
<YStack minW={0} flex={1}>
|
|
<Text display="block" fontFamily="$mono" fontSize={11} lineHeight="1.25rem" $platform-web={{ color: muted(60) }}>
|
|
{displayNum(h.num)}
|
|
</Text>
|
|
<Text display="block" fontSize="$2" lineHeight="1.1172rem" fontWeight="600" color="var(--foreground)">
|
|
{englishLabel(h)}
|
|
</Text>
|
|
</YStack>
|
|
</>
|
|
);
|
|
}
|
|
|
|
// A self-contained, keyboard-accessible disclosure card (native <details>).
|
|
// Works without JavaScript; content is present in the HTML for search + a11y.
|
|
// Closed, open and hovered, the card is the same hairline over the same ground.
|
|
//
|
|
// The chevron turns a half as the card opens: down-pointing becomes up-pointing.
|
|
export function HexagramCard({ h }: { h: Hexagram }) {
|
|
const [open, setOpen] = useState(false)
|
|
return (
|
|
<YStack
|
|
render={<details onToggle={(e) => setOpen((e.currentTarget as HTMLDetailsElement).open)} />}
|
|
display="block"
|
|
rounded="var(--radius-xl)"
|
|
borderWidth={1}
|
|
borderColor="var(--border)"
|
|
$platform-web={{ backgroundColor: 'color-mix(in oklab, var(--secondary) 20%, transparent)' }}
|
|
>
|
|
<XStack render="summary" items="center" gap={16} p={16} cursor="pointer" $platform-web={{ listStyle: 'none' }}>
|
|
<HexagramHead h={h} />
|
|
<XStack shrink={0} rotate={open ? '180deg' : '0deg'} $platform-web={{ color: muted(50), transition: 'transform var(--duration-fast) var(--ease-in-out)' }}>
|
|
<svg aria-hidden="true" viewBox="0 0 24 24" width={16} height={16} fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
|
<path d="M6 9l6 6 6-6" />
|
|
</svg>
|
|
</XStack>
|
|
</XStack>
|
|
<Box borderTopWidth={1} borderColor="$edge" px={16} pt={16} pb={16}>
|
|
<HexagramDetail h={h} />
|
|
</Box>
|
|
</YStack>
|
|
);
|
|
}
|