@norskvideo/ctl-sdk 0.1.12 → 0.1.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,37 @@
1
+ import type { ReactElement } from "react";
2
+ /** One compose layer, matching a videoCompose preset generator's layer shape. */
3
+ export interface ComposeLayer {
4
+ sourceName: string;
5
+ zIndex: number;
6
+ opacity: number;
7
+ destRect?: {
8
+ x: number;
9
+ y: number;
10
+ width: number;
11
+ height: number;
12
+ };
13
+ }
14
+ export interface ComposeLayerPreviewProps {
15
+ /** Layers to composite (any order — sorted by `zIndex` here). Typically the
16
+ * product passes `PRESETS[preset](...).finalConfig.layers`. */
17
+ layers: ComposeLayer[];
18
+ /** The reference (canvas) resolution the layer `destRect`s are expressed in. */
19
+ reference: {
20
+ width: number;
21
+ height: number;
22
+ };
23
+ /** Latest still per source (sourceName → url), e.g. compose `sourceJpegUrls`. */
24
+ sourceJpegUrls?: Record<string, string>;
25
+ /** Raw HTML per browser source (sourceName → page). A layer whose `sourceName`
26
+ * has an entry renders the page in a transparent iframe keyed over the layers
27
+ * beneath, instead of that source's still — so a graphic preview shows the
28
+ * shot's real target page. Pages must be self-contained (inline CSS/JS). */
29
+ overlayHtmlBySource?: Record<string, string>;
30
+ /** Optional override for the backdrop shown where no layer covers. */
31
+ background?: string;
32
+ }
33
+ /**
34
+ * Composite a set of compose layers into a preview still. Pure presentational —
35
+ * no data fetching, no compose-preset knowledge.
36
+ */
37
+ export declare function ComposeLayerPreview({ layers, reference, sourceJpegUrls, overlayHtmlBySource, background, }: ComposeLayerPreviewProps): ReactElement;
@@ -0,0 +1,42 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ const MISSING_STILL = "#374151";
3
+ /**
4
+ * Composite a set of compose layers into a preview still. Pure presentational —
5
+ * no data fetching, no compose-preset knowledge.
6
+ */
7
+ export function ComposeLayerPreview({ layers, reference, sourceJpegUrls, overlayHtmlBySource, background = "#111827", }) {
8
+ const { width: refW, height: refH } = reference;
9
+ return (_jsx("div", { style: { position: "absolute", inset: 0, background, overflow: "hidden" }, children: [...layers]
10
+ .sort((a, b) => a.zIndex - b.zIndex)
11
+ .map((layer, idx) => {
12
+ if (!layer.destRect)
13
+ return null;
14
+ const boxStyle = {
15
+ position: "absolute",
16
+ left: `${(layer.destRect.x / refW) * 100}%`,
17
+ top: `${(layer.destRect.y / refH) * 100}%`,
18
+ width: `${(layer.destRect.width / refW) * 100}%`,
19
+ height: `${(layer.destRect.height / refH) * 100}%`,
20
+ opacity: layer.opacity,
21
+ zIndex: layer.zIndex,
22
+ overflow: "hidden",
23
+ };
24
+ const html = overlayHtmlBySource?.[layer.sourceName];
25
+ const jpeg = sourceJpegUrls?.[layer.sourceName];
26
+ return (
27
+ // biome-ignore lint/suspicious/noArrayIndexKey: layers can share a sourceName; the zIndex-sorted index disambiguates them in this stable list
28
+ _jsx("div", { style: boxStyle, children: html ? (_jsx("iframe", { title: "Overlay graphic preview", srcDoc: html, sandbox: "allow-scripts", style: {
29
+ display: "block",
30
+ width: "100%",
31
+ height: "100%",
32
+ border: 0,
33
+ background: "transparent",
34
+ // `color-scheme` inherits into the iframe; a dark-mode host
35
+ // would make the UA paint a dark canvas and hide the layers
36
+ // beneath. Force light so the transparent page keys over them
37
+ // (alpha), matching the engine composite.
38
+ colorScheme: "light",
39
+ pointerEvents: "none",
40
+ } })) : jpeg ? (_jsx("img", { src: jpeg, alt: "", style: { display: "block", width: "100%", height: "100%", objectFit: "contain" } })) : (_jsx("div", { style: { width: "100%", height: "100%", background: MISSING_STILL } })) }, `${layer.sourceName}-${idx}`));
41
+ }) }));
42
+ }
@@ -24,9 +24,23 @@ export interface ShotBarProps {
24
24
  onTake: (commands: TakeCommand[], shot: ResolvedShot) => void;
25
25
  /** Optional preview bus — omit to hide the Preview control. */
26
26
  onPreview?: (commands: TakeCommand[], shot: ResolvedShot) => void;
27
+ /**
28
+ * True while a preview node is up (between Preview and Take/Cancel). Product-
29
+ * owned: the ShotBar just reflects it — swapping the Preview control for
30
+ * Cancel and letting the preview monitor show the live feed. Take commits it.
31
+ */
32
+ previewing?: boolean;
33
+ /** Back out of a preview without taking it (tears the preview node down). */
34
+ onCancelPreview?: () => void;
27
35
  /** Drop-in program monitor (e.g. a WHEP player). */
28
36
  renderProgramMonitor?: () => ReactNode;
29
- /** Drop-in preview monitor. */
30
- renderPreviewMonitor?: () => ReactNode;
37
+ /**
38
+ * Drop-in preview monitor. Receives the shot currently selected in the bar
39
+ * (or null) so the product can render *that shot's* what-if — the JPEG preset
40
+ * preview before Preview is pressed, swapping to the live preview-node feed
41
+ * once previewing. Selection lives inside the ShotBar, so this is the only way
42
+ * the product learns which shot to preview.
43
+ */
44
+ renderPreviewMonitor?: (selected: ResolvedShot | null) => ReactNode;
31
45
  }
