@norskvideo/ctl-sdk 0.1.10 → 0.1.12

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.
@@ -1,21 +1,34 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { useMemo, useState } from "react";
2
+ 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
6
  export function ShotBar({ shots, pgmShotId, pvwShotId, graphicBaseUrl, transition, editableGraphics = true, onTake, onPreview, renderProgramMonitor, renderPreviewMonitor, }) {
7
7
  const { Button, Input } = useUIPrimitives();
8
- const firstReady = shots.find((s) => s.status === "ready")?.id ?? null;
9
- const [selectedId, setSelectedId] = useState(firstReady);
8
+ const [selectedId, setSelectedId] = useState(null);
10
9
  const [editedUrls, setEditedUrls] = useState({});
11
10
  const selected = shots.find((s) => s.id === selectedId && s.status === "ready");
11
+ // Auto-select the first ready shot once live state arrives (shots start empty
12
+ // / all-hidden before the first WS frame, so a mount-time pick would be null),
13
+ // and re-select if the current one stops being ready.
14
+ useEffect(() => {
15
+ if (selectedId && shots.some((s) => s.id === selectedId && s.status === "ready"))
16
+ return;
17
+ const firstReady = shots.find((s) => s.status === "ready")?.id ?? null;
18
+ if (firstReady !== selectedId)
19
+ setSelectedId(firstReady);
20
+ }, [shots, selectedId]);
12
21
  const commands = useMemo(() => (selected ? prepareTake(selected, { graphicBaseUrl, editedUrl: editedUrls[selected.id], transition }) : []), [selected, graphicBaseUrl, editedUrls, transition]);
13
22
  // A graphic shot whose page is relative and has no engine-reachable base
14
23
  // can't safely change-url — surface it instead of firing an unfetchable page.
15
24
  const takeableGraphic = selected
16
25
  ? graphicResolvable(editedUrls[selected.id] ?? selected.graphicUrl, graphicBaseUrl)
17
26
  : true;
18
- return (_jsxs("div", { className: "flex flex-col gap-4 text-zinc-100", children: [_jsxs("div", { className: "grid grid-cols-2 gap-3", children: [_jsx(Monitor, { label: "Preview", tone: "pvw", shot: findLabel(shots, pvwShotId), render: renderPreviewMonitor }), _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("pre", { className: "max-h-40 overflow-auto rounded bg-zinc-950 p-2 font-mono text-[11px] leading-relaxed text-zinc-400", children: JSON.stringify(commands, null, 2) })] })) : (_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." }))] }));
27
+ // Only show the Preview monitor when the product runs a preview bus (a
28
+ // preview take handler or a preview monitor renderer). Otherwise it's a dead
29
+ // pane — a direct-take product has no PVW feed — so give On Air the full row.
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." }))] }));
19
32
  }
20
33
  function findLabel(shots, id) {
21
34
  if (!id)
@@ -13,6 +13,11 @@ export declare function hideReasonLabel(reason: HideReason): string;
13
13
  * (`location.origin`) yields a plausible-but-unreachable URL that 404s in the
14
14
  * container; refuse it (`null`) rather than emit that. Absolute inputs pass
15
15
  * through untouched, so an operator-typed full URL is honoured as-is.
16
+ *
17
+ * `file://` is a first-class scheme here: overlay pages are shipped into the
18
+ * media container's shared `/data/.norsk` mount and handed to `input.browser`
19
+ * as `file:///data/.norsk/overlays/x.html` — a fixed container path, so no auth
20
+ * proxy, no network, and instance-independent.
16
21
  */
17
22
  export declare function resolveGraphicUrl(url: string, base: string | undefined): string | null;
18
23
  /**
@@ -37,18 +37,22 @@ export function hideReasonLabel(reason) {
37
37
  * (`location.origin`) yields a plausible-but-unreachable URL that 404s in the
38
38
  * container; refuse it (`null`) rather than emit that. Absolute inputs pass
39
39
  * through untouched, so an operator-typed full URL is honoured as-is.
40
+ *
41
+ * `file://` is a first-class scheme here: overlay pages are shipped into the
42
+ * media container's shared `/data/.norsk` mount and handed to `input.browser`
43
+ * as `file:///data/.norsk/overlays/x.html` — a fixed container path, so no auth
44
+ * proxy, no network, and instance-independent.
40
45
  */
41
46
  export function resolveGraphicUrl(url, base) {
42
- if (/^https?:\/\//i.test(url))
47
+ if (/^(https?|file):\/\//i.test(url))
43
48
  return url;
44
49
  if (!base)
45
50
  return null;
46
51
  try {
47
52
  const resolved = new URL(url, base);
48
- // A relative path against a bare/relative base can still land on a
49
- // non-http(s) or otherwise unreachable origin only absolute web URLs are
50
- // safe to hand the engine.
51
- return /^https?:$/i.test(resolved.protocol) ? resolved.toString() : null;
53
+ // A relative path against a bare/relative base can still land on an
54
+ // unreachable scheme — only http(s) and file: are safe to hand the engine.
55
+ return /^(https?|file):$/i.test(resolved.protocol) ? resolved.toString() : null;
52
56
  }
53
57
  catch {
54
58
  return null;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@norskvideo/ctl-sdk",
3
- "version": "0.1.10",
3
+ "version": "0.1.12",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": {