UNPKG

analytica-frontend-lib

Version:

Repositório público dos componentes utilizados nas plataformas da Analytica Ensino

1 lines 6.91 kB
{"version":3,"sources":["../src/components/RangeGauge/RangeGauge.tsx"],"sourcesContent":["import { HTMLAttributes } from 'react';\nimport { cn } from '../../utils/utils';\n\n/**\n * A band of the gauge (e.g. \"below expected\", \"above expected\").\n */\nexport interface RangeGaugeZone {\n /**\n * Exclusive upper bound of the band, in the same unit as `value`.\n * Omit on the last zone: it is open-ended and bounded by `max`.\n */\n to?: number;\n /** Tailwind background class painting the band (e.g. `bg-indicator-positive`). */\n colorClass: string;\n /** Human-readable name of the band, exposed to assistive tech. */\n label?: string;\n}\n\nexport interface RangeGaugeProps extends Omit<\n HTMLAttributes<HTMLDivElement>,\n 'children'\n> {\n /** Current measurement (e.g. words per minute). */\n value: number;\n /** Bands, in ascending order. Every zone but the last needs a `to`. */\n zones: RangeGaugeZone[];\n /** Start of the scale. Default `0`. */\n min?: number;\n /** End of the scale. Defaults to the last bounded zone's `to`. */\n max?: number;\n /** Hides the pointer (e.g. while the measurement is unavailable). */\n showPointer?: boolean;\n /** Extra classes for the bar track. */\n barClassName?: string;\n}\n\n/** Index of the zone `value` falls into. */\nconst zoneIndexOf = (value: number, zones: RangeGaugeZone[]): number => {\n const index = zones.findIndex(\n (zone) => zone.to !== undefined && value < zone.to\n );\n return index === -1 ? zones.length - 1 : index;\n};\n\n/**\n * Pointer offset, as a 0-100 percentage of the track.\n *\n * Zones are drawn with EQUAL widths (as designed), but their thresholds are\n * not evenly spaced — so a naive `value / max` would drift into the wrong\n * band (a value above the \"expected\" threshold could still render over the\n * orange zone). Instead the value is placed inside its own band and then\n * mapped onto that band's equal-width slot, which keeps the pointer and the\n * colour underneath it always in agreement.\n */\nexport const pointerPercent = (\n value: number,\n zones: RangeGaugeZone[],\n min: number,\n max: number\n): number => {\n if (zones.length === 0) return 0;\n // Pin to the scale ends. Without this an overflowing value on an\n // open-ended last band (whose span is 0 when `max` is omitted) would land\n // at the band's start instead of the end of the track.\n if (value <= min) return 0;\n if (value >= max) return 100;\n\n const index = zoneIndexOf(value, zones);\n const lower = index === 0 ? min : (zones[index - 1].to as number);\n const upper = zones[index].to ?? max;\n\n const span = upper - lower;\n const ratio = span > 0 ? (value - lower) / span : 0;\n const clamped = Math.min(Math.max(ratio, 0), 1);\n\n return ((index + clamped) / zones.length) * 100;\n};\n\n/**\n * Horizontal banded gauge: a track split into coloured zones with a pointer\n * marking where a measurement sits. Used by the reading-fluency results to\n * show reading speed against the expected range.\n *\n * The thresholds live in the consumer (they are pedagogical rules, not a\n * design concern) — this component only renders what it is given.\n *\n * @example\n * ```tsx\n * <RangeGauge\n * value={38}\n * max={120}\n * zones={[\n * { to: 40, colorClass: 'bg-indicator-positive', label: 'Ainda decodificando' },\n * { to: 65, colorClass: 'bg-warning-400', label: 'Abaixo do esperado' },\n * { colorClass: 'bg-success-400', label: 'Acima do esperado' },\n * ]}\n * />\n * ```\n */\nexport const RangeGauge = ({\n value,\n zones,\n min = 0,\n max,\n showPointer = true,\n className = '',\n barClassName = '',\n ...props\n}: RangeGaugeProps) => {\n const lastBounded = [...zones]\n .reverse()\n .find((zone) => zone.to !== undefined);\n const scaleMax = max ?? lastBounded?.to ?? min;\n const percent = pointerPercent(value, zones, min, scaleMax);\n\n return (\n // Decorative: the measurement and its band are always rendered as adjacent\n // text (\"38 palavras/min\" / \"Abaixo do esperado\"), so announcing the bar\n // too would just repeat them. Wrap it with your own label if you render it\n // without that text.\n <div\n data-component=\"RangeGauge\"\n className={cn('flex flex-col gap-1 w-full', className)}\n aria-hidden=\"true\"\n {...props}\n >\n {/* Pointer rail — reserves the height even when the pointer is hidden */}\n <div className=\"relative h-2 w-full\">\n {showPointer && (\n <span\n data-testid=\"range-gauge-pointer\"\n className=\"absolute top-0 -translate-x-1/2\"\n style={{ left: `${percent}%` }}\n >\n <svg width=\"12\" height=\"8\" viewBox=\"0 0 12 8\" fill=\"none\">\n <path d=\"M6 8 0 0h12L6 8Z\" className=\"fill-text-600\" />\n </svg>\n </span>\n )}\n </div>\n\n <div\n className={cn(\n 'flex w-full h-1.5 overflow-hidden rounded-full',\n barClassName\n )}\n >\n {zones.map((zone, index) => (\n <span\n key={zone.label ?? `${zone.colorClass}-${index}`}\n className={cn('flex-1', zone.colorClass)}\n />\n ))}\n </div>\n </div>\n );\n};\n\nexport default RangeGauge;\n"],"mappings":";;;;;AAwHI,SAeU,KAfV;AAnFJ,IAAM,cAAc,CAAC,OAAe,UAAoC;AACtE,QAAM,QAAQ,MAAM;AAAA,IAClB,CAAC,SAAS,KAAK,OAAO,UAAa,QAAQ,KAAK;AAAA,EAClD;AACA,SAAO,UAAU,KAAK,MAAM,SAAS,IAAI;AAC3C;AAYO,IAAM,iBAAiB,CAC5B,OACA,OACA,KACA,QACW;AACX,MAAI,MAAM,WAAW,EAAG,QAAO;AAI/B,MAAI,SAAS,IAAK,QAAO;AACzB,MAAI,SAAS,IAAK,QAAO;AAEzB,QAAM,QAAQ,YAAY,OAAO,KAAK;AACtC,QAAM,QAAQ,UAAU,IAAI,MAAO,MAAM,QAAQ,CAAC,EAAE;AACpD,QAAM,QAAQ,MAAM,KAAK,EAAE,MAAM;AAEjC,QAAM,OAAO,QAAQ;AACrB,QAAM,QAAQ,OAAO,KAAK,QAAQ,SAAS,OAAO;AAClD,QAAM,UAAU,KAAK,IAAI,KAAK,IAAI,OAAO,CAAC,GAAG,CAAC;AAE9C,UAAS,QAAQ,WAAW,MAAM,SAAU;AAC9C;AAuBO,IAAM,aAAa,CAAC;AAAA,EACzB;AAAA,EACA;AAAA,EACA,MAAM;AAAA,EACN;AAAA,EACA,cAAc;AAAA,EACd,YAAY;AAAA,EACZ,eAAe;AAAA,EACf,GAAG;AACL,MAAuB;AACrB,QAAM,cAAc,CAAC,GAAG,KAAK,EAC1B,QAAQ,EACR,KAAK,CAAC,SAAS,KAAK,OAAO,MAAS;AACvC,QAAM,WAAW,OAAO,aAAa,MAAM;AAC3C,QAAM,UAAU,eAAe,OAAO,OAAO,KAAK,QAAQ;AAE1D;AAAA;AAAA;AAAA;AAAA;AAAA,IAKE;AAAA,MAAC;AAAA;AAAA,QACC,kBAAe;AAAA,QACf,WAAW,GAAG,8BAA8B,SAAS;AAAA,QACrD,eAAY;AAAA,QACX,GAAG;AAAA,QAGJ;AAAA,8BAAC,SAAI,WAAU,uBACZ,yBACC;AAAA,YAAC;AAAA;AAAA,cACC,eAAY;AAAA,cACZ,WAAU;AAAA,cACV,OAAO,EAAE,MAAM,GAAG,OAAO,IAAI;AAAA,cAE7B,8BAAC,SAAI,OAAM,MAAK,QAAO,KAAI,SAAQ,YAAW,MAAK,QACjD,8BAAC,UAAK,GAAE,oBAAmB,WAAU,iBAAgB,GACvD;AAAA;AAAA,UACF,GAEJ;AAAA,UAEA;AAAA,YAAC;AAAA;AAAA,cACC,WAAW;AAAA,gBACT;AAAA,gBACA;AAAA,cACF;AAAA,cAEC,gBAAM,IAAI,CAAC,MAAM,UAChB;AAAA,gBAAC;AAAA;AAAA,kBAEC,WAAW,GAAG,UAAU,KAAK,UAAU;AAAA;AAAA,gBADlC,KAAK,SAAS,GAAG,KAAK,UAAU,IAAI,KAAK;AAAA,cAEhD,CACD;AAAA;AAAA,UACH;AAAA;AAAA;AAAA,IACF;AAAA;AAEJ;AAEA,IAAO,qBAAQ;","names":[]}