- audit log: a job run named from its result data (nested phases too), a skipped run says so, a secret's reset reads as a reset, choices named as Configuration names them, interval / batch / counts localized; the inbox shares the same jobRunText - HU: remux is "Közvetlen közvetítés" (it shared "Közvetlen lejátszás" with direct play); backfill is behúzás, enrich is dúsítás, collection is kollekció, the allow-list is fehérlista; the last "jobnak" / "transzkódolás"; a test holds the options of one choice to distinct names - contract: every skipped / reason word a job returns is named in both languages - person page: the bio wait is timed from the open and a failing ask stops the polling; ClampedText (ui) shared with the collection page, whose short summary lost its dead More - a playlist names season 0 the Specials; the channel re-slot compares in the reader's alphabet; an invite's date is in the admin's language; the language-ordered lists are asked again once a language switch is saved (saveLanguage) - poster grids' column minimum in rem; lint refuses minmax(Npx,…)
56 lines
1.6 KiB
TypeScript
56 lines
1.6 KiB
TypeScript
import { useLayoutEffect, useRef, useState } from "react";
|
|
import { Button } from "./Button";
|
|
|
|
// Static class names, so Tailwind sees them.
|
|
const CLAMP = { 3: "line-clamp-3", 8: "line-clamp-8" } as const;
|
|
|
|
/** A long text (a biography, a summary) clamped to a few lines, with a More / Less toggle offered only
|
|
* when the text is actually cut — measured, so a short text carries no dead button. Re-measured on a
|
|
* resize: the window and the text-size slider both move where the cut falls. */
|
|
export function ClampedText({
|
|
text,
|
|
lines,
|
|
moreLabel,
|
|
lessLabel,
|
|
className,
|
|
}: {
|
|
text: string;
|
|
lines: keyof typeof CLAMP;
|
|
moreLabel: string;
|
|
lessLabel: string;
|
|
className?: string;
|
|
}) {
|
|
const [expanded, setExpanded] = useState(false);
|
|
const [clipped, setClipped] = useState(false);
|
|
const ref = useRef<HTMLParagraphElement>(null);
|
|
useLayoutEffect(() => {
|
|
const el = ref.current;
|
|
if (!el || expanded) return;
|
|
const measure = () => setClipped(el.scrollHeight > el.clientHeight + 1);
|
|
measure();
|
|
const ro = new ResizeObserver(measure);
|
|
ro.observe(el);
|
|
return () => ro.disconnect();
|
|
}, [text, expanded]);
|
|
return (
|
|
<div className={className}>
|
|
<p
|
|
ref={ref}
|
|
className={`measure whitespace-pre-line text-sm leading-relaxed text-muted ${expanded ? "" : CLAMP[lines]}`}
|
|
>
|
|
{text}
|
|
</p>
|
|
{(clipped || expanded) && (
|
|
<Button
|
|
variant="link"
|
|
aria-expanded={expanded}
|
|
onClick={() => setExpanded((v) => !v)}
|
|
className="mt-1 text-xs"
|
|
>
|
|
{expanded ? lessLabel : moreLabel}
|
|
</Button>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|