Files
Signage/src/display/index.tsx
T
2026-09-29 19:33:56 -05:00

70 lines
2.8 KiB
TypeScript

import { defineModuleUi, type ModuleUiHostContext } from "@ekklesia/module-ui-sdk";
import { type ReactElement, useEffect, useRef } from "react";
import { createRoot } from "react-dom/client";
import { SignageSlate } from "../browser/slate.js";
import { createDisplayController } from "./controller.js";
import displayStyles from "./styles.css";
/**
* Signage's full-viewport display.
*
* React renders the fixed furniture — the stage's root, the blackout cover, and the slate — and the
* controller owns everything that moves. That split is deliberate: the two scene layers are created
* once and never rebuilt, and a React tree that re-created them on every state change would be the
* `v1` renderer again.
*
* The artifact performs no permission check of its own. The host resolved an authorization-bearing
* descriptor before these bytes were ever fetched, and a television has nobody standing at it to act
* on a denial — telling a lobby wall "you may not see this" would only report something about the
* installation's authorization to whoever walks past.
*/
function SignageDisplay({ context }: { readonly context: ModuleUiHostContext }): ReactElement {
const stageRef = useRef<HTMLDivElement | null>(null);
useEffect(() => {
const root = stageRef.current;
if (!root) {
return;
}
const controller = createDisplayController({ context, root });
void controller.start();
return () => {
void controller.stop();
};
}, [context]);
return (
<main className="signage-display" data-testid="signage-display">
<div className="signage-stage" data-testid="signage-stage" ref={stageRef}>
{/* Shown only when the stage says there is nothing renderable *and* no good frame. */}
<div className="signage-slate-host">
<SignageSlate />
</div>
{/* A deliberate blackout: pure black over everything, while the hidden timeline runs on. */}
<div aria-hidden="true" className="signage-blackout" data-testid="signage-blackout" />
{/* Continuing from this screen's own last authoritative state. Small and in a corner: a
congregation is looking at this wall, and a network is not their problem. */}
<p className="signage-offline-note" data-testid="signage-offline-note">
Offline — continuing from the last update
</p>
</div>
</main>
);
}
/**
* The compiled full-viewport display artifact. It uses the same role-neutral browser ABI as the
* management UI; its separate manifest entrypoint — not a mode flag — is what tells a host which
* artifact it is mounting.
*/
export default defineModuleUi({
id: "signage",
version: "0.1.1",
styles: displayStyles,
mount(container, context) {
const root = createRoot(container);
root.render(<SignageDisplay context={context} />);
return () => root.unmount();
},
});