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

103 lines
3.7 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
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 { 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;