first publish
This commit is contained in:
commit
b8e41b4d55
147 files changed
+19367
No files matched your search
@@ -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;
|
||||
Reference in new issue
Block a user