@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,164 @@
1
+ # Loading data over the network
2
+
3
+ ## Overview
4
+
5
+ Requests in an RSG app go through a `fetch` the firmware provides. It looks like the web's and
6
+ differs in three ways that all fail quietly: it takes **one argument**, an error status
7
+ **resolves** rather than rejecting, and the types your editor shows you are the browser's
8
+ rather than this platform's.
9
+
10
+ That last one is the root of the other two showing up late. `fetch` is declared in no ambient
11
+ file the compiler reads, so TypeScript falls through to its default DOM library — which means
12
+ the unsupported two-argument call **compiles cleanly and fails at runtime**.
13
+
14
+ **Reaching a host behind your own certificate authority is in
15
+ `references/https-and-certificates.md`.** Read it when a request works against production and
16
+ fails against staging. The tables below name the option; that file has where the bundle lives,
17
+ how to make it the default for every request, and why a trust failure surfaces in a different
18
+ place from an HTTP error.
19
+
20
+ ## When to Use
21
+
22
+ | If the request is | Then |
23
+ |---|---|
24
+ | Calling an HTTP API from an RSG app | Here |
25
+ | A POST or PUT with headers and a body | Here — and not the way the web writes it |
26
+ | An error status that never reached your error handling | Here — `fetch` resolved |
27
+ | Which type describes a response, when the editor says `Response` | Here |
28
+ | A host whose certificate this box does not trust | `references/https-and-certificates.md` |
29
+ | Whether aborting or streaming is available on this firmware | Here — ask at runtime |
30
+
31
+ ### When not to use
32
+
33
+ | Situation | Use instead | Because |
34
+ |---|---|---|
35
+ | Filling a virtualized shelf's rows as the viewer approaches them | The `flex-grid` skill | That grid owns *when* a row is fetched. This skill owns the call it makes |
36
+ | Feeding a SceneGraph-native list its items, or telling which components take data that way | The `sdk-components` skill | Recognising a `content`-fed node is a component question, not a transport one |
37
+ | Which packages the app must declare, or where a file has to live to be imported | The `project-setup` skill | The scaffold already covers what this skill needs |
38
+ | Reading what Roku itself holds about the viewer, or selling something in-channel | The `user-accounts-and-purchases` skill | That goes through a component and a command, not through HTTP |
39
+ | Getting logs or a crash off a device | The `cli-dev-loop` or `mcp-ai-dev-loop` skill | Those are about observing the app, not about what it requests |
40
+
41
+ ## Quick Reference
42
+
43
+ | Want | Do |
44
+ |---|---|
45
+ | A GET | `fetch(url)` — one argument |
46
+ | Anything with a method, headers or a body | Build a `Request` and pass that: `fetch(new Request(url, init))` |
47
+ | The parsed body | `await response.json()`, then assert your own shape |
48
+ | Raw text | `await response.text()` |
49
+ | To know an error status happened | Check `response.ok`. **The promise already resolved** |
50
+ | A custom CA | `certificatesFile` on an `HttpsAgent` — see the reference |
51
+ | To know if abort or streaming works here | `fetch.isFeatureSupported(...)` with a `FetchFeature` member |
52
+
53
+ Where things come from:
54
+
55
+ | | |
56
+ |---|---|
57
+ | `fetch` | A firmware global. Imported from nothing, declared in no ambient file — which is why the editor shows the DOM's |
58
+ | `Request`, `Headers`, `HttpsAgent` | Globals too, but **installed by `@roku-sdk/native`**, which the scaffold already depends on |
59
+ | `ResponseInstance`, `RequestInit`, `HttpsAgentOptions`, `FetchFeature` | Types, from `@roku-sdk/interfaces` |
60
+
61
+ ## Instructions
62
+
63
+ 1. **Pass one argument.** `fetch(url)` or `fetch(request)`. The two-argument form is the web's,
64
+ it typechecks here because the compiler is reading the DOM library, and the second argument
65
+ is ignored at runtime — so a POST silently becomes a GET.
66
+
67
+ 2. **Put method, headers and body on a `Request`.** That is where `RequestInit` belongs, and
68
+ `body` is typed **`string` only** — no form data, no object, no stream. Serialize first.
69
+
70
+ 3. **Check `ok` before reading the body.** A 4xx or 5xx is a *resolved* response, so error
71
+ handling written as a `catch` never sees it. Only a transport failure rejects.
72
+
73
+ 4. **Do not expect the response type your editor shows.** What a call really resolves to is
74
+ `ResponseInstance` from `@roku-sdk/interfaces`. Against the DOM types the compiler applies,
75
+ `await response.json()` comes back loose, so assert your own interface on it rather than
76
+ reaching for a generic that will not compile.
77
+
78
+ 5. **Ask before relying on an optional capability.** `fetch.isFeatureSupported(...)` answers for
79
+ `FetchFeature.STREAMING`, `ABORT_CONTROLLER` and `REQUEST_INIT`. A `false` is a branch to
80
+ write, not a failure to report.
81
+
82
+ 6. **Load from `onMount`, and let a signal carry the state.** Nothing about the request is
83
+ reactive on its own, so the pattern is: fire it on mount, write the result into a signal, and
84
+ render off that signal's three states — not yet, failed, arrived.
85
+
86
+ 7. **Do not switch a base URL on a build variable.** Those are compiled away, and a request to
87
+ the wrong environment is the one bug that looks like a server problem. Read the environment
88
+ from configuration the app can actually see at runtime.
89
+
90
+ ## Example
91
+
92
+ A GET whose result drives the view, a POST built the way this runtime wants, and the `ok` check
93
+ in the place it belongs.
94
+
95
+ ```tsx rsg-skill=complete
96
+ import { Component, createSignal, onMount } from "@roku-sdk/solid";
97
+
98
+ interface Title {
99
+ id: string;
100
+ name: string;
101
+ }
102
+
103
+ export const Shelf: Component<{ endpoint: string }> = props => {
104
+ const [titles, setTitles] = createSignal<Title[] | undefined>(undefined);
105
+ const [failed, setFailed] = createSignal(false);
106
+
107
+ const load = async () => {
108
+ try {
109
+ // One argument. A second one would compile and be ignored.
110
+ const response = await fetch(`${props.endpoint}/titles`);
111
+
112
+ // Already resolved, even on 404 — so this check is the error handling.
113
+ if (!response.ok) {
114
+ setFailed(true);
115
+ return;
116
+ }
117
+
118
+ // The compiler is using the DOM's Response here, so assert the shape.
119
+ const body = (await response.json()) as { titles: Title[] };
120
+ setTitles(body.titles);
121
+ } catch {
122
+ // Only a transport or trust failure lands here.
123
+ setFailed(true);
124
+ }
125
+ };
126
+
127
+ onMount(() => void load());
128
+
129
+ return (
130
+ <group>
131
+ <label text={failed() ? "could not load" : titles() === undefined ? "loading" : ""} />
132
+ <label text={(titles() ?? []).map(t => t.name).join(", ")} translation={[0, 48]} />
133
+ </group>
134
+ );
135
+ };
136
+ ```
137
+
138
+ A POST carries its method, headers and body on a `Request`, and the body is a string:
139
+
140
+ ```tsx
141
+ const request = new Request(`${endpoint}/progress`, {
142
+ method: "POST",
143
+ headers: new Headers({ "Content-Type": "application/json" }),
144
+ body: JSON.stringify({ titleId, seconds })
145
+ });
146
+
147
+ const response = await fetch(request);
148
+ ```
149
+
150
+ ## Common Mistakes
151
+
152
+ | Symptom | Cause | Fix |
153
+ |---|---|---|
154
+ | A POST arrives at the server as a GET with no body | The web's two-argument `fetch` — it compiled, and the second argument was ignored | Build a `Request` and pass that as the single argument |
155
+ | A 404 or 500 never reaches the error path | The promise resolved; only a transport failure rejects | Check `response.ok` before reading the body |
156
+ | A generic on the body parse will not compile | That generic is on this platform's response type, and the compiler is applying the DOM's | `await response.json()` and assert your own interface |
157
+ | Sending an object or form data as `body` fails | `body` is typed `string` | Serialize it yourself |
158
+ | `Request` resolves in the editor with no import, and it feels like a mistake | It is a global that `@roku-sdk/native` installs | Nothing to import for the value; take the *types* from `@roku-sdk/interfaces` |
159
+ | Aborting silently does nothing on an older box | Abort support is optional on this platform | Gate it on `fetch.isFeatureSupported(FetchFeature.ABORT_CONTROLLER)` |
160
+ | Requests work in production and fail against staging | The staging certificate is not in the platform trust store | `references/https-and-certificates.md` |
161
+ | The app talks to the wrong environment in a release build | The base URL was chosen from a build variable, which the build removed | Read it from runtime configuration |
162
+
163
+ Request and response types are documented with TSDoc in `@roku-sdk/interfaces`. Read a field's
164
+ exact shape there rather than assuming the browser's.
@@ -0,0 +1,79 @@
1
+ # Reaching a host behind your own certificate authority
2
+
3
+ The body says *which* option points a request at a CA bundle. This file is the shape of it:
4
+ where the bundle has to live, how to make it the default for every request instead of one, and
5
+ what the failure looks like when it is missing.
6
+
7
+ ## The two scopes
8
+
9
+ `HttpsAgent` is a constructor, and the agent it produces can be attached in two ways.
10
+
11
+ **Per request.** Build the agent, put it on the request's `agent` field, and only that request
12
+ uses it. Right for a single staging host while everything else keeps the platform trust store.
13
+
14
+ **For the whole app.** `HttpsAgent.setGlobalAgent(agent)` installs it as the default, so every
15
+ later call uses it without naming it. Right when every host the app talks to sits behind the
16
+ same internal authority — which is the case that otherwise ends up copy-pasting an `agent`
17
+ onto two dozen call sites and missing one.
18
+
19
+ `setGlobalAgent` is a static on the constructor, not a method on an instance, and it takes the
20
+ instance you built:
21
+
22
+ ```tsx
23
+ const agent = new HttpsAgent({ certificatesFile: "pkg:/certs/internal-ca.crt" });
24
+ HttpsAgent.setGlobalAgent(agent);
25
+ ```
26
+
27
+ Install it once, before anything fetches — `main()` or the app's bootstrap, not inside a
28
+ component that may mount twice.
29
+
30
+ ## Where the bundle lives
31
+
32
+ `certificatesFile` is a URI the firmware reads, not a path the bundler resolves, so it is a
33
+ `pkg:/` location and the file has to be inside the package. That makes it one of the few places
34
+ in an RSG app where a hardcoded `pkg:/` URI is correct: the image-import rule exists because
35
+ the build pipeline rewrites image paths, and it does not process a certificate bundle.
36
+
37
+ Keep it under the app's `pkg/` directory so it ships, and remember that anything there is
38
+ readable by anyone who unpacks the app — a CA bundle is public by nature, but a client
39
+ certificate with a private key is not, and does not belong in a package.
40
+
41
+ ## Which type describes the options
42
+
43
+ `HttpsAgentOptions` is `{ certificatesFile?: string }` and is exported from
44
+ `@roku-sdk/interfaces`. Note that it is the *only* field: there is no verification-disabling
45
+ switch here, and no per-host override. If a host fails to verify, the answer is the right
46
+ bundle, not a way around the check.
47
+
48
+ ## What the failure looks like
49
+
50
+ A missing or wrong bundle does not produce a distinct error class. The call rejects the way a
51
+ transport failure rejects, because from the runtime's point of view the connection never
52
+ completed — so it reaches a `catch`, not the `ok` check the body describes for HTTP statuses.
53
+
54
+ That distinction is worth holding onto, because the two failures need opposite fixes:
55
+
56
+ | What happened | Where it surfaces | Fix |
57
+ |---|---|---|
58
+ | The certificate could not be verified | The promise rejects, so a `catch` | Attach the CA bundle |
59
+ | The server answered with 4xx or 5xx | The promise **resolves**; only `ok` is false | Read the status and handle it |
60
+
61
+ So a request that works against production and rejects against staging is usually a trust
62
+ problem rather than a server problem, and a request that "fails" with a body you can read is
63
+ the opposite.
64
+
65
+ ## Checking the capability first
66
+
67
+ Aborting a request mid-flight is optional on this platform. Ask before relying on it rather
68
+ than discovering it on an older box:
69
+
70
+ ```tsx
71
+ import { FetchFeature } from "@roku-sdk/interfaces";
72
+
73
+ if (fetch.isFeatureSupported(FetchFeature.ABORT_CONTROLLER)) {
74
+ // safe to wire an AbortController to this request
75
+ }
76
+ ```
77
+
78
+ The same question answers for `FetchFeature.STREAMING` and `FetchFeature.REQUEST_INIT`. A
79
+ `false` here is not an error to report — it is a branch to write.
@@ -0,0 +1,236 @@
1
+ # FlexGrid
2
+
3
+ ## Overview
4
+
5
+ `@roku-sdk/flex-grid` renders the shelves and poster grids a Roku app is mostly made of, at a
6
+ size a device can hold: cells come from a recycled pool, only the rows near the viewport exist,
7
+ and rows may be loaded page by page while the user is already moving. You describe the geometry
8
+ and the cell component; the grid owns scrolling, focus movement inside itself, and the focus
9
+ highlight.
10
+
11
+ Every sample here is written against the **3.x line**. 2.x is maintained in parallel and differs in
12
+ the plugin surface and in how a cell takes focus, so read the installed package's major before
13
+ copying one — `BREAKING_CHANGES.md` in the install carries the 2.x-to-3.x table.
14
+
15
+ ## Read the reference first for these tasks
16
+
17
+ | If the request is | Read now, before answering |
18
+ |---|---|
19
+ | A grid inside another grid's cell, or a grid handing focus to a sibling region at its edge | `references/focus-integration.md` |
20
+ | The focus highlight, zoom, row titles, or any other plugin | `references/plugins.md` |
21
+
22
+ The tables below name the callback or the factory and stop there: the arguments, the return
23
+ contract that decides whether a key bubbles, and the subpath each plugin is imported from are
24
+ only in the references. Answer these two from the reference and quote it — an answer assembled
25
+ from a table row here is an invented API, and this package has several names that look right
26
+ and do not exist.
27
+
28
+ ## When to Use
29
+
30
+ - a row of cards, a shelf-of-shelves screen, or a poster grid that scrolls
31
+ - rows whose data arrives after the screen is up, page by page
32
+ - per-row geometry: different cell widths, heights or column counts per row
33
+ - a grid that has to give focus back to a nav bar or menu at its edge
34
+ - a second grid inside a cell of the first one
35
+ - cells that must not all exist at once, because there are hundreds of them
36
+
37
+ ### When not to use
38
+
39
+ | Situation | Use instead | Because |
40
+ |---|---|---|
41
+ | A menu, settings list or short row of buttons | `FocusBoundary` + `useFocusable` from `@roku-sdk/navigation` | Nothing to virtualize, and the grid's geometry config costs more than the screen needs |
42
+ | A full-screen image carousel | `@roku-sdk/slideshow` | It owns the transition and the one-item-at-a-time model the grid has no notion of |
43
+ | The network call that fills a row — its method, headers, error status or a host this box does not trust | The `fetch-and-data-loading` skill | This skill owns *when* a row is fetched, which is the focus-driven half. What the request itself looks like is not a grid concern |
44
+ | A `RowList`, `MarkupGrid` or another list that takes `itemComponentName` | The `sdk-components` skill names them | Those are SceneGraph-native lists exported by `@roku-sdk/components`. They render each item from compiled BrightScript XML, so a TypeScript component cannot be their item — which is the reason this renderer exists rather than wrapping them |
45
+
46
+ ## Quick Reference
47
+
48
+ | Operation | API |
49
+ |---|---|
50
+ | Declare the packages (the scaffold ships neither) | `@roku-sdk/flex-grid`, plus `@roku-sdk/navigation` for a top-level grid, in the app's `dependencies` |
51
+ | A grid in JSX | `defineFlexGrid<T>()` at module scope, rendered with the config as props |
52
+ | A grid built imperatively | `createFlexGrid<T>(config)`, then render `handle.element` |
53
+ | Reach the live grid from app code | `gridHandleRef={setGrid}` in JSX, or the `createFlexGrid` return value |
54
+ | Required config | `numRows`, `numCols`, `cellSize`, `focus` (both axes), `getCellComponent` |
55
+ | Where a row moves on screen | `focus.horizontal` / `focus.vertical`: `fixedFocusStrategy`, `floatingFocusStrategy`, `floatingFocusYStrategy`, `fixedFocusWrapStrategy` |
56
+ | Feed rows as they load | `rows` (accessor) in JSX, or `handle.updateRows(startRow, rows)`; `undefined` is a row not loaded yet |
57
+ | React to focus moving | `onFocusChanged(row, col, item)` — the prefetch hook |
58
+ | Act on OK | `onItemSelected(row, col, item)`, returning `true` to consume it |
59
+ | Focus this grid as a region | `focusId` — an identity the enclosing boundary's `initialFocus` or `setFocus` names |
60
+ | Hand focus on at an edge | `onEdgeReached(edge)` — `edge` is `"top"`, `"bottom"`, `"left"` or `"right"`, and it returns nothing → `references/focus-integration.md` |
61
+ | Send a key into a nested grid | `handle.forwardKeyEvent` → `references/focus-integration.md` |
62
+ | Clip to a viewport | `enableClipping: true` with `width` and `height`, which the type then requires |
63
+ | Clip one row's cards to a rect | `createRowCardsClipPlugin` from `@roku-sdk/flex-grid/row-clip` — in row coordinates and fixed within the row, so cards scroll through it; row chrome stays outside |
64
+ | One backdrop behind several rows | `createRowBackdropPlugin` → `references/plugins.md` |
65
+ | Jump somewhere | `jumpTo: [row, col]`, or `handle.jumpTo(row, col, animate)` for an unconditional repeat |
66
+ | A row refilled with unrelated items starts at its first item | `handle.resetRowPosition(row)` beside the update — `updateRows` keeps the row's saved column and scroll |
67
+
68
+ ## Instructions
69
+
70
+ 1. **Declare the dependencies first.** `rk create` scaffolds neither `@roku-sdk/flex-grid` nor
71
+ `@roku-sdk/navigation` — which a top-level grid also needs, for the `RootFocusBoundary` it
72
+ registers under — and pnpm does not hoist, so until both are in the app's `dependencies`
73
+ the imports do not resolve and nothing below compiles. Give them whatever range the app's
74
+ other `@roku-sdk/*` packages already carry — the published version for an app installing
75
+ from the registry, `workspace:*` only inside the SDK's own pnpm workspace, where that
76
+ protocol resolves. Then run `rk build` once: until a build has generated this package's XML
77
+ type declarations, bare `tsc` reports errors inside the SDK's own files, so the build is the
78
+ type gate, not `type-check`.
79
+
80
+ 2. **Create the grid inside the root `FocusBoundary`'s owner.** A top-level grid *is* a nested
81
+ boundary: it reads the boundary context when its JSX is created. Built outside — in the
82
+ anchor node's body, or in a module-level factory — it becomes a second root, registers with
83
+ nobody, and every key is dropped while the screen looks correct. A scaffolded app has no
84
+ boundary at all: its anchor node still `extends: "Group"`, so wire that first — skill
85
+ `roku-sdk-focus-management`, Instruction 1 — and render the grid under it.
86
+
87
+ 3. **Give both axes a focus strategy.** `focus` is required and has no default. `fixed` keeps
88
+ the focused cell at one position and scrolls the content past it; `floating` moves the
89
+ highlight until it reaches the offset and scrolls after that.
90
+
91
+ 4. **Write the cell as a component over `CellRenderProps<T>`.** Read `focused()` inside JSX,
92
+ size from `width` / `height`, and treat `item` as possibly `undefined` — that is a row whose
93
+ data has not arrived, not an error.
94
+
95
+ 5. **Return the cell component from `getCellComponent` by identity, not by a fresh closure.**
96
+ Structural props are a remount dependency: a new function on every parent render rebuilds
97
+ the grid, which drops focus and re-warms the pool.
98
+
99
+ 6. **Load rows behind `onFocusChanged`.** Set `numRows` to what the dataset will hold, pass
100
+ `undefined` for rows you have not fetched, and fill them in as focus approaches.
101
+
102
+ 7. **Pass `focusId` only for a top-level grid.** Omitting it is what selects nested mode, where
103
+ the parent cell forwards keys instead — see the reference before writing either.
104
+
105
+ ## Example
106
+
107
+ A top-level shelf grid: pooled cells, rows filled in as focus moves, a viewport to scroll
108
+ inside, and the grid registered as a focus region under the screen's boundary.
109
+
110
+ ```tsx rsg-skill=complete
111
+ import { Group, Label, Rectangle } from "@roku-sdk/components";
112
+ import type { CellRenderProps } from "@roku-sdk/flex-grid";
113
+ import { defineFlexGrid, fixedFocusStrategy, floatingFocusYStrategy } from "@roku-sdk/flex-grid";
114
+ import { type Component, createSignal } from "@roku-sdk/solid";
115
+
116
+ interface Card {
117
+ title: string;
118
+ }
119
+
120
+ const CELL_SIZE: [number, number] = [280, 158];
121
+ const TOTAL_ROWS = 40;
122
+ const COLUMNS = 6;
123
+
124
+ // Module scope: the component identity has to be stable, because the grid
125
+ // remounts when a structural prop changes identity.
126
+ const Shelf = defineFlexGrid<Card>();
127
+
128
+ const CardCell: Component<CellRenderProps<Card>> = props => (
129
+ <group>
130
+ <rectangle
131
+ width={props.width}
132
+ height={props.height}
133
+ color={props.focused() ? "0x0055FFFF" : "0x1A1A1AFF"}
134
+ />
135
+ <label text={props.item?.title ?? "…"} translation={[16, 120]} />
136
+ </group>
137
+ );
138
+
139
+ function cardCellComponent(): Component<CellRenderProps<Card>> {
140
+ return CardCell;
141
+ }
142
+
143
+ async function fetchRow(row: number): Promise<Card[]> {
144
+ // The app's own paging call.
145
+ return [{ title: `Row ${row}` }];
146
+ }
147
+
148
+ /**
149
+ * Rendered inside the screen's `RootFocusBoundary`, whose `initialFocus` names
150
+ * the same `focusId` this receives.
151
+ */
152
+ export const PosterShelves: Component<{ focusId: object }> = props => {
153
+ const [rows, setRows] = createSignal<(Card[] | undefined)[]>(Array.from({ length: TOTAL_ROWS }));
154
+
155
+ const loadRow = (row: number): void => {
156
+ if (rows()[row] !== undefined) return;
157
+ void fetchRow(row).then(items =>
158
+ setRows(previous => {
159
+ const next = [...previous];
160
+ next[row] = items;
161
+ return next;
162
+ })
163
+ );
164
+ };
165
+
166
+ return (
167
+ <Shelf
168
+ focusId={props.focusId}
169
+ numRows={TOTAL_ROWS}
170
+ numCols={COLUMNS}
171
+ cellSize={CELL_SIZE}
172
+ cellSpacing={[24, 32]}
173
+ translation={[80, 240]}
174
+ // Without a viewport the scrolled rows paint over whatever is above
175
+ // the grid; `enableClipping` is the union arm that requires both sizes.
176
+ enableClipping={true}
177
+ width={1760}
178
+ height={840}
179
+ focus={{
180
+ horizontal: { strategy: fixedFocusStrategy, options: { focusOffset: 80 } },
181
+ vertical: { strategy: floatingFocusYStrategy, options: { focusOffset: 0 } }
182
+ }}
183
+ getCellComponent={cardCellComponent}
184
+ rows={rows}
185
+ onFocusChanged={row => loadRow(row)}
186
+ onItemSelected={(row, col, item) => {
187
+ if (item === undefined) return false;
188
+ openTitle(item);
189
+ return true;
190
+ }}
191
+ />
192
+ );
193
+ };
194
+
195
+ function openTitle(card: Card): void {
196
+ // app logic for the selected card
197
+ }
198
+ ```
199
+
200
+ ## Common Mistakes
201
+
202
+ | Symptom | Cause | Fix |
203
+ |---|---|---|
204
+ | The grid draws but no key reaches it, and the root boundary's `initialFocus` never resolves | The grid was created outside the root boundary's owner, so it became a second root | Create it in a component rendered inside the boundary |
205
+ | `Module '"@roku-sdk/flex-grid"' has no exported member 'Position'` (or `'PositionValue'`) | Both names arrived in 2.15.0; an older install types the edge argument as the bare literal union and exports neither | Compare `edge` against `"top"`, `"bottom"`, `"left"` and `"right"`. The enum's members hold those same strings, so the literal form compiles and matches on every version |
206
+ | `Property 'setFocusBoundary' does not exist on type 'CellRenderProps<T>'`, or `declineUnhandledNavigation` rejected in the config | Both are 3.x additions later than 3.0.0, and an install predating them has neither | Give a composite cell its keys through `setKeyDelegate`, which every version has, and expect a nested grid there to claim movement keys rather than decline them → `references/focus-integration.md` |
207
+ | Returning `true` from the edge callback changes nothing | `onEdgeReached` returns `void`: it reports, it does not consume | Move focus in the handler and expect no consume semantics |
208
+ | `Property 'focusRef' does not exist` | The prop is `focusId`; `focusRef` appears in older prose and never existed | Use `focusId` |
209
+ | The highlight config's fields are rejected | Two types share the name `FocusHighlightConfig`, and the main barrel's one is dead | Import it from `@roku-sdk/flex-grid/focus-highlight` |
210
+ | Scrolled rows paint over the header, the nav bar, or the screen's edge | The grid has no viewport until one is asked for, so its rows draw outside its box | `enableClipping: true` with `width` and `height`; nothing but a screenshot catches this — the build and the tests both pass |
211
+ | `width`/`height` suddenly required | `enableClipping: true` is a discriminated union arm that needs a viewport | Pass both, or drop the clipping |
212
+ | A translated grid renders blank: the cells exist and hold their data, but nothing is inside the clip rect | `height` is the size of the grid's box, measured from its own origin, not a bottom edge on the screen. Versions before this fix read it both ways and gave a grid offset further down the screen than it was tall a negative viewport, scrolling every row above its own rect | Size the band itself and let `translation` place it: a 200-tall row at y=220 is `translation: [70, 220], height: 200`. Omitting `height` runs the grid to the bottom of the screen. A custom vertical strategy reading `viewportHeight` as a bottom edge has the same bug — the room below the focus line is `viewportTop + viewportHeight - focusOffset` |
213
+ | A card shows where something opaque is drawn over the row — in the gap beside a panel, or past the end of the row | Nothing clips the row there. Hiding the card is the usual stand-in and it is not the same thing: it removes the card mid-slide, so the slot it was heading for fills by appearing instead of gliding in | `createRowCardsClipPlugin({ clip: row => [x, y, w, h] })` from `@roku-sdk/flex-grid/row-clip`, answering `undefined` for every other row. The rect is in the row's own coordinates and does not move with the row's horizontal scroll, so cards are cut at its edges and their motion is untouched — run it well past every edge nothing covers, or a focus ring or zoomed card is cut there too; row chrome stays outside, so a title beyond the rect still draws. Where the rect goes depends on where that row puts its cards in each of its states, which is worth measuring rather than assuming |
214
+ | A clipped row renders blank, or loses its first cards | The rect is returned in a state where the opaque thing it hides behind is not drawn — most often the row is no longer focused, which in the same stroke usually undoes whatever spread the cards out. The cut then has nothing over it | Gate `clip` on exactly what draws the covering element, and answer `undefined` in every other state. Clipping a row unconditionally looks right until that row is not the focused one |
215
+ | A band meant to sit behind two rows draws over part of one of them, or breaks at the seam between them | It was drawn as one row's `registerRowChrome(…, "back")` reaching past that row. Row chrome belongs to its own row | `createRowBackdropPlugin` from `@roku-sdk/flex-grid/row-backdrop`, one region over the rows → `references/plugins.md` |
216
+ | A `layer: "back"` focus highlight disappears behind a row backdrop | `createRowBackdropPlugin` comes after `createFocusHighlightPlugin` in `plugins`, and the back layer draws in that order | List the backdrop plugin first |
217
+ | Backdrop art squashes while the bob opens inside the band | A `size.fill` poster takes the region's height, which grows with the gap | `loadDisplayMode="scaleToZoom"` on the poster; place content that must not stretch against `size.height()` |
218
+ | Focus resets and the grid flashes on unrelated state changes | A structural prop — `getCellComponent`, `plugins`, an options object — arrives as a new value each render | Hoist it, or keep it in a memo |
219
+ | Every cell looks unfocused, or the wrong one is highlighted | `focused` was read at the top of the cell instead of inside JSX, or `props` was destructured | Call `props.focused()` in the JSX that depends on it |
220
+ | A key the app wanted never arrives, though the grid handles the arrows | The grid claims seven keys, not five: the four movement keys, `OK`, `fastforward` and `rewind` | Name the extra ones in `handleAdditionalKeyEvents` and handle them where the reference shows. That list is filtered against the seven the grid already claims, so listing `fastforward` or `rewind` there is silently dropped rather than rejected |
221
+ | `up` on the first row does nothing, and the boundary's `onUnhandledKey` never fires for it | The grid claimed that key and consumes it even though it moved nothing, so it never reaches the boundary the hand-off was written on | Move the hand-off to the grid's `onEdgeReached`, per `references/focus-integration.md`. `declineUnhandledNavigation: true` makes claimed keys that moved nothing bubble instead, which is how a nested grid escapes its own edges — reach for it there, not to rescue a top-level hand-off |
222
+ | `tsc` reports `Property 'floatTween' does not exist on JSX.IntrinsicElements`, inside the SDK's files rather than yours | Those intrinsics come from XML type declarations that only `rk build` / `rk dev` generate, and a fresh install has none | Build once, then re-run. The build's own type check is the authoritative gate; bare `tsc` on a never-built app is not |
223
+ | The device log floods with `Could not update the interpolator field ""` | By design: the grid creates its interpolators with an empty `fieldToInterp` and patches the target in from BrightScript | Ignore them, and search the log for the real error rather than reading it in order |
224
+
225
+ `FlexGridConfig`, `FlexGridHandle` and `CellRenderProps` are typed with TSDoc in the installed
226
+ package — read a field there rather than guessing its name, and prefer it over any prose,
227
+ including this file, when the two disagree. The package ships raw TypeScript (`main` is
228
+ `src/index.ts`), so read `node_modules/@roku-sdk/flex-grid/src/`: the config and the handle are
229
+ in `flex-grid-base.tsx`, the cell props in `types.ts`. There are no `dist/*.d.ts` to consult.
230
+
231
+ ## Tween ownership
232
+
233
+ Import `Tween` and `TweenProps` from `@roku-sdk/components`, not `@roku-sdk/flex-grid`.
234
+ The grid export is deprecated and retained for one minor release. The lowercase `<tween>`
235
+ intrinsic is declared by `@roku-sdk/components/jsx` and uses the native field contract.
236
+ See the Tween value-shape contract in `documentation/core-concepts/tween.md` in the SDK source tree.