PageSourceSearch

https://hogardepaz.netlify.app/app/decision-visualizations.js?v=1dc4866f1910e597

js hogardepaz.netlify.app collected 2026-10-03 10:52:37 UTC 26,864 bytes, 752 lines download raw bytes

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.