feat: initial Signage 0.1.0
This commit is contained in:
@@ -0,0 +1,69 @@
|
||||
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.0",
|
||||
styles: displayStyles,
|
||||
mount(container, context) {
|
||||
const root = createRoot(container);
|
||||
root.render(<SignageDisplay context={context} />);
|
||||
return () => root.unmount();
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user