32
- export declare function ShotBar({ shots, pgmShotId, pvwShotId, graphicBaseUrl, transition, editableGraphics, onTake, onPreview, renderProgramMonitor, renderPreviewMonitor, }: ShotBarProps): import("react").JSX.Element;
46
+ export declare function ShotBar({ shots, pgmShotId, pvwShotId, graphicBaseUrl, transition, editableGraphics, onTake, onPreview, previewing, onCancelPreview, renderProgramMonitor, renderPreviewMonitor, }: ShotBarProps): import("react").JSX.Element;
@@ -3,7 +3,7 @@ import { useEffect, useMemo, useState } from "react";
3
3
  import { graphicResolvable, hideReasonLabel, prepareTake } from "./shot-bar-view-model.js";
4
4
  import { useUIPrimitives } from "./ui-primitives.js";
5
5
  const FAMILY_BADGE = { geometry: "Geometry", graphic: "Graphic" };
6
- export function ShotBar({ shots, pgmShotId, pvwShotId, graphicBaseUrl, transition, editableGraphics = true, onTake, onPreview, renderProgramMonitor, renderPreviewMonitor, }) {
6
+ export function ShotBar({ shots, pgmShotId, pvwShotId, graphicBaseUrl, transition, editableGraphics = true, onTake, onPreview, previewing = false, onCancelPreview, renderProgramMonitor, renderPreviewMonitor, }) {
7
7
  const { Button, Input } = useUIPrimitives();
8
8
  const [selectedId, setSelectedId] = useState(null);
9
9
  const [editedUrls, setEditedUrls] = useState({});
@@ -28,7 +28,7 @@ export function ShotBar({ shots, pgmShotId, pvwShotId, graphicBaseUrl, transitio
28
28
  // preview take handler or a preview monitor renderer). Otherwise it's a dead
29
29
  // pane — a direct-take product has no PVW feed — so give On Air the full row.
30
30
  const showPreview = Boolean(onPreview || renderPreviewMonitor);
31
- return (_jsxs("div", { className: "flex flex-col gap-4 text-zinc-100", children: [_jsxs("div", { className: `grid gap-3 ${showPreview ? "grid-cols-2" : "grid-cols-1"}`, children: [showPreview ? (_jsx(Monitor, { label: "Preview", tone: "pvw", shot: findLabel(shots, pvwShotId), render: renderPreviewMonitor })) : null, _jsx(Monitor, { label: "On Air", tone: "pgm", shot: findLabel(shots, pgmShotId), render: renderProgramMonitor })] }), _jsx("div", { className: "flex gap-3 overflow-x-auto pb-1", children: shots.map((shot) => (_jsx(ShotCard, { shot: shot, selected: shot.id === selectedId, onAir: shot.id === pgmShotId, config: shot.status === "ready" ? shot.config : undefined, onSelect: () => shot.status === "ready" && setSelectedId(shot.id) }, shot.id))) }), selected ? (_jsxs("div", { className: "flex flex-col gap-3 rounded-lg border border-zinc-800 bg-zinc-900/60 p-3", children: [_jsxs("div", { className: "flex items-center justify-between gap-3", children: [_jsx("div", { className: "text-sm font-medium", children: selected.label }), _jsxs("div", { className: "flex gap-2", children: [onPreview ? (_jsx(Button, { variant: "ghost", size: "sm", disabled: !takeableGraphic, onClick: () => onPreview(commands, selected), children: "Preview" })) : null, _jsx(Button, { variant: "primary", size: "sm", disabled: !takeableGraphic, onClick: () => onTake(commands, selected), children: "Take" })] })] }), !takeableGraphic ? (_jsx("p", { className: "text-xs text-amber-500/90", children: "This shot's graphic page is a relative path but no engine-reachable graphic base is configured, so it can't be loaded into the overlay. Set an absolute page URL, or configure the overlay base." })) : null, editableGraphics && selected.graphicUrl !== undefined ? (_jsxs("div", { className: "flex flex-col gap-1 text-xs text-zinc-400", children: [_jsx("label", { htmlFor: "shotbar-graphic-url", children: "Graphic page" }), _jsx(Input, { id: "shotbar-graphic-url", type: "text", value: editedUrls[selected.id] ?? selected.graphicUrl, placeholder: selected.graphicUrl, onChange: (e) => setEditedUrls((prev) => ({ ...prev, [selected.id]: e.target.value })) })] })) : null] })) : (_jsx("div", { className: "rounded-lg border border-dashed border-zinc-800 p-4 text-center text-sm text-zinc-500", children: "No shot selected \u2014 pick an available shot above." }))] }));
31
+ return (_jsxs("div", { className: "flex flex-col gap-4 text-zinc-100", children: [_jsxs("div", { className: `grid gap-3 ${showPreview ? "grid-cols-2" : "grid-cols-1"}`, children: [showPreview ? (_jsx(Monitor, { label: "Preview", tone: "pvw", shot: findLabel(shots, pvwShotId), render: renderPreviewMonitor ? () => renderPreviewMonitor(selected ?? null) : undefined })) : null, _jsx(Monitor, { label: "On Air", tone: "pgm", shot: findLabel(shots, pgmShotId), render: renderProgramMonitor })] }), _jsx("div", { className: "flex gap-3 overflow-x-auto pb-1", children: shots.map((shot) => (_jsx(ShotCard, { shot: shot, selected: shot.id === selectedId, onAir: shot.id === pgmShotId, config: shot.status === "ready" ? shot.config : undefined, onSelect: () => shot.status === "ready" && setSelectedId(shot.id) }, shot.id))) }), selected ? (_jsxs("div", { className: "flex flex-col gap-3 rounded-lg border border-zinc-800 bg-zinc-900/60 p-3", children: [_jsxs("div", { className: "flex items-center justify-between gap-3", children: [_jsxs("div", { className: "flex items-center gap-2 text-sm font-medium", children: [selected.label, previewing ? (_jsx("span", { className: "rounded bg-emerald-600/20 px-1.5 py-0.5 text-[10px] font-semibold uppercase tracking-wide text-emerald-400", children: "Previewing" })) : null] }), _jsxs("div", { className: "flex gap-2", children: [previewing ? (onCancelPreview ? (_jsx(Button, { variant: "ghost", size: "sm", onClick: onCancelPreview, children: "Cancel" })) : null) : onPreview ? (_jsx(Button, { variant: "ghost", size: "sm", disabled: !takeableGraphic, onClick: () => onPreview(commands, selected), children: "Preview" })) : null, _jsx(Button, { variant: "primary", size: "sm", disabled: !takeableGraphic, onClick: () => onTake(commands, selected), children: "Take" })] })] }), !takeableGraphic ? (_jsx("p", { className: "text-xs text-amber-500/90", children: "This shot's graphic page is a relative path but no engine-reachable graphic base is configured, so it can't be loaded into the overlay. Set an absolute page URL, or configure the overlay base." })) : null, editableGraphics && selected.graphicUrl !== undefined ? (_jsxs("div", { className: "flex flex-col gap-1 text-xs text-zinc-400", children: [_jsx("label", { htmlFor: "shotbar-graphic-url", children: "Graphic page" }), _jsx(Input, { id: "shotbar-graphic-url", type: "text", value: editedUrls[selected.id] ?? selected.graphicUrl, placeholder: selected.graphicUrl, onChange: (e) => setEditedUrls((prev) => ({ ...prev, [selected.id]: e.target.value })) })] })) : null] })) : (_jsx("div", { className: "rounded-lg border border-dashed border-zinc-800 p-4 text-center text-sm text-zinc-500", children: "No shot selected \u2014 pick an available shot above." }))] }));
32
32
  }
33
33
  function findLabel(shots, id) {
34
34
  if (!id)
@@ -1,3 +1,4 @@
1
+ export { type ComposeLayer, ComposeLayerPreview, type ComposeLayerPreviewProps } from "./ComposeLayerPreview.js";
1
2
  export { ProductIframe, type ProductIframeHandle, type SubmitResult } from "./ProductIframe.js";
2
3
  export { ProductTemplateBuildForm, type ProductTemplateBuildOutcome, type ProductTemplateBuildRequest, } from "./ProductTemplateBuildForm.js";
3
4
  export { ShotBar, type ShotBarProps } from "./ShotBar.js";
@@ -1,3 +1,4 @@
1
+ export { ComposeLayerPreview } from "./ComposeLayerPreview.js";
1
2
  export { ProductIframe } from "./ProductIframe.js";
2
3
  export { ProductTemplateBuildForm, } from "./ProductTemplateBuildForm.js";
3
4
  export { ShotBar } from "./ShotBar.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@norskvideo/ctl-sdk",
3
- "version": "0.1.12",
3
+ "version": "0.1.14",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": {