103 lines
3.7 KiB
TypeScript
103 lines
3.7 KiB
TypeScript
import { cn } from "@/lib/utils";
|
||
import { flags } from "@/lib/site";
|
||
import { isPlaceholder, splitPlaceholders } from "@/lib/legal";
|
||
import type { FactItem } from "@/content/home";
|
||
|
||
export type { FactItem };
|
||
|
||
export interface FactsProps {
|
||
items: readonly FactItem[];
|
||
/**
|
||
* `stack` = mono lines with gold ticks (hero, top right).
|
||
* `table` = two-column label / value table on bone with a hairline frame (Rahmen).
|
||
*/
|
||
variant?: "stack" | "table";
|
||
/** Table heading (DM Mono caps), e.g. „Rahmen“. */
|
||
title?: string;
|
||
className?: string;
|
||
}
|
||
|
||
/**
|
||
* Renders „[…]“ segments in the accent colour (`--gold-deep` on light, `--gold` on dark).
|
||
*
|
||
* Deliberately flag-agnostic: cutting a placeholder out of a running sentence leaves
|
||
* fragments („Auswertungsgespräch .“), so the launch switch is a build guard, not a
|
||
* strip – `assertNoPlaceholders` in every content module aborts the build while a
|
||
* placeholder is still open (src/lib/legal.ts, list in CONTENT_TODO).
|
||
*/
|
||
export function PlaceholderText({ text }: { text: string }) {
|
||
return (
|
||
<>
|
||
{splitPlaceholders(text).map((seg, i) =>
|
||
seg.placeholder ? (
|
||
<span key={i} className="text-accent">
|
||
{seg.text}
|
||
</span>
|
||
) : (
|
||
<span key={i}>{seg.text}</span>
|
||
),
|
||
)}
|
||
</>
|
||
);
|
||
}
|
||
|
||
/** Drops whole placeholder ROWS when `flags.showPlaceholders` is off (inline placeholders
|
||
inside a value are the build guard's job, not this filter's – see PlaceholderText). */
|
||
export function visibleFacts(items: readonly FactItem[]): FactItem[] {
|
||
return items.filter((f) => flags.showPlaceholders || !(f.placeholder || isPlaceholder(f.value)));
|
||
}
|
||
|
||
/**
|
||
* Facts stack (Format · Sprache · Ort · Dauer): DM Mono 12 px, one gold tick
|
||
* per line, label muted, value in the foreground colour. Both variants are
|
||
* ledgers – tick / label / value are columns with one left edge each, not an
|
||
* inline run that starts the value at a different x on every row.
|
||
*/
|
||
export function Facts({ items, variant = "stack", title, className }: FactsProps) {
|
||
const rows = visibleFacts(items);
|
||
if (!rows.length) return null;
|
||
|
||
if (variant === "table") {
|
||
return (
|
||
<div className={cn("border border-line bg-surface p-6 sm:p-10", className)}>
|
||
{title && <p className="mb-6 font-mono text-xs uppercase tracking-[0.12em] text-accent">{title}</p>}
|
||
<dl className="font-mono text-xs tracking-[0.06em] tabular-nums">
|
||
{rows.map((f) => (
|
||
/* Below sm the value column would be ~150 px inside the padded box and long
|
||
values („Inhouse-Workshop 1–2 Tage / Trainingsreihe / Online“) break over four
|
||
lines – so the label sits above the value there, identically on every page. */
|
||
<div key={f.label} className="grid grid-cols-1 gap-x-6 gap-y-1 border-b border-line py-3 last:border-b-0 sm:grid-cols-[1fr_3fr]">
|
||
<dt className="text-fg-muted">{f.label}</dt>
|
||
<dd className="text-fg">
|
||
<PlaceholderText text={f.value} />
|
||
</dd>
|
||
</div>
|
||
))}
|
||
</dl>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
return (
|
||
/* Three rails: tick | label (max-content, so every label ends on one edge) | value. */
|
||
<dl
|
||
className={cn(
|
||
"grid grid-cols-[1.5rem_max-content_1fr] gap-x-3 gap-y-3 font-mono text-xs tracking-[0.06em] tabular-nums",
|
||
className,
|
||
)}
|
||
>
|
||
{rows.map((f) => (
|
||
<div key={f.label} className="contents">
|
||
<span aria-hidden="true" className="mt-[0.55em] h-px w-6 self-start bg-accent" />
|
||
<dt className="text-fg-muted">{f.label}</dt>
|
||
<dd className="text-fg">
|
||
<PlaceholderText text={f.value} />
|
||
</dd>
|
||
</div>
|
||
))}
|
||
</dl>
|
||
);
|
||
}
|
||
|
||
export default Facts;
|