first publish

This commit is contained in:
knso committed 2026-09-06 17:21:52 +02:00
commit b8e41b4d55
147 files changed
+19367

No files matched your search

+186
View File
@@ -0,0 +1,186 @@
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;