"use client" /** * Student score bands — linear bars (min–max scale, class avg marker, student score) * and radial summary. ChartFigure wiring for the radial lives in charts-overview.tsx. */ import * as React from "react" import { PolarAngleAxis, RadialBar, RadialBarChart } from "recharts" import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card" import { ChartContainer, ChartTooltip, chartTooltipKeyboardSyncProps, ChartTooltipContent, type ChartConfig, } from "@/components/ui/chart" import { cn } from "@/lib/utils" import { isEditableTarget } from "@/lib/editable-target" import { DASHBOARD_STUDENT_SCORES, formatBandScore, scoreToTrackPercent, type DashboardStudentScoresData, type StudentScoreMetric, type StudentScoreRadial, } from "@/lib/mock/dashboard" const activeIndexProps = (activeIndex: number | null) => activeIndex == null ? {} : ({ activeIndex } as Record) const scoreRadialCfg: ChartConfig = { score: { label: "Student score", color: "var(--brand-color)" }, } /** Same structure as ChartDataTable — local to avoid importing charts-overview (cycle). */ function SrOnlyMetricTable({ caption, headers, rows, }: { caption: string headers: string[] rows: (string | number)[][] }) { return ( {headers.map((h) => )} {rows.map((row, i) => ( {row.map((cell, j) => )} ))}
{caption}
{h}
{cell}
) } const scaleEndsClass = "flex justify-between gap-2 px-0.5 text-xs tabular-nums leading-none text-muted-foreground" const linearProgressFocusClass = "rounded-md p-1.5 -m-1.5 outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background" function StudentScoreProgressRow({ label, scaleMin, scaleMax, classAverage, studentScore, averageMarkerLabel = "Class avg", }: { label: string scaleMin: number scaleMax: number classAverage: number studentScore: number averageMarkerLabel?: string }) { const fillPct = scoreToTrackPercent(studentScore, scaleMin, scaleMax) const avgPct = scoreToTrackPercent(classAverage, scaleMin, scaleMax) const minStr = formatBandScore(scaleMin) const maxStr = formatBandScore(scaleMax) const labelId = React.useId() const kbdHintId = React.useId() const valueText = `Score ${formatBandScore(studentScore)}. ${averageMarkerLabel} ${formatBandScore(classAverage)}. Scale ${minStr} through ${maxStr}.` function handleKeyDown(e: React.KeyboardEvent) { if (e.key !== "Escape") return if (isEditableTarget(e.target)) return e.preventDefault() e.stopPropagation() e.currentTarget.blur() } /** Clicks on the track do not always move focus — align with ChartFigure pointer focus. */ function handlePointerDownCapture(e: React.PointerEvent) { const root = e.currentTarget const el = e.target as HTMLElement | null if ( el?.closest?.( "button, a, [role='tab'], [role='option'], input, select, textarea, [contenteditable='true']", ) ) return queueMicrotask(() => root.focus()) } return (

{label}

Tab to focus this score bar. Press Escape to leave focus.
{/* High-contrast (data-contrast="high") & Windows forced-colors: without these overrides the track, fill, and avg marker all collapse to the same value in HC themes (see a11y bug). - track: keep an outlined container so it's visible on the HC bg - fill: use foreground color (full contrast) instead of tinted brand - pill: invert with a visible border so label stays legible */} {/* HC dark: track = transparent with a thin border (so card bg shows through), fill = foreground (white on dark HC). Light HC: same pattern — fill resolves to near-black on light. Never invert: the FILL must be the high-contrast stroke, never the track. */}
) } /** Recharts radial: ring = position of student score on scale; center shows raw score. */ export function QuotaRadialChartInner({ radial, activeIndex, }: { radial: StudentScoreRadial activeIndex: number | null }) { const fill = scoreToTrackPercent(radial.studentScore, radial.scaleMin, radial.scaleMax) /* Fill + track reference CSS vars so HC mode can override them without re-rendering the chart. Default: brand fill over muted track. HC (`data-contrast="high"`): fill = foreground (full contrast), track = transparent with a visible ring via strokeOpacity on the bg bar. */ const chartData = [{ name: "score", value: fill, fill: "var(--progress-fill, var(--brand-color))" }] return (
( Score {formatBandScore(radial.studentScore)} · {formatBandScore(radial.scaleMin)}– {formatBandScore(radial.scaleMax)} · Class avg {formatBandScore(radial.classAverage)} )} /> )} />
{formatBandScore(radial.studentScore)} {radial.caption}
) } export function QuotaRadialGaugeStatic({ radial }: { radial: StudentScoreRadial }) { return (

Class avg{" "} {formatBandScore(radial.classAverage)} {" "} · scale {formatBandScore(radial.scaleMin)}–{formatBandScore(radial.scaleMax)}

) } export function QuotaLinearProgressCardBody({ metric, suiteContext, }: { metric: StudentScoreMetric suiteContext: string }) { const summaryId = React.useId() const { scaleMin, scaleMax, classAverage, studentScore } = metric return (

{metric.label}: student score {formatBandScore(studentScore)}. Class average {formatBandScore(classAverage)}. Scale from {formatBandScore(scaleMin)} to {formatBandScore(scaleMax)}. {suiteContext}

) } export function DashboardQuotaProgressCard({ data = DASHBOARD_STUDENT_SCORES, className, }: { data?: DashboardStudentScoresData className?: string }) { const desc = data.description ?? "" return (
{data.metrics.map((m) => ( {m.label} {m.description ?? desc} ))} {data.radial.title} {desc}
) }