/**
* PBL v2 — Star rating (two variants).
*
* One renderer, two presentation policies. Stars are always 0–5 in
* 0.5 increments (clamped + rounded server-side by normalizeStars;
* we render whatever number we get).
*
* - plain row, NO "n/5" tail.
* used for milestone reflection
* (the user explicitly asked for
* icon vibes, not graded vibes).
* -
* row + "n / 5" trailing text in
* monospace. used by task-eval
* card where the score IS a
* judgement.
*
* Half-star rendering: a CSS gradient mask paints the left half of
* a star glyph gold and the right half neutral. No SVG paint-server
* hacks, no duplicate glyphs.
*/
import { cn } from '@/lib/utils';
interface Props {
/** 0-5 in 0.5 increments. Out-of-range values are clamped at
* render time; non-finite values fall back to 0. */
value: number;
showDenominator?: boolean;
/** Override star size (px). Defaults to 16. */
size?: number;
className?: string;
}
export function StarRating({ value, showDenominator, size = 16, className }: Props) {
const safe = Number.isFinite(value) ? Math.max(0, Math.min(5, value)) : 0;
return (
{[0, 1, 2, 3, 4].map((i) => {
const filled = safe >= i + 1;
const half = !filled && safe > i;
return (
★
);
})}
{showDenominator && (
{safe.toFixed(safe % 1 === 0 ? 0 : 1)} / 5
)}
);
}