@roku-sdk/ai 0.0.0-stage → 2.18.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.
- package/LICENSE +21 -0
- package/README.md +88 -2
- package/package.json +47 -3
- package/skills/ads-and-raf/SKILL.md +231 -0
- package/skills/ads-and-raf/references/integration-flows.md +332 -0
- package/skills/app-packaging/SKILL.md +107 -0
- package/skills/app-performance/SKILL.md +133 -0
- package/skills/cli-dev-loop/SKILL.md +217 -0
- package/skills/cli-dev-loop/references/logs-and-crashes.md +114 -0
- package/skills/cpu-and-heap-profiling/SKILL.md +108 -0
- package/skills/cpu-and-heap-profiling/references/reading-profiles.md +52 -0
- package/skills/data-entry-keyboards/SKILL.md +160 -0
- package/skills/device-management/SKILL.md +158 -0
- package/skills/dialogs-and-modals/SKILL.md +210 -0
- package/skills/effects/SKILL.md +181 -0
- package/skills/fetch-and-data-loading/SKILL.md +164 -0
- package/skills/fetch-and-data-loading/references/https-and-certificates.md +79 -0
- package/skills/flex-grid/SKILL.md +236 -0
- package/skills/flex-grid/references/focus-integration.md +355 -0
- package/skills/flex-grid/references/plugins.md +201 -0
- package/skills/focus-management/SKILL.md +205 -0
- package/skills/focus-management/references/focus-recipes.md +282 -0
- package/skills/fonts-and-glyphs/SKILL.md +176 -0
- package/skills/fonts-and-glyphs/references/glyph-tables.md +128 -0
- package/skills/logging-and-errors/SKILL.md +227 -0
- package/skills/logging-and-errors/references/uncaught-exceptions.md +137 -0
- package/skills/mcp-ai-dev-loop/SKILL.md +189 -0
- package/skills/mcp-ai-dev-loop/references/crash-and-console.md +68 -0
- package/skills/mcp-ai-dev-loop/references/live-debugging.md +129 -0
- package/skills/mcp-setup/SKILL.md +131 -0
- package/skills/project-setup/SKILL.md +210 -0
- package/skills/project-setup/references/roku-config.md +120 -0
- package/skills/screen-navigation/SKILL.md +166 -0
- package/skills/sdk-components/SKILL.md +213 -0
- package/skills/sdk-components/references/dialogs.md +148 -0
- package/skills/sdk-components/references/scenegraph-node-spelling.md +123 -0
- package/skills/sdk-installation/SKILL.md +129 -0
- package/skills/slideshow/SKILL.md +157 -0
- package/skills/slideshow/references/focus-and-handoff.md +187 -0
- package/skills/solid-on-roku/SKILL.md +181 -0
- package/skills/solid-on-roku/references/hermes-and-exports.md +130 -0
- package/skills/testing/SKILL.md +147 -0
- package/skills/testing/references/async-and-timing.md +97 -0
- package/skills/user-accounts-and-purchases/SKILL.md +164 -0
- package/skills-manifest.json +316 -0
- package/skills-manifest.schema.json +62 -0
- package/src/__tests__/manifest.test.ts +267 -0
- package/src/__tests__/snippets.test.ts +221 -0
- package/src/__tests__/trigger-routing.test.ts +77 -0
- package/src/index.ts +36 -0
- package/src/manifest.ts +215 -0
- package/src/router.ts +147 -0
- 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,181 @@
|
|
|
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.** A gradient draws nothing while `gradientStyle` is
|
|
107
|
+
`"none"`, its default, or with fewer than two or more than eight `gradientColors`.
|
|
108
|
+
`gradientStops` needs one entry per colour, and a mismatch is not reported. `borderPadding` is
|
|
109
|
+
the gap between the border and the content, so with `borderWidth` at `0` it changes nothing —
|
|
110
|
+
inset a child with layout instead. `gradientFillContent` defaults to `true` and
|
|
111
|
+
`gradientFillBorder` to `false`.
|
|
112
|
+
|
|
113
|
+
8. **`setPropMethod("effect")` is not how you share.** It builds a property descriptor for
|
|
114
|
+
`registerImperativeMethods`, which gives every node of a tag an imperative method that sets
|
|
115
|
+
`effect` — the same call the prop makes. Reach for it only when *which* node a consumer points at
|
|
116
|
+
changes outside render and a reactive expression would be worse, such as moving one focus effect
|
|
117
|
+
between the cells of a large grid from a key handler. Register once per tag, before the first
|
|
118
|
+
node of that tag is created.
|
|
119
|
+
|
|
120
|
+
## Example
|
|
121
|
+
|
|
122
|
+
A tile whose frame follows focus, shared by its background and its artwork, with a plain outline
|
|
123
|
+
on devices that do not draw effects:
|
|
124
|
+
|
|
125
|
+
```tsx rsg-skill=complete
|
|
126
|
+
import { getDeviceInfo } from "@roku-sdk/rsg-ts/runtime";
|
|
127
|
+
import { Show } from "@roku-sdk/solid";
|
|
128
|
+
|
|
129
|
+
let drawsEffects: boolean | undefined;
|
|
130
|
+
|
|
131
|
+
function deviceDrawsEffects(): boolean {
|
|
132
|
+
if (drawsEffects === undefined) {
|
|
133
|
+
const info = getDeviceInfo();
|
|
134
|
+
drawsEffects = info !== null && Number(info.osVersion.major) >= 16 && info.graphicsPlatform === "opengl";
|
|
135
|
+
}
|
|
136
|
+
return drawsEffects;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export function Tile(props: { posterUri: string; focused: boolean }) {
|
|
140
|
+
const effectsOn = deviceDrawsEffects();
|
|
141
|
+
|
|
142
|
+
const idle = <effect borderRadius={24} />;
|
|
143
|
+
const ring = <effect borderRadius={24} borderWidth={6} borderColor="0xFFFFFFFF" />;
|
|
144
|
+
|
|
145
|
+
return (
|
|
146
|
+
<>
|
|
147
|
+
<Show when={!effectsOn && props.focused}>
|
|
148
|
+
<rectangle width={312} height={182} translation={[-6, -6]} color="0xFFFFFFFF" />
|
|
149
|
+
</Show>
|
|
150
|
+
<rectangle width={300} height={170} color="0x1A1A2EFF" effect={props.focused ? ring : idle} />
|
|
151
|
+
<poster
|
|
152
|
+
width={300}
|
|
153
|
+
height={170}
|
|
154
|
+
loadDisplayMode="scaleToZoom"
|
|
155
|
+
shaderCompatible
|
|
156
|
+
effect={props.focused ? ring : idle}
|
|
157
|
+
uri={props.posterUri}
|
|
158
|
+
/>
|
|
159
|
+
</>
|
|
160
|
+
);
|
|
161
|
+
}
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
## Common Mistakes
|
|
165
|
+
|
|
166
|
+
| Symptom | Cause | Fix |
|
|
167
|
+
|---|---|---|
|
|
168
|
+
| 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 |
|
|
169
|
+
| 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` |
|
|
170
|
+
| 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 |
|
|
171
|
+
| 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 |
|
|
172
|
+
| `gradientColors` is set and no gradient draws | `gradientStyle` is still `"none"`, or there are fewer than two colours | Set `gradientStyle` to `"linear"` or `"radial"` |
|
|
173
|
+
| 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` |
|
|
174
|
+
| `borderPadding` has no visible effect | It only spaces a border from the content, and `borderWidth` is `0` | Inset the child with layout |
|
|
175
|
+
| A tuple `borderRadius` will not animate | Per-corner radii are one Vector4 field, not a float | Animate a single-number `borderRadius` |
|
|
176
|
+
|
|
177
|
+
## Source of truth
|
|
178
|
+
|
|
179
|
+
Prefer the typed source over any prose, including this file: read
|
|
180
|
+
`node_modules/@roku-sdk/components/src/sg-nodes/effect.props.ts` for every field, its type and its
|
|
181
|
+
default. The same wiring runs in the **Effect** entry of the SDK's showcase example.
|