/* RADAR1 — Program Out: stage composition, layout state machine, scaling */
const DSa = window.Radar1DesignSystem_1f4661;
const STAGE_W = 1920, STAGE_H = 1080, HERO_W = 1280, HERO_H = 720, MARGIN = 48, RAIL_GAP = 44, INFO_H = 64;
const APP_DEFAULTS = {
layout: "A", auto: true, mode: "normal",
heroScene: "studio", tile: null, boxes: 0, cams: [], camTimes: {}, countdown: false,
h24: true, l3Variant: "story",
insets: { l3: true, ots: false, phone: false, map: false, loc: false, source: false },
};
const TWEAK_DEFAULTS = /*EDITMODE-BEGIN*/{
"tickerDur": 46,
"railDwell": 18,
"swapMin": 10,
"safeGuides": false,
"backdrop": "auto"
}/*EDITMODE-END*/;
function loadState() {
try {
const raw = localStorage.getItem("r1-program-v1");
if (!raw) return APP_DEFAULTS;
const p = JSON.parse(raw);
const merged = { ...APP_DEFAULTS, ...p, insets: { ...APP_DEFAULTS.insets, ...(p.insets || {}) } };
if (merged.mode === "critical") merged.mode = "situation"; /* legacy name */
if (merged.mode !== "normal" && merged.mode !== "situation") merged.mode = "normal";
if (merged.tile === "cutspot" && !(window.R1DATA || {}).cutspot) merged.tile = null; /* persisted spot with no content — don't blank the hero */
return merged;
} catch (e) { return APP_DEFAULTS; }
}
/* ---------- Hero inset (1280×720 framed program video) ---------- */
function HeroInset({ st, h24 }) {
const now = useNow();
const ins = st.insets;
const tileActive = !!st.tile, boxesActive = st.boxes > 0;
const situation = st.mode === "situation";
const showL3 = ins.l3 && !tileActive;
const TileComp = tileActive ? window.TILES[st.tile].comp : null;
return (
{/* content layer — program video is keyed in downstream: the inlay is a clean chroma hole.
Magenta: green/blue/cyan all collide with our signal accents; nothing in the package is magenta. */}
{tileActive ?
: boxesActive ?
:
}
{/* scrim under lower third (never over the chroma hole — it would spoil the key) */}
{showL3 && tileActive && (
)}
{/* dressings */}
{ins.ots && !tileActive && !boxesActive && !ins.phone &&
}
{ins.phone && !tileActive && !boxesActive &&
}
{ins.map && !tileActive &&
}
{showL3 &&
}
{/* hairline frame drawn OVER the feed — content box stays a true 1280×720 */}
);
}
/* ---------- Corner flag (unused in current modes; kept for future events) ---------- */
function CornerBrand() { return null; }
/* ---------- Animated stage backdrop (subtle, 4 looks, slow crossfade) ---------- */
const BACKDROPS = ["sweep", "grid3d", "spotlight", "lake"];
/* deterministic droplet field for the lake look */
const LAKE_DROPS = Array.from({ length: 9 }, (_, i) => {
let s = i * 971 + 31;
const r = () => (s = (s * 16807) % 2147483647) / 2147483647;
return { x: 4 + r() * 90, y: 10 + r() * 78, dur: 5 + r() * 4, delay: -r() * 9, size: 220 + r() * 260 };
});
function StageBackdrop({ mode, situation }) {
const auto = mode === "auto" || !BACKDROPS.includes(mode);
const ai = useCycle(BACKDROPS.length, 150000);
const active = situation ? "sweep" : (auto ? BACKDROPS[ai] : mode);
const beam = situation ? "rgba(228,3,46,0.05)" : "rgba(31,168,224,0.065)";
const beamFaint = situation ? "rgba(228,3,46,0.014)" : "rgba(31,168,224,0.018)";
const layer = (name, on, children) => (
{children}
);
const GRID_C = 20, GRID_R = 12;
return (
{/* radar sweep — faint rotating beam + range rings from stage center */}
{layer("sweep", active === "sweep", (
{[520, 860, 1240].map((d) => (
))}
))}
{/* floating 3D tile plane — a diagonal ripple breathes through the squares */}
{layer("grid3d", active === "grid3d", (
{Array.from({ length: GRID_C * GRID_R }, (_, i) => {
const r = i / GRID_C | 0, c = i % GRID_C;
return (
);
})}
))}
{/* searching spotlight — a soft beam wanders the studio floor */}
{layer("spotlight", active === "spotlight", (
))}
{/* lake — droplets strike and rings spread across still water */}
{layer("lake", active === "lake", (
{LAKE_DROPS.map((d, i) => (
{[0, 0.45].map((off, k) => (
))}
))}
))}
);
}
/* ---------- The 1080p stage ---------- */
function Stage({ st, tweaks, pendingLayout, pendingMode, lastSwapKind, breakout, onBreakoutDone }) {
const situation = st.mode === "situation";
/* furniture height is identical in both modes so the hero never shifts */
const stackH = 104;
const furn = INFO_H + stackH;
const isA = st.layout === "A";
const swapping = pendingLayout != null && pendingLayout !== st.layout;
const modeSwapping = pendingMode != null && pendingMode !== st.mode;
/* layout swaps: hero + furniture share ONE 980ms broadcast-ease glide — old furniture
slides out while the new slides in, all in lockstep, like tiles moved as a single unit.
Mode swaps keep the slower fade choreography. */
const SWAP_MS = 980;
const exitDur = modeSwapping ? 1500 : SWAP_MS;
const enterDur = lastSwapKind === "mode" ? 1440 : 480;
/* hero slides toward the TARGET layout as soon as a swap starts */
const heroIsA = (pendingLayout || st.layout) === "A";
const heroY = heroIsA ? Math.round((STAGE_H - furn - HERO_H) / 2) : furn + Math.round((STAGE_H - furn - HERO_H) / 2);
const heroX = heroIsA ? STAGE_W - HERO_W - MARGIN : MARGIN;
const railW = STAGE_W - HERO_W - 2 * MARGIN - RAIL_GAP;
const railXFor = (a) => (a ? MARGIN : MARGIN + HERO_W + RAIL_GAP);
const railYFor = (a) => (a ? Math.round((STAGE_H - furn - HERO_H) / 2) : furn + Math.round((STAGE_H - furn - HERO_H) / 2));
const railX = railXFor(isA);
const railHeroY = railYFor(isA);
/* furniture choreography — the main rail/bars element lives in the TARGET slot during a swap
and persists after it (same key, same animation string), so nothing remounts or snaps at the end */
const railAnim = modeSwapping
? `r1-out-${isA ? "left" : "right"} ${exitDur}ms var(--ease-in) both`
: lastSwapKind === "mode" && !swapping
? `r1-in-${isA ? "left" : "right"} ${enterDur}ms var(--ease-broadcast) both`
: `r1-tile-in-${heroIsA ? "left" : "right"} ${SWAP_MS}ms var(--ease-broadcast) both`;
const barAnim = modeSwapping
? `r1-out-${isA ? "bottom" : "top"} ${exitDur}ms var(--ease-in) both`
: lastSwapKind === "mode" && !swapping
? `r1-in-${isA ? "bottom" : "top"} ${enterDur}ms var(--ease-broadcast) both`
: `r1-tile-in-${heroIsA ? "down" : "up"} ${SWAP_MS}ms var(--ease-broadcast) both`;
const brandAnim = swapping || modeSwapping
? `r1-out-fade ${exitDur}ms var(--ease-in) both`
: `r1-in-fade ${enterDur}ms var(--ease-out) both`;
const BreakoutComp = breakout ? window.BREAKOUTS[breakout] : null;
return (
{/* crisis tone — the studio navy shifts to a somber deep red */}
{/* corner brand in negative space */}
{/* side rail */}
{/* outgoing rail — keyed as the pre-swap main element, so the SAME instance slides out */}
{swapping && (
{st.countdown && }
)}
{/* main rail — sits in the target slot from swap start and persists past the flip */}
{st.countdown && }
{/* hero — glides between corners on layout swap */}
{/* frame-attached boxes (outside the inlay) */}
{st.insets.loc && !st.tile && (
)}
{st.insets.source && !st.tile && (
)}
{/* info bar + tickers move as one edge group; situation merges the info bar into the double flipper */}
{/* outgoing bars — same instance as the pre-swap main bars, sliding off its edge */}
{swapping && (
)}
{/* main bars — target edge from swap start, persists past the flip */}
{/* title-safe guides */}
{tweaks.safeGuides && (
TITLE SAFE
)}
{/* breakout scenes */}
{BreakoutComp &&
}
);
}
/* ---------- App: scaling + state + console ---------- */
function App() {
const [t, setTweak] = useTweaks(TWEAK_DEFAULTS);
const [st, setSt] = React.useState(loadState);
const [pendingLayout, setPendingLayout] = React.useState(null);
const pendingRef = React.useRef(null);
const [pendingMode, setPendingMode] = React.useState(null);
const pendingModeRef = React.useRef(null);
const [lastSwapKind, setLastSwapKind] = React.useState("layout");
const [breakout, setBreakout] = React.useState(null);
const [nextSwapAt, setNextSwapAt] = React.useState(() => Date.now() + t.swapMin * 60000);
const [scale, setScale] = React.useState(0.5);
const [fit, setFit] = React.useState("fit");
const monitorRef = React.useRef(null);
/* mode swap: same choreography as a layout swap but at 1/3 speed —
background fades while the old furniture leaves and the new bar styles come in */
const swapModeTo = React.useCallback((next) => {
setSt((s) => {
if (next === s.mode || pendingModeRef.current || pendingRef.current) return s;
pendingModeRef.current = next;
setPendingMode(next);
setTimeout(() => {
setSt((s2) => ({ ...s2, mode: next }));
setPendingMode(null);
pendingModeRef.current = null;
setLastSwapKind("mode");
}, 1500);
return s;
});
}, []);
const set = React.useCallback((k, v) => {
if (k === "mode") { swapModeTo(v); return; }
setSt((s) => {
const val = typeof v === "function" ? v(s[k]) : v;
const n = { ...s, [k]: val };
return n;
});
}, [swapModeTo]);
React.useEffect(() => {
try { localStorage.setItem("r1-program-v1", JSON.stringify(st)); } catch (e) {}
}, [st]);
/* stage scaling */
React.useEffect(() => {
const el = monitorRef.current;
if (!el) return;
const ro = new ResizeObserver(() => {
const r = el.getBoundingClientRect();
setScale(Math.min(r.width / STAGE_W, r.height / STAGE_H));
});
ro.observe(el);
return () => ro.disconnect();
}, []);
/* layout swap: hero glides to its new corner while old furniture exits
toward its edge and new furniture pushes in from the opposite side */
const swapNow = React.useCallback((target) => {
if (pendingRef.current) return;
const next = target || (st.layout === "A" ? "B" : "A");
if (next === st.layout) return;
pendingRef.current = next;
setPendingLayout(next);
setTimeout(() => {
setSt((s) => ({ ...s, layout: next }));
setPendingLayout(null);
pendingRef.current = null;
setLastSwapKind("layout");
setNextSwapAt(Date.now() + t.swapMin * 60000);
}, 980);
}, [st.layout, t.swapMin]);
/* auto-swap timer */
const now = useNow();
React.useEffect(() => { setNextSwapAt(Date.now() + t.swapMin * 60000); }, [t.swapMin, st.auto]);
React.useEffect(() => {
if (st.auto && Date.now() >= nextSwapAt && !breakout) swapNow();
}, [now, st.auto, nextSwapAt, breakout, swapNow]);
const nextSwapIn = Math.max(0, Math.round((nextSwapAt - now.getTime()) / 1000));
const playBreakout = React.useCallback((k) => setBreakout((b) => (b === k ? null : k)), []);
/* live director (js/live.js dispatches "r1director" on change): the server drives
the on-air state through the same API the hidden console uses */
const lastBreakoutRef = React.useRef(null);
React.useEffect(() => {
const applyDirector = (d) => {
if (!d) return;
if (d.mode === "normal" || d.mode === "situation") set("mode", d.mode);
const tile = d.tile && window.TILES[d.tile] ? d.tile : null;
set("tile", tile);
set("countdown", !!d.countdown);
/* lower third: register server content as a variant, toggle the inset from l3_on */
if (d.l3 && d.l3.headline && window.L3_VARIANTS) {
window.L3_VARIANTS.live_server = {
kicker: d.l3.kicker || "TOP STORY", accent: d.l3.accent || "var(--radar-red)",
headline: d.l3.headline, subline: d.l3.subline || "",
};
set("l3Variant", "live_server");
}
if (typeof d.l3_on === "boolean") set("insets", (p) => ({ ...p, l3: d.l3_on }));
/* breakout is one-shot: fire only when the value changes; null clears nothing */
const prev = lastBreakoutRef.current;
lastBreakoutRef.current = d.breakout || null;
if (d.breakout && d.breakout !== prev && window.BREAKOUTS[d.breakout]) {
if (window.R1LIVE) window.R1LIVE.appliedBreakout = d.breakout;
playBreakout(d.breakout);
}
};
const onDirector = (e) => applyDirector((e && e.detail) || (window.R1LIVE && window.R1LIVE.director));
window.addEventListener("r1director", onDirector);
/* live.js may have connected before we mounted */
if (window.R1LIVE && window.R1LIVE.director) applyDirector(window.R1LIVE.director);
return () => window.removeEventListener("r1director", onDirector);
}, [set, playBreakout]);
useLucide([st]);
return (
{/* monitor area */}
{fit === "exact" ? (
setBreakout(null)} />
) : (
setBreakout(null)} />
)}
{/* director console — HIDDEN, not removed: the state API (set/swapNow/playBreakout) is the
toggle surface the companion app will drive; keep it mounted so that wiring stays live */}
setTweak("tickerDur", v)} />
setTweak("railDwell", v)} />
setTweak("swapMin", v)} />
setTweak("backdrop", v)} />
setTweak("safeGuides", v)} />
);
}
ReactDOM.createRoot(document.getElementById("root")).render();