/* * Shared presentation helpers for the reference exhibits. * * Generic DOM plumbing only: element lookup, safe text writing, and the * announcement strip. It has no contract awareness and no domain knowledge, * so an exhibit can use it, ignore it, or replace it without touching the * contract layer. * * The one rule worth stating out loud: every piece of text that reaches the * DOM goes through `textContent`. Nothing in this project ever assigns * `innerHTML`, so a string arriving from a host or a scenario can never * become markup or script (Contract §20, Authoring Guide §O). */ (function () { 'use strict'; function el(id) { return document.getElementById(id); } /** Write text safely. Always textContent, never innerHTML. */ function setText(node, text) { if (!node) return; node.textContent = text === null || text === undefined ? '' : String(text); } /** * Announcement strip. Text is data: it is truncated to the declared * maximum and written with textContent. */ function Announcer(options) { this.node = options.node; this.idleText = options.idleText || ''; this.maxLength = options.maxLength || 120; this._timer = null; this.setText(this.idleText, true); } Announcer.prototype.setText = function (text, idle) { if (!this.node) return; var value = text === null || text === undefined ? '' : String(text); if (value.length > this.maxLength) value = value.slice(0, this.maxLength); this.node.textContent = value; if (idle) { this.node.classList.add('is-idle'); } else { this.node.classList.remove('is-idle'); } }; /** Show a transient message, then fall back to the idle text. */ Announcer.prototype.flash = function (text, ms) { this.setText(text, false); if (this._timer) clearTimeout(this._timer); var self = this; this._timer = setTimeout(function () { self.setText(self.idleText, true); }, ms || 2600); }; /** Format a number for a readout, with a fixed number of decimals. */ function formatNumber(value, decimals) { if (typeof value !== 'number' || !isFinite(value)) return '--'; return value.toFixed(decimals === undefined ? 2 : decimals); } /** Format a 0..1 value as a whole percentage. */ function formatPercent(value) { if (typeof value !== 'number' || !isFinite(value)) return '--'; return Math.round(value * 100) + '%'; } /** Format a 0..1 value as a whole number of degrees. */ function formatDegrees(value) { if (typeof value !== 'number' || !isFinite(value)) return '--'; return Math.round(value * 360) + '\u00b0'; } /** Format a 0..1 value as a whole number of minutes. */ function formatMinutes(value) { if (typeof value !== 'number' || !isFinite(value)) return '--'; return Math.round(value * 60) + ' min'; } window.XZBTShell = { el: el, setText: setText, Announcer: Announcer, formatNumber: formatNumber, formatPercent: formatPercent, formatDegrees: formatDegrees, formatMinutes: formatMinutes }; })();