{"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":[]}