@norskvideo/ctl-sdk 0.1.9 → 0.1.10
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.
package/components/ShotBar.d.ts
CHANGED
|
@@ -7,8 +7,15 @@ export interface ShotBarProps {
|
|
|
7
7
|
pgmShotId?: string | null;
|
|
8
8
|
/** The shot currently in preview, if the product runs a preview bus. */
|
|
9
9
|
pvwShotId?: string | null;
|
|
10
|
-
/**
|
|
11
|
-
|
|
10
|
+
/**
|
|
11
|
+
* Engine-reachable base for resolving relative graphic pages (`/overlays/…`)
|
|
12
|
+
* to the absolute URL `change-url` needs — the origin of the overlay
|
|
13
|
+
* `input.browser`'s configured URL, NOT the operator's `location.origin`
|
|
14
|
+
* (the media container's chromium fetches the page, not the browser). Omit
|
|
15
|
+
* only when the deployment has no overlay graphics; a graphic shot whose page
|
|
16
|
+
* can't be resolved without it is shown but can't be taken.
|
|
17
|
+
*/
|
|
18
|
+
graphicBaseUrl?: string;
|
|
12
19
|
/** Default transition applied to takes. */
|
|
13
20
|
transition?: Transition;
|
|
14
21
|
/** Show the editable graphic-URL field on graphic-bearing cards (default true). */
|
|
@@ -22,4 +29,4 @@ export interface ShotBarProps {
|
|
|
22
29
|
/** Drop-in preview monitor. */
|
|
23
30
|
renderPreviewMonitor?: () => ReactNode;
|
|
24
31
|
}
|
|
25
|
-
export declare function ShotBar({ shots, pgmShotId, pvwShotId,
|
|
32
|
+
export declare function ShotBar({ shots, pgmShotId, pvwShotId, graphicBaseUrl, transition, editableGraphics, onTake, onPreview, renderProgramMonitor, renderPreviewMonitor, }: ShotBarProps): import("react").JSX.Element;
|
package/components/ShotBar.js
CHANGED
|
@@ -1,20 +1,21 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { useMemo, useState } from "react";
|
|
3
|
-
import { hideReasonLabel, prepareTake } from "./shot-bar-view-model.js";
|
|
3
|
+
import { graphicResolvable, hideReasonLabel, prepareTake } from "./shot-bar-view-model.js";
|
|
4
4
|
import { useUIPrimitives } from "./ui-primitives.js";
|
|
5
|
-
function defaultOrigin() {
|
|
6
|
-
return typeof location !== "undefined" ? location.origin : "";
|
|
7
|
-
}
|
|
8
5
|
const FAMILY_BADGE = { geometry: "Geometry", graphic: "Graphic" };
|
|
9
|
-
export function ShotBar({ shots, pgmShotId, pvwShotId,
|
|
6
|
+
export function ShotBar({ shots, pgmShotId, pvwShotId, graphicBaseUrl, transition, editableGraphics = true, onTake, onPreview, renderProgramMonitor, renderPreviewMonitor, }) {
|
|
10
7
|
const { Button, Input } = useUIPrimitives();
|
|
11
|
-
const resolvedOrigin = origin ?? defaultOrigin();
|
|
12
8
|
const firstReady = shots.find((s) => s.status === "ready")?.id ?? null;
|
|
13
9
|
const [selectedId, setSelectedId] = useState(firstReady);
|
|
14
10
|
const [editedUrls, setEditedUrls] = useState({});
|
|
15
11
|
const selected = shots.find((s) => s.id === selectedId && s.status === "ready");
|
|
16
|
-
const commands = useMemo(() => selected ? prepareTake(selected, {
|
|
17
|
-
|
|
12
|
+
const commands = useMemo(() => (selected ? prepareTake(selected, { graphicBaseUrl, editedUrl: editedUrls[selected.id], transition }) : []), [selected, graphicBaseUrl, editedUrls, transition]);
|
|
13
|
+
// A graphic shot whose page is relative and has no engine-reachable base
|
|
14
|
+
// can't safely change-url — surface it instead of firing an unfetchable page.
|
|
15
|
+
const takeableGraphic = selected
|
|
16
|
+
? graphicResolvable(editedUrls[selected.id] ?? selected.graphicUrl, graphicBaseUrl)
|
|
17
|
+
: 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." }))] }));
|
|
18
19
|
}
|
|
19
20
|
function findLabel(shots, id) {
|
|
20
21
|
if (!id)
|
package/components/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export { ProductIframe, type ProductIframeHandle, type SubmitResult } from "./ProductIframe.js";
|
|
2
2
|
export { ProductTemplateBuildForm, type ProductTemplateBuildOutcome, type ProductTemplateBuildRequest, } from "./ProductTemplateBuildForm.js";
|
|
3
3
|
export { ShotBar, type ShotBarProps } from "./ShotBar.js";
|
|
4
|
-
export { hideReasonLabel, type PrepareTakeOptions, prepareTake, resolveGraphicUrl, } from "./shot-bar-view-model.js";
|
|
4
|
+
export { graphicResolvable, hideReasonLabel, type PrepareTakeOptions, prepareTake, resolveGraphicUrl, } from "./shot-bar-view-model.js";
|
|
5
5
|
export { type ButtonProps, type ButtonSize, type ButtonVariant, type InputProps, type LabelProps, type UIPrimitives, UIPrimitivesProvider, useUIPrimitives, } from "./ui-primitives.js";
|
package/components/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export { ProductIframe } from "./ProductIframe.js";
|
|
2
2
|
export { ProductTemplateBuildForm, } from "./ProductTemplateBuildForm.js";
|
|
3
3
|
export { ShotBar } from "./ShotBar.js";
|
|
4
|
-
export { hideReasonLabel, prepareTake, resolveGraphicUrl, } from "./shot-bar-view-model.js";
|
|
4
|
+
export { graphicResolvable, hideReasonLabel, prepareTake, resolveGraphicUrl, } from "./shot-bar-view-model.js";
|
|
5
5
|
export { UIPrimitivesProvider, useUIPrimitives, } from "./ui-primitives.js";
|
|
@@ -2,15 +2,33 @@ import { type HideReason, type ResolvedShot, type TakeCommand, type Transition }
|
|
|
2
2
|
/** Operator-facing text for why a shot can't be taken right now. */
|
|
3
3
|
export declare function hideReasonLabel(reason: HideReason): string;
|
|
4
4
|
/**
|
|
5
|
-
* Resolve a shot's graphic page to an absolute URL for `change-url
|
|
6
|
-
*
|
|
7
|
-
*
|
|
5
|
+
* Resolve a shot's graphic page to an absolute URL for `change-url`, or `null`
|
|
6
|
+
* if it can't be resolved safely.
|
|
7
|
+
*
|
|
8
|
+
* Manifests ship relative paths (`/overlays/lbar.html`) so they're
|
|
9
|
+
* host-independent, but `change-url` is fetched by the media container's
|
|
10
|
+
* chromium (`input.browser`), NOT the operator's browser — so the base MUST be
|
|
11
|
+
* an engine-reachable origin the product supplies (the overlay layer's own
|
|
12
|
+
* configured URL origin). Resolving a relative path against the operator origin
|
|
13
|
+
* (`location.origin`) yields a plausible-but-unreachable URL that 404s in the
|
|
14
|
+
* container; refuse it (`null`) rather than emit that. Absolute inputs pass
|
|
8
15
|
* through untouched, so an operator-typed full URL is honoured as-is.
|
|
9
16
|
*/
|
|
10
|
-
export declare function resolveGraphicUrl(url: string,
|
|
17
|
+
export declare function resolveGraphicUrl(url: string, base: string | undefined): string | null;
|
|
18
|
+
/**
|
|
19
|
+
* Whether a shot's graphic page can be taken to air: an absolute page always
|
|
20
|
+
* can, a relative one only with an engine-reachable base, and a shot naming no
|
|
21
|
+
* page is trivially fine. The ShotBar uses this to disable Take (and say why)
|
|
22
|
+
* rather than fire a `change-url` the media container can't fetch.
|
|
23
|
+
*/
|
|
24
|
+
export declare function graphicResolvable(graphicUrl: string | undefined, base: string | undefined): boolean;
|
|
11
25
|
export interface PrepareTakeOptions {
|
|
12
|
-
/**
|
|
13
|
-
|
|
26
|
+
/**
|
|
27
|
+
* Engine-reachable base to resolve a relative graphic path against — the
|
|
28
|
+
* origin of the overlay `input.browser`'s configured URL, NOT the operator's
|
|
29
|
+
* `location.origin`. Omit only for deployments with no overlay graphics.
|
|
30
|
+
*/
|
|
31
|
+
graphicBaseUrl?: string;
|
|
14
32
|
/** Operator-edited graphic URL from the card, if they changed it. */
|
|
15
33
|
editedUrl?: string;
|
|
16
34
|
transition?: Transition;
|
|
@@ -18,7 +36,9 @@ export interface PrepareTakeOptions {
|
|
|
18
36
|
}
|
|
19
37
|
/**
|
|
20
38
|
* Assemble the exact `TakeCommand[]` for a shot: pick the graphic URL (edited
|
|
21
|
-
* over manifest), resolve it
|
|
22
|
-
*
|
|
39
|
+
* over manifest), resolve it against the engine-reachable base, and hand
|
|
40
|
+
* `buildTake` the right opts. A relative page with no usable base resolves to
|
|
41
|
+
* `null` and its `change-url` is dropped — better to squeeze the layout with a
|
|
42
|
+
* stale overlay than to fire a page the media container can't reach.
|
|
23
43
|
*/
|
|
24
44
|
export declare function prepareTake(shot: ResolvedShot, opts: PrepareTakeOptions): TakeCommand[];
|
|
@@ -26,32 +26,60 @@ export function hideReasonLabel(reason) {
|
|
|
26
26
|
}
|
|
27
27
|
}
|
|
28
28
|
/**
|
|
29
|
-
* Resolve a shot's graphic page to an absolute URL for `change-url
|
|
30
|
-
*
|
|
31
|
-
*
|
|
29
|
+
* Resolve a shot's graphic page to an absolute URL for `change-url`, or `null`
|
|
30
|
+
* if it can't be resolved safely.
|
|
31
|
+
*
|
|
32
|
+
* Manifests ship relative paths (`/overlays/lbar.html`) so they're
|
|
33
|
+
* host-independent, but `change-url` is fetched by the media container's
|
|
34
|
+
* chromium (`input.browser`), NOT the operator's browser — so the base MUST be
|
|
35
|
+
* an engine-reachable origin the product supplies (the overlay layer's own
|
|
36
|
+
* configured URL origin). Resolving a relative path against the operator origin
|
|
37
|
+
* (`location.origin`) yields a plausible-but-unreachable URL that 404s in the
|
|
38
|
+
* container; refuse it (`null`) rather than emit that. Absolute inputs pass
|
|
32
39
|
* through untouched, so an operator-typed full URL is honoured as-is.
|
|
33
40
|
*/
|
|
34
|
-
export function resolveGraphicUrl(url,
|
|
41
|
+
export function resolveGraphicUrl(url, base) {
|
|
35
42
|
if (/^https?:\/\//i.test(url))
|
|
36
43
|
return url;
|
|
44
|
+
if (!base)
|
|
45
|
+
return null;
|
|
37
46
|
try {
|
|
38
|
-
|
|
47
|
+
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;
|
|
39
52
|
}
|
|
40
53
|
catch {
|
|
41
|
-
|
|
42
|
-
// value rather than throwing; the caller/engine will reject a bad URL.
|
|
43
|
-
return url;
|
|
54
|
+
return null;
|
|
44
55
|
}
|
|
45
56
|
}
|
|
57
|
+
/**
|
|
58
|
+
* Whether a shot's graphic page can be taken to air: an absolute page always
|
|
59
|
+
* can, a relative one only with an engine-reachable base, and a shot naming no
|
|
60
|
+
* page is trivially fine. The ShotBar uses this to disable Take (and say why)
|
|
61
|
+
* rather than fire a `change-url` the media container can't fetch.
|
|
62
|
+
*/
|
|
63
|
+
export function graphicResolvable(graphicUrl, base) {
|
|
64
|
+
if (!graphicUrl)
|
|
65
|
+
return true;
|
|
66
|
+
return resolveGraphicUrl(graphicUrl, base) !== null;
|
|
67
|
+
}
|
|
46
68
|
/**
|
|
47
69
|
* Assemble the exact `TakeCommand[]` for a shot: pick the graphic URL (edited
|
|
48
|
-
* over manifest), resolve it
|
|
49
|
-
*
|
|
70
|
+
* over manifest), resolve it against the engine-reachable base, and hand
|
|
71
|
+
* `buildTake` the right opts. A relative page with no usable base resolves to
|
|
72
|
+
* `null` and its `change-url` is dropped — better to squeeze the layout with a
|
|
73
|
+
* stale overlay than to fire a page the media container can't reach.
|
|
50
74
|
*/
|
|
51
75
|
export function prepareTake(shot, opts) {
|
|
52
76
|
const rawUrl = opts.editedUrl ?? shot.graphicUrl;
|
|
53
|
-
const graphicUrl = rawUrl ? resolveGraphicUrl(rawUrl, opts.
|
|
54
|
-
|
|
77
|
+
const graphicUrl = rawUrl ? resolveGraphicUrl(rawUrl, opts.graphicBaseUrl) : null;
|
|
78
|
+
// An unresolvable page must be dropped, not passed through: buildTake falls
|
|
79
|
+
// back to `shot.graphicUrl` (the raw, relative, unreachable manifest path)
|
|
80
|
+
// when no resolved url is supplied, so strip it from the shot as well.
|
|
81
|
+
const effectiveShot = rawUrl && graphicUrl === null ? { ...shot, graphicUrl: undefined } : shot;
|
|
82
|
+
return buildTake(effectiveShot, {
|
|
55
83
|
...(graphicUrl ? { graphicUrl } : {}),
|
|
56
84
|
...(opts.transition ? { transition: opts.transition } : {}),
|
|
57
85
|
...(opts.configOverride ? { configOverride: opts.configOverride } : {}),
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@norskvideo/ctl-sdk",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.10",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"exports": {
|
|
6
6
|
".": {
|
|
@@ -37,7 +37,7 @@
|
|
|
37
37
|
"types": "./index.d.ts",
|
|
38
38
|
"dependencies": {
|
|
39
39
|
"@norskvideo/ctl-foundation": "^0.1.0",
|
|
40
|
-
"@norskvideo/ctl-product-template-schema": "^0.1.
|
|
40
|
+
"@norskvideo/ctl-product-template-schema": "^0.1.5",
|
|
41
41
|
"express": "5",
|
|
42
42
|
"lucide-react": "^0.483.0",
|
|
43
43
|
"react": "^19.0.0",
|