@roku-sdk/components 2.18.0 → 2.19.1

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/package.json CHANGED
@@ -1,11 +1,12 @@
1
1
  {
2
2
  "name": "@roku-sdk/components",
3
- "version": "2.18.0",
3
+ "version": "2.19.1",
4
4
  "description": "Roku SceneGraph Solid components",
5
5
  "sideEffects": [
6
6
  "src/**/*.brs",
7
7
  "src/sg-nodes/animation.tsx",
8
8
  "src/sg-nodes/audio.tsx",
9
+ "src/sg-nodes/effect.tsx",
9
10
  "src/sg-nodes/sound-effect.tsx",
10
11
  "src/sg-nodes/video.tsx"
11
12
  ],
@@ -47,20 +48,20 @@
47
48
  "node": ">=24.0.0"
48
49
  },
49
50
  "dependencies": {
50
- "@roku-sdk/common": "2.18.0",
51
- "@roku-sdk/runtime": "2.18.0",
52
- "@roku-sdk/solid": "2.18.0",
53
- "@roku-sdk/navigation": "2.18.0",
54
- "@roku-sdk/interfaces": "2.18.0"
51
+ "@roku-sdk/common": "2.19.1",
52
+ "@roku-sdk/interfaces": "2.19.1",
53
+ "@roku-sdk/navigation": "2.19.1",
54
+ "@roku-sdk/solid": "2.19.1",
55
+ "@roku-sdk/runtime": "2.19.1"
55
56
  },
56
57
  "devDependencies": {
57
58
  "@jest/globals": "^30.4.1",
58
59
  "@types/jest": "^30.0.0",
59
60
  "jest": "^30.4.1",
60
61
  "jest-transform-stub": "^2.0.0",
61
- "@roku-sdk/dev-tools": "2.18.0",
62
- "@roku-sdk/testing-library": "2.18.0",
63
- "@roku-sdk/compat": "2.18.0"
62
+ "@roku-sdk/dev-tools": "2.19.1",
63
+ "@roku-sdk/testing-library": "2.19.1",
64
+ "@roku-sdk/compat": "2.19.1"
64
65
  },
65
66
  "author": "Roku Inc",
66
67
  "license": "MIT",
@@ -0,0 +1,133 @@
1
+ // Copyright (c) 2026 Roku, Inc.
2
+ // SPDX-License-Identifier: MIT
3
+ // ---------------------------------------------------------------------------
4
+ // Effect warns in __DEV__ when gradientColors / gradientStops violate the firmware
5
+ // constraints (2–8 colors, stops length and [0, 1] range) while gradientStyle is not
6
+ // "none". Quiet for valid gradients and for style "none" with empty arrays (effects-lab
7
+ // edit-demo). Each distinct problem is reported once per node; nothing without __DEV__.
8
+ // ---------------------------------------------------------------------------
9
+
10
+ import "@roku-sdk/compat";
11
+
12
+ import { afterEach, beforeEach, describe, expect, jest, test } from "@jest/globals";
13
+ import { createSignal, type JSX } from "@roku-sdk/solid";
14
+
15
+ import { Effect, installEffectGradientDevValidation } from "../sg-nodes/effect";
16
+ import { commitEarly, createAnchorNode, installSolidTestRenderer } from "../test-helpers/tween-test-setup";
17
+
18
+ async function flushMicrotasks(): Promise<void> {
19
+ await Promise.resolve();
20
+ await Promise.resolve();
21
+ }
22
+
23
+ describe("Effect gradient prop warning (dev only)", () => {
24
+ let render: ReturnType<typeof installSolidTestRenderer>;
25
+ let warn: jest.SpiedFunction<typeof console.warn>;
26
+ let realDev: boolean | undefined;
27
+ let dispose: (() => void) | undefined;
28
+
29
+ beforeEach(() => {
30
+ realDev = (globalThis as { __DEV__?: boolean }).__DEV__;
31
+ (globalThis as { __DEV__?: boolean }).__DEV__ = true;
32
+ installEffectGradientDevValidation();
33
+ render = installSolidTestRenderer();
34
+ warn = jest.spyOn(console, "warn").mockImplementation(() => {});
35
+ });
36
+
37
+ afterEach(() => {
38
+ dispose?.();
39
+ dispose = undefined;
40
+ (globalThis as { __DEV__?: boolean }).__DEV__ = realDev;
41
+ warn.mockRestore();
42
+ });
43
+
44
+ const mount = (element: JSX.Element): void => {
45
+ dispose = render(() => <group>{element}</group>, createAnchorNode());
46
+ commitEarly();
47
+ };
48
+
49
+ const effectWarnings = (): string[] =>
50
+ warn.mock.calls.map(c => String(c[0])).filter(message => message.startsWith("Effect"));
51
+
52
+ test("warns when gradientColors has fewer than 2 entries", async () => {
53
+ mount(<effect gradientStyle="linear" gradientColors={["0xFF0000FF"]} />);
54
+ await flushMicrotasks();
55
+ expect(effectWarnings()).toEqual([
56
+ 'Effect: gradientColors must have 2–8 entries when gradientStyle is "linear" (got 1).'
57
+ ]);
58
+ });
59
+
60
+ test("warns when gradientColors has more than 8 entries", async () => {
61
+ const colors = Array.from({ length: 9 }, (_, i) => `0xFF00000${i}`);
62
+ mount(<effect gradientStyle="radial" gradientColors={colors} />);
63
+ await flushMicrotasks();
64
+ expect(effectWarnings()).toEqual([
65
+ 'Effect: gradientColors must have 2–8 entries when gradientStyle is "radial" (got 9).'
66
+ ]);
67
+ });
68
+
69
+ test("warns when gradientStops length does not match gradientColors", async () => {
70
+ mount(
71
+ <effect gradientStyle="linear" gradientColors={["0xFF0000FF", "0x0000FFFF"]} gradientStops={[0, 0.5, 1]} />
72
+ );
73
+ await flushMicrotasks();
74
+ expect(effectWarnings()).toEqual(["Effect: gradientStops length (3) must equal gradientColors length (2)."]);
75
+ });
76
+
77
+ test("warns when a gradientStops entry is outside [0, 1]", async () => {
78
+ mount(<effect gradientStyle="linear" gradientColors={["0xFF0000FF", "0x0000FFFF"]} gradientStops={[0, 1.5]} />);
79
+ await flushMicrotasks();
80
+ expect(effectWarnings()).toEqual(["Effect: gradientStops entries must be in [0, 1] (got 1.5 at index 1)."]);
81
+ });
82
+
83
+ test("includes id in the warning when the effect has one", async () => {
84
+ mount(<effect id="hero-fill" gradientStyle="linear" gradientColors={["0xFF0000FF"]} />);
85
+ await flushMicrotasks();
86
+ expect(effectWarnings()).toEqual([
87
+ 'Effect[id=hero-fill]: gradientColors must have 2–8 entries when gradientStyle is "linear" (got 1).'
88
+ ]);
89
+ });
90
+
91
+ test("warns through the PascalCase Effect wrapper", async () => {
92
+ mount(<Effect gradientStyle="linear" gradientColors={["0xFF0000FF"]} />);
93
+ await flushMicrotasks();
94
+ expect(effectWarnings()).toEqual([
95
+ 'Effect: gradientColors must have 2–8 entries when gradientStyle is "linear" (got 1).'
96
+ ]);
97
+ });
98
+
99
+ test("is silent for a valid gradient", async () => {
100
+ mount(<effect gradientStyle="linear" gradientColors={["0xFF0000FF", "0x0000FFFF"]} gradientStops={[0, 1]} />);
101
+ await flushMicrotasks();
102
+ expect(effectWarnings()).toEqual([]);
103
+ });
104
+
105
+ test("is silent for gradientStyle none with empty colors and stops (effects-lab edit-demo)", async () => {
106
+ mount(<effect gradientStyle="none" gradientColors={[]} gradientStops={[]} />);
107
+ await flushMicrotasks();
108
+ expect(effectWarnings()).toEqual([]);
109
+ });
110
+
111
+ test("reports each distinct problem once per node across updates", async () => {
112
+ const [colors, setColors] = createSignal<string[]>(["0xFF0000FF"]);
113
+ mount(<effect gradientStyle="linear" gradientColors={colors()} />);
114
+ await flushMicrotasks();
115
+ setColors(["0xFF0000FF"]);
116
+ commitEarly();
117
+ await flushMicrotasks();
118
+ setColors(Array.from({ length: 9 }, (_, i) => `0xFF00000${i}`));
119
+ commitEarly();
120
+ await flushMicrotasks();
121
+ // Same key "colors:count" — still one warning even though the count changed.
122
+ expect(effectWarnings()).toEqual([
123
+ 'Effect: gradientColors must have 2–8 entries when gradientStyle is "linear" (got 1).'
124
+ ]);
125
+ });
126
+
127
+ test("is silent without __DEV__", async () => {
128
+ (globalThis as { __DEV__?: boolean }).__DEV__ = false;
129
+ mount(<effect gradientStyle="linear" gradientColors={["0xFF0000FF"]} />);
130
+ await flushMicrotasks();
131
+ expect(effectWarnings()).toEqual([]);
132
+ });
133
+ });
@@ -264,5 +264,13 @@ describe("imperative methods", () => {
264
264
  const unlisted = registrars.filter(file => !patterns.some(pattern => pattern.test(file)));
265
265
  expect(unlisted).toEqual([]);
266
266
  });
267
+
268
+ it("lists effect.tsx in sideEffects so the gradient dev observer is not tree-shaken", () => {
269
+ const { sideEffects } = JSON.parse(fs.readFileSync(path.join(packageRoot, "package.json"), "utf8")) as {
270
+ sideEffects: string[];
271
+ };
272
+ const patterns = sideEffects.map(globToRegExp);
273
+ expect(patterns.some(pattern => pattern.test("src/sg-nodes/effect.tsx"))).toBe(true);
274
+ });
267
275
  });
268
276
  });
@@ -1303,7 +1303,7 @@ export interface IVideoContentNodeProps extends IContentNodeProps {
1303
1303
  *
1304
1304
  * Valid values: `"mp4"`, `"hls"`, `"ism"`, `"dash"`, `"mkv"`.
1305
1305
  *
1306
- * @remarks The RSG SDK does not currently wire this through for
1306
+ * @remarks The RSG 2.0 SDK does not currently wire this through for
1307
1307
  * `VideoContentNode`; omit it and rely on URL / player behavior until support
1308
1308
  * lands.
1309
1309
  */
@@ -129,7 +129,7 @@ export const ContentNode: Component<IContentNodeProps> = props => {
129
129
  * content with stream URL and format. Typically used as a child of a Video node.
130
130
  *
131
131
  * @remarks Assigning {@link IVideoContentNodeProps.streamformat} is not supported
132
- * by the RSG SDK yet; use `url` (and other forwarded fields) only.
132
+ * by the RSG 2.0 SDK yet; use `url` (and other forwarded fields) only.
133
133
  *
134
134
  * @example
135
135
  * ```tsx
@@ -12,7 +12,7 @@ export type { IDialogProps } from "./dialog.props";
12
12
  * regions. Dialogs intercept all key events except Home and are closed automatically
13
13
  * by Back or Home.
14
14
  *
15
- * > **Note:** Consider using StandardDialog (Roku OS 10.0+) for new development, which
15
+ * > **Note:** Consider using StandardDialog for new development, which
16
16
  * > provides updated graphics and color palette support.
17
17
  *
18
18
  * @example
@@ -39,7 +39,7 @@ export interface IEffectProps extends INodeProps {
39
39
  * Per-corner border radius in pixels.
40
40
  * - A single number applies a uniform radius to all corners.
41
41
  * - A four-element tuple sets each corner independently: `[topLeft, topRight, bottomRight, bottomLeft]`.
42
- * The tuple is one Vector4 field, so a `FloatFieldInterpolator` cannot animate it.
42
+ * The tuple is one Vector4 field, so a FloatFieldInterpolator cannot animate it.
43
43
  * @default 0
44
44
  */
45
45
  borderRadius?: number | [number, number, number, number];
@@ -72,12 +72,15 @@ export interface IEffectProps extends INodeProps {
72
72
  /**
73
73
  * Ordered list of RGBA color stops for the gradient (e.g. `["0xFF0000FF", "0x0000FFFF"]`).
74
74
  * At least two colors are required for the gradient to render. Maximum of 8 colors.
75
+ * Outside that range the gradient does not render; in `__DEV__` builds the SDK warns once
76
+ * when `gradientStyle` is not `"none"`.
75
77
  */
76
78
  gradientColors?: string[];
77
79
 
78
80
  /**
79
81
  * Normalized stop positions `[0, 1]` corresponding to each entry in `gradientColors`.
80
- * If omitted, colors are distributed evenly across the gradient. A length mismatch is not reported.
82
+ * If omitted, colors are distributed evenly across the gradient. In `__DEV__` builds the SDK
83
+ * warns once on a length mismatch or an entry outside `[0, 1]` when `gradientStyle` is not `"none"`.
81
84
  */
82
85
  gradientStops?: number[];
83
86
 
@@ -1,10 +1,140 @@
1
1
  // Copyright (c) 2026 Roku, Inc.
2
2
  // SPDX-License-Identifier: MIT
3
3
 
4
- import type { IEffectProps } from "./effect.props";
4
+ import type { DomNode, TmValue } from "@roku-sdk/interfaces";
5
+ import { registerSetPropertyObserver } from "@roku-sdk/solid";
6
+
7
+ import type { GradientStyle, IEffectProps } from "./effect.props";
5
8
  import { Component } from "./types";
6
9
  export type { GradientStyle, IEffectProps } from "./effect.props";
7
10
 
11
+ declare const __DEV__: boolean;
12
+
13
+ const TRACKED_PROPS: ReadonlySet<string> = new Set(["gradientStyle", "gradientColors", "gradientStops", "id"]);
14
+
15
+ interface EffectGradientState {
16
+ gradientStyle?: GradientStyle;
17
+ gradientColors?: string[];
18
+ gradientStops?: number[];
19
+ id?: string;
20
+ warned: Set<string>;
21
+ scheduled: boolean;
22
+ }
23
+
24
+ /**
25
+ * Dev-only: describe each way gradient props violate the firmware's silent constraints.
26
+ * Returns `[key, message]` pairs so a caller can report each distinct problem once.
27
+ *
28
+ * Quiet while `gradientStyle` is `"none"` or unset — authors may park empty colors/stops
29
+ * and turn the style on later (see apps/effects-lab edit-demo).
30
+ */
31
+ function gradientPropProblems(
32
+ style: GradientStyle | undefined,
33
+ colors: readonly string[] | undefined,
34
+ stops: readonly number[] | undefined,
35
+ nodeLabel: string
36
+ ): [key: string, message: string][] {
37
+ if (style === undefined || style === "none") {
38
+ return [];
39
+ }
40
+
41
+ const problems: [string, string][] = [];
42
+ const colorList = Array.isArray(colors) ? colors : [];
43
+ const colorCount = colorList.length;
44
+
45
+ if (colorCount < 2 || colorCount > 8) {
46
+ problems.push([
47
+ "colors:count",
48
+ `${nodeLabel}: gradientColors must have 2–8 entries when gradientStyle is "${style}" (got ${colorCount}).`
49
+ ]);
50
+ }
51
+
52
+ if (stops !== undefined) {
53
+ const stopList = Array.isArray(stops) ? stops : [];
54
+ if (stopList.length !== colorCount) {
55
+ problems.push([
56
+ "stops:length",
57
+ `${nodeLabel}: gradientStops length (${stopList.length}) must equal gradientColors length (${colorCount}).`
58
+ ]);
59
+ }
60
+ const outOfRange = stopList.findIndex(stop => !(typeof stop === "number" && stop >= 0 && stop <= 1));
61
+ if (outOfRange !== -1) {
62
+ problems.push([
63
+ "stops:range",
64
+ `${nodeLabel}: gradientStops entries must be in [0, 1] (got ${String(stopList[outOfRange])} at index ${outOfRange}).`
65
+ ]);
66
+ }
67
+ }
68
+
69
+ return problems;
70
+ }
71
+
72
+ function nodeLabelFor(state: EffectGradientState): string {
73
+ return state.id !== undefined && state.id !== "" ? `Effect[id=${state.id}]` : "Effect";
74
+ }
75
+
76
+ let effectGradientObserverInstalled = false;
77
+
78
+ /**
79
+ * Installs the `__DEV__` setProperty observer that validates Effect gradient props for both
80
+ * `<effect>` and `<Effect>`. Safe to call more than once; no-ops when `__DEV__` is off.
81
+ * Tests call this after enabling `__DEV__` because Jest loads modules with `__DEV__` false.
82
+ */
83
+ export function installEffectGradientDevValidation(): void {
84
+ if (typeof __DEV__ === "undefined" || !__DEV__) return;
85
+ if (effectGradientObserverInstalled) return;
86
+ effectGradientObserverInstalled = true;
87
+
88
+ const stateByNode = new WeakMap<DomNode, EffectGradientState>();
89
+
90
+ registerSetPropertyObserver((node: DomNode, name: string, value: TmValue): void => {
91
+ if (typeof __DEV__ === "undefined" || !__DEV__) return;
92
+ if (node.t !== "effect") return;
93
+ if (!TRACKED_PROPS.has(name)) return;
94
+
95
+ let state = stateByNode.get(node);
96
+ if (!state) {
97
+ state = { warned: new Set(), scheduled: false };
98
+ stateByNode.set(node, state);
99
+ }
100
+
101
+ switch (name) {
102
+ case "gradientStyle":
103
+ state.gradientStyle = value as GradientStyle;
104
+ break;
105
+ case "gradientColors":
106
+ state.gradientColors = value as string[];
107
+ break;
108
+ case "gradientStops":
109
+ state.gradientStops = value as number[];
110
+ break;
111
+ case "id":
112
+ state.id = value as string;
113
+ break;
114
+ }
115
+
116
+ if (state.scheduled) return;
117
+ state.scheduled = true;
118
+ const pending = state;
119
+ queueMicrotask(() => {
120
+ pending.scheduled = false;
121
+ const label = nodeLabelFor(pending);
122
+ for (const [key, message] of gradientPropProblems(
123
+ pending.gradientStyle,
124
+ pending.gradientColors,
125
+ pending.gradientStops,
126
+ label
127
+ )) {
128
+ if (pending.warned.has(key)) continue;
129
+ pending.warned.add(key);
130
+ console.warn(message);
131
+ }
132
+ });
133
+ });
134
+ }
135
+
136
+ installEffectGradientDevValidation();
137
+
8
138
  /**
9
139
  * Effect applies GPU shader-based rendering to a Rectangle or Poster.
10
140
  *
@@ -12,9 +142,7 @@ export type { GradientStyle, IEffectProps } from "./effect.props";
12
142
  * Poster to enable rounded corners, borders, and gradient fills. Set
13
143
  * `shaderCompatible` on a Poster that takes one.
14
144
  *
15
- * Prefer the `<effect>` intrinsic for most uses. The `Effect` wrapper is a pure
16
- * passthrough (`<effect {...props} />`) and exists for consistency with the component
17
- * library.
145
+ * Prefer the `<effect>` intrinsic for most uses. The Effect wrapper is a pure passthrough (`<effect {...props} />`) and exists for consistency with the component library.
18
146
  *
19
147
  * {@includeCode ../../../../examples/showcase/src/components/showcases/effect-showcase/shared-effect-showcase.tsx#docs:effect-shared}
20
148
  *
@@ -12,11 +12,11 @@ export type { IFontProps } from "./font.props";
12
12
  * be specified using the `font:` prefix (e.g., `font:MediumBoldSystemFont`).
13
13
  *
14
14
  * **Components that support Font:**
15
- * - `Label` / `MultiStyleLabel`: via `font` prop
16
- * - `ScrollableText`: via `font` prop
17
- * - `ScrollingLabel`: via `font` prop
18
- * - `LabelList`: via `font` and `focusedFont` props
19
- * - `PosterGrid`: via `caption1Font` and `caption2Font` props
15
+ * - Label / MultiStyleLabel: via `font` prop
16
+ * - ScrollableText: via `font` prop
17
+ * - ScrollingLabel: via `font` prop
18
+ * - LabelList: via `font` and `focusedFont` props
19
+ * - PosterGrid: via `caption1Font` and `caption2Font` props
20
20
  *
21
21
  * @see {@link https://developer.roku.com/dev/docs/font}
22
22
  *
@@ -72,13 +72,12 @@ export interface IListPanelProps extends IPanelProps {
72
72
  }
73
73
 
74
74
  /**
75
- * Properties for the unmanaged SceneGraph ListPanel component.
75
+ * Properties for the raw `<listPanel>` SceneGraph element.
76
76
  *
77
- * This interface represents the raw ListPanel properties without the automatic
78
- * child management provided by the managed `ListPanel` component. It is used
79
- * internally and is not exposed to developers.
80
- *
81
- * @internal
77
+ * These are the {@link IListPanelProps} plus the `list` and `nextPanel` node fields, which the
78
+ * managed `ListPanel` component fills in from its children. They type the lower-case
79
+ * `listPanel` JSX intrinsic and the generated SceneGraph declarations. Prefer the managed
80
+ * `ListPanel` component; use the raw element only when you set `list` and `nextPanel` yourself.
82
81
  */
83
82
  export interface IFullPanelProps extends IListPanelProps {
84
83
  /**
@@ -16,7 +16,7 @@ export type { IListPanelProps, IFullPanelProps } from "./list-panel.props";
16
16
  * - Children extending `ArrayGrid` (LabelList, MarkupList) are automatically assigned to `list`
17
17
  * - Children extending `Panel` are automatically assigned to `nextPanel`
18
18
  *
19
- * Declare the list child **before** the panel child. The panel is matched to the row it was
19
+ * Declare the list child before the panel child. The panel is matched to the row it was
20
20
  * built for by consulting the list, so a panel declared first cannot be attributed to a row.
21
21
  *
22
22
  * While an asynchronously supplied `nextPanel` is still the previous row's, `ListPanel`
@@ -27,7 +27,7 @@ export type { IListPanelProps, IFullPanelProps } from "./list-panel.props";
27
27
  * its associated next panel as JSX children rather than explicit property assignments.
28
28
  *
29
29
  * @see {@link https://developer.roku.com/dev/docs/listpanel | Roku ListPanel Documentation}
30
- * @see **ListPanel, GridPanel Best Practices** at the internal RSG SDK documentation (knowledge-base/list-grid-panel-best-practice.md)
30
+ * @see **ListPanel, GridPanel Best Practices** at the internal RSG 2.0 SDK documentation (knowledge-base/list-grid-panel-best-practice.md)
31
31
  *
32
32
  * @example
33
33
  * ```tsx
@@ -54,9 +54,7 @@ export interface IMarkupListProps extends IArrayGridProps {
54
54
  /**
55
55
  * Specifies the content for the list.
56
56
  *
57
- * Should be a single root `ContentNode` with one child `ContentNode` per item.
58
- * If the data contains section markers (`CONTENTTYPE="SECTION"`), section
59
- * dividers will be drawn between each section, optionally with an icon and/or label.
57
+ * Should be a single root ContentNode with one child ContentNode per item. If the data contains section markers (`CONTENTTYPE="SECTION"`), section dividers will be drawn between each section, optionally with an icon and/or label.
60
58
  *
61
59
  * **Important:** Place `content` before `jumpToItem` and `animateToItem` in the
62
60
  * component. When content is updated dynamically, update `jumpToItem`/`animateToItem`
@@ -59,7 +59,7 @@ export interface IPanelSetProps extends IGroupProps {
59
59
  * The value is `true` when the PanelSet panels are sliding right, back towards the original
60
60
  * home position, as a result of a Left or Back key press. The value is `false` otherwise.
61
61
  *
62
- * @see **ListPanel, GridPanel Best Practices** at the internal RSG SDK documentation (knowledge-base/list-grid-panel-best-practice.md) for handling slidingBack prop
62
+ * @see **ListPanel, GridPanel Best Practices** at the internal RSG 2.0 SDK documentation (knowledge-base/list-grid-panel-best-practice.md) for handling slidingBack prop
63
63
  */
64
64
  onIsGoingBack?: EventHandler<boolean>;
65
65
 
@@ -14,7 +14,7 @@ export type { PosterDisplayMode, LoadDisplayMode, PosterLoadStatus, BitmapMargin
14
14
  * dimension information. See the Static Image Management documentation for details.
15
15
  *
16
16
  * **Attribute ordering pitfall:** When using `<poster>` as a JSX intrinsic,
17
- * write event handlers (`onLoadStatus`, etc.) **before** `uri` and other props that trigger
17
+ * write event handlers (`onLoadStatus`, etc.) before `uri` and other props that trigger
18
18
  * synchronous events on assignment. If `uri` is set first and the image is already cached,
19
19
  * the load event fires before your handler is registered and is silently dropped.
20
20
  * Writing handlers first is good practice regardless.
@@ -11,8 +11,7 @@ export type { IKeyboardDomain, IStandardKeyboardDialogProps } from "./standard-k
11
11
  /**
12
12
  * Displays a keyboard dialog for text and voice entry of alphanumeric characters and symbols.
13
13
  *
14
- * `StandardKeyboardDialog` is a pre-built dialog similar to the legacy KeyboardDialog node,
15
- * but includes voice entry functionality through its internal DynamicKeyboard node.
14
+ * StandardKeyboardDialog is a pre-built dialog similar to the legacy KeyboardDialog node, but includes voice entry functionality through its internal DynamicKeyboard node.
16
15
  *
17
16
  * The {@link IStandardKeyboardDialogProps.keyboardDomain | keyboardDomain} property controls
18
17
  * how voice input is processed:
@@ -11,8 +11,7 @@ export type { IStandardBulletType, IStandardMessageDialogProps } from "./standar
11
11
  /**
12
12
  * Displays a message dialog with optional title, text blocks, bulleted list, and buttons.
13
13
  *
14
- * `StandardMessageDialog` is a pre-built dialog similar to the legacy Dialog node. It provides
15
- * a convenient way to display informational messages with:
14
+ * StandardMessageDialog is a pre-built dialog similar to the legacy Dialog node. It provides a convenient way to display informational messages with:
16
15
  * - One or more text blocks at the top ({@link IStandardMessageDialogProps.message | message})
17
16
  * - An optional bulleted/numbered/lettered list ({@link IStandardMessageDialogProps.bulletText | bulletText})
18
17
  * - One or more text blocks at the bottom ({@link IStandardMessageDialogProps.bottomMessage | bottomMessage})
@@ -11,9 +11,7 @@ export type { IStandardPinPadDialogProps } from "./standard-pinpad-dialog.props"
11
11
  /**
12
12
  * Displays a PIN pad dialog for text and voice entry of numeric PIN codes.
13
13
  *
14
- * `StandardPinPadDialog` is a pre-built dialog similar to the legacy PinDialog node,
15
- * but includes voice entry of numeric digits through its internal DynamicPinPad and
16
- * VoiceTextEditBox nodes.
14
+ * StandardPinPadDialog is a pre-built dialog similar to the legacy PinDialog node, but includes voice entry of numeric digits through its internal DynamicPinPad and VoiceTextEditBox nodes.
17
15
  *
18
16
  * @example
19
17
  * ```tsx
@@ -11,12 +11,9 @@ export type { IStandardProgressDialogProps } from "./standard-progress-dialog.pr
11
11
  /**
12
12
  * Displays a progress dialog with a spinning indicator and message.
13
13
  *
14
- * `StandardProgressDialog` is a pre-built dialog similar to the legacy ProgressDialog node.
15
- * It displays a spinning progress indicator for tasks with an indeterminate duration.
14
+ * StandardProgressDialog is a pre-built dialog similar to the legacy ProgressDialog node. It displays a spinning progress indicator for tasks with an indeterminate duration.
16
15
  *
17
- * Note: Unlike other standard dialogs, `StandardProgressDialog` has no buttons — it only
18
- * displays a spinner with a message. Close the dialog programmatically by setting
19
- * {@link IStandardProgressDialogProps.close | close} to `true` or by unmounting the component.
16
+ * Note: Unlike other standard dialogs, StandardProgressDialog has no buttons — it only displays a spinner with a message. Close the dialog programmatically by setting {@link IStandardProgressDialogProps.close | close} to `true` or by unmounting the component.
20
17
  *
21
18
  * @example
22
19
  * ```tsx
@@ -604,9 +604,9 @@ export interface ITimeGridNowNextModeProps {
604
604
  * and each cell within a row represents a program with its time slot.
605
605
  *
606
606
  * The content tree structure is:
607
- * - Root `ContentNode`
608
- * - Channel `ContentNode` per row (with `title`, `hdPosterUrl` for logo)
609
- * - Program `ContentNode` per cell (with `title`, `description`, `playStart`, `playDuration`)
607
+ * - Root ContentNode
608
+ * - Channel ContentNode per row (with `title`, `hdPosterUrl` for logo)
609
+ * - Program ContentNode per cell (with `title`, `description`, `playStart`, `playDuration`)
610
610
  *
611
611
  * Program nodes use `playStart` (epoch seconds) and `playDuration` (seconds)
612
612
  * to determine their horizontal position and width in the grid.
@@ -151,7 +151,7 @@ export type SubtitleSelectionPreferenceRule =
151
151
  { language: string[] } | { caption: string } | { descriptive: string | string[] } | { easyReader: string };
152
152
 
153
153
  /**
154
- * Subtitle track selection preferences (Roku OS 12.5+).
154
+ * Subtitle track selection preferences.
155
155
  *
156
156
  * @see {@link https://developer.roku.com/dev/docs/video}
157
157
  */
@@ -168,7 +168,7 @@ export interface ISubtitleSelectionPreferences {
168
168
  export type AudioSelectionPreferenceRule = { language: string[] } | { descriptive: string | string[] };
169
169
 
170
170
  /**
171
- * Audio track selection preferences (Roku OS 12.5+).
171
+ * Audio track selection preferences.
172
172
  *
173
173
  * @see {@link https://developer.roku.com/dev/docs/video}
174
174
  */
@@ -198,7 +198,7 @@ export interface IVideoPlaybackProps {
198
198
  /** Play state command (play, pause, stop, prebuffer, etc.). */
199
199
  control?: VideoControl;
200
200
  /**
201
- * When true, STOP runs asynchronously (Roku OS 12.5+). `state` becomes `stopping` then `stopped`.
201
+ * When true, STOP runs asynchronously. `state` becomes `stopping` then `stopped`.
202
202
  */
203
203
  asyncStopSemantics?: boolean;
204
204
  /** Fired when playback state changes (buffering, playing, error, etc.). */
@@ -363,6 +363,62 @@ export interface IVideoUiProps {
363
363
  pivotNode?: JSX.Element;
364
364
  }
365
365
 
366
+ /**
367
+ * One entry of the Video node `availableAudioTracks` field.
368
+ *
369
+ * Firmware always emits every key below; the TypeScript and BrightScript
370
+ * bridges both build each entry from the media player's audio track list.
371
+ *
372
+ * @see {@link https://developer.roku.com/dev/docs/video}
373
+ */
374
+ export interface IVideoAudioTrack {
375
+ /**
376
+ * Language code as declared by the stream. Roku docs describe an ISO 639-2
377
+ * three-letter code (`"eng"`), but the value is passed through from the
378
+ * manifest, so two-letter codes such as `"en"` also occur.
379
+ */
380
+ Language: string;
381
+ /** Descriptive name of the audio track. */
382
+ Name: string;
383
+ /**
384
+ * Human-readable audio format description from the demuxer, for example
385
+ * `"ISO/IEC 14496-3, Advanced Audio Coding, ADTS container"`. Not listed in
386
+ * the Roku docs; may be an empty string when the demuxer reports no format.
387
+ */
388
+ Format: string;
389
+ /** Track identifier; assign it to `audioTrack` to select the track. */
390
+ Track: string;
391
+ /** HLS "public.accessibility.describes-video", or a DASH audio description. */
392
+ HasAccessibilityDescription: boolean;
393
+ /** DASH Enhanced Audio Intelligibility track. */
394
+ HasAccessibilityEAI: boolean;
395
+ }
396
+
397
+ /**
398
+ * One entry of the Video node `availableSubtitleTracks` field.
399
+ *
400
+ * Firmware always emits every key below.
401
+ *
402
+ * @see {@link https://developer.roku.com/dev/docs/video}
403
+ */
404
+ export interface IVideoSubtitleTrack {
405
+ /** Descriptive name of the subtitle track. */
406
+ Description: string;
407
+ /**
408
+ * Language code as declared by the stream. Roku docs describe an ISO 639-2
409
+ * three-letter code (`"eng"`), but two-letter codes such as `"en"` also occur.
410
+ */
411
+ Language: string;
412
+ /** Track identifier; assign it to `subtitleTrack` to select the track. */
413
+ TrackName: string;
414
+ /** HLS "public.accessibility.describes-music-and-sound". */
415
+ HasAccessibilityDescription: boolean;
416
+ /** HLS "public.accessibility.transcribes-spoken-dialog", or DASH captions. */
417
+ HasAccessibilityCaption: boolean;
418
+ /** DASH sign-language interpretation. */
419
+ HasAccessibilitySign: boolean;
420
+ }
421
+
366
422
  /**
367
423
  * Closed caption and subtitle styling fields.
368
424
  *
@@ -375,6 +431,10 @@ export interface IVideoCaptionProps {
375
431
  suppressCaptions?: boolean;
376
432
  /** Selected subtitle track identifier (read/write). */
377
433
  subtitleTrack?: string;
434
+ /** Fired with the subtitle tracks found in the stream and content metadata (read-only on the node). */
435
+ onAvailableSubtitleTracks?: EventHandler<IVideoSubtitleTrack[]>;
436
+ /** Fired when the playing subtitle track changes; carries the track identifier (`TrackName`). */
437
+ onCurrentSubtitleTrack?: EventHandler<string>;
378
438
  /**
379
439
  * Caption styling keys (Text/Font, Text/Size, Background/Color, etc.) per Roku docs.
380
440
  */
@@ -391,6 +451,15 @@ export interface IVideoAudioProps {
391
451
  mute?: boolean;
392
452
  /** Selected audio track identifier (prefer platform selection when possible). */
393
453
  audioTrack?: string;
454
+ /**
455
+ * Fired with the audio tracks found in the stream (read-only on the node).
456
+ *
457
+ * @example
458
+ * <Video onAvailableAudioTracks={tracks => setTracks(tracks)} onCurrentAudioTrack={id => setActive(id)} />
459
+ */
460
+ onAvailableAudioTracks?: EventHandler<IVideoAudioTrack[]>;
461
+ /** Fired when the playing audio track changes; carries the track identifier (`Track`). */
462
+ onCurrentAudioTrack?: EventHandler<string>;
394
463
  /**
395
464
  * When true, continues video on audio track switch (HLS only; set before any control command).
396
465
  */
@@ -478,7 +547,7 @@ export interface VideoRef extends ComponentRef<IVideoProps> {
478
547
  /**
479
548
  * Stop playback and release the video stream. Equivalent to setting
480
549
  * `control="stop"`. With {@link IVideoPlaybackProps.asyncStopSemantics} true
481
- * (Roku OS 12.5+) the state transitions through `stopping` to `stopped`;
550
+ * the state transitions through `stopping` to `stopped`;
482
551
  * otherwise the transition to `stopped` is synchronous.
483
552
  */
484
553
  stop: () => void;
@@ -24,6 +24,8 @@ export type {
24
24
  IDownloadedSegmentInfo,
25
25
  ITrickPlayProps,
26
26
  IVideoUiProps,
27
+ IVideoAudioTrack,
28
+ IVideoSubtitleTrack,
27
29
  IVideoCaptionProps,
28
30
  IVideoAudioProps,
29
31
  IVideoMiscProps,