@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.18.0",
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": "^3.23.8"
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.18.0"
28
+ "@roku-sdk/dev-tools": "2.19.0"
29
29
  },
30
30
  "author": "Roku Inc",
31
31
  "license": "MIT",
@@ -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 `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
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