@roku-sdk/ai 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 (53) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +88 -2
  3. package/package.json +47 -3
  4. package/skills/ads-and-raf/SKILL.md +231 -0
  5. package/skills/ads-and-raf/references/integration-flows.md +332 -0
  6. package/skills/app-packaging/SKILL.md +107 -0
  7. package/skills/app-performance/SKILL.md +133 -0
  8. package/skills/cli-dev-loop/SKILL.md +217 -0
  9. package/skills/cli-dev-loop/references/logs-and-crashes.md +114 -0
  10. package/skills/cpu-and-heap-profiling/SKILL.md +108 -0
  11. package/skills/cpu-and-heap-profiling/references/reading-profiles.md +52 -0
  12. package/skills/data-entry-keyboards/SKILL.md +160 -0
  13. package/skills/device-management/SKILL.md +158 -0
  14. package/skills/dialogs-and-modals/SKILL.md +210 -0
  15. package/skills/effects/SKILL.md +183 -0
  16. package/skills/fetch-and-data-loading/SKILL.md +164 -0
  17. package/skills/fetch-and-data-loading/references/https-and-certificates.md +79 -0
  18. package/skills/flex-grid/SKILL.md +236 -0
  19. package/skills/flex-grid/references/focus-integration.md +355 -0
  20. package/skills/flex-grid/references/plugins.md +201 -0
  21. package/skills/focus-management/SKILL.md +210 -0
  22. package/skills/focus-management/references/focus-recipes.md +290 -0
  23. package/skills/fonts-and-glyphs/SKILL.md +231 -0
  24. package/skills/fonts-and-glyphs/references/glyph-tables.md +128 -0
  25. package/skills/logging-and-errors/SKILL.md +227 -0
  26. package/skills/logging-and-errors/references/uncaught-exceptions.md +137 -0
  27. package/skills/mcp-ai-dev-loop/SKILL.md +189 -0
  28. package/skills/mcp-ai-dev-loop/references/crash-and-console.md +68 -0
  29. package/skills/mcp-ai-dev-loop/references/live-debugging.md +129 -0
  30. package/skills/mcp-setup/SKILL.md +131 -0
  31. package/skills/project-setup/SKILL.md +210 -0
  32. package/skills/project-setup/references/roku-config.md +120 -0
  33. package/skills/screen-navigation/SKILL.md +166 -0
  34. package/skills/sdk-components/SKILL.md +213 -0
  35. package/skills/sdk-components/references/dialogs.md +148 -0
  36. package/skills/sdk-components/references/scenegraph-node-spelling.md +123 -0
  37. package/skills/sdk-installation/SKILL.md +129 -0
  38. package/skills/slideshow/SKILL.md +157 -0
  39. package/skills/slideshow/references/focus-and-handoff.md +187 -0
  40. package/skills/solid-on-roku/SKILL.md +181 -0
  41. package/skills/solid-on-roku/references/hermes-and-exports.md +130 -0
  42. package/skills/testing/SKILL.md +147 -0
  43. package/skills/testing/references/async-and-timing.md +97 -0
  44. package/skills/user-accounts-and-purchases/SKILL.md +164 -0
  45. package/skills-manifest.json +316 -0
  46. package/skills-manifest.schema.json +62 -0
  47. package/src/__tests__/manifest.test.ts +267 -0
  48. package/src/__tests__/snippets.test.ts +221 -0
  49. package/src/__tests__/trigger-routing.test.ts +77 -0
  50. package/src/index.ts +36 -0
  51. package/src/manifest.ts +215 -0
  52. package/src/router.ts +147 -0
  53. package/src/snippets.ts +212 -0
