Files
Anelia-Wolf/src/components/ui/Ledger.tsx
T
2026-09-06 17:21:52 +02:00

187 lines
7.0 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { cn } from "@/lib/utils";
import { TransitionLink } from "@/components/motion/TransitionLink";
import { RevealGroup, RevealItem } from "@/components/motion/Reveal";
import { termine as termineDe } from "@/content/home";
import type { GoalRow, SituationRow, TermineRow, TocRow, WerdegangRow } from "@/content/home";
import { TEXT_LINK } from "./TextLink";
export type { GoalRow, SituationRow, TermineRow, TocRow, WerdegangRow };
export type LedgerRow = TocRow | WerdegangRow | TermineRow | SituationRow | GoalRow;
interface LedgerBase {
className?: string;
/** Stagger between rows (default 0.1 s). */
stagger?: number;
}
export type LedgerProps =
| (LedgerBase & { variant: "toc"; rows: readonly TocRow[] })
| (LedgerBase & { variant: "werdegang"; rows: readonly WerdegangRow[] })
| (LedgerBase & { variant: "termine"; rows: readonly TermineRow[]; heading?: string })
| (LedgerBase & { variant: "situations"; rows: readonly SituationRow[] })
| (LedgerBase & { variant: "goals"; rows: readonly GoalRow[] });
const MONO = "font-mono text-xs tracking-[0.06em] tabular-nums";
/* ---------------- toc ---------------- */
/**
* Table-of-contents row = ONE <TransitionLink> (keyboard: one tab stop).
* Hover: ground `--surface`, lower hairline turns gold (scaleX from the left,
* 0.6 s), arrow +8 px. No italic flip, nothing reflows – everything is
* always visible.
*/
function TocRowView({ row }: { row: TocRow }) {
return (
<TransitionLink
href={row.link.href}
label={row.title}
className="group relative grid grid-cols-1 gap-y-4 py-7 transition-colors duration-300 hover:bg-surface focus-visible:bg-surface lg:grid-cols-12 lg:gap-x-[clamp(16px,2vw,32px)] lg:py-10"
>
<span className="flex items-baseline gap-4 lg:col-span-5 lg:contents">
<span className={cn(MONO, "text-accent lg:col-span-1")}>{row.numeral}</span>
<span className="font-display text-[clamp(1.75rem,1.4rem+1.6vw,3rem)] font-normal leading-[1.05] text-fg lg:col-span-4">
{row.title}
</span>
</span>
<span className="flex flex-col gap-2 lg:col-span-5">
<span className="font-sans text-[1.0625rem] leading-snug text-fg">{row.oneLiner}</span>
<span className={cn(MONO, "text-fg-muted")}>{row.keywords.join(" · ")}</span>
</span>
<span className={cn(TEXT_LINK, "whitespace-nowrap lg:col-span-2 lg:justify-self-end lg:self-start")}>
<span>{row.link.label}</span>
<span aria-hidden="true" className="transition-transform duration-300 ease-[var(--ease-out-quart)] group-hover:translate-x-2">
→
</span>
</span>
{/* Lower hairline: recolours to gold from the left on hover. */}
<span
aria-hidden="true"
className="rule-gold absolute inset-x-0 bottom-[-1px] origin-left scale-x-0 transition-transform duration-[600ms] ease-[var(--ease-out-expo)] group-hover:scale-x-100 group-focus-visible:scale-x-100"
/>
</TransitionLink>
);
}
/* ---------------- termine ---------------- */
function TermineRowView({ row }: { row: TermineRow }) {
const inner = (
<>
<span className={cn(MONO, "text-[13px] text-fg")}>{row.date}</span>
<span className="font-sans text-[0.8125rem] font-medium uppercase tracking-[0.16em] text-fg-muted">{row.format}</span>
<span className="font-sans text-lg leading-snug text-fg lg:font-display lg:text-[1.75rem] lg:font-normal lg:leading-[1.1]">
{row.title}
</span>
<span className={cn(MONO, "text-fg-muted")}>
{row.venue}
{row.href && (
<>
{" "}
<span aria-hidden="true">↗</span>
</>
)}
</span>
</>
);
const cls =
"grid grid-cols-1 gap-y-1 py-5 lg:grid-cols-[1fr_1fr_2fr_1fr] lg:items-baseline lg:gap-x-8";
if (row.href) {
return (
<a href={row.href} target="_blank" rel="noopener noreferrer" className={cn(cls, "group/row")}>
{inner}
</a>
);
}
return <div className={cls}>{inner}</div>;
}
/* ---------------- Ledger ---------------- */
/**
* Typographic ledgers instead of cards. Rows are hairline-separated and
* stagger in via <RevealGroup/>; the toc variant's rows are links.
*/
export function Ledger(props: LedgerProps) {
const { className, stagger = 0.1 } = props;
if (props.variant === "toc") {
return (
<RevealGroup as="ol" stagger={stagger} className={cn("border-t border-line", className)}>
{props.rows.map((row) => (
<RevealItem key={row.numeral} as="li" className="border-b border-line">
<TocRowView row={row} />
</RevealItem>
))}
</RevealGroup>
);
}
if (props.variant === "werdegang") {
return (
<RevealGroup as="dl" stagger={stagger} className={cn("border-t border-line", className)}>
{props.rows.map((row) => (
<RevealItem
key={row.keyword}
className="grid grid-cols-1 gap-y-1 border-b border-line py-[18px] sm:grid-cols-4 sm:gap-x-6"
>
<dt className={cn(MONO, "uppercase text-accent sm:col-span-1")}>{row.keyword}</dt>
<dd className="font-sans text-[1.0625rem] leading-snug text-fg sm:col-span-3">{row.text}</dd>
</RevealItem>
))}
</RevealGroup>
);
}
if (props.variant === "termine") {
const heading = props.heading ?? termineDe.heading;
return (
<div className={cn("border border-line bg-surface p-6 sm:p-10", className)}>
<p className={cn(MONO, "mb-4 flex items-center gap-3 uppercase tracking-[0.12em] text-accent")}>
<span aria-hidden="true" className="inline-block h-1.5 w-1.5 shrink-0 bg-accent" />
{heading}
</p>
<RevealGroup as="ol" stagger={stagger} className="border-t border-line">
{props.rows.map((row) => (
<RevealItem key={`${row.date}-${row.title}`} as="li" className="border-b border-line">
<TermineRowView row={row} />
</RevealItem>
))}
</RevealGroup>
</div>
);
}
if (props.variant === "situations") {
return (
<RevealGroup as="ol" stagger={stagger} className={cn("border-t border-line", className)}>
{props.rows.map((row) => (
<RevealItem
key={row.numeral}
as="li"
className="grid grid-cols-[2.5rem_1fr] gap-x-4 border-b border-line py-5 sm:grid-cols-[3rem_1fr] sm:gap-x-8"
>
<span className={cn(MONO, "pt-[0.5em] text-accent")}>{row.numeral}</span>
<span className="font-display text-[clamp(1.75rem,1.3rem+1.2vw,2rem)] font-normal italic leading-[1.2] text-fg">
{row.text}
</span>
</RevealItem>
))}
</RevealGroup>
);
}
return (
<RevealGroup as="ul" stagger={stagger} className={cn("border-t border-line", className)}>
{props.rows.map((row) => (
<RevealItem key={row.text} as="li" className="border-b border-line py-4 font-sans text-[1.0625rem] leading-snug text-fg">
{row.text}
</RevealItem>
))}
</RevealGroup>
);
}
export default Ledger;