70 lines
2.8 KiB
TypeScript
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();
|
|
},
|
|
});
|