@roku-sdk/components 0.0.0-stage → 2.19.0

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.
Files changed (212) hide show
  1. package/BREAKING_CHANGES.md +84 -0
  2. package/LICENSE +21 -0
  3. package/README.md +19 -2
  4. package/core-jsx.d.ts +53 -0
  5. package/eslint.config.mjs +3 -0
  6. package/jsx.d.ts +273 -0
  7. package/package.json +85 -3
  8. package/src/__tests__/container.test.tsx +150 -0
  9. package/src/__tests__/effect-gradient-validation.test.tsx +133 -0
  10. package/src/__tests__/glyphs.test.ts +202 -0
  11. package/src/__tests__/modal-boundary-export.test.ts +12 -0
  12. package/src/__tests__/native-modal.test.tsx +224 -0
  13. package/src/__tests__/tween-release.test.tsx +125 -0
  14. package/src/__tests__/tween-stops-shape.test.tsx +165 -0
  15. package/src/__tests__/tween-without-compat.test.tsx +64 -0
  16. package/src/__tests__/tween.test.tsx +195 -0
  17. package/src/blended-busy-spinner.props.ts +37 -0
  18. package/src/blended-busy-spinner.tsx +13 -0
  19. package/src/bounding-tracker.props.ts +18 -0
  20. package/src/bounding-tracker.tsx +13 -0
  21. package/src/button-row.tsx +126 -0
  22. package/src/channel-store.props.ts +425 -0
  23. package/src/channel-store.tsx +76 -0
  24. package/src/container.tsx +130 -0
  25. package/src/core/jsx-runtime.ts +39 -0
  26. package/src/focusable-group.props.ts +8 -0
  27. package/src/focusable-group.tsx +17 -0
  28. package/src/focusable.props.ts +14 -0
  29. package/src/focusable.tsx +17 -0
  30. package/src/glyphs.ts +116 -0
  31. package/src/helpers/index.tsx +4 -0
  32. package/src/helpers/resolution-context.tsx +68 -0
  33. package/src/helpers/scale-for-device-resolution.ts +31 -0
  34. package/src/index.tsx +70 -0
  35. package/src/jsx-props.ts +87 -0
  36. package/src/kdfs.d.ts +14 -0
  37. package/src/kdfs.ts +15 -0
  38. package/src/key-handler.props.ts +33 -0
  39. package/src/key-handler.tsx +24 -0
  40. package/src/modal-dialog.props.ts +38 -0
  41. package/src/modal-dialog.tsx +41 -0
  42. package/src/retain-asset.ts +24 -0
  43. package/src/sg-intrinsics.json +1153 -0
  44. package/src/sg-nodes/__tests__/__snapshots__/font.test.tsx.snap +13 -0
  45. package/src/sg-nodes/__tests__/__snapshots__/prop-forwarding.test.tsx.snap +4409 -0
  46. package/src/sg-nodes/__tests__/core-props.test.ts +301 -0
  47. package/src/sg-nodes/__tests__/font.test.tsx +141 -0
  48. package/src/sg-nodes/__tests__/imperative-methods.test.ts +276 -0
  49. package/src/sg-nodes/__tests__/prop-forwarding.test.tsx +1049 -0
  50. package/src/sg-nodes/__tests__/solid-integration.test.tsx +719 -0
  51. package/src/sg-nodes/animated-image.props.ts +64 -0
  52. package/src/sg-nodes/animated-image.tsx +23 -0
  53. package/src/sg-nodes/animation.props.ts +292 -0
  54. package/src/sg-nodes/animation.tsx +190 -0
  55. package/src/sg-nodes/array-grid.ts +602 -0
  56. package/src/sg-nodes/audio.props.ts +302 -0
  57. package/src/sg-nodes/audio.tsx +57 -0
  58. package/src/sg-nodes/busy-spinner.props.ts +59 -0
  59. package/src/sg-nodes/busy-spinner.tsx +84 -0
  60. package/src/sg-nodes/button-group.props.ts +141 -0
  61. package/src/sg-nodes/button-group.tsx +30 -0
  62. package/src/sg-nodes/button.props.ts +119 -0
  63. package/src/sg-nodes/button.tsx +31 -0
  64. package/src/sg-nodes/check-list.props.ts +71 -0
  65. package/src/sg-nodes/check-list.tsx +55 -0
  66. package/src/sg-nodes/content-node.props.ts +1316 -0
  67. package/src/sg-nodes/content-node.tsx +153 -0
  68. package/src/sg-nodes/core-props.ts +197 -0
  69. package/src/sg-nodes/custom-keyboard.props.ts +108 -0
  70. package/src/sg-nodes/custom-keyboard.tsx +48 -0
  71. package/src/sg-nodes/dialog.props.ts +159 -0
  72. package/src/sg-nodes/dialog.tsx +36 -0
  73. package/src/sg-nodes/dynamic-key-grid.props.ts +22 -0
  74. package/src/sg-nodes/dynamic-key-grid.tsx +16 -0
  75. package/src/sg-nodes/dynamic-keyboard.props.ts +30 -0
  76. package/src/sg-nodes/dynamic-keyboard.tsx +18 -0
  77. package/src/sg-nodes/dynamic-mini-keyboard.props.ts +22 -0
  78. package/src/sg-nodes/dynamic-mini-keyboard.tsx +18 -0
  79. package/src/sg-nodes/dynamic-pinpad.props.ts +57 -0
  80. package/src/sg-nodes/dynamic-pinpad.tsx +18 -0
  81. package/src/sg-nodes/effect.props.ts +119 -0
  82. package/src/sg-nodes/effect.tsx +167 -0
  83. package/src/sg-nodes/font.props.ts +45 -0
  84. package/src/sg-nodes/font.tsx +80 -0
  85. package/src/sg-nodes/group.props.ts +105 -0
  86. package/src/sg-nodes/group.tsx +30 -0
  87. package/src/sg-nodes/label-list.props.ts +86 -0
  88. package/src/sg-nodes/label-list.tsx +64 -0
  89. package/src/sg-nodes/label.props.ts +207 -0
  90. package/src/sg-nodes/label.tsx +56 -0
  91. package/src/sg-nodes/layout-group.props.ts +69 -0
  92. package/src/sg-nodes/layout-group.tsx +75 -0
  93. package/src/sg-nodes/list-panel.props.ts +103 -0
  94. package/src/sg-nodes/list-panel.tsx +49 -0
  95. package/src/sg-nodes/markup-grid.props.ts +78 -0
  96. package/src/sg-nodes/markup-grid.tsx +38 -0
  97. package/src/sg-nodes/markup-list.props.ts +66 -0
  98. package/src/sg-nodes/markup-list.tsx +38 -0
  99. package/src/sg-nodes/mask-group.props.ts +78 -0
  100. package/src/sg-nodes/mask-group.tsx +59 -0
  101. package/src/sg-nodes/media-playback-methods.ts +20 -0
  102. package/src/sg-nodes/node.ts +77 -0
  103. package/src/sg-nodes/overhang.props.ts +169 -0
  104. package/src/sg-nodes/overhang.tsx +29 -0
  105. package/src/sg-nodes/panel-set.props.ts +71 -0
  106. package/src/sg-nodes/panel-set.tsx +53 -0
  107. package/src/sg-nodes/panel.props.ts +176 -0
  108. package/src/sg-nodes/panel.tsx +56 -0
  109. package/src/sg-nodes/poster-grid.props.ts +277 -0
  110. package/src/sg-nodes/poster-grid.tsx +45 -0
  111. package/src/sg-nodes/poster.props.ts +125 -0
  112. package/src/sg-nodes/poster.tsx +44 -0
  113. package/src/sg-nodes/radio-button-list.props.ts +59 -0
  114. package/src/sg-nodes/radio-button-list.tsx +68 -0
  115. package/src/sg-nodes/rectangle.props.ts +29 -0
  116. package/src/sg-nodes/rectangle.tsx +27 -0
  117. package/src/sg-nodes/row-list.props.ts +288 -0
  118. package/src/sg-nodes/row-list.tsx +47 -0
  119. package/src/sg-nodes/scene.props.ts +65 -0
  120. package/src/sg-nodes/scene.tsx +15 -0
  121. package/src/sg-nodes/scrollable-text.props.ts +133 -0
  122. package/src/sg-nodes/scrollable-text.tsx +64 -0
  123. package/src/sg-nodes/scrolling-label.props.ts +106 -0
  124. package/src/sg-nodes/scrolling-label.tsx +42 -0
  125. package/src/sg-nodes/simple-label.props.ts +156 -0
  126. package/src/sg-nodes/simple-label.tsx +59 -0
  127. package/src/sg-nodes/sound-effect.props.ts +130 -0
  128. package/src/sg-nodes/sound-effect.tsx +65 -0
  129. package/src/sg-nodes/standard-dialog.props.ts +520 -0
  130. package/src/sg-nodes/standard-dialog.tsx +428 -0
  131. package/src/sg-nodes/standard-keyboard-dialog.props.ts +84 -0
  132. package/src/sg-nodes/standard-keyboard-dialog.tsx +58 -0
  133. package/src/sg-nodes/standard-message-dialog.props.ts +56 -0
  134. package/src/sg-nodes/standard-message-dialog.tsx +47 -0
  135. package/src/sg-nodes/standard-pinpad-dialog.props.ts +64 -0
  136. package/src/sg-nodes/standard-pinpad-dialog.tsx +44 -0
  137. package/src/sg-nodes/standard-progress-dialog.props.ts +30 -0
  138. package/src/sg-nodes/standard-progress-dialog.tsx +40 -0
  139. package/src/sg-nodes/target-group.props.ts +278 -0
  140. package/src/sg-nodes/target-group.tsx +63 -0
  141. package/src/sg-nodes/target-list.props.ts +128 -0
  142. package/src/sg-nodes/target-list.tsx +66 -0
  143. package/src/sg-nodes/target-set.props.ts +63 -0
  144. package/src/sg-nodes/target-set.tsx +42 -0
  145. package/src/sg-nodes/time-grid.props.ts +646 -0
  146. package/src/sg-nodes/time-grid.tsx +87 -0
  147. package/src/sg-nodes/tween.props.ts +115 -0
  148. package/src/sg-nodes/types.ts +150 -0
  149. package/src/sg-nodes/video.props.ts +642 -0
  150. package/src/sg-nodes/video.tsx +75 -0
  151. package/src/sg-nodes/voice-text-edit-box.props.ts +91 -0
  152. package/src/sg-nodes/voice-text-edit-box.tsx +15 -0
  153. package/src/sg-nodes/zoom-row-list.props.ts +416 -0
  154. package/src/sg-nodes/zoom-row-list.tsx +42 -0
  155. package/src/standard-dialog-next/__tests__/standard-dialog-next.test.tsx +186 -0
  156. package/src/standard-dialog-next/index.ts +5 -0
  157. package/src/standard-dialog-next/standard-dialog-next.props.ts +80 -0
  158. package/src/standard-dialog-next/standard-dialog-next.tsx +326 -0
  159. package/src/test-helpers/tween-test-setup.ts +78 -0
  160. package/src/tween.tsx +173 -0
  161. package/src/types.ts +8 -0
  162. package/src/xml-components/blended-busy-spinner/BlendedBusySpinner.brs +13 -0
  163. package/src/xml-components/blended-busy-spinner/BlendedBusySpinner.xml +10 -0
  164. package/src/xml-components/blended-busy-spinner/BlendedBusySpinner.xml.d.ts +42 -0
  165. package/src/xml-components/bounding-tracker/BoundingTracker.brs +26 -0
  166. package/src/xml-components/bounding-tracker/BoundingTracker.xml +14 -0
  167. package/src/xml-components/bounding-tracker/BoundingTracker.xml.d.ts +42 -0
  168. package/src/xml-components/channel-store/channelStoreExt.brs +159 -0
  169. package/src/xml-components/channel-store/channelStoreExt.xml +29 -0
  170. package/src/xml-components/channel-store/channelStoreExt.xml.d.ts +57 -0
  171. package/src/xml-components/focus/focusable-group.brs +251 -0
  172. package/src/xml-components/focus/focusable-group.xml +15 -0
  173. package/src/xml-components/focus/focusable-group.xml.d.ts +42 -0
  174. package/src/xml-components/focus/focusable.xml +18 -0
  175. package/src/xml-components/focus/focusable.xml.d.ts +48 -0
  176. package/src/xml-components/keyboards/CustomAddressKDF.keyboard.json +547 -0
  177. package/src/xml-components/keyboards/CustomKeyboard.brs +322 -0
  178. package/src/xml-components/keyboards/CustomKeyboard.xml +45 -0
  179. package/src/xml-components/keyboards/CustomKeyboard.xml.d.ts +73 -0
  180. package/src/xml-components/keyboards/KeyGridExt.xml +4 -0
  181. package/src/xml-components/keyboards/KeyGridExt.xml.d.ts +37 -0
  182. package/src/xml-components/keyboards/KeyboardExt.xml +4 -0
  183. package/src/xml-components/keyboards/KeyboardExt.xml.d.ts +37 -0
  184. package/src/xml-components/keyboards/MOPPinPadCustomKDF.keyboard.json +151 -0
  185. package/src/xml-components/keyboards/MiniKeyboardExt.xml +4 -0
  186. package/src/xml-components/keyboards/MiniKeyboardExt.xml.d.ts +37 -0
  187. package/src/xml-components/keyboards/PhonePadCustomKDF.keyboard.json +42 -0
  188. package/src/xml-components/keyboards/PinPadExt.brs +102 -0
  189. package/src/xml-components/keyboards/PinPadExt.xml +17 -0
  190. package/src/xml-components/keyboards/PinPadExt.xml.d.ts +49 -0
  191. package/src/xml-components/keyboards/WifiCustomKDF.keyboard.json +1242 -0
  192. package/src/xml-components/keyhandler/KeyHandler.brs +30 -0
  193. package/src/xml-components/keyhandler/KeyHandler.xml +16 -0
  194. package/src/xml-components/keyhandler/KeyHandler.xml.d.ts +45 -0
  195. package/src/xml-components/managed-list-panel/ManagedListPanel.brs +137 -0
  196. package/src/xml-components/managed-list-panel/ManagedListPanel.xml +10 -0
  197. package/src/xml-components/managed-list-panel/ManagedListPanel.xml.d.ts +37 -0
  198. package/src/xml-components/modal/Modal.brs +105 -0
  199. package/src/xml-components/modal/Modal.xml +22 -0
  200. package/src/xml-components/modal/Modal.xml.d.ts +54 -0
  201. package/src/xml-components/modal/StandardDialogExt.xml +10 -0
  202. package/src/xml-components/modal/StandardDialogExt.xml.d.ts +37 -0
  203. package/src/xml-components/modal/StandardKeyboardDialogExt.xml +10 -0
  204. package/src/xml-components/modal/StandardKeyboardDialogExt.xml.d.ts +37 -0
  205. package/src/xml-components/modal/StandardMessageDialogExt.xml +10 -0
  206. package/src/xml-components/modal/StandardMessageDialogExt.xml.d.ts +37 -0
  207. package/src/xml-components/modal/StandardPinPadDialogExt.xml +10 -0
  208. package/src/xml-components/modal/StandardPinPadDialogExt.xml.d.ts +37 -0
  209. package/src/xml-components/modal/StandardProgressDialogExt.xml +10 -0
  210. package/src/xml-components/modal/StandardProgressDialogExt.xml.d.ts +37 -0
  211. package/src/xml-components/modal/tests/Modal.test.brs +195 -0
  212. package/tsconfig.json +10 -0
@@ -0,0 +1,195 @@
1
+ // Copyright (c) 2026 Roku, Inc.
2
+ // SPDX-License-Identifier: MIT
3
+
4
+ // ---------------------------------------------------------------------------
5
+ // Tween renders one `<tween>`: the native node when firmware reports the "tween" feature,
6
+ // otherwise the compat shim. It sends the native node only the fields it has.
7
+ // ---------------------------------------------------------------------------
8
+
9
+ import "@roku-sdk/compat";
10
+
11
+ import { afterEach, beforeEach, describe, expect, jest, test } from "@jest/globals";
12
+ import type { AnimationState, Vector2D } from "@roku-sdk/components";
13
+ import type { DomNode } from "@roku-sdk/interfaces";
14
+ import { hasFeature } from "@roku-sdk/runtime/has-feature";
15
+ import { batch, createSignal, type JSX, tmTransaction } from "@roku-sdk/solid";
16
+
17
+ import { commitEarly, createAnchorNode, installSolidTestRenderer } from "../test-helpers/tween-test-setup";
18
+ import { Tween } from "../tween";
19
+
20
+ jest.mock("@roku-sdk/runtime/has-feature", () => ({
21
+ // The side-effect compat import loads this factory before the imported jest binding.
22
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
23
+ hasFeature: require("@jest/globals").jest.fn()
24
+ }));
25
+
26
+ describe("Tween", () => {
27
+ let render: ReturnType<typeof installSolidTestRenderer>;
28
+ let setProperty: jest.SpiedFunction<typeof tmTransaction.setProperty>;
29
+ let createElement: jest.SpiedFunction<typeof tmTransaction.createElement>;
30
+ let dispose: (() => void) | undefined;
31
+
32
+ beforeEach(() => {
33
+ render = installSolidTestRenderer();
34
+ setProperty = jest.spyOn(tmTransaction, "setProperty");
35
+ createElement = jest.spyOn(tmTransaction, "createElement");
36
+ });
37
+
38
+ afterEach(() => {
39
+ dispose?.();
40
+ dispose = undefined;
41
+ jest.mocked(hasFeature).mockReset();
42
+ setProperty.mockRestore();
43
+ createElement.mockRestore();
44
+ });
45
+
46
+ const createdTypes = (): string[] => createElement.mock.results.map(r => (r.value as DomNode).t);
47
+
48
+ const mount = (native: boolean, renderTween: (target: () => DomNode | undefined) => JSX.Element): DomNode => {
49
+ jest.mocked(hasFeature).mockReturnValue(native);
50
+ let target: DomNode | undefined;
51
+ dispose = render(
52
+ () => (
53
+ <group>
54
+ <rectangle ref={(el: unknown) => (target = el as DomNode)} />
55
+ {renderTween(() => target)}
56
+ </group>
57
+ ),
58
+ createAnchorNode()
59
+ );
60
+ commitEarly();
61
+ const index = createElement.mock.calls.findIndex(([tag]) => tag === "tween");
62
+ return createElement.mock.results[index].value as DomNode;
63
+ };
64
+ const writes = (node: DomNode, name: string): unknown[] =>
65
+ setProperty.mock.calls.filter(([n, field]) => n === node && field === name).map(([, , value]) => value);
66
+
67
+ describe("node selection", () => {
68
+ test("creates the native node alone when the firmware reports the feature", () => {
69
+ const tween = mount(true, target => <Tween targetRef={target} axis="x" targetValue={100} />);
70
+
71
+ expect(tween.t).toBe("tween");
72
+ expect(createdTypes()).toEqual(["group", "rectangle", "tween"]);
73
+ });
74
+
75
+ test("creates the compat shim alone when the firmware does not report the feature", () => {
76
+ const tween = mount(false, target => <Tween targetRef={target} axis="x" targetValue={100} />);
77
+
78
+ expect(tween.t).toBe("RsgTweenShim");
79
+ expect(createdTypes()).toEqual(["group", "rectangle", "RsgTweenShim"]);
80
+ });
81
+ });
82
+
83
+ describe("native field contract", () => {
84
+ test('sends an axis="all" targetVector as given and writes no targetValue', () => {
85
+ const tween = mount(true, target => <Tween targetRef={target} axis="all" targetVector={[40, 90]} />);
86
+
87
+ expect(writes(tween, "targetVector")).toContainEqual([40, 90]);
88
+ expect(writes(tween, "targetValue").filter(v => v !== undefined)).toHaveLength(0);
89
+ });
90
+
91
+ test("sends the target immediately", () => {
92
+ const tween = mount(true, target => <Tween targetRef={target} axis="x" targetValue={100} />);
93
+
94
+ expect(writes(tween, "targetValue")).toContain(100);
95
+ });
96
+
97
+ test.each([true, false])("omits the state observer without a callback (native=%s)", native => {
98
+ const tween = mount(native, target => <Tween targetRef={target} targetValue={100} />);
99
+
100
+ expect(writes(tween, "onAnimationState").filter(value => value !== undefined)).toHaveLength(0);
101
+ });
102
+
103
+ test("reactively installs, replaces and removes the state callback", () => {
104
+ type Handler = NonNullable<Parameters<typeof Tween>[0]["onAnimationState"]>;
105
+ const [callback, setCallback] = createSignal<Handler | undefined>();
106
+ const first = jest.fn<Handler>();
107
+ const second = jest.fn<Handler>();
108
+ const tween = mount(true, target => <Tween targetRef={target} onAnimationState={callback()} />);
109
+ expect(writes(tween, "onAnimationState").filter(value => value !== undefined)).toHaveLength(0);
110
+
111
+ setCallback(() => first);
112
+ commitEarly();
113
+ const adapter = writes(tween, "onAnimationState").at(-1) as (t: [AnimationState, Vector2D]) => void;
114
+ expect(typeof adapter).toBe("function");
115
+ adapter(["running", [10, 20]]);
116
+ expect(first).toHaveBeenCalledWith("running", [10, 20]);
117
+
118
+ setCallback(() => second);
119
+ commitEarly();
120
+ adapter(["stopped", [30, 40]]);
121
+ expect(second).toHaveBeenCalledWith("stopped", [30, 40]);
122
+ expect(first).toHaveBeenCalledTimes(1);
123
+
124
+ setCallback(undefined);
125
+ commitEarly();
126
+ expect(writes(tween, "onAnimationState").at(-1)).toBeUndefined();
127
+ });
128
+
129
+ test("forwards onAnimationState as (state, value)", () => {
130
+ const onAnimationState = jest.fn();
131
+ const tween = mount(true, target => (
132
+ <Tween targetRef={target} axis="x" targetValue={100} onAnimationState={onAnimationState} />
133
+ ));
134
+
135
+ const handler = writes(tween, "onAnimationState")[0] as (t: [AnimationState, Vector2D]) => void;
136
+ handler(["running", [10, 20]]);
137
+
138
+ expect(onAnimationState).toHaveBeenCalledWith("running", [10, 20]);
139
+ });
140
+ });
141
+
142
+ test("targetNode takes precedence over the deprecated targetRef alias", () => {
143
+ const legacy = { t: "rectangle", r: { id: 999 }, c: [] } as unknown as DomNode;
144
+ const tween = mount(true, target => <Tween targetNode={target} targetRef={legacy} targetValue={100} />);
145
+ const targets = writes(tween, "targetNode");
146
+ expect(targets).toHaveLength(1);
147
+ expect(targets[0]).not.toBe(legacy);
148
+ });
149
+
150
+ describe("target switch", () => {
151
+ test.each([
152
+ ["value first", true],
153
+ ["target first", false]
154
+ ])("writes the new targetNode before the trigger that goes with it (%s)", (_, valueFirst) => {
155
+ jest.mocked(hasFeature).mockReturnValue(true);
156
+ const nodes: Record<string, DomNode> = {};
157
+ const [onB, setOnB] = createSignal(false);
158
+ const [to, setTo] = createSignal(100);
159
+ dispose = render(
160
+ () => (
161
+ <group>
162
+ <rectangle ref={(el: unknown) => (nodes.a = el as DomNode)} />
163
+ <rectangle ref={(el: unknown) => (nodes.b = el as DomNode)} />
164
+ <Tween targetNode={() => (onB() ? nodes.b : nodes.a)} axis="x" targetValue={to()} />
165
+ </group>
166
+ ),
167
+ createAnchorNode()
168
+ );
169
+ commitEarly();
170
+ const tween = createElement.mock.results[createElement.mock.calls.findIndex(([t]) => t === "tween")]
171
+ .value as DomNode;
172
+ setProperty.mockClear();
173
+
174
+ batch(() => {
175
+ if (valueFirst) setTo(300);
176
+ setOnB(true);
177
+ if (!valueFirst) setTo(300);
178
+ });
179
+ commitEarly();
180
+
181
+ const order = setProperty.mock.calls
182
+ .filter(([n, field]) => n === tween && (field === "targetNode" || field === "targetValue"))
183
+ .map(([, field, value]) => (field === "targetNode" ? (value === nodes.b ? "B" : "?") : value));
184
+ expect(order).toEqual(["B", 300]);
185
+ });
186
+ });
187
+
188
+ describe("shim field contract", () => {
189
+ test("writes no animationNode", () => {
190
+ const tween = mount(false, target => <Tween targetRef={target} axis="x" targetValue={100} />);
191
+
192
+ expect(writes(tween, "animationNode")).toHaveLength(0);
193
+ });
194
+ });
195
+ });
@@ -0,0 +1,37 @@
1
+ // Copyright (c) 2026 Roku, Inc.
2
+ // SPDX-License-Identifier: MIT
3
+
4
+ import type { IBusySpinnerProps } from "./sg-nodes/busy-spinner.props";
5
+
6
+ /**
7
+ * Properties for the BlendedBusySpinner node.
8
+ *
9
+ * BlendedBusySpinner extends BusySpinner with blend color support, displaying an animated
10
+ * spinning indicator to show that content is loading or an operation is in progress.
11
+ *
12
+ * The spinner inherits all BusySpinner properties (control, clockwise, spinInterval, uri)
13
+ * and adds blend color customization.
14
+ *
15
+ * @see {@link https://developer.roku.com/dev/docs/busyspinner}
16
+ *
17
+ * @example
18
+ * // Basic usage with default appearance
19
+ * <BlendedBusySpinner control="start" translation={[100, 100]} uri="pkg:/images/loader.png" />
20
+ *
21
+ * @example
22
+ * // Custom spinner with speed control and blend color
23
+ * <BlendedBusySpinner
24
+ * control="start"
25
+ * clockwise={false}
26
+ * spinInterval={1}
27
+ * uri="pkg:/images/custom-spinner.png"
28
+ * blendColor="0xFF0000FF"
29
+ * />
30
+ */
31
+ export interface IBlendedBusySpinnerProps extends IBusySpinnerProps {
32
+ /**
33
+ * Specifies the blend color for the spinner image.
34
+ * The blend color is applied to the spinner bitmap to modify its appearance.
35
+ */
36
+ blendColor?: string;
37
+ }
@@ -0,0 +1,13 @@
1
+ // Copyright (c) 2026 Roku, Inc.
2
+ // SPDX-License-Identifier: MIT
3
+
4
+ import type { IBlendedBusySpinnerProps } from "./blended-busy-spinner.props";
5
+ import { retainAsset } from "./retain-asset";
6
+ import { Component } from "./sg-nodes/types";
7
+ import BlendedBusySpinnerXML from "./xml-components/blended-busy-spinner/BlendedBusySpinner.xml";
8
+ export type { IBlendedBusySpinnerProps } from "./blended-busy-spinner.props";
9
+
10
+ export const BlendedBusySpinner: Component<IBlendedBusySpinnerProps> = props => {
11
+ retainAsset(BlendedBusySpinnerXML);
12
+ return <blendedBusySpinner {...props}></blendedBusySpinner>;
13
+ };
@@ -0,0 +1,18 @@
1
+ // Copyright (c) 2026 Roku, Inc.
2
+ // SPDX-License-Identifier: MIT
3
+
4
+ import type { IGroupProps } from "./sg-nodes/group.props";
5
+ import type { EventHandler, JSX } from "./sg-nodes/types";
6
+
7
+ export interface IBoundingRect {
8
+ x: number;
9
+ y: number;
10
+ width: number;
11
+ height: number;
12
+ }
13
+
14
+ export interface IBoundingTrackerProps extends IGroupProps {
15
+ doMeasure?: boolean;
16
+ onBoundsChange?: EventHandler<IBoundingRect>;
17
+ children?: JSX.Element;
18
+ }
@@ -0,0 +1,13 @@
1
+ // Copyright (c) 2026 Roku, Inc.
2
+ // SPDX-License-Identifier: MIT
3
+
4
+ import type { IBoundingTrackerProps } from "./bounding-tracker.props";
5
+ import { Component } from "./index";
6
+ import { retainAsset } from "./retain-asset";
7
+ import BoundingTrackerXML from "./xml-components/bounding-tracker/BoundingTracker.xml";
8
+ export type { IBoundingRect, IBoundingTrackerProps } from "./bounding-tracker.props";
9
+
10
+ export const BoundingTracker: Component<IBoundingTrackerProps> = props => {
11
+ retainAsset(BoundingTrackerXML);
12
+ return <bounding-tracker {...props}>{props.children}</bounding-tracker>;
13
+ };
@@ -0,0 +1,126 @@
1
+ // Copyright (c) 2026 Roku, Inc.
2
+ // SPDX-License-Identifier: MIT
3
+ import {
4
+ Component,
5
+ EventHandler,
6
+ Focusable,
7
+ FocusableGroup,
8
+ IButtonDataProps,
9
+ ResType,
10
+ Uri,
11
+ Vector2D
12
+ } from "@roku-sdk/components";
13
+ import { createMemo, createSelector, For, Show } from "@roku-sdk/solid";
14
+
15
+ interface ISingleButtonProps extends IButtonDataProps {
16
+ text?: string;
17
+ }
18
+
19
+ export type IButtonRecord = ISingleButtonProps | string;
20
+
21
+ export interface IButtonRowProps extends IButtonDataProps {
22
+ buttons: IButtonRecord[];
23
+ translation?: ResType<Vector2D>;
24
+ onButtonFocused?: EventHandler<number>;
25
+ onButtonSelected?: EventHandler<number>;
26
+ opacity?: number;
27
+ focusIdx?: number;
28
+ itemSpacing?: number;
29
+ gainFocus?: boolean;
30
+ }
31
+
32
+ const buttonDataKeys: (keyof IButtonDataProps)[] = [
33
+ "textColor",
34
+ "focusedTextColor",
35
+ "textFont",
36
+ "focusedTextFont",
37
+ "focusFootprintBitmapUri",
38
+ "focusBitmapUri",
39
+ "iconUri",
40
+ "focusedIconUri",
41
+ "height",
42
+ "minWidth",
43
+ "maxWidth",
44
+ "showFocusFootprint"
45
+ ];
46
+
47
+ const defaultButtonData: ISingleButtonProps = {
48
+ showFocusFootprint: true,
49
+ iconUri: "",
50
+ focusedIconUri: "",
51
+ textFont: "font:TinySystemFont",
52
+ focusedTextFont: "font:TinyBoldSystemFont"
53
+ };
54
+
55
+ const createButtonProps = (button: IButtonRecord, componentProps: IButtonDataProps): ISingleButtonProps => {
56
+ let buttonProps: ISingleButtonProps = buttonDataKeys.reduce(
57
+ (acc: ISingleButtonProps, key) => {
58
+ const componentValue = componentProps[key];
59
+ if (componentValue !== undefined) {
60
+ (acc[key] as number | boolean | Uri) = componentValue;
61
+ }
62
+ return acc;
63
+ },
64
+ { ...defaultButtonData }
65
+ );
66
+
67
+ if (typeof button === "string") {
68
+ buttonProps.text = button;
69
+ } else {
70
+ buttonProps = { ...buttonProps, ...button };
71
+ }
72
+ return buttonProps;
73
+ };
74
+
75
+ export const ButtonRow: Component<IButtonRowProps> = props => {
76
+ const focusIdx = () => props.focusIdx || 0;
77
+
78
+ const onButtonFocused: EventHandler<string | null> = (id: string | null) => {
79
+ if (props.onButtonFocused && id) {
80
+ props.onButtonFocused(parseInt(id.replace(props.id || "", "")));
81
+ }
82
+ };
83
+
84
+ const buttonsLength = createMemo(() => props.buttons.length);
85
+
86
+ // Create a stable array of button indices that only changes when length changes
87
+ const buttonIndices = createMemo(() => Array.from({ length: buttonsLength() }, (_, i) => i));
88
+ const isFocused = createSelector(focusIdx, (a, b) => (props.gainFocus === undefined || props.gainFocus) && a === b);
89
+
90
+ return (
91
+ <FocusableGroup id={props.id} rememberLastFocus={true} gainFocus={props.gainFocus} opacity={props.opacity}>
92
+ <Show when={buttonsLength() > 0}>
93
+ <layoutGroup
94
+ layoutDirection="horiz"
95
+ horizAlignment="left"
96
+ translation={props.translation}
97
+ itemSpacings={[props.itemSpacing || 32]}
98
+ >
99
+ <For
100
+ each={buttonIndices()}
101
+ children={(index: number) => {
102
+ const buttonAtIndex = (): IButtonRecord | undefined => props.buttons[index];
103
+ const onButtonSelected: EventHandler<never> = () => props.onButtonSelected?.(index);
104
+ return (
105
+ <Show when={buttonAtIndex() !== undefined}>
106
+ <Focusable
107
+ nextFocusRight={`${props.id || ""}${index < buttonsLength() - 1 ? (index + 1).toString() : undefined}`}
108
+ nextFocusLeft={`${props.id || ""}${index > 0 ? (index - 1).toString() : undefined}`}
109
+ >
110
+ <button
111
+ gainFocus={isFocused(index)}
112
+ onFocusedChild={onButtonFocused}
113
+ onButtonSelected={onButtonSelected}
114
+ {...createButtonProps(buttonAtIndex()!, props)}
115
+ id={`${props.id || ""}${index.toString()}`}
116
+ />
117
+ </Focusable>
118
+ </Show>
119
+ );
120
+ }}
121
+ />
122
+ </layoutGroup>
123
+ </Show>
124
+ </FocusableGroup>
125
+ );
126
+ };