Files

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>
);
}