1(function(){ 2/* global React */ 3(function initializeDecisionVisualizations(global) { 4 "use strict"; 5 const VALID_STATES = new Set([ 6 "ready", 7 "partial", 8 "empty", 9 "loading", 10 "error", 11 "unknown", 12 "unverified" 13 ]); 14 const DEVICE_TYPES = [ 15 "phone", 16 "tablet", 17 "desktop" 18 ]; 19 const DEFAULT_COPY = Object.freeze({ 20 source: "Fuente", 21 period: "Periodo", 22 unit: "Unidad", 23 timezone: "Zona horaria", 24 status: "Estado", 25 ready: "Actualizado", 26 partial: "Datos parciales", 27 empty: "Sin datos", 28 loading: "Cargando", 29 error: "No disponible", 30 unknown: "Estado desconocido", 31 unverified: "Datos sin confirmación del servidor", 32 unknownShort: "Incompleto", 33 unverifiedShort: "Sin confirmar", 34 incompleteMetadata: "Faltan metadatos requeridos", 35 inconsistentState: "El estado no coincide con los datos recibidos", 36 invalidData: "Los datos confirmados contienen valores incompatibles", 37 missingValue: "Sin dato", 38 zeroValue: "Cero", 39 noDenominator: "Sin base de comparación", 40 missingMetadata: "No especificado", 41 missingTitle: "Conclusión no disponible", 42 dataTable: "Ver tabla de datos", 43 tableView: "Tabla", 44 listView: "Lista", 45 category: "CategorÃa", 46 value: "Valor", 47 total: "Total", 48 coverage: "Cobertura", 49 of: "de", 50 phone: "Celular", 51 tablet: "Tableta", 52 desktop: "Computadora", 53 question: "Pregunta", 54 audience: "Audiencia", 55 decision: "Decisión", 56 trendInstructions: "Use las flechas para recorrer los puntos de la tendencia." 57 }); 58 function mergeCopy(copy) { 59 return Object.assign({}, DEFAULT_COPY, copy || {}); 60 } 61 function finiteNumber(value) { 62 if (value === null || value === undefined || value === "") return null; 63 const numeric = Number(value); 64 return Number.isFinite(numeric) ? numeric : null; 65 } 66 function formatNumber(value, formatter) { 67 if (value === null) return null; 68 if (typeof formatter === "function") return String(formatter(value)); 69 return new Intl.NumberFormat(undefined, { maximumFractionDigits: 2 }).format(value); 70 } 71 function formatPercent(value, formatter) { 72 if (value === null) return null; 73 if (typeof formatter === "function") return String(formatter(value)); 74 return new Intl.NumberFormat(undefined, { 75 style: "percent", 76 maximumFractionDigits: 0 77 }).format(value); 78 } 79 function resolveVisualizationState({ provenance, requestedState, metadata, values, invalidData = false, copy }) { 80 if (!["server_confirmed", "demo_fixture"].includes(provenance)) { 81 return { 82 state: "unverified", 83 message: copy.unverified 84 }; 85 } 86 if (!VALID_STATES.has(requestedState) || metadata.some((value) => !value)) { 87 return { 88 state: "unknown", 89 message: copy.incompleteMetadata 90 }; 91 } 92 if (invalidData) return { 93 state: "error", 94 message: copy.invalidData 95 }; 96 const available = values.filter((value) => value !== null).length; 97 if (requestedState === "ready" && (!values.length || available !== values.length)) { 98 return { 99 state: "error", 100 message: copy.inconsistentState 101 }; 102 } 103 if (requestedState === "partial" && (!available || available === values.length)) { 104 return { 105 state: "error", 106 message: copy.inconsistentState 107 }; 108 } 109 return { 110 state: requestedState, 111 message: null 112 }; 113 } 114 function statusText(state, copy, supplied) { 115 if (state === "unverified") return copy.unverifiedShort || copy.unverified; 116 if (state === "unknown") return copy.unknownShort || copy.unknown; 117 return supplied || copy[state] || copy.missingMetadata; 118 } 119 function valueState(value) { 120 if (value === null) return "missing"; 121 return value === 0 ? "zero" : "ready"; 122 } 123 function DecisionIcon({ name, size = 20 }) { 124 const aliases = { 125 Smartphone: [ 126 "Smartphone", 127 "Device", 128 "Phone" 129 ], 130 Tablet: ["Tablet"], 131 Monitor: [ 132 "Monitor", 133 "Desktop", 134 "Laptop" 135 ] 136 }; 137 const candidates = aliases[name] || [name]; 138 const Icon = candidates.map((candidate) => { 139 var _global$I; 140 return (_global$I = global.I) === null || _global$I === void 0 ? void 0 : _global$I[candidate]; 141 }).find(Boolean); 142 return Icon ? /* @__PURE__ */ React.createElement(Icon, { 143 size, 144 stroke: "currentColor", 145 "aria-hidden": "true" 146 }) : /* @__PURE__ */ React.createElement("span", { 147 className: "hp-viz-icon-fallback", 148 "aria-hidden": "true" 149 }); 150 } 151 function EvidenceHeader({ id, title, source, period, unit, timezone, state, stateLabel, question, audience, decision, provenance, copy }) { 152 const titleText = title || copy.missingTitle; 153 const confirmed = state === "ready" && provenance === "server_confirmed"; 154 const meta = [ 155 ...!confirmed ? [[copy.source, source]] : [], 156 [copy.period, period], 157 [copy.unit, unit] 158 ]; 159 return /* @__PURE__ */ React.createElement("header", { className: "hp-viz-header" }
159, /* @__PURE__ */ React.createElement("div", { className: "hp-viz-heading" }, /* @__PURE__ */ React.createElement("h3", { id }, titleText), !confirmed && /* @__PURE__ */ React.createElement("span", { 160 className: "hp-viz-state", 161 "data-state": state 162 }, statusText(state, copy, stateLabel))), /* @__PURE__ */ React.createElement("dl", { className: "hp-viz-evidence" }, meta.map(([label, value]) => /* @__PURE__ */ React.createElement("div", { 163 key: label, 164 "data-state": value ? "ready" : "missing" 165 }, /* @__PURE__ */ React.createElement("dt", null, label), /* @__PURE__ */ React.createElement("dd", null, value || copy.missingMetadata)))), /* @__PURE__ */ React.createElement("span", { className: "hp-ds-visually-hidden" }, question ? `${copy.question}: ${question}. ` : "", audience ? `${copy.audience}: ${audience}. ` : "", decision ? `${copy.decision}: ${decision}.` : "")); 166 } 167 function VisualizationFrame({ kind, title, source, period, unit, timezone, state, stateLabel, question, audience, decision, provenance, copy, children }) { 168 const headingId = React.useId(); 169 return /* @__PURE__ */ React.createElement("section", { 170 className: "hp-viz", 171 "data-testid": "decision-visualization", 172 "data-visualization": kind, 173 "data-state": state, 174 "data-question": question || undefined, 175 "data-audience": audience || undefined, 176 "data-decision": decision || undefined, 177 "data-provenance": provenance || "unverified", 178 "aria-labelledby": headingId 179 }, /* @__PURE__ */ React.createElement(EvidenceHeader, { 180 id: headingId, 181 title, 182 source, 183 period, 184 unit, 185 timezone, 186 state, 187 stateLabel, 188 question, 189 audience, 190 decision, 191 provenance, 192 copy 193 }), children); 194 } 195 function EmptyOrLoadingState({ state, message, copy }) { 196 if (state === "ready" || state === "partial") return null; 197 return /* @__PURE__ */ React.createElement("div", { 198 className: "hp-viz-message", 199 "data-state": state, 200 role: state === "error" ? "alert" : "status", 201 "aria-live": "polite" 202 }, /* @__PURE__ */ React.createElement("span", { 203 className: "hp-viz-message-marker", 204 "aria-hidden": "true" 205 }), /* @__PURE__ */ React.createElement("span", null, message || statusText(state, copy))); 206 } 207 function DataTableDisclosure({ label, testId, headers, rows, copy }) { 208 const [mode, setMode] = React.useState("table"); 209 return /* @__PURE__ */ React.createElement("details", { className: "hp-viz-table-disclosure" }, /* @__PURE__ */ React.createElement("summary", null, label), /* @__PURE__ */ React.createElement("div", { 210 className: "hp-viz-data-switch", 211 role: "group", 212 "aria-label": label 213 }, /* @__PURE__ */ React.createElement("button", { 214 type: "button", 215 "data-active": mode === "table", 216 "aria-pressed": mode === "table", 217 onClick: () => setMode("table") 218 }, copy.tableView), /* @__PURE__ */ React.createElement("button", { 219 type: "button", 220 "data-active": mode === "list", 221 "aria-pressed": mode === "list", 222 onClick: () => setMode("list") 223 }, copy.listView)), /* @__PURE__ */ React.createElement("div", { 224 className: "hp-viz-table-scroll", 225 hidden: mode !== "table" 226 }, /* @__PURE__ */ React.createElement("table", { 227 className: "hp-viz-table", 228 "data-testid": testId 229 }, /* @__PURE__ */ React.createElement("thead", null, /* @__PURE__ */ React.createElement("tr", null, headers.map((header) => /* @__PURE__ */ React.createElement("th", { 230 key: header.key, 231 scope: "col" 232 }, header.label)))), /* @__PURE__ */ React.createElement("tbody", null, rows.map((row) => /* @__PURE__ */ React.createElement("tr", { 233 key: row.id, 234 "data-state": row.state 235 }, headers.map((header, index) => index === 0 ? /* @__PURE__ */ React.createElement("th", { 236 key: header.key, 237 scope: "row" 238 }, row.cells[header.key]) : /* @__PURE__ */ React.createElement("td", { key: header.key }, row.cells[header.key]))))))), /* @__PURE__ */ React.createElement("ul", { 239 className: "hp-viz-data-list", 240 "data-testid": `${testId}-list`, 241 hidden: mode !== "list" 242 }, rows.map((row) => /* @__PURE__ */ React.createElement("li", { 243 key: row.id, 244 "data-state": row.state 245 }, headers.map((header) => /* @__PURE__ */ React.createElement("div", { key: header.key }
245, /* @__PURE__ */ React.createElement("span", null, header.label), /* @__PURE__ */ React.createElement("strong", null, row.cells[header.key]))))))); 246 } 247 function compactLabel(value, limit = 12) { 248 const text = String(value || ""); 249 return text.length > limit ? `${text.slice(0, Math.max(1, limit - 1))}â¦` : text; 250 } 251 function buildTrendGeometry(points, width, height) { 252 const left = 52; 253 const right = 16; 254 const top = 20; 255 const bottom = 48; 256 const plotWidth = width - left - right; 257 const plotHeight = height - top - bottom; 258 const available = points.map((point) => point.value).filter((value) => value !== null); 259 if (!available.length) return { 260 left, 261 top, 262 plotWidth, 263 plotHeight, 264 min: 0, 265 max: 1, 266 points: [] 267 }; 268 let min = Math.min(...available); 269 let max = Math.max(...available); 270 if (min === max) { 271 const padding = Math.abs(min) * .1 || 1; 272 min -= padding; 273 max += padding; 274 } 275 return { 276 left, 277 top, 278 plotWidth, 279 plotHeight, 280 min, 281 max, 282 points: points.map((point, index) => ({ 283 ...point, 284 x: left + (points.length === 1 ? plotWidth / 2 : index / (points.length - 1) * plotWidth), 285 y: point.value === null ? null : top + (max - point.value) / (max - min) * plotHeight 286 })) 287 }; 288 } 289 function trendSegments(points) { 290 const segments = []; 291 let current = []; 292 points.forEach((point) => { 293 if (point.value === null) { 294 if (current.length) segments.push(current); 295 current = []; 296 return; 297 } 298 current.push(point); 299 }); 300 if (current.length) segments.push(current); 301 return segments; 302 } 303 function AccessibleTrend({ title, source, period, unit, timezone, state: requestedState, statusLabel, stateMessage, question, audience, decision, provenance, points = [], formatValue: valueFormatter, copy: copyOverrides }) { 304 const copy = mergeCopy(copyOverrides); 305 const normalized = points.map((point, index) => ({ 306 id: (point === null || point === void 0 ? void 0 : point.id) || `point-${index + 1}`, 307 label: String((point === null || point === void 0 ? void 0 : point.label) || ""), 308 value: (point === null || point === void 0 ? void 0 : point.status) === "missing" ? null : finiteNumber(point === null || point === void 0 ? void 0 : point.value), 309 detail: (point === null || point === void 0 ? void 0 : point.detail) ? String(point.detail) : "" 310 })); 311 const resolution = resolveVisualizationState({ 312 provenance, 313 requestedState, 314 metadata: [ 315 title, 316 question, 317 audience, 318 decision, 319 source, 320 period, 321 timezone, 322 unit 323 ], 324 values: normalized.map((point) => point.value), 325 copy 326 }); 327 const state = resolution.state; 328 const resolvedStateMessage = resolution.message || stateMessage; 329 const trendInstructionsId = React.useId(); 330 const pointRefs = React.useRef([]); 331 const [activePointIndex, setActivePointIndex] = React.useState(0); 332 const width = 720; 333 const height = 280; 334 const geometry = buildTrendGeometry(normalized, width, height); 335 const segments = trendSegments(geometry.points); 336 const tickValues = [ 337 geometry.max, 338 (geometry.max + geometry.min) / 2, 339 geometry.min 340 ]; 341 const labelStep = Math.max(1, Math.ceil(normalized.length / 6)); 342 React.useEffect(() => {
343 if (activePointIndex < normalized.length) return; 344 setActivePointIndex(Math.max(0, normalized.length - 1)); 345 }, [activePointIndex, normalized.length]); 346 const focusPoint = (index) => { 347 var _pointRefs$current$bo; 348 const bounded = Math.max(0, Math.min(normalized.length - 1, index)); 349 setActivePointIndex(bounded); 350 (_pointRefs$current$bo = pointRefs.current[bounded]) === null || _pointRefs$current$bo === void 0 ? void 0 : _pointRefs$current$bo.focus({ preventScroll: true }); 351 }; 352 const handlePointKeyDown = (event, index) => { 353 if (!normalized.length) return; 354 const keys = { 355 ArrowLeft: index - 1, 356 ArrowUp: index - 1, 357 ArrowRight: index + 1, 358 ArrowDown: index + 1, 359 Home: 0, 360 End: normalized.length - 1 361 }; 362 if (!(event.key in keys)) return; 363 event.preventDefault(); 364 focusPoint(keys[event.key]); 365 }; 366 const tableRows = normalized.map((point) => ({ 367 id: point.id, 368 state: valueState(point.value), 369 cells: { 370 period: point.label || copy.missingMetadata, 371 value: point.value === null ? copy.missingValue : formatNumber(point.value, valueFormatter) 372 } 373 })); 374 return /* @__PURE__ */ React.createElement(VisualizationFrame, { 375 kind: "trend", 376 title, 377 source, 378 period, 379 unit, 380 timezone, 381 state, 382 stateLabel: statusLabel, 383 question, 384 audience, 385 decision, 386 provenance, 387 copy 388 }, /* @__PURE__ */ React.createElement(EmptyOrLoadingState, { 389 state, 390 message: resolvedStateMessage, 391 copy 392 }
392), (state === "ready" || state === "partial") && /* @__PURE__ */ React.createElement("div", { 393 className: "hp-viz-trend", 394 "data-testid": "decision-trend" 395 }, /* @__PURE__ */ React.createElement("p", { 396 className: "hp-ds-visually-hidden", 397 id: trendInstructionsId 398 }, copy.trendInstructions), /* @__PURE__ */ React.createElement("div", { 399 className: "hp-viz-trend-chart-scroll", 400 tabIndex: "0" 401 }, /* @__PURE__ */ React.createElement("svg", { 402 className: "hp-viz-trend-svg", 403 viewBox: `0 0 ${width} ${height}`, 404 role: "group", 405 "aria-describedby": trendInstructionsId, 406 "aria-label": `${title || copy.missingTitle}. ${period || copy.missingMetadata}.` 407 }, tickValues.map((tick, index) => { 408 const y = geometry.top + index / 2 * geometry.plotHeight; 409 return /* @__PURE__ */ React.createElement("g", { 410 key: `tick-${index}`, 411 className: "hp-viz-gridline" 412 }, /* @__PURE__ */ React.createElement("line", { 413 x1: geometry.left, 414 y1: y, 415 x2: geometry.left + geometry.plotWidth, 416 y2: y 417 }), /* @__PURE__ */ React.createElement("text", { 418 x: geometry.left - 8, 419 y: y + 4, 420 textAnchor: "end" 421 }, formatNumber(tick, valueFormatter))); 422 }), segments.map((segment, index) => /* @__PURE__ */ React.createElement("polyline", { 423 key: `segment-${index}`, 424 className: "hp-viz-trend-line", 425 points: segment.map((point) => `${point.x},${point.y}`).join(" "), 426 fill: "none", 427 vectorEffect: "non-scaling-stroke" 428 })), geometry.points.map((point, index) => /* @__PURE__ */ React.createElement("g", { 429 key: point.id, 430 className: "hp-viz-point", 431 "data-state": valueState(point.value), 432 tabIndex: index === activePointIndex ? 0 : -1, 433 role: "img", 434 ref: (node) => { 435 pointRefs.current[index] = node; 436 }, 437 onFocus: () => setActivePointIndex(index), 438 onKeyDown: (event) => handlePointKeyDown(event, index), 439 onPointerDown: (event) => { 440 setActivePointIndex(index); 441 event.currentTarget.focus({ preventScroll: true }); 442 }, 443 "aria-label": `${point.label}: ${point.value === null ? copy.missingValue : formatNumber(point.value, valueFormatter)}${point.detail ? `. ${point.detail}` : ""}` 444 }, /* @__PURE__ */ React.createElement("circle", { 445 className: "hp-viz-point-target", 446 cx: point.x, 447 cy: point.value === null ? geometry.top + geometry.plotHeight : point.y, 448 r: "25" 449 }
449), point.value === null ? /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement("line", { 450 className: "hp-viz-missing-mark", 451 x1: point.x - 5, 452 y1: geometry.top + geometry.plotHeight - 5, 453 x2: point.x + 5, 454 y2: geometry.top + geometry.plotHeight + 5 455 }), /* @__PURE__ */ React.createElement("line", { 456 className: "hp-viz-missing-mark", 457 x1: point.x + 5, 458 y1: geometry.top + geometry.plotHeight - 5, 459 x2: point.x - 5, 460 y2: geometry.top + geometry.plotHeight + 5 461 })) : /* @__PURE__ */ React.createElement("circle", { 462 className: "hp-viz-point-dot", 463 cx: point.x, 464 cy: point.y, 465 r: "4.5" 466 }), (index % labelStep === 0 || index === geometry.points.length - 1) && /* @__PURE__ */ React.createElement("text", { 467 className: "hp-viz-axis-label", 468 x: point.x, 469 y: height - 16, 470 textAnchor: "middle" 471 }, compactLabel(point.label)), point.value !== null && /* @__PURE__ */ React.createElement("g", { 472 className: "hp-viz-tooltip", 473 "aria-hidden": "true" 474 }, /* @__PURE__ */ React.createElement("rect", { 475 x: Math.max(4, Math.min(width - 108, point.x - 50)), 476 y: Math.max(4, point.y - 42), 477 width: "100", 478 height: "28", 479 rx: "4" 480 }), /* @__PURE__ */ React.createElement("text", { 481 x: Math.max(54, Math.min(width - 58, point.x)), 482 y: Math.max(23, point.y - 23), 483 textAnchor: "middle" 484 }, formatNumber(point.value, valueFormatter))))))), /* @__PURE__ */ React.createElement(DataTableDisclosure, { 485 label: copy.dataTable, 486 testId: "decision-trend-table", 487 headers: [{ 488 key: "period", 489 label: copy.period 490 }, { 491 key: "value", 492 label: copy.value 493 }], 494 rows: tableRows, 495 copy 496 }))); 497 } 498 function CoverageBars({ title, source, period, unit, timezone, state: requestedState, statusLabel, stateMessage, question, audience, decision, provenance, items = [], formatValue: valueFormatter, formatPercent: percentFormatter, copy: copyOverrides }) { 499 const copy = mergeCopy(copyOverrides); 500 const normalized = items.map((item, index) => { 501 const value = (item === null || item === void 0 ? void 0 : item.status) === "missing" ? null : finiteNumber(item === null || item === void 0 ? void 0 : item.value); 502 const total = (item === null || item === void 0 ? void 0 : item.status) === "missing" ? null : finiteNumber(item === null || item === void 0 ? void 0 : item.total); 503 const present = value !== null && total !== null; 504 const invalid = present && (value < 0 || total < 0 || value > total); 505 const available = present && !invalid; 506 const zeroBasis = available && total === 0; 507 return { 508 id: (item === null || item === void 0 ? void 0 : item.id) || `coverage-${index + 1}`, 509 label: String((item === null || item === void 0 ? void 0 : item.label) || copy.missingMetadata), 510 value: available ? value : null, 511 total: available ? total : null, 512 ratio: available ? zeroBasis ? 0 : value / total : null, 513 zeroBasis, 514 state: invalid ? "error" : available ? valueState(value) : "missing" 515 }; 516 }).sort((a, b) => { 517 if (a.ratio === null) return 1; 518 if (b.ratio === null) return -1; 519 return b.ratio - a.ratio; 520 }); 521 const resolution = resolveVisualizationState({ 522 provenance, 523 requestedState, 524 metadata: [ 525 title, 526 question, 527 audience, 528 decision, 529 source, 530 period, 531 timezone, 532 unit 533 ], 534 values: normalized.map((item) => item.ratio), 535 invalidData: normalized.some((item) => item.state === "error"), 536 copy 537 }); 538 const state = resolution.state; 539 const resolvedStateMessage = resolution.message || stateMessage; 540 const tableRows = normalized.map((item) => ({ 541 id: item.id, 542 state: item.state, 543 cells: { 544 category: item.label, 545 value: item.value === null ? copy.missingValue : formatNumber(item.value, valueFormatter), 546 total: item.total === null ? copy.missingValue : formatNumber(item.total, valueFormatter), 547 coverage: item.ratio === null ? copy.missingValue : item.zeroBasis ? copy.noDenominator : formatPercent(item.ratio, percentFormatter) 548 } 549 })); 550 return /* @__PURE__ */ React.createElement(VisualizationFrame, { 551 kind: "coverage", 552 title, 553 source, 554 period, 555 unit, 556 timezone, 557 state, 558 stateLabel: statusLabel, 559 question, 560 audience, 561 decision, 562 provenance, 563 copy 564 }
564, /* @__PURE__ */ React.createElement(EmptyOrLoadingState, { 565 state, 566 message: resolvedStateMessage, 567 copy 568 }), (state === "ready" || state === "partial") && /* @__PURE__ */ React.createElement("ol", { 569 className: "hp-viz-bars", 570 "data-testid": "decision-coverage-bars" 571 }, normalized.map((item) => /* @__PURE__ */ React.createElement("li", { 572 key: item.id, 573 "data-state": item.state 574 }, /* @__PURE__ */ React.createElement("div", { className: "hp-viz-bar-label" }, /* @__PURE__ */ React.createElement("span", null, item.label), /* @__PURE__ */ React.createElement("strong", null, item.ratio === null ? copy.missingValue : `${formatNumber(item.value, valueFormatter)} / ${formatNumber(item.total, valueFormatter)} (${item.zeroBasis ? copy.noDenominator : formatPercent(item.ratio, percentFormatter)})`)), item.ratio === null ? /* @__PURE__ */ React.createElement("div", { 575 className: "hp-viz-progress-missing", 576 "aria-hidden": "true" 577 }) : /* @__PURE__ */ React.createElement("progress", { 578 className: "hp-viz-progress", 579 max: item.total || 1, 580 value: item.total === 0 ? 0 : item.value, 581 "aria-valuetext": item.zeroBasis ? copy.noDenominator : undefined, 582 "aria-label": `${item.label}: ${formatNumber(item.value, valueFormatter)} ${copy.of} ${formatNumber(item.total, valueFormatter)}` 583 }
583)))), (state === "ready" || state === "partial") && /* @__PURE__ */ React.createElement(DataTableDisclosure, { 584 label: copy.dataTable, 585 testId: "decision-coverage-table", 586 headers: [ 587 { 588 key: "category", 589 label: copy.category 590 }, 591 { 592 key: "value", 593 label: copy.value 594 }, 595 { 596 key: "total", 597 label: copy.total 598 }, 599 { 600 key: "coverage", 601 label: copy.coverage 602 } 603 ], 604 rows: tableRows, 605 copy 606 })); 607 } 608 function DeviceCounterItem({ type, item, label, copy, valueFormatter }) { 609 const numericCount = (item === null || item === void 0 ? void 0 : item.status) === "missing" ? null : finiteNumber(item === null || item === void 0 ? void 0 : item.count); 610 const previousCount = React.useRef(numericCount); 611 const [changed, setChanged] = React.useState(false); 612 React.useEffect(() => { 613 if (previousCount.current === numericCount) return undefined; 614 previousCount.current = numericCount; 615 setChanged(true); 616 const timer = global.setTimeout(() => setChanged(false), 220); 617 return () => global.clearTimeout(timer); 618 }, [numericCount]); 619 const icons = { 620 phone: "Smartphone", 621 tablet: "Tablet", 622 desktop: "Monitor" 623 }; 624 return /* @__PURE__ */ React.createElement("li", { 625 "data-device": type, 626 "data-state": valueState(numericCount), 627 "data-changed": changed ? "true" : "false" 628 }, /* @__PURE__ */ React.createElement("span", { className: "hp-viz-device-icon" }, /* @__PURE__ */ React.createElement(DecisionIcon, { 629 name: icons[type], 630 size: 20 631 })), /* @__PURE__ */ React.createElement("span", { className: "hp-viz-device-label" }, label), /* @__PURE__ */ React.createElement("strong", { 632 className: "hp-viz-device-count", 633 "aria-live": "polite", 634 "aria-atomic": "true", 635 "aria-label": `${label}: ${numericCount === null ? copy.missingValue : numericCount}` 636 }, numericCount === null ? "â" : formatNumber(numericCount, valueFormatter))); 637 } 638 function DeviceCounters({ title, source, period, unit, timezone, state: requestedState, statusLabel, stateMessage, question, audience, decision, provenance, devices = [], labels, formatValue: valueFormatter, copy: copyOverrides }) { 639 const copy = mergeCopy(copyOverrides); 640 const byType = new Map(devices.map((item) => [item === null || item === void 0 ? void 0 : item.type, item])); 641 const values = DEVICE_TYPES.map((type) => { 642 const item = byType.get(type); 643 return (item === null || item === void 0 ? void 0 : item.status) === "missing" ? null : finiteNumber(item === null || item === void 0 ? void 0 : item.count); 644 }); 645 const resolution = resolveVisualizationState({ 646 provenance, 647 requestedState, 648 metadata: [ 649 title, 650 question, 651 audience, 652 decision, 653 source, 654 period, 655 timezone, 656 unit 657 ], 658 values, 659 copy 660 }); 661 const state = resolution.state; 662 const resolvedStateMessage = resolution.message || stateMessage; 663 const typeLabels = Object.assign({ 664 phone: copy.phone, 665 tablet: copy.tablet, 666 desktop: copy.desktop 667 }, labels || {}); 668 const tableRows = DEVICE_TYPES.map((type, index) => ({ 669 id: type, 670 state: valueState(values[index]), 671 cells: { 672 category: typeLabels[type], 673 value: values[index] === null ? copy.missingValue : formatNumber(values[index], valueFormatter) 674 } 675 })); 676 return /* @__PURE__ */ React.createElement(VisualizationFrame, { 677 kind: "devices", 678 title, 679 source, 680 period, 681 unit, 682 timezone, 683 state, 684 stateLabel: statusLabel, 685 question, 686 audience, 687 decision, 688 provenance, 689 copy 690 }, /* @__PURE__ */ React.createElement(EmptyOrLoadingState, { 691 state, 692 message: resolvedStateMessage, 693 copy 694 }
694), (state === "ready" || state === "partial") && /* @__PURE__ */ React.createElement("ul", { 695 className: "hp-viz-devices", 696 "data-testid": "decision-device-counters", 697 "aria-label": title || copy.missingTitle 698 }, DEVICE_TYPES.map((type) => /* @__PURE__ */ React.createElement(DeviceCounterItem, { 699 key: type, 700 type, 701 item: byType.get(type), 702 label: typeLabels[type], 703 copy, 704 valueFormatter 705 }))), (state === "ready" || state === "partial") && /* @__PURE__ */ React.createElement(DataTableDisclosure, { 706 label: copy.dataTable, 707 testId: "decision-device-table", 708 headers: [{ 709 key: "category", 710 label: copy.category 711 }, { 712 key: "value", 713 label: copy.value 714 }], 715 rows: tableRows, 716 copy 717 })); 718 } 719 global.HDPDecisionVisualizations = Object.freeze({ 720 contract: Object.freeze({ 721 schema: "2026-08-24.vis-sec", 722 states: Object.freeze([...VALID_STATES]), 723 deviceTypes: Object.freeze([...DEVICE_TYPES]), 724 missingValuesAreNeverZero: true, 725 explicitZeroState: true, 726 alternativeTables: "table_and_list", 727 trendKeyboardNavigation: "arrow_home_end", 728 touchTarget: 44, 729 requiredProvenance: "server_confirmed", 730 previewProvenance: "demo_fixture", 731 requiredMetadata: Object.freeze([ 732 "source", 733 "period", 734 "timezone", 735 "unit", 736 "state" 737 ]), 738 requiredDecisionContext: Object.freeze([ 739 "title", 740 "question", 741 "audience", 742 "decision" 743 ]), 744 routeIntegration: false 745 }), 746 AccessibleTrend, 747 CoverageBars, 748 DeviceCounters 749 }); 750})(window); 751 752})();
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.