/* * Museum Gallery — Artifact Display boot (non-primary surface). * * This document deliberately never loads contract-core.js, exhibit.js, or * contract-adapter.js. It has no way to construct an Exhibit State Core -- * it can only attach to the one the Control Room document created, over * surface-bus.js. If the Control Room is not open, this degrades to a clear * waiting state rather than inventing its own state (Contract 5.3 Section * 31.8, Step 6.1 Section 7.2). */ (function () { 'use strict'; var Shell = window.XZBTShell; var el = Shell.el; var statusEl = el('status'); var announcer = new Shell.Announcer({ node: el('announcement'), idleText: 'Waiting for shared state.' }); var cycleButton = el('cycle-button'); var ARTIFACT_ORDER = ['the-orrery', 'star-map', 'meteorite']; var ARTIFACT_LABELS = { 'the-orrery': 'The Orrery', 'star-map': 'Star Map', meteorite: 'Meteorite Fragment' }; /* This is a read-only MIRROR of authoritative state, not a second copy of * exhibit state: it is discarded and re-seeded from a fresh snapshot on * every attach, and every write attempt is a mutate() call routed back * through the owner's canonical mutation path -- this document never * writes to `latest` in response to a user action, only in response to * a snapshot or an event relayed from the one Core. */ var latest = null; function render() { if (!latest) return; Shell.setText(el('artifact-name'), ARTIFACT_LABELS[latest['artifact.selected']] || latest['artifact.selected']); Shell.setText(el('lighting-readout'), Shell.formatPercent(latest['lighting.level'])); Shell.setText(el('rotation-readout'), latest['rotation.speed'].toFixed(1) + 'x'); } var link = window.MuseumGallerySurfaceBus.attach({ timeoutMs: 1500, onSnapshot: function (snapshot) { latest = snapshot.values; Shell.setText(statusEl, 'Attached to Control Room.'); statusEl.classList.remove('is-waiting'); cycleButton.disabled = false; render(); }, onEvent: function (event) { if (!latest) return; if (event.type === 'state.changed' || event.type === 'selection.changed') { latest[event.target] = event.value; render(); } else if (event.type === 'action.executed' && event.target === 'action.spotlight-flash') { announcer.flash('Spotlight flashed.'); } }, onTimeout: function () { Shell.setText(statusEl, 'Waiting for the Control Room surface to be open…'); statusEl.classList.add('is-waiting'); cycleButton.disabled = true; } }); cycleButton.addEventListener('click', function () { if (!latest) return; var idx = ARTIFACT_ORDER.indexOf(latest['artifact.selected']); var next = ARTIFACT_ORDER[(idx + 1) % ARTIFACT_ORDER.length]; /* Routed to the owner's canonical mutation path -- this is the required * proof that a native interaction on a non-primary surface obeys the * same mutation/revision/source/event semantics as primary UI or a * contract set/invoke (Contract 5.3 Section 31.7). */ link.mutate('set', 'artifact.selected', next); }); /* `beforeunload` is best-effort only -- it does not reliably fire when * NGN removes/replaces this frame's iframe element directly (as opposed * to a real navigation), so it is not the correctness mechanism here. * `__xzbtSurfaceDispose` is a well-known, optional hook: local-surfaces.js * calls it synchronously, if present, on this frame's contentWindow right * before removing the iframe -- deterministic because it is a direct * same-origin call, not a queued postMessage that a doomed frame might * never get to process. detach() is idempotent, so it is safe for both * this hook and beforeunload to fire (or neither, or either alone). */ window.__xzbtSurfaceDispose = function () { link.detach(); }; window.addEventListener('beforeunload', function () { link.detach(); }); window.__museumGalleryDebug = { link: link }; })();