@@ -0,0 +1,158 @@
1
+ # Device Management
2
+
3
+ ## Overview
4
+
5
+ The `rk` CLI keeps its own list of Rokus, and that list is not part of any project. Every command
6
+ that touches a device resolves a target from it — or from a flag that overrides it, or from the
7
+ environment when it is empty. Almost everything that goes wrong here is a question of which of
8
+ those three won, so the useful first move is to find out rather than to guess.
9
+
10
+ ## When to Use
11
+
12
+ - registering a Roku, or getting a developer password stored against one
13
+ - keeping a stored developer password out of a shared repository
14
+ - choosing which registered box commands land on, and changing it
15
+ - running a single command against a box that was never registered
16
+ - several worktrees or checkouts that disagree about which devices exist
17
+ - a device list that is empty, or that holds an entry no file seems to declare
18
+ - a device subcommand that runs, prints one line, and tells you nothing
19
+ - registering, starting or stopping a Roku Cloud Emulator device, which is listed with the others but registered in a file of its own
20
+
21
+ ### When not to use
22
+
23
+ | Situation | Use instead | Because |
24
+ |---|---|---|
25
+ | An agent session whose MCP tools answer, and a tool call that reached the wrong box | The `mcp-ai-dev-loop` skill | A tool call carries its own target argument, so the fix is there and not in the CLI's store — even though the symptom reads identically |
26
+ | Building, sideloading, driving a device or reading its logs | The `cli-dev-loop` skill | This skill ends the moment the box answers |
27
+ | Signing an app, publishing it, or clearing what is installed | The `app-packaging` skill | Those act on the artifact, not on how the box is reached |
28
+ | What `roku-config.ts` declares about the app | The `project-setup` skill | The device store is not project configuration and lives outside the repository |
29
+ | What a freshly scaffolded app's `.gitignore` covers, beyond the device store | The `project-setup` skill | The scaffold's own output is its subject; the one entry that protects a stored password is below |
30
+ | Leasing a box out of a shared pool | `documentation/advanced/rk/device-lease.md` and `documentation/advanced/rk/device-pool.md` | Leases are a separate mechanism from the device store; `rk` ships the `local` provider, and any other pool kind has to be registered by the caller |
31
+ | Turning on developer mode on the television itself | Roku's own documentation | That happens on the box, and no CLI command can do it |
32
+
33
+ ## Quick Reference
34
+
35
+ | Goal | Do |
36
+ |---|---|
37
+ | Register a Roku | `rk device add <ip>` |
38
+ | Store its password | `rk device update <target> --password <pwd>` |
39
+ | One password for every box | `rk device set-default-password` |
40
+ | See the list and which one is active | `rk device ls` |
41
+ | Switch the active box | `rk device set-active <target>` |
42
+ | Stop pinning one | `rk device clear-active` |
43
+ | Remove one | `rk device rm <target>` |
44
+ | Find out which file is being read | `rk device store-path` |
45
+ | Keep a stored password out of a shared repo | add `.roku` to `.gitignore` |
46
+ | Send one command elsewhere | `--device <id\|ip\|name>`, or `--host` with `--password` |
47
+ | Register a cloud device | `rk device add --rce <name> --snapshot <snapshot>`, then `rk device set-active rce:<name>` |
48
+ | See every cloud device in the organisation, registered or not | `rk device ls --org` |
49
+ | Start and stop it (billed while running) | `rk device start rce:<name>`, `rk device stop rce:<name>` |
50
+
51
+ ## Instructions
52
+
53
+ 1. **Two files can hold the list, and a local one wins outright.** The nearest
54
+ `.roku/roku-dev-config.json` found by walking upwards is used whenever it carries a `devices`
55
+ array; otherwise `~/roku-dev-config.json` supplies the list. When both exist the local one
56
+ still wins, and the CLI warns that it is doing so. `ROKU_DEV_CONFIG_PATH` replaces the home
57
+ path entirely. `rk device store-path` prints the file actually in effect, which settles the
58
+ question faster than reasoning about it.
59
+
60
+ 2. **The home file is the one worth keeping, because worktrees share it.** A local list belongs
61
+ to one checkout, so five worktrees mean five lists that drift apart. Delete the `devices`
62
+ array from the local config — the file may stay — and every checkout reads the central list.
63
+
64
+ 3. **The active pointer is resolved separately from the list.** A local config's
65
+ `activeDeviceId` outranks the home one even when every device is declared centrally. One
66
+ shared list with a different active box per worktree is therefore a supported arrangement
67
+ rather than a conflict, and it is worth knowing before "fixing" it.
68
+
69
+ 4. **`rk device add` cannot take a password.** For a box on your network it accepts `--name`,
70
+ `--id`, `--tag` and `--json`, and nothing else. Set the password afterwards with `rk device update <target> --password`, or set
71
+ one for every box at once with `rk device set-default-password`.
72
+
73
+ 5. **`--device` and `--host` are alternatives, not partners.** `--device` looks the box up in the
74
+ store, so it needs nothing else. `--host` bypasses the store entirely, so it needs
75
+ `--password` beside it. Passing both fails with a message saying exactly that, and the reason
76
+ is that they answer the same question two incompatible ways.
77
+
78
+ 6. **An empty store does not mean there is no device.** With `ROKU_DEV_TARGET` set to a valid
79
+ address, a synthetic entry is used, taking its password from `ROKU_DEV_PASSWORD` and showing
80
+ up tagged as having come from the environment. A CI image with no config file that still
81
+ reaches a box is almost always doing this.
82
+
83
+ 7. **Two advertised subcommands do nothing yet.** `ping` and `info` appear in the CLI's own
84
+ help and each prints a single line saying it is not yet implemented. Nothing is wrong with
85
+ the box; use `rk device ls` for what is registered. `rk device status` does work, but only
86
+ for a Roku Cloud Emulator device: with a box on your network active it says so and exits 0,
87
+ and naming one fails with `DEVICE_KIND_UNSUPPORTED`.
88
+
89
+ 8. **A local store puts a developer password inside the repository.** The password is written in
90
+ clear text into whichever `roku-dev-config.json` is in effect, so a local one lands at
91
+ `.roku/roku-dev-config.json` — a committable path. An app scaffolded by `rk create` ignores the
92
+ whole `.roku/` directory and is covered; any other project needs that entry added by hand,
93
+ before the first commit. `rk device add` warns while the directory is unignored and accepts any
94
+ of `.roku`, `/.roku`, `.roku/` or `/.roku/`, so the spelling its own warning suggests does
95
+ clear it — but it compares against those four forms and nothing else, so a pattern such as
96
+ `**/.roku` ignores the directory while leaving the warning in place. If the file was committed
97
+ already, `git rm --cached` it and treat the password as disclosed.
98
+
99
+ 9. **A Roku Cloud Emulator device is a second kind in the same listing, kept in its own file.**
100
+ `rk device ls` shows both kinds, but a registration lives in `~/roku-rce-devices.json`, beside
101
+ the RCE token in `~/roku-rce-config.json`; `ROKU_RCE_CONFIG_PATH` moves both, and
102
+ `~/roku-dev-config.json` holds LAN devices only. Neither is ever read from a project's
103
+ `.roku/`. `RK_RCE_TOKEN` outranks the saved token, and `rk device set-rce-token` saves one
104
+ without taking it as an argument. Name the device `rce:<name>` wherever a command takes one:
105
+ a bare name also resolves, but when a box on your network shares it the LAN box wins, with a
106
+ warning on stderr naming the `rce:` spelling. It has no address, so `--host` can never reach
107
+ it — use `--device`. Its developer password, like the token, is read from standard input,
108
+ never from an argument, so `rk device update <target> --password` is refused for it; from a
109
+ pipe only the first line is read. `rk device update rce:<name>` prints only what it changed,
110
+ and `--json` prints `{ device }`. It is billed while it runs and nothing starts it behind your
111
+ back: a command that needs it says `rk dev --start` or `rk device start`, and whoever starts it
112
+ should stop it. `documentation/advanced/rk/rce.md` covers creating one, snapshots, and what works on it.
113
+
114
+ ## Example
115
+
116
+ Moving a checkout off its own private list and onto the shared one, then confirming what the CLI
117
+ now reads.
118
+
119
+ ```bash
120
+ # What is actually in effect right now.
121
+ rk device store-path
122
+ rk device ls
123
+
124
+ # Register centrally and give the box its password in two steps, not one.
125
+ rk device add 10.0.0.42 --name desk-ultra
126
+ rk device update desk-ultra --password <pwd>
127
+ rk device set-active desk-ultra
128
+ ```
129
+
130
+ If `store-path` reported a `.roku/roku-dev-config.json` inside the checkout, remove the `devices`
131
+ array from it first — otherwise the commands above write to a list only this worktree can see.
132
+
133
+ A one-off box that should never be registered skips all of it:
134
+
135
+ ```bash
136
+ rk ecp query/device-info --host 10.0.0.99 --password <pwd>
137
+ ```
138
+
139
+ ## Common Mistakes
140
+
141
+ | Symptom | Cause | Fix |
142
+ |---|---|---|
143
+ | `rk device add` rejects a password option | `add` takes only `--name`, `--id`, `--tag` and `--json` | `rk device update <target> --password` |
144
+ | `rk device remove` is not recognised | The subcommand is `rm` | `rk device rm <target>` |
145
+ | A tag-clearing option is rejected | No such option exists, though older documentation names one | Re-tag with `rk device update --tag` |
146
+ | Two worktrees see different devices | Each has its own local `devices` array | Drop the local array; keep `~/roku-dev-config.json` |
147
+ | A warning about a legacy local device list | Both files exist and the local one is winning | Migrate the list to the home file |
148
+ | `--device` together with `--host` fails | They resolve the target two incompatible ways | `--device` for a stored box, `--host` with `--password` for an ad-hoc one |
149
+ | `--host` alone is refused for want of a password | Bypassing the store also bypasses the stored password | Add `--password`, or register the box and use `--device` |
150
+ | A device health subcommand prints one line and stops | `ping` and `info` are not implemented | `rk device ls` |
151
+ | `rk device status` only prints that it reports on cloud devices, or refuses a box on your network | It reports on Roku Cloud Emulator devices only | `rk device ls` for a network box |
152
+ | A command warns that a name "names a LAN device and a Roku Cloud Emulator device" | A box on your network shares the cloud device's name, and wins | `rce:<name>` for the cloud device |
153
+ | `--host rce:<name>` is refused | A cloud device has no address of its own | `--device rce:<name>` |
154
+ | A command says the cloud device is not running | Nothing starts one implicitly, since it is billed | `rk device start rce:<name>`, or `rk dev --start`; stop it afterwards |
155
+ | `rk device ls` shows a cloud device as `unknown` | No RCE token is configured, or the service did not answer | `rk device set-rce-token`, or set `RK_RCE_TOKEN` |
156
+ | The list is empty but commands still reach a box | `ROKU_DEV_TARGET` is set in the environment | `rk device store-path`, then check the environment |
157
+ | `git status` offers to commit a file holding the developer password | A local store writes it to `.roku/roku-dev-config.json`, and the project was not scaffolded by `rk create`, so nothing ignores it | Ignore `.roku/`, then `git rm --cached` it if it was already committed |
158
+ | The unignored-directory warning survives a `.gitignore` entry | The check recognises `.roku`, `/.roku`, `.roku/` and `/.roku/`, and no other pattern | Use one of those four forms rather than a glob |
@@ -0,0 +1,210 @@
1
+ # Dialog lifecycle and multi-step flows
2
+
3
+ ## Overview
4
+
5
+ A `StandardDialog` knows how to draw itself and how to take focus. What it does not know is
6
+ *when* it should be up, what the caller learns when it goes down, or how one dialog leads to
7
+ the next. That is app state, it lives above the component, and it is what this skill covers.
8
+
9
+ The pattern is a provider at the app root that owns which dialog is showing, plus a registry
10
+ so the scene can render the current one without knowing any of them by name. It is an
11
+ **app-level pattern, not an SDK export** — there is no dialog provider to import. Copy it into
12
+ the app and own it.
13
+
14
+ ## When to Use
15
+
16
+ | If the request is | Then |
17
+ |---|---|
18
+ | Deciding when a dialog opens and closes, from somewhere other than the dialog itself | Here |
19
+ | Telling apart "the viewer chose something" from "the viewer left" | Here — the close reason |
20
+ | A flow of two or more dialogs where backing out returns to the previous step | Here — child dialogs |
21
+ | A wait indicator that has to come down when the work it covers finishes | Here |
22
+ | Teardown that runs twice when a dialog closes | Keep action completion idempotent; StandardDialog normalizes native close notifications |
23
+
24
+ ### When not to use
25
+
26
+ | Situation | Use instead | Because |
27
+ |---|---|---|
28
+ | Which component draws the dialog, which areas and item types go inside it, or which pre-assembled variant fits | The `sdk-components` skill, and the dialog reference it declares | That skill owns composition. This one never assembles a dialog; it decides when one is up |
29
+ | An overlay drawn out of ordinary nodes that has to hold the D-pad and dismiss on a key | The `focus-management` skill, and the focus-recipes reference it declares | `ModalBoundary` and `useModal` are the other path entirely. A hand-drawn overlay has no native modal to hand focus to, and that skill owns the wiring |
30
+ | Focus inside a dialog, or which key dismisses it | The `focus-management` skill | A `StandardDialog` renders a native modal, so focus enters and returns on its own. Nothing here changes that |
31
+ | A keyboard or PIN pad the viewer types into | The `sdk-components` skill for the component | Entry is its own subject. This skill only carries the flow the entry sits in |
32
+ | Moving between whole screens rather than showing something over one | The `screen-navigation` skill | A dialog leaves the screen underneath mounted and focused-inactive; a screen change does not |
33
+
34
+ ## Quick Reference
35
+
36
+ The provider's surface. Names are the ones the showcase pattern uses — keep them, because
37
+ every dialog in the app reads them.
38
+
39
+ | Member | Does |
40
+ |---|---|
41
+ | `setDialog(id, onClose, data?)` | Opens a dialog by id, with the callback that receives its result |
42
+ | `closeDialog(params?)` | Closes the current one. Pops to the parent if there is one, otherwise calls `onClose` |
43
+ | `openChildDialog(childId, data, parentData)` | Pushes a step. `parentData` is what gets restored underneath |
44
+ | `getDialog()` | The current id, or `null` |
45
+ | `getDialogData<T>()` | The data the current dialog was opened with |
46
+ | `registerDialog(id, factory)` / `deregisterDialog(id)` | Registry, driven from `onMount` / `onCleanup` |
47
+ | `getDialogElement()` | The current dialog as an element, for the scene to render |
48
+
49
+ Two values come back through `closeDialog`:
50
+
51
+ | Close param | Means |
52
+ |---|---|
53
+ | `reason: DialogCloseReason.ButtonPressed` | The viewer chose an action. `buttonId` says which |
54
+ | `reason: DialogCloseReason.DialogCancelled` | The viewer left, or the dialog dismissed itself |
55
+
56
+ ## Instructions
57
+
58
+ 1. **Mount one provider at the app root** and have the scene render `getDialogElement()`.
59
+ The scene stays ignorant of every individual dialog, so adding one is a registration
60
+ rather than an edit to the scene.
61
+
62
+ 2. **Register from `onMount`, deregister from `onCleanup`.** A factory left registered after
63
+ its dialog is gone can still be built, which produces a dialog nothing owns.
64
+
65
+ 3. **Open with `setDialog(id, onClose, data)`.** The caller's `onClose` is where the result
66
+ lands, so the code that opened the dialog is the code that reacts to it — not the dialog.
67
+
68
+ 4. **Every dialog reports its own close.** Wire the component's `onWasClosed` to
69
+ `closeDialog(params)` and pass a reason. The component reports the native close event;
70
+ the provider decides what that means.
71
+
72
+ 5. **Keep action completion idempotent.** `StandardDialog` and its ready-made variants
73
+ report `onWasClosed` once per presented lifetime. A button handler and the later close
74
+ notification can still both reach app completion; guard that shared action path.
75
+ Raw native nodes and the legacy `Dialog` may report duplicate native close events.
76
+
77
+ 6. **Branch on the reason, not on the button index.** `ButtonPressed` with a `buttonId`
78
+ is a choice; `DialogCancelled` is the viewer leaving. Treating "closed" as "confirmed"
79
+ is how a flow commits something nobody agreed to.
80
+
81
+ 7. **Chain steps with `openChildDialog`, and read the result from `_childCloseParams`.**
82
+ When a child closes, the provider re-opens the parent with the child's close params merged
83
+ into its data, so the parent resumes rather than restarts. `parentData` is how the fields
84
+ the viewer already changed survive the round trip.
85
+
86
+ 8. **Close a progress dialog from the signal that measures it, not from a timer.** An effect
87
+ watching the value reacts the moment it lands, so what is displayed and what triggers the
88
+ close cannot drift apart. A timer sized to "about how long it takes" will sometimes close
89
+ at 95%.
90
+
91
+ 9. **Clear intervals and timeouts in `onCleanup`.** A dialog can go away while its work is
92
+ still running, and a timer that outlives it writes to signals nothing is reading.
93
+
94
+ ## Example
95
+
96
+ A provider small enough to read, one dialog, and the close reason coming back to the caller.
97
+ `DialogProvider` and `useDialogs` here are app code — this is the shape to copy, not an
98
+ import to add.
99
+
100
+ ```tsx rsg-skill=complete
101
+ import { StandardDialog, StdDlgButton, StdDlgButtonArea, StdDlgTextItem, StdDlgTitleArea } from "@roku-sdk/components";
102
+ import { Accessor, Component, createContext, createSignal, JSX, Show, useContext } from "@roku-sdk/solid";
103
+
104
+ export enum CloseReason {
105
+ ButtonPressed = 0,
106
+ DialogCancelled = 1
107
+ }
108
+
109
+ export interface CloseParams {
110
+ reason: CloseReason;
111
+ buttonId?: string;
112
+ }
113
+
114
+ interface FlowApi {
115
+ current: Accessor<string | null>;
116
+ open: (id: string, onClose: (params: CloseParams) => void) => void;
117
+ close: (params: CloseParams) => void;
118
+ }
119
+
120
+ const FlowContext = createContext<FlowApi>();
121
+
122
+ export const DialogProvider: Component<{ children: JSX.Element }> = props => {
123
+ const [current, setCurrent] = createSignal<string | null>(null);
124
+ let onClose: ((params: CloseParams) => void) | undefined;
125
+
126
+ const open = (id: string, handler: (params: CloseParams) => void) => {
127
+ onClose = handler;
128
+ setCurrent(id);
129
+ };
130
+
131
+ const close = (params: CloseParams) => {
132
+ const handler = onClose;
133
+ onClose = undefined;
134
+ setCurrent(null);
135
+ handler?.(params);
136
+ };
137
+
138
+ return <FlowContext.Provider value={{ current, open, close }}>{props.children}</FlowContext.Provider>;
139
+ };
140
+
141
+ export function useDialogs(): FlowApi {
142
+ const api = useContext(FlowContext);
143
+ if (!api) {
144
+ throw new Error("useDialogs called outside DialogProvider");
145
+ }
146
+ return api;
147
+ }
148
+
149
+ export function ConfirmDelete() {
150
+ const dialogs = useDialogs();
151
+
152
+ // The button and the subsequent close can both complete this app action.
153
+ let reported = false;
154
+ const report = (params: CloseParams) => {
155
+ if (reported) {
156
+ return;
157
+ }
158
+ reported = true;
159
+ dialogs.close(params);
160
+ };
161
+
162
+ return (
163
+ <Show when={dialogs.current() === "confirm-delete"}>
164
+ <StandardDialog
165
+ initialFocusId="keep"
166
+ onButtonSelected={index =>
167
+ report({ reason: CloseReason.ButtonPressed, buttonId: index === 0 ? "keep" : "delete" })
168
+ }
169
+ onWasClosed={() => report({ reason: CloseReason.DialogCancelled })}
170
+ >
171
+ <stdDlgTitleArea primaryTitle="Delete recording?" />
172
+ <stdDlgButtonArea>
173
+ <stdDlgButton id="keep" text="Keep" />
174
+ <stdDlgButton text="Delete" />
175
+ </stdDlgButtonArea>
176
+ </StandardDialog>
177
+ </Show>
178
+ );
179
+ }
180
+ ```
181
+
182
+ The caller decides what a close means:
183
+
184
+ ```tsx
185
+ import { CloseReason, useDialogs } from "./dialog-flow";
186
+
187
+ const dialogs = useDialogs();
188
+
189
+ dialogs.open("confirm-delete", params => {
190
+ if (params.reason === CloseReason.ButtonPressed && params.buttonId === "delete") {
191
+ void removeRecording();
192
+ }
193
+ // DialogCancelled: the viewer left. Do nothing.
194
+ });
195
+ ```
196
+
197
+ ## Common Mistakes
198
+
199
+ | Symptom | Cause | Fix |
200
+ |---|---|---|
201
+ | Teardown runs twice and the second run undoes something | Both a button handler and its subsequent close complete the app action | Guard shared completion with a flag, as in the example |
202
+ | A flow commits an action the viewer never chose | "The dialog closed" was read as "the viewer confirmed" | Branch on `DialogCloseReason` — only `ButtonPressed` is a choice |
203
+ | A child dialog closes and the parent comes back empty | The parent was re-opened without its previous data | Pass `parentData` to `openChildDialog`; read the child's result out of `_childCloseParams` |
204
+ | A wait indicator closes slightly before or after the work finishes | A timer was sized to the expected duration | Drive the close from the signal that measures progress |
205
+ | A dialog is gone but its timer still fires | The interval outlived the component | `clearInterval` / `clearTimeout` in `onCleanup` |
206
+ | The scene shows nothing though the provider says a dialog is current | The dialog's factory was never registered, or was deregistered early | Register in `onMount`, deregister in `onCleanup`, and render `getDialogElement()` |
207
+ | A second dialog replaces the first instead of stacking on it | `setDialog` was used where a step was meant | `openChildDialog` pushes; `setDialog` replaces |
208
+
209
+ Props for the components themselves are typed with TSDoc in `@roku-sdk/components`. Read a
210
+ prop there rather than guessing it from here.
@@ -0,0 +1,183 @@
1
+ # Effects
2
+
3
+ ## Overview
4
+
5
+ `Effect` from `@roku-sdk/components` restyles a `Rectangle` or a `Poster` on the GPU: rounded
6
+ corners, a border, a linear or radial gradient. It draws nothing itself. You attach it through the
7
+ consumer's `effect` prop, which holds a **reference** to the node rather than a child, so one
8
+ effect can serve any number of consumers, and it restyles only its consumer — it clips nothing.
9
+ Effects need Roku OS 16.0 or later and the OpenGL backend; anywhere else the prop is ignored
10
+ without an error.
11
+
12
+ ## When to Use
13
+
14
+ - rounded corners, a border or a gradient on a panel, a card, a button surface or a poster
15
+ - the same look on several nodes, or a look that changes with focus
16
+ - a border or a gradient that animates
17
+ - corners that come out on one device and not another, or on some images and not others
18
+ - a gradient or a padding value that is set and changes nothing
19
+
20
+ ### When not to use
21
+
22
+ | Situation | Use instead | Because |
23
+ |---|---|---|
24
+ | Clipping or fading a group of children through an image | `MaskGroup` and its `maskUri`, in the `sdk-components` skill | An Effect restyles its consumer only; it never touches the subtree |
25
+ | `createEffect`, or any other reactive primitive | The `solid-on-roku` skill | Same word, unrelated API |
26
+ | A click or a chime | `SoundEffect` from `@roku-sdk/components`; no skill covers audio playback | It plays audio and draws nothing |
27
+ | The focus ring on a FlexGrid's focused cell | `createFocusHighlightPlugin`, in the `flex-grid` skill | The grid draws its own focus chrome; an Effect can style what that plugin's `render` returns |
28
+ | Rounded corners on Roku OS 15 or earlier, or in BrightScript | A 9-patch image on a `Poster` | The Effect node does not exist below OS 16 |
29
+
30
+ ## Quick Reference
31
+
32
+ | Job | Write |
33
+ |---|---|
34
+ | Rounded corners | `const fx = <effect borderRadius={24} />;` then `effect={fx}` |
35
+ | One radius per corner | `borderRadius={[topLeft, topRight, bottomRight, bottomLeft]}` |
36
+ | A border | `borderWidth` and `borderColor`; it is drawn **outside** the consumer's rect |
37
+ | A linear gradient | `gradientStyle="linear"`, `gradientColors` (two to eight), `gradientAngle` in **degrees** |
38
+ | A radial gradient | `gradientStyle="radial"`, `gradientCentre` and `gradientRadius`, both normalised |
39
+ | The same look on several nodes | Pass the one `const` to each consumer's `effect` |
40
+ | A look that follows focus | One hoisted effect whose fields read the signal, or two hoisted effects and `effect={focused() ? on : off}` |
41
+ | An animated field | An `id` on the effect, `{fx}` rendered in the tree, `fieldToInterp="<id>.<field>"` |
42
+ | A Poster | Add `shaderCompatible` |
43
+ | Whether this device draws effects | `getDeviceInfo()` from `@roku-sdk/rsg-ts/runtime` — step 5 |
44
+
45
+ Colours are RGBA hex strings such as `"0xFF0000FF"`. The samples use the lowercase `<effect>`
46
+ intrinsic, as the `sdk-components` skill recommends for new code; the capitalised `Effect`
47
+ component is a pass-through for it, and every rule here holds for both spellings unchanged.
48
+
49
+ ## Instructions
50
+
51
+ 1. **Hoist every effect into a `const` and pass the identifier.** A signal read by an effect's own
52
+ fields updates that node in place, hoisted or inline. What rebuilds the node is a signal read by
53
+ the `effect={…}` expression itself — a ternary or a function call there builds a new effect node
54
+ on every change and discards the old one, and nothing looks wrong on screen. Hoist each look and
55
+ choose between the constants, and the expression can only ever pick an existing node. Prefer the
56
+ `const` to `createMemo`, which reaches the same result with more machinery.
57
+
58
+ ```tsx rsg-skill=no-check
59
+ // Rebuilt on every focus change: the expression builds the node.
60
+ <rectangle effect={focused() ? <effect borderWidth={6} /> : <effect borderWidth={0} />} />
61
+ ```
62
+
63
+ 2. **Share by passing the same `const`.** The consumer never parents a node it references, so one
64
+ effect on a `Rectangle` and a `Poster` at once is well defined. No helper and no imperative
65
+ setter is involved.
66
+
67
+ 3. **To animate a field, give the effect an `id` and also render it into the tree.**
68
+ `fieldToInterp` resolves its target through `findNode()`, which walks the scene tree, and a node
69
+ held only by reference has no parent — the animation starts and silently moves nothing.
70
+ Rendering `{fx}` as a sibling costs nothing, since an Effect is not drawn. Keep it in the same
71
+ scope as the `Animation`. A tuple `borderRadius` cannot be interpolated; animate a single number.
72
+
73
+ ```tsx
74
+ import { Animation, FloatFieldInterpolator } from "@roku-sdk/components";
75
+
76
+ const pulse = <effect id="tile-pulse" borderRadius={24} borderWidth={2} borderColor="0x9B59B6FF" />;
77
+
78
+ <group>
79
+ {pulse}
80
+ <rectangle width={300} height={300} color="0x1A1A2EFF" effect={pulse} />
81
+ <Animation ref={animRef} duration={2} repeat={true} easeFunction="linear">
82
+ <FloatFieldInterpolator fieldToInterp="tile-pulse.borderWidth" key={[0, 0.5, 1]} keyValue={[2, 14, 2]} />
83
+ </Animation>
84
+ </group>;
85
+ ```
86
+
87
+ 4. **Set `shaderCompatible` on every `Poster` that takes an effect.** It asks for the bitmap to be
88
+ decoded into a format shaders can use. Without it the effect may still apply, depending on how
89
+ each source image decodes — which is how corners stay square on a few images and not the rest.
90
+ A `Rectangle` needs nothing.
91
+
92
+ 5. **Gate on the platform when the unstyled look is not acceptable.** Below Roku OS 16, or on a
93
+ backend other than OpenGL, the prop is ignored with no error and the node type does not even
94
+ exist. Check with `getDeviceInfo()` from `@roku-sdk/rsg-ts/runtime`: it can return `null`, and
95
+ the test is `Number(info.osVersion.major) >= 16 && info.graphicsPlatform === "opengl"`. Answer it
96
+ once per app and keep the result, as the example does: each call builds a new object, and a grid
97
+ of tiles would otherwise ask once per tile. Asking again never changes the answer — the SDK keeps
98
+ whatever the first call returned, `null` included. The firmware's own `supported`
99
+ field on the Effect node cannot be read from TypeScript — it is set once when the node is
100
+ created, before any handler can observe it — so do not reach for it. Nor can this check see
101
+ effects switched off in the device's configuration.
102
+
103
+ 6. **An Effect clips nothing.** A child of a rounded `Rectangle` can paint into and past the
104
+ corners. Lay children out inside the rounded area; to clip them to a shape, use `MaskGroup`.
105
+
106
+ 7. **Know the fields that fail silently on device.** A gradient draws nothing while
107
+ `gradientStyle` is `"none"`, its default, or with fewer than two or more than eight
108
+ `gradientColors`. `gradientStops` needs one entry per colour in `[0, 1]`. In `__DEV__` builds
109
+ the SDK warns once per distinct problem (and stays quiet while `gradientStyle` is `"none"`,
110
+ including empty colors/stops). `borderPadding` is the gap between the border and the content,
111
+ so with `borderWidth` at `0` it changes nothing — inset a child with layout instead.
112
+ `gradientFillContent` defaults to `true` and
113
+ `gradientFillBorder` to `false`.
114
+
115
+ 8. **`setPropMethod("effect")` is not how you share.** It builds a property descriptor for
116
+ `registerImperativeMethods`, which gives every node of a tag an imperative method that sets
117
+ `effect` — the same call the prop makes. Reach for it only when *which* node a consumer points at
118
+ changes outside render and a reactive expression would be worse, such as moving one focus effect
119
+ between the cells of a large grid from a key handler. Register once per tag, before the first
120
+ node of that tag is created.
121
+
122
+ ## Example
123
+
124
+ A tile whose frame follows focus, shared by its background and its artwork, with a plain outline
125
+ on devices that do not draw effects:
126
+
127
+ ```tsx rsg-skill=complete
128
+ import { getDeviceInfo } from "@roku-sdk/rsg-ts/runtime";
129
+ import { Show } from "@roku-sdk/solid";
130
+
131
+ let drawsEffects: boolean | undefined;
132
+
133
+ function deviceDrawsEffects(): boolean {
134
+ if (drawsEffects === undefined) {
135
+ const info = getDeviceInfo();
136
+ drawsEffects = info !== null && Number(info.osVersion.major) >= 16 && info.graphicsPlatform === "opengl";
137
+ }
138
+ return drawsEffects;
139
+ }
140
+
141
+ export function Tile(props: { posterUri: string; focused: boolean }) {
142
+ const effectsOn = deviceDrawsEffects();
143
+
144
+ const idle = <effect borderRadius={24} />;
145
+ const ring = <effect borderRadius={24} borderWidth={6} borderColor="0xFFFFFFFF" />;
146
+
147
+ return (
148
+ <>
149
+ <Show when={!effectsOn && props.focused}>
150
+ <rectangle width={312} height={182} translation={[-6, -6]} color="0xFFFFFFFF" />
151
+ </Show>
152
+ <rectangle width={300} height={170} color="0x1A1A2EFF" effect={props.focused ? ring : idle} />
153
+ <poster
154
+ width={300}
155
+ height={170}
156
+ loadDisplayMode="scaleToZoom"
157
+ shaderCompatible
158
+ effect={props.focused ? ring : idle}
159
+ uri={props.posterUri}
160
+ />
161
+ </>
162
+ );
163
+ }
164
+ ```
165
+
166
+ ## Common Mistakes
167
+
168
+ | Symptom | Cause | Fix |
169
+ |---|---|---|
170
+ | Moving focus or changing selection feels heavier, and nothing looks wrong | The `effect={…}` expression reads a signal and builds a new node on every change | Hoist each effect into a `const` and choose between the constants |
171
+ | An `Animation` targeting an effect field starts and moves nothing | `findNode()` cannot see a node held only by reference | Give the effect an `id` and render `{fx}` next to the `Animation` |
172
+ | Some Posters show the effect and a few stay square | Whether the bitmap decodes into a shader-usable format varies by image | Set `shaderCompatible` on every Poster that takes an effect |
173
+ | Corners, borders and gradients show on one device and not on another, with no error | That device runs Roku OS 15 or earlier, or not OpenGL | Gate on `getDeviceInfo()` and give those devices a fallback look |
174
+ | `gradientColors` is set and no gradient draws | `gradientStyle` is still `"none"`, or there are fewer than two colours | Set `gradientStyle` to `"linear"` or `"radial"` |
175
+ | A child paints over the rounded corners | An Effect restyles its consumer and never clips | Keep the child inside the rounded area, or clip it with `MaskGroup` |
176
+ | `borderPadding` has no visible effect | It only spaces a border from the content, and `borderWidth` is `0` | Inset the child with layout |
177
+ | A tuple `borderRadius` will not animate | Per-corner radii are one Vector4 field, not a float | Animate a single-number `borderRadius` |
178
+
179
+ ## Source of truth
180
+
181
+ Prefer the typed source over any prose, including this file: read
182
+ `node_modules/@roku-sdk/components/src/sg-nodes/effect.props.ts` for every field, its type and its
183
+ default. The same wiring runs in the **Effect** entry of the SDK's showcase example.