@roku-sdk/ai 2.18.0 → 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.
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@roku-sdk/ai",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.19.0",
|
|
4
4
|
"description": "AI skills library and manifest for the Roku RSG TypeScript SDK",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.ts",
|
|
@@ -19,13 +19,13 @@
|
|
|
19
19
|
"node": ">=24.0.0"
|
|
20
20
|
},
|
|
21
21
|
"dependencies": {
|
|
22
|
-
"zod": "^
|
|
22
|
+
"zod": "^4.6.6"
|
|
23
23
|
},
|
|
24
24
|
"devDependencies": {
|
|
25
25
|
"@jest/globals": "^30.4.1",
|
|
26
26
|
"@types/jest": "^30.0.0",
|
|
27
27
|
"jest": "^30.4.1",
|
|
28
|
-
"@roku-sdk/dev-tools": "2.
|
|
28
|
+
"@roku-sdk/dev-tools": "2.19.0"
|
|
29
29
|
},
|
|
30
30
|
"author": "Roku Inc",
|
|
31
31
|
"license": "MIT",
|
package/skills/effects/SKILL.md
CHANGED
|
@@ -103,11 +103,13 @@ const pulse = <effect id="tile-pulse" borderRadius={24} borderWidth={2} borderCo
|
|
|
103
103
|
6. **An Effect clips nothing.** A child of a rounded `Rectangle` can paint into and past the
|
|
104
104
|
corners. Lay children out inside the rounded area; to clip them to a shape, use `MaskGroup`.
|
|
105
105
|
|
|
106
|
-
7. **Know the fields that fail silently.** A gradient draws nothing while
|
|
107
|
-
`"none"`, its default, or with fewer than two or more than eight
|
|
108
|
-
`gradientStops` needs one entry per colour
|
|
109
|
-
the
|
|
110
|
-
|
|
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
|
|
111
113
|
`gradientFillBorder` to `false`.
|
|
112
114
|
|
|
113
115
|
8. **`setPropMethod("effect")` is not how you share.** It builds a property descriptor for
|
|
@@ -57,7 +57,7 @@ Use this whenever the question is which item is focused, or what a key press doe
|
|
|
57
57
|
| Move focus from app logic | `useFocusActions()` → `setFocus(id)`; `focusedId()` to read it |
|
|
58
58
|
| Act on a key nobody handled, such as `back` | `onUnhandledKey` on the boundary |
|
|
59
59
|
| Reopen a region at its first item | `rememberLastFocus={false}` — the default `true` restores the last item |
|
|
60
|
-
| Mark the item an inactive region will return to — dialog open, or focus in another region | `hasFootprint()` from `useFocusable` |
|
|
60
|
+
| Mark the item an inactive region will return to — dialog open, or focus in another region | `hasFootprint()` from `useFocusable`; `hasReturnFootprint()` when nested regions or `rememberLastFocus={false}` must mark only where focus will land, and `useFocusActions().hasReturnFootprint()` for the region itself |
|
|
61
61
|
|
|
62
62
|
## Instructions
|
|
63
63
|
|
|
@@ -70,6 +70,11 @@ app's other `@roku-sdk/*` packages are on — or every import below fails to res
|
|
|
70
70
|
pass all three to `RootFocusBoundary`. Without this no key reaches TypeScript and nothing
|
|
71
71
|
moves. One per anchor node — never give a nested boundary its own `keyEvent`.
|
|
72
72
|
|
|
73
|
+
For TypeScript descriptors, keep `FocusBoundaryBase`: registration selects the native
|
|
74
|
+
component when firmware reports `hasFeature("focusBoundaryBase")`, otherwise
|
|
75
|
+
`FocusBoundaryBaseShim`. XML/BrightScript does not go through that remap: extend or
|
|
76
|
+
create `FocusBoundaryBaseShim` by name to keep working on existing firmware.
|
|
77
|
+
|
|
73
78
|
2. **Wrap each region in a `FocusBoundary`** and set `navigation` to the axis the region
|
|
74
79
|
moves on. Nest a region by giving it a `focusId` that its parent's `initialFocus` or
|
|
75
80
|
`navigationMap` names.
|
|
@@ -271,6 +271,14 @@ sibling region is another. A row styled from it therefore keeps its subdued mark
|
|
|
271
271
|
is over in the menu — usually what you want, but decide it rather than discover it. To mark only
|
|
272
272
|
the dialog case, pass the dialog's `isOpen()` down and gate the marker on it.
|
|
273
273
|
|
|
274
|
+
It is also evaluated against its own boundary alone, so a nested region focus will not pass back
|
|
275
|
+
through keeps marking its last child, and so does a region with `rememberLastFocus={false}` that
|
|
276
|
+
will reopen at `initialFocus`. When only items focus will actually land on should be marked,
|
|
277
|
+
use `hasReturnFootprint()` from `useFocusable` instead; `useFocusActions().hasReturnFootprint()`
|
|
278
|
+
answers the same question for the whole region. Each dormant sibling region under the nearest
|
|
279
|
+
active ancestor still marks its own item — the one focus reaches when the user moves there — so
|
|
280
|
+
the tree shows a single footprint only while the root itself is inactive.
|
|
281
|
+
|
|
274
282
|
### Common mistakes
|
|
275
283
|
|
|
276
284
|
| Symptom | Cause | Fix |
|
|
@@ -48,6 +48,7 @@ mistake here that compiles.
|
|
|
48
48
|
| Built-in font names | Ten. The eight `font:…SystemFont` names are **fixed size and ignore `fontSize`**; only `font:SystemFontFile` and `font:BoldSystemFontFile` respect it |
|
|
49
49
|
| Ship a typeface with the app | `import fontUri from "./fonts/whatever.ttf"` — the import resolves to the packaged path at build time |
|
|
50
50
|
| Two font files with the same filename | A build error if their contents differ, reuse if identical. Rename one |
|
|
51
|
+
| Wait until a font can be measured | `whenFontReady(ref)` or `createFontReady(fontRef)` from `@roku-sdk/rsg-ts/runtime` — never a fixed timer |
|
|
51
52
|
| Text arriving from outside with characters your font lacks | Constrain the text or widen the typeface. **`Font.fallbackGlyph` does not substitute anything on current firmware** — see instruction 8 |
|
|
52
53
|
| A `Font` with a `size` and no `uri` | Draws nothing at all. It does not fall back to the system font — omit the `Font` node instead |
|
|
53
54
|
| Which symbols survive a custom app font | The 32 private-use glyphs and `●` `★` `☆`. The other ten do not |
|
|
@@ -110,6 +111,59 @@ mistake here that compiles.
|
|
|
110
111
|
firmware's own string layer, not TypeScript symbols; putting one in a label renders the braces
|
|
111
112
|
literally. Interpolate the imported constant instead.
|
|
112
113
|
|
|
114
|
+
10. **Wait for a font with `whenFontReady`, not a timer.** `getFontMeasure()` throws
|
|
115
|
+
`"Font is not yet loaded"` until the GfxFont is published. Current firmware has no Font
|
|
116
|
+
`loadStatus` field; use `whenFontReady(ref)` (promise) or `createFontReady(fontRef)`
|
|
117
|
+
(Solid accessor that cancels on cleanup; pass `() => props.uri` or similar as its second
|
|
118
|
+
argument when the `<font>` node's `uri` or `size` can change) from `@roku-sdk/rsg-ts/runtime`, which keep
|
|
119
|
+
working when one ships. Do not copy the fixed 100ms / 500ms timer stubs from older
|
|
120
|
+
examples — those race on a cold cache. A `<font>` that is not on a drawn label is only
|
|
121
|
+
published if `preUpdate` is set before `uri` and `size`; without it the font never loads
|
|
122
|
+
and the wait never resolves.
|
|
123
|
+
|
|
124
|
+
## Measuring a font
|
|
125
|
+
|
|
126
|
+
`whenFontReady` and `createFontReady` ship in SDK 2.19.0. On 2.18.0 and earlier they are
|
|
127
|
+
not exported from `@roku-sdk/rsg-ts/runtime`.
|
|
128
|
+
|
|
129
|
+
Capture the Font node's `TmNodeRef` from `ref={node => … node.r}`, then wait before measuring:
|
|
130
|
+
|
|
131
|
+
```tsx rsg-skill=complete
|
|
132
|
+
import { getFontMeasure, whenFontReady, createFontReady } from "@roku-sdk/rsg-ts/runtime";
|
|
133
|
+
import { createEffect, createSignal, type Component } from "@roku-sdk/solid";
|
|
134
|
+
import type { TmNodeRef } from "@roku-sdk/interfaces";
|
|
135
|
+
|
|
136
|
+
export const MeasuredTitle: Component<{ text: string; uri: string }> = props => {
|
|
137
|
+
const [fontRef, setFontRef] = createSignal<TmNodeRef>();
|
|
138
|
+
// The key restarts the wait when `uri` changes on this same <font> node.
|
|
139
|
+
const fontReady = createFontReady(fontRef, () => props.uri);
|
|
140
|
+
const [width, setWidth] = createSignal(0);
|
|
141
|
+
|
|
142
|
+
createEffect(() => {
|
|
143
|
+
if (!fontReady()) return;
|
|
144
|
+
const ref = fontRef();
|
|
145
|
+
if (!ref) return;
|
|
146
|
+
setWidth(getFontMeasure().getOneLineWidth(ref, props.text, 1920));
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
return (
|
|
150
|
+
<label
|
|
151
|
+
text={props.text}
|
|
152
|
+
width={width() || 600}
|
|
153
|
+
height={48}
|
|
154
|
+
color="0xFFFFFFFF"
|
|
155
|
+
font={<font uri={props.uri} size={32} ref={node => setFontRef(node.r)} />}
|
|
156
|
+
/>
|
|
157
|
+
);
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
// Imperative form (same wait, no Solid helper):
|
|
161
|
+
async function measureOnce(ref: TmNodeRef, text: string): Promise<number> {
|
|
162
|
+
await whenFontReady(ref);
|
|
163
|
+
return getFontMeasure().getOneLineWidth(ref, text, 1920);
|
|
164
|
+
}
|
|
165
|
+
```
|
|
166
|
+
|
|
113
167
|
## Example
|
|
114
168
|
|
|
115
169
|
A row of chevrons and a rating, using a bundled typeface — imported, sized, and with a fallback
|
|
@@ -169,6 +223,7 @@ Keyed by what you see on screen, because that is what arrives first.
|
|
|
169
223
|
| The build fails naming two font files | Same filename, different contents, keyed by filename | Rename one of them |
|
|
170
224
|
| A private-use escape like `"\uE0nn"` sits in app code | The constant exists and the literal does not say which symbol it is | Import the constant |
|
|
171
225
|
| Braces render literally in a label | Firmware placeholder syntax used as app text | Interpolate the constant |
|
|
226
|
+
| `getFontMeasure` throws `"Font is not yet loaded"` or returns 0 | Measured before the GfxFont published; a fixed timer was too short | `await whenFontReady(ref)` or `createFontReady(fontRef)` — never a fixed timer |
|
|
172
227
|
|
|
173
228
|
The glyph groups here are device-verified against firmware 15.3.5 on a Roku Express 4K+ — every
|
|
174
229
|
constant rendered in a label under a custom typeface. Re-verify on a device rather than re-wording
|