@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.
- 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 +183 -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 +210 -0
- package/skills/focus-management/references/focus-recipes.md +290 -0
- package/skills/fonts-and-glyphs/SKILL.md +231 -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,157 @@
|
|
|
1
|
+
# Slideshow
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
|
|
5
|
+
`@roku-sdk/slideshow` shows **one thing at a time** and animates between them — a hero banner, a
|
|
6
|
+
full-screen image carousel, an onboarding or promo pager. You give it a slide count, a slide size
|
|
7
|
+
and a `renderSlide` callback; it owns the tween, which slide is current, and the arrows and dots
|
|
8
|
+
drawn around the strip. Three factories render the same handle in three different ways, and they
|
|
9
|
+
differ most in how focus works, not in what they draw.
|
|
10
|
+
|
|
11
|
+
## Read the reference first for these tasks
|
|
12
|
+
|
|
13
|
+
| If the request is | Read now, before answering |
|
|
14
|
+
|---|---|
|
|
15
|
+
| Making a slide focusable, or a slide with buttons inside it | `references/focus-and-handoff.md` |
|
|
16
|
+
| Moving focus out of the slideshow at the first or last slide, or into it from a sibling region | `references/focus-and-handoff.md` |
|
|
17
|
+
|
|
18
|
+
The tables below name the callback and stop there. The three modes build focus in three different
|
|
19
|
+
ways, and the wiring each one needs — including which of them moves focus for you and which only
|
|
20
|
+
tells you it is time — is only in the reference. Answer a focus question from the reference and
|
|
21
|
+
quote it; an answer assembled from a table row here will describe the wrong mode.
|
|
22
|
+
|
|
23
|
+
## When to Use
|
|
24
|
+
|
|
25
|
+
- one item on screen at a time, with a transition between items
|
|
26
|
+
- a hero banner or promo pager the viewer steps through with left and right
|
|
27
|
+
- a full-screen image carousel
|
|
28
|
+
- an onboarding flow of panels
|
|
29
|
+
- any of the above where the panels themselves contain buttons
|
|
30
|
+
|
|
31
|
+
### When not to use
|
|
32
|
+
|
|
33
|
+
| Situation | Use instead | Because |
|
|
34
|
+
|---|---|---|
|
|
35
|
+
| A row, shelf, poster grid, or any screen showing many items at once | The `flex-grid` skill | It owns rows, columns and cell recycling. This package has no notion of a row — it advances one whole slide at a time |
|
|
36
|
+
| Which node draws the picture, the text or the button inside a slide | The `sdk-components` skill | A slide's contents are ordinary components. This skill owns the strip around them, not what you put in one |
|
|
37
|
+
| Moving between whole screens rather than between panels of one screen | The `screen-navigation` skill | A slideshow is one screen's content; pushing and popping screens is a different mechanism |
|
|
38
|
+
| A row of buttons or a menu you laid out yourself | `FocusBoundary` + `useFocusable` from `@roku-sdk/navigation` | Nothing transitions, so the tween and slide bookkeeping cost more than the screen needs |
|
|
39
|
+
|
|
40
|
+
## Quick Reference
|
|
41
|
+
|
|
42
|
+
### Choosing a factory
|
|
43
|
+
|
|
44
|
+
| Your situation | Use | Import from |
|
|
45
|
+
|---|---|---|
|
|
46
|
+
| A handful of slides, and/or slides containing focusable buttons | `createSlideshow` | `@roku-sdk/slideshow` |
|
|
47
|
+
| Many slides, each cheap to build, and you do not want them all mounted | `createVirtualSlideshow` | `@roku-sdk/slideshow/virtual` |
|
|
48
|
+
| Many slides of a few repeated shapes, where building SceneGraph nodes is the cost | `createPooledSlideshow` | `@roku-sdk/slideshow/pooled` |
|
|
49
|
+
| You would rather write it as JSX than call a factory | `defineSlideshow<T>()` or `Slideshow` | `@roku-sdk/slideshow` |
|
|
50
|
+
|
|
51
|
+
`defineSlideshow` wraps **eager mode only**. There is no `defineVirtualSlideshow` and no
|
|
52
|
+
`definePooledSlideshow` — for those two, call the factory.
|
|
53
|
+
|
|
54
|
+
Eager is the only mode whose individual slides can take focus in the ordinary way. Pooled is the
|
|
55
|
+
opposite extreme: its recycled template slides cannot be focused at all.
|
|
56
|
+
|
|
57
|
+
### Config
|
|
58
|
+
|
|
59
|
+
| Field | Notes |
|
|
60
|
+
|---|---|
|
|
61
|
+
| `slideCount`, `slideWidth`, `slideHeight` | The three required fields |
|
|
62
|
+
| `items` | `(T \| undefined)[]`; `undefined` is a slide whose data has not arrived |
|
|
63
|
+
| `margin` | Gap **between** slides, default `0`. It is not padding around the strip |
|
|
64
|
+
| `translation` | `[x, y]` of the whole slideshow on screen |
|
|
65
|
+
| `renderSlide` | Optional; receives `index`, `item`, `focused`, `width`, `height`, `slideFocusId`, `goTo` |
|
|
66
|
+
| `initialIndex` | Default `0` |
|
|
67
|
+
| `animation` | `{ durationSec }` default `0.333`, `{ easeFunction }` default `"outCubic"` |
|
|
68
|
+
| `onSlideChange`, `onSelect` | `(index, item)` |
|
|
69
|
+
| `onEdgeReached` | `(edge)` where `edge` is `"first"` or `"last"` → `references/focus-and-handoff.md` |
|
|
70
|
+
| `focusId` | Identity the enclosing boundary's `initialFocus` or `setFocus` names |
|
|
71
|
+
| `chevrons`, `dots` | A component, or `false` to disable |
|
|
72
|
+
| `windowSize` | Virtual only; slides kept alive each side of current, default `1` |
|
|
73
|
+
| `templates`, `getTemplateId`, `poolSize`, `uniqueSlides` | Pooled only; `poolSize` defaults to `3` per template |
|
|
74
|
+
|
|
75
|
+
`onKeyEvent` also exists, but treat it as legacy: prefer letting keys reach the slideshow through
|
|
76
|
+
the focus chain, and use `onEdgeReached` for hand-off.
|
|
77
|
+
|
|
78
|
+
### The handle
|
|
79
|
+
|
|
80
|
+
All three factories return the same shape: `element`, `currentIndex`, `currentItem`,
|
|
81
|
+
`goTo(index, animated?)`, `next()`, `prev()`, `getCurrentIndex()`,
|
|
82
|
+
`updateSlides(startIndex, items)`, `setSlideCount(n)`.
|
|
83
|
+
|
|
84
|
+
Render it as `{element()}` — always call it.
|
|
85
|
+
|
|
86
|
+
### Chrome
|
|
87
|
+
|
|
88
|
+
| Component | Import from | Props |
|
|
89
|
+
|---|---|---|
|
|
90
|
+
| `DefaultChevrons` | `@roku-sdk/slideshow/chrome/chevrons` | `showLeft`, `showRight`, `onLeft?`, `onRight?`, `width`, `height` |
|
|
91
|
+
| `DefaultDotIndicator` | `@roku-sdk/slideshow/chrome/dots` | `slideCount`, `currentIndex`, `width`, `height` |
|
|
92
|
+
|
|
93
|
+
Pass the component itself — `chevrons: DefaultChevrons` — not an element. The factory binds the
|
|
94
|
+
props. Both are also re-exported from the package root for convenience.
|
|
95
|
+
|
|
96
|
+
## Example
|
|
97
|
+
|
|
98
|
+
A five-slide hero banner with chrome, taking focus from the root boundary:
|
|
99
|
+
|
|
100
|
+
```tsx rsg-skill=complete
|
|
101
|
+
import { FocusBoundary, useFocusable } from "@roku-sdk/navigation";
|
|
102
|
+
import { createSlideshow, DefaultChevrons, DefaultDotIndicator, FocusableSlide } from "@roku-sdk/slideshow";
|
|
103
|
+
import type { Component } from "@roku-sdk/solid";
|
|
104
|
+
|
|
105
|
+
const COLORS = ["0x1A1A2EFF", "0x0F3460FF", "0x533483FF", "0x2D6A4FFF", "0xE94560FF"];
|
|
106
|
+
|
|
107
|
+
const HeroBanner: Component = () => {
|
|
108
|
+
const slideshowId = {};
|
|
109
|
+
|
|
110
|
+
const { element } = createSlideshow<undefined>({
|
|
111
|
+
slideCount: 5,
|
|
112
|
+
slideWidth: 1720,
|
|
113
|
+
slideHeight: 940,
|
|
114
|
+
translation: [100, 40],
|
|
115
|
+
margin: 200,
|
|
116
|
+
focusId: slideshowId,
|
|
117
|
+
chevrons: DefaultChevrons,
|
|
118
|
+
dots: DefaultDotIndicator,
|
|
119
|
+
onEdgeReached: (edge: "first" | "last") => console.log(`at the ${edge} slide`),
|
|
120
|
+
renderSlide: ({ index, focused, width, height, slideFocusId, goTo }) => (
|
|
121
|
+
<FocusableSlide slideFocusId={slideFocusId} onFocused={goTo}>
|
|
122
|
+
<rectangle width={width} height={height} color={COLORS[index]} />
|
|
123
|
+
<label text={`Slide ${index + 1}`} translation={[60, 60]} color="0xFFFFFFFF" />
|
|
124
|
+
<label text={focused() ? "[ FOCUSED ]" : ""} translation={[60, 130]} color="0x88FF88FF" />
|
|
125
|
+
</FocusableSlide>
|
|
126
|
+
),
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
return (
|
|
130
|
+
<FocusBoundary focusId={{}} initialFocus={slideshowId}>
|
|
131
|
+
{element()}
|
|
132
|
+
</FocusBoundary>
|
|
133
|
+
);
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
// `useFocusable` is what FocusableSlide uses internally for a non-interactive slide.
|
|
137
|
+
void useFocusable;
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
## Common Mistakes
|
|
141
|
+
|
|
142
|
+
| Symptom | Cause | Fix |
|
|
143
|
+
|---|---|---|
|
|
144
|
+
| Slides render, but left and right do nothing and no slide is ever focused | `renderSlide` returns bare nodes, so nothing registers with the boundary | Wrap a non-interactive slide in `FocusableSlide` and hand it `slideFocusId` → `references/focus-and-handoff.md` |
|
|
145
|
+
| Nothing renders at all, or the strip appears only after a key press | `element` was rendered without calling it | It is a thunk: write `{element()}` |
|
|
146
|
+
| At the last slide, `onEdgeReached` fires but focus stays put | In eager and virtual modes the callback only *notifies* — it does not move focus | Call `setFocus` yourself in the callback → `references/focus-and-handoff.md` |
|
|
147
|
+
| A chevron is missing or half off the screen | Chevrons are drawn outside the slide box, roughly 80px beyond each edge | Give the slideshow enough `translation` room on both sides; `margin` will not help, it is the gap between slides |
|
|
148
|
+
| Chevrons or dots never appear | An element was passed instead of a component | Pass `chevrons: DefaultChevrons`, not `chevrons: <DefaultChevrons … />` |
|
|
149
|
+
| A pooled slide's buttons cannot be reached with the remote | Pooled templates render outside the focus context and can never be focused | List that index in `uniqueSlides`, which routes it through `renderSlide` instead of the pool |
|
|
150
|
+
| A virtual slideshow scrolls but focus lags a slide behind | `goTo` was wired into `onFocusGained` as eager mode requires | Virtual mode drives navigation itself — leave `goTo` for programmatic jumps such as an auto-advance timer |
|
|
151
|
+
| `slideRef` is not defined on the render props | It was renamed | It is `slideFocusId` |
|
|
152
|
+
|
|
153
|
+
## Source of truth
|
|
154
|
+
|
|
155
|
+
Prefer the typed source over any prose, including this file: read
|
|
156
|
+
`node_modules/@roku-sdk/slideshow/src/types.ts` for the config, the render props and the handle,
|
|
157
|
+
and the factory's own destructure for a default this file does not list.
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
# Slideshow focus and hand-off
|
|
2
|
+
|
|
3
|
+
Everything focus-related imports from `@roku-sdk/navigation`, never from `@roku-sdk/components`.
|
|
4
|
+
|
|
5
|
+
## The three modes build focus three different ways
|
|
6
|
+
|
|
7
|
+
This is the fact the body cannot compress, and it decides every answer below.
|
|
8
|
+
|
|
9
|
+
| Mode | What it renders | How left/right move | At the first or last slide |
|
|
10
|
+
|---|---|---|---|
|
|
11
|
+
| `createSlideshow` (eager) | A `FocusBoundary` with `navigation="horizontal"` | The boundary walks between slide positions | Fires `onEdgeReached`, and **keeps** the key |
|
|
12
|
+
| `createVirtualSlideshow` | A `FocusBoundary` with no `navigation` | The factory calls `prev()` / `next()` and moves focus itself | Fires `onEdgeReached`, and **keeps** the key |
|
|
13
|
+
| `createPooledSlideshow` | **No boundary** — a single leaf `useFocusable` | Its `onKey` handlers advance the index | Declines the key, so focus leaves on its own |
|
|
14
|
+
|
|
15
|
+
Two consequences worth stating outright:
|
|
16
|
+
|
|
17
|
+
- **In eager and virtual modes, `onEdgeReached` does not move focus.** It tells you the viewer
|
|
18
|
+
pushed past the end. Moving focus is your job, with `setFocus` from `useFocusActions()`.
|
|
19
|
+
- **In pooled mode you often need no hand-off code at all.** Its edge handler returns `bubble`, so
|
|
20
|
+
the key bubbles and the enclosing boundary's `navigationMap` decides where focus goes.
|
|
21
|
+
|
|
22
|
+
## `slideFocusId`
|
|
23
|
+
|
|
24
|
+
`renderSlide` receives `slideFocusId: object` — an opaque identity created once per slide
|
|
25
|
+
**position** at factory init, not a node ref and not derived from the item. It stays with the
|
|
26
|
+
position when the data changes, and `setSlideCount` grows the list.
|
|
27
|
+
|
|
28
|
+
Also on the render props: `goTo()`, already bound to that slide's index, so you call it with no
|
|
29
|
+
arguments.
|
|
30
|
+
|
|
31
|
+
## A non-interactive slide
|
|
32
|
+
|
|
33
|
+
A slide that is only a picture and a caption still has to register, or nothing in the slideshow
|
|
34
|
+
is focusable and no key does anything. `FocusableSlide` is the helper for exactly this:
|
|
35
|
+
|
|
36
|
+
```tsx rsg-skill=complete
|
|
37
|
+
import { createSlideshow, FocusableSlide } from "@roku-sdk/slideshow";
|
|
38
|
+
import type { Component } from "@roku-sdk/solid";
|
|
39
|
+
|
|
40
|
+
const Banner: Component = () => {
|
|
41
|
+
const { element } = createSlideshow<undefined>({
|
|
42
|
+
slideCount: 3,
|
|
43
|
+
slideWidth: 1720,
|
|
44
|
+
slideHeight: 940,
|
|
45
|
+
focusId: {},
|
|
46
|
+
renderSlide: ({ index, width, height, slideFocusId, goTo }) => (
|
|
47
|
+
<FocusableSlide slideFocusId={slideFocusId} onFocused={goTo}>
|
|
48
|
+
<rectangle width={width} height={height} color="0x1A1A2EFF" />
|
|
49
|
+
<label text={`Panel ${index + 1}`} translation={[60, 60]} color="0xFFFFFFFF" />
|
|
50
|
+
</FocusableSlide>
|
|
51
|
+
),
|
|
52
|
+
});
|
|
53
|
+
return <>{element()}</>;
|
|
54
|
+
};
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
`onFocused={goTo}` is what keeps the strip in sync when focus lands on a slide. In **eager** mode
|
|
58
|
+
wire it. In **virtual** mode you may leave it off — that factory moves focus itself — and `goTo`
|
|
59
|
+
is then only for programmatic jumps such as an auto-advance timer.
|
|
60
|
+
|
|
61
|
+
## A slide with buttons in it
|
|
62
|
+
|
|
63
|
+
An interactive slide is a nested boundary, and `slideFocusId` is its `focusId`. Wire each
|
|
64
|
+
button's `onFocusGained` to `goTo` so the strip follows focus into the slide:
|
|
65
|
+
|
|
66
|
+
```tsx rsg-skill=complete
|
|
67
|
+
import { FocusBoundary, useFocusable } from "@roku-sdk/navigation";
|
|
68
|
+
import { createSlideshow } from "@roku-sdk/slideshow";
|
|
69
|
+
import type { Component } from "@roku-sdk/solid";
|
|
70
|
+
|
|
71
|
+
const Button: Component<{ label: string; x: number; focusId: object; onFocused: () => void }> = props => {
|
|
72
|
+
const { isFocused, ref } = useFocusable({
|
|
73
|
+
focusId: props.focusId,
|
|
74
|
+
onFocusGained: () => props.onFocused(),
|
|
75
|
+
onKey: { OK: (press: boolean) => { if (press) console.log(props.label); } },
|
|
76
|
+
});
|
|
77
|
+
return (
|
|
78
|
+
<group ref={ref} translation={[props.x, 0]}>
|
|
79
|
+
<rectangle width={200} height={70} color={isFocused() ? "0x0066FFFF" : "0x333333FF"} />
|
|
80
|
+
<label text={props.label} width={200} height={70} horizAlign="center" vertAlign="center" />
|
|
81
|
+
</group>
|
|
82
|
+
);
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
const Pager: Component = () => {
|
|
86
|
+
const { element } = createSlideshow<undefined>({
|
|
87
|
+
slideCount: 3,
|
|
88
|
+
slideWidth: 1720,
|
|
89
|
+
slideHeight: 940,
|
|
90
|
+
focusId: {},
|
|
91
|
+
renderSlide: ({ width, height, slideFocusId, goTo }) => {
|
|
92
|
+
const btnIds: object[] = [{}, {}];
|
|
93
|
+
return (
|
|
94
|
+
<group>
|
|
95
|
+
<rectangle width={width} height={height} color="0x0F3460FF" />
|
|
96
|
+
<FocusBoundary focusId={slideFocusId} initialFocus={btnIds[0]} navigation="horizontal">
|
|
97
|
+
<Button label="Play" x={0} focusId={btnIds[0]} onFocused={goTo} />
|
|
98
|
+
<Button label="More" x={220} focusId={btnIds[1]} onFocused={goTo} />
|
|
99
|
+
</FocusBoundary>
|
|
100
|
+
</group>
|
|
101
|
+
);
|
|
102
|
+
},
|
|
103
|
+
});
|
|
104
|
+
return <>{element()}</>;
|
|
105
|
+
};
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
Only **eager** mode supports this. Pooled `templates` receive `PooledSlideRenderProps`, which
|
|
109
|
+
carries neither `slideFocusId` nor `goTo` — a pooled template can never be focusable. To make one
|
|
110
|
+
particular pooled slide interactive, name its index in `uniqueSlides`; those bypass the pool and
|
|
111
|
+
go through `renderSlide` with the full render props.
|
|
112
|
+
|
|
113
|
+
## Taking focus in
|
|
114
|
+
|
|
115
|
+
Give the slideshow a `focusId` and name it from the enclosing boundary:
|
|
116
|
+
|
|
117
|
+
```tsx rsg-skill=complete
|
|
118
|
+
import { FocusBoundary } from "@roku-sdk/navigation";
|
|
119
|
+
import { createSlideshow } from "@roku-sdk/slideshow";
|
|
120
|
+
import type { Component } from "@roku-sdk/solid";
|
|
121
|
+
|
|
122
|
+
const Screen: Component = () => {
|
|
123
|
+
const slideshowId = {};
|
|
124
|
+
const { element } = createSlideshow<undefined>({
|
|
125
|
+
slideCount: 4,
|
|
126
|
+
slideWidth: 1720,
|
|
127
|
+
slideHeight: 700,
|
|
128
|
+
focusId: slideshowId,
|
|
129
|
+
});
|
|
130
|
+
return (
|
|
131
|
+
<FocusBoundary focusId={{}} initialFocus={slideshowId}>
|
|
132
|
+
{element()}
|
|
133
|
+
</FocusBoundary>
|
|
134
|
+
);
|
|
135
|
+
};
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
When `focusId` is set, the handle's `element()` **must** be rendered inside a `FocusBoundary`.
|
|
139
|
+
|
|
140
|
+
## Handing focus back out
|
|
141
|
+
|
|
142
|
+
In eager and virtual modes, do it in `onEdgeReached`. The argument is `"first"` or `"last"` —
|
|
143
|
+
horizontal ends, not `"left"` / `"right"` and not a vertical edge:
|
|
144
|
+
|
|
145
|
+
```tsx rsg-skill=complete
|
|
146
|
+
import { FocusBoundary, useFocusActions } from "@roku-sdk/navigation";
|
|
147
|
+
import { createSlideshow } from "@roku-sdk/slideshow";
|
|
148
|
+
import type { Component } from "@roku-sdk/solid";
|
|
149
|
+
|
|
150
|
+
const Screen: Component = () => {
|
|
151
|
+
const slideshowId = {};
|
|
152
|
+
const menuId = {};
|
|
153
|
+
|
|
154
|
+
const Inner: Component = () => {
|
|
155
|
+
const { setFocus } = useFocusActions();
|
|
156
|
+
const { element } = createSlideshow<undefined>({
|
|
157
|
+
slideCount: 4,
|
|
158
|
+
slideWidth: 1720,
|
|
159
|
+
slideHeight: 700,
|
|
160
|
+
focusId: slideshowId,
|
|
161
|
+
onEdgeReached: (edge: "first" | "last") => {
|
|
162
|
+
if (edge === "last") setFocus(menuId);
|
|
163
|
+
},
|
|
164
|
+
});
|
|
165
|
+
return <>{element()}</>;
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
return (
|
|
169
|
+
<FocusBoundary focusId={{}} initialFocus={slideshowId}>
|
|
170
|
+
<Inner />
|
|
171
|
+
</FocusBoundary>
|
|
172
|
+
);
|
|
173
|
+
};
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
`useFocusActions()` must be called inside the boundary, which is why the slideshow is built in an
|
|
177
|
+
inner component here rather than beside the boundary that owns it.
|
|
178
|
+
|
|
179
|
+
For **pooled** mode this is usually unnecessary: its edge handler returns `bubble`, the key bubbles
|
|
180
|
+
to the enclosing boundary, and a `navigationMap` entry there moves focus. Add `onEdgeReached` only
|
|
181
|
+
when you need to run something else at the edge as well.
|
|
182
|
+
|
|
183
|
+
## Things that do not exist
|
|
184
|
+
|
|
185
|
+
`slideRef` was renamed to `slideFocusId`, and the config field `focusRef` to `focusId`. There is
|
|
186
|
+
no `navigationMap` on a slideshow config, no per-slide `nextFocus*` property, and no
|
|
187
|
+
`onSlideExit`. Hand-off is `onEdgeReached` plus `setFocus`, or bubbling in pooled mode.
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
# Solid on Roku
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
|
|
5
|
+
An RSG SDK TypeScript app is a SolidJS app whose reactivity is unchanged and whose
|
|
6
|
+
*surroundings* are not: primitives that need a browser scheduler are not re-exported, the engine
|
|
7
|
+
running the code hoists `let` like `var`, a development build swaps three primitives for
|
|
8
|
+
instrumented ones, and the anchor node hands the app its input fields as signal getters rather
|
|
9
|
+
than as a plain object. This skill is the seam — what the platform does differently from what the
|
|
10
|
+
SolidJS documentation describes.
|
|
11
|
+
|
|
12
|
+
## When to Use
|
|
13
|
+
|
|
14
|
+
Use this when Solid code is correct by the framework's rules and the platform disagrees:
|
|
15
|
+
|
|
16
|
+
- an import from `@roku-sdk/solid` that does not resolve, for a primitive `solid-js` documents
|
|
17
|
+
- an anchor node input field read once that never changes again, or an effect that re-runs on
|
|
18
|
+
fields it never named
|
|
19
|
+
- a `let` or `const` inside a loop behaving as if it were `var`
|
|
20
|
+
- a generator function the app cannot use
|
|
21
|
+
- a signal that behaves one way under a development build and another under a shipped one
|
|
22
|
+
- a conditional element with no fallback, and whether SceneGraph's lack of text nodes makes that
|
|
23
|
+
unsafe
|
|
24
|
+
|
|
25
|
+
### When not to use
|
|
26
|
+
|
|
27
|
+
| Situation | Use instead | Because |
|
|
28
|
+
|---|---|---|
|
|
29
|
+
| Handling or recovering from something that threw | The `logging-and-errors` skill | This skill owns which primitives exist, not what to do when one of them throws |
|
|
30
|
+
| An assertion reading stale state in a test | The `testing` skill | When effects flush under a test harness is a testing concern, not a platform one |
|
|
31
|
+
| Which node draws a thing, or what to pass it | The `sdk-components` skill | Reactivity is the same question everywhere; the component surface is its own |
|
|
32
|
+
| Which item is focused, or what a key press does | The `focus-management` skill | Focus is a signal, but which signal and who owns it is that skill's whole subject |
|
|
33
|
+
| Which package a Solid import comes from, and declaring it | The `project-setup` skill | Import every primitive from `@roku-sdk/solid`, never `solid-js` — that rule and the `package.json` it needs live there |
|
|
34
|
+
|
|
35
|
+
## Read the reference first for two of these tasks
|
|
36
|
+
|
|
37
|
+
| If the request is | Read now, before answering |
|
|
38
|
+
|---|---|
|
|
39
|
+
| The build rejected a module, or a transform did something to it that has to be explained | `references/hermes-and-exports.md` |
|
|
40
|
+
| Whether a specific `solid-js` symbol exists here, beyond the ones named below | `references/hermes-and-exports.md` — the full excluded table, with a reason per row |
|
|
41
|
+
|
|
42
|
+
The tables below name every fact; the reference carries the shapes and the complete lists. An
|
|
43
|
+
answer that needs one exact symbol should not open the file.
|
|
44
|
+
|
|
45
|
+
## Quick Reference
|
|
46
|
+
|
|
47
|
+
| Question | Answer |
|
|
48
|
+
|---|---|
|
|
49
|
+
| Where do primitives come from | `@roku-sdk/solid`, always |
|
|
50
|
+
| What is missing, and why | `createDeferred` (needs `MessageChannel`), `lazy` (needs dynamic `import()`), `SuspenseList` and `enableScheduling` (need the concurrent scheduler), `enableHydration` (SSR), `onError` (deprecated upstream), plus thirteen `solid-js` internals |
|
|
51
|
+
| Reading an anchor node input | Name the field inside a reactive scope: `createMemo(() => inputs.title)` |
|
|
52
|
+
| Why a top-level read of `inputs` never updates | The component function is invoked inside `untrack`, so nothing read while it runs subscribes |
|
|
53
|
+
| Why an effect re-runs on fields it never named | Every field is an **enumerable** getter, so spreading, `Object.keys` + read, or stringifying `inputs` subscribes to all of them |
|
|
54
|
+
| Why `inputs.title` is `possibly 'undefined'` | The inputs type is `Partial`, though a declared field always holds its descriptor default at runtime — restate the default: `inputs.title ?? "Loading"` |
|
|
55
|
+
| `let` in a loop | Hermes hoists it to function scope; the SDK's block-scoping transform restores per-iteration capture |
|
|
56
|
+
| Generator functions | Not usable — Hermes does not support them, so no transform covers them |
|
|
57
|
+
| What a development build changes | `createSignal`, `createMemo` and `createEffect` are instrumented wrappers, and `__DEV__` is set |
|
|
58
|
+
| A conditional with no fallback | Safe. Between siblings the renderer's `createTextNode("")` returns an empty `group` to hold the position; as a sole child there is no node at all |
|
|
59
|
+
| Reading a prop | Inside JSX, `createMemo`, `createEffect` or a callback — never destructured, never once at the top |
|
|
60
|
+
|
|
61
|
+
## Instructions
|
|
62
|
+
|
|
63
|
+
1. **Import every primitive from `@roku-sdk/solid`.** It re-exports the part of `solid-js` that
|
|
64
|
+
works under the SceneGraph renderer, plus the SDK's own symbols. `project-setup` owns the
|
|
65
|
+
dependency rule; what matters here is that a primitive absent from this package is absent on
|
|
66
|
+
purpose, and the reference gives the reason for each one.
|
|
67
|
+
|
|
68
|
+
2. **Do not reach for a missing primitive's `solid-js` original.** Installing `solid-js` beside
|
|
69
|
+
the SDK gives you a second reactive graph, not the missing feature — and the feature is missing
|
|
70
|
+
because the runtime cannot carry it. For a deferred value, throttle the *source* signal
|
|
71
|
+
yourself, with the SDK's own timing helpers; for code splitting, there is none to do, since the
|
|
72
|
+
module system is static. Do not substitute a browser scheduling or frame-timing API either —
|
|
73
|
+
this is not a browser, and none of them exist here.
|
|
74
|
+
|
|
75
|
+
3. **Read anchor node inputs by naming one field, inside a reactive scope.** Each input field is
|
|
76
|
+
an enumerable getter over its own signal, so `inputs.title` inside a `createMemo` subscribes to
|
|
77
|
+
`title` alone. Two consequences follow, and they pull in opposite directions:
|
|
78
|
+
- A read at the top of the component body subscribes to **nothing** — the body is invoked
|
|
79
|
+
inside `untrack` — so the value is whatever it was at construction, forever.
|
|
80
|
+
- A read that touches the object *as a whole* inside a tracking scope subscribes to
|
|
81
|
+
**everything**. Spreading it, iterating its keys, or putting it in a log line does this.
|
|
82
|
+
|
|
83
|
+
The inputs type is `Partial`, so every field reads as possibly `undefined` even though a
|
|
84
|
+
declared field always holds its descriptor default. Restate that default at the read —
|
|
85
|
+
`inputs.title ?? "Loading"` — rather than asserting non-null.
|
|
86
|
+
|
|
87
|
+
4. **Never destructure `props`, and never read `props.x` once at the top of a component.** The
|
|
88
|
+
compiler passes a component's props as an object of memoized getters, so a read is a call and
|
|
89
|
+
destructuring calls each one immediately and keeps the value. Read inside JSX, `createMemo`,
|
|
90
|
+
`createEffect` or a callback. `splitProps` and `mergeProps` are the sanctioned exceptions.
|
|
91
|
+
|
|
92
|
+
5. **Write loops as you would anywhere, and know why they work.** Hermes hoists `let` and `const`
|
|
93
|
+
to function scope, which would make every closure created in a loop share one variable. The
|
|
94
|
+
SDK's block-scoping transform runs after `babel-preset-solid` and restores per-iteration
|
|
95
|
+
capture, so the ordinary shape is correct. Do not hand-write an immediately invoked wrapper to
|
|
96
|
+
"fix" it.
|
|
97
|
+
|
|
98
|
+
6. **Do not use generator functions.** Hermes does not support them at all, so this is a language
|
|
99
|
+
limit rather than a transform gap — and it is why the block-scoping transform does not handle
|
|
100
|
+
`yield` inside a loop. Return an array, or expose a `next()` closure over an index.
|
|
101
|
+
|
|
102
|
+
7. **Assume a development build is not the build you ship.** It sets `__DEV__` and replaces
|
|
103
|
+
`createSignal`, `createMemo` and `createEffect` with instrumented wrappers that record reactive
|
|
104
|
+
activity. Behaviour matches, but timing and anything you measure through them do not — so never
|
|
105
|
+
conclude a performance question from a development build, and never leave code that depends on
|
|
106
|
+
the wrappers being there.
|
|
107
|
+
|
|
108
|
+
8. **Leave `fallback` off a conditional when there is nothing to show.** SceneGraph has no text
|
|
109
|
+
nodes, so the universal renderer's `createTextNode("")` is answered with an empty `group`
|
|
110
|
+
instead — childless, zero-size, and silent, because the renderer only warns for non-empty
|
|
111
|
+
text. One is created to hold the position when a conditional between siblings renders
|
|
112
|
+
nothing. Supplying a fallback purely to avoid it buys nothing.
|
|
113
|
+
|
|
114
|
+
## Example
|
|
115
|
+
|
|
116
|
+
An anchor node whose two input fields are written by the host while the app runs. It reads each
|
|
117
|
+
field by name inside a reactive scope, so each subscription is to one field.
|
|
118
|
+
|
|
119
|
+
```tsx rsg-skill=complete
|
|
120
|
+
import { Label, LayoutGroup, Rectangle } from "@roku-sdk/components";
|
|
121
|
+
import { type AnchorNodeImplementation, defineAnchorNode, FieldType } from "@roku-sdk/rsg-ts";
|
|
122
|
+
import { createEffect, createMemo, Show } from "@roku-sdk/solid";
|
|
123
|
+
|
|
124
|
+
const ProgressPanelDescription = {
|
|
125
|
+
componentName: "ProgressPanel",
|
|
126
|
+
extends: "Group" as const,
|
|
127
|
+
inputFields: {
|
|
128
|
+
title: FieldType.String("Loading"),
|
|
129
|
+
progress: FieldType.Float(0)
|
|
130
|
+
},
|
|
131
|
+
outputFields: {
|
|
132
|
+
settled: FieldType.Boolean()
|
|
133
|
+
}
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
const ProgressPanel: AnchorNodeImplementation<typeof ProgressPanelDescription> = (inputs, outputs) => {
|
|
137
|
+
// Naming one field subscribes to that field alone. Reading `inputs` as a whole
|
|
138
|
+
// here — spreading it, or putting it in a log line — would subscribe to both.
|
|
139
|
+
// Every field is typed optional, so restate the descriptor's default.
|
|
140
|
+
const heading = createMemo(() => (inputs.title ?? "Loading").toUpperCase());
|
|
141
|
+
const progress = createMemo(() => inputs.progress ?? 0);
|
|
142
|
+
const barWidth = createMemo(() => Math.round(progress() * 600));
|
|
143
|
+
|
|
144
|
+
createEffect(() => {
|
|
145
|
+
if (progress() >= 1) {
|
|
146
|
+
outputs.settled(true);
|
|
147
|
+
}
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
return (
|
|
151
|
+
<layoutGroup layoutDirection="vert">
|
|
152
|
+
<label text={heading()} width={600} height={48} color="0xFFFFFFFF" />
|
|
153
|
+
{/* No fallback: the renderer holds the position with one empty placeholder. */}
|
|
154
|
+
<Show when={barWidth() > 0}>
|
|
155
|
+
<rectangle width={barWidth()} height={8} color="0x0055FFFF" />
|
|
156
|
+
</Show>
|
|
157
|
+
</layoutGroup>
|
|
158
|
+
);
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
export default defineAnchorNode(ProgressPanelDescription, ProgressPanel);
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
## Common Mistakes
|
|
165
|
+
|
|
166
|
+
| Symptom | Cause | Fix |
|
|
167
|
+
|---|---|---|
|
|
168
|
+
| An import from `@roku-sdk/solid` does not resolve, though `solid-js` documents the symbol | It is one of the deliberately excluded exports | Read the reference's table for the reason and the alternative; do not install `solid-js` to get it |
|
|
169
|
+
| An anchor node input field holds its startup value forever | It was read at the top of the component body, which runs inside `untrack` | Read it inside `createMemo`, `createEffect` or JSX |
|
|
170
|
+
| An effect re-runs whenever any input field changes | The effect read `inputs` as a whole — a spread, a key iteration, or a log line — and every field is an enumerable getter | Name the one field the effect depends on |
|
|
171
|
+
| A component renders once with the right value and never updates | `props` was destructured, or read once into a local at the top | Read `props.x` inside a reactive scope; use `splitProps` when a subset is genuinely needed |
|
|
172
|
+
| `TS18048: 'inputs.x' is possibly 'undefined'` on a field the descriptor gives a default | The inputs type is `Partial`, so it is wider than the runtime | Restate the descriptor's default at the read — `inputs.x ?? <default>` — rather than `!` |
|
|
173
|
+
| A measurement taken around signals disappears from the shipped build | Those numbers came from the development build's instrumented `createSignal` / `createMemo` / `createEffect` | Measure the build you ship; treat the wrappers as a development aid only |
|
|
174
|
+
| A generator function fails on the device | Hermes has no generator support, and no transform adds it | Return an array, or expose a `next()` closure |
|
|
175
|
+
| A hand-written wrapper was added around a loop body to capture the index | The block-scoping transform already does this, after `babel-preset-solid` | Delete the wrapper and write the ordinary loop |
|
|
176
|
+
| A fallback was added to every conditional to avoid text nodes | `createTextNode("")` yields an empty `group` here, not a text node | Leave `fallback` off unless there is something to show |
|
|
177
|
+
| A node passed by reference, such as `effect={focused() ? <effect …/> : <effect …/>}`, is rebuilt on every change | The prop expression reads the signal itself, so each change builds a new node; a signal read by that node's own fields would update it in place | Hoist each node into a `const` and choose between them — the `effects` skill. This is about node-valued fields, not `fallback` or children, which are inline by design |
|
|
178
|
+
|
|
179
|
+
The `@roku-sdk/solid` export surface is enumerated in the package's own `EXPORTS.md`, including
|
|
180
|
+
the SDK-specific symbols this skill does not cover. Prefer it over any prose, including this file,
|
|
181
|
+
when the two disagree.
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
# Hermes and the Export Surface
|
|
2
|
+
|
|
3
|
+
Two things the SDK does to ordinary Solid code before it runs: it withholds primitives the
|
|
4
|
+
runtime cannot carry, and it rewrites loops the engine would otherwise get wrong. The body names
|
|
5
|
+
every fact here; this file is the complete lists and the shapes.
|
|
6
|
+
|
|
7
|
+
## The compile pipeline, in order
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
TypeScript → Babel (babel-preset-solid) → block-scoping transform → Hermes bytecode
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Order is the part worth holding on to. The block-scoping transform runs **after** Babel has
|
|
14
|
+
already turned components into reactive calls, so it operates on the emitted code rather than on
|
|
15
|
+
what was written. A closure Babel generated is treated exactly like one authored by hand.
|
|
16
|
+
|
|
17
|
+
## Why the loop transform exists
|
|
18
|
+
|
|
19
|
+
Hermes does not implement block scoping. `let` and `const` are hoisted to function scope and
|
|
20
|
+
behave as `var`, so every closure created in a loop would capture the same binding and observe
|
|
21
|
+
the final value.
|
|
22
|
+
|
|
23
|
+
```ts
|
|
24
|
+
// Authored — correct everywhere block scoping is real
|
|
25
|
+
for (let i = 0; i < 5; i += 1) {
|
|
26
|
+
schedule(() => report(i));
|
|
27
|
+
}
|
|
28
|
+
// Without the transform, on Hermes: five closures, all reporting 5.
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
The transform restores per-iteration capture, so the ordinary shape is the correct one. Two
|
|
32
|
+
things follow:
|
|
33
|
+
|
|
34
|
+
- **Do not hand-write a wrapper** — an immediately invoked function around the loop body, or a
|
|
35
|
+
`.forEach` used purely to get a fresh binding. It is redundant, and it hides the intent.
|
|
36
|
+
- **The fix is compile-time.** Code that never goes through the SDK's build — a snippet evaluated
|
|
37
|
+
some other way — does not get it.
|
|
38
|
+
|
|
39
|
+
This is a workaround with an expiry: Hermes is gaining native block scoping, and the transform is
|
|
40
|
+
documented as removable once that lands. Treat the shape as permanent and the mechanism as not.
|
|
41
|
+
|
|
42
|
+
## Generator functions
|
|
43
|
+
|
|
44
|
+
Hermes does not support `function*` at all. This is not a gap in the transform — there is nothing
|
|
45
|
+
for it to transform into, which is why the transform explicitly does not handle `yield` inside a
|
|
46
|
+
loop.
|
|
47
|
+
|
|
48
|
+
```ts
|
|
49
|
+
// Not usable on the device
|
|
50
|
+
function* pages(): Generator<number> {
|
|
51
|
+
for (let i = 0; i < 5; i += 1) {
|
|
52
|
+
yield i;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Replace with whichever fits:
|
|
58
|
+
|
|
59
|
+
- return the whole array when it is small enough to build eagerly;
|
|
60
|
+
- expose a closure over an index — `const next = () => items[cursor++]` — when it is not;
|
|
61
|
+
- use a signal for the cursor when the consumer is reactive.
|
|
62
|
+
|
|
63
|
+
## Excluded exports, with the reason for each
|
|
64
|
+
|
|
65
|
+
These exist in `solid-js` or `solid-js/store` and are **not** re-exported by `@roku-sdk/solid`.
|
|
66
|
+
Reaching past the package for one does not work: the reason is the runtime, not the packaging.
|
|
67
|
+
|
|
68
|
+
### Withheld because the platform cannot carry them
|
|
69
|
+
|
|
70
|
+
| Export | Reason | What to do instead |
|
|
71
|
+
|---|---|---|
|
|
72
|
+
| `createDeferred` | Solid's scheduler needs `MessageChannel`, which the Roku Hermes runtime does not have — the import fails with `ReferenceError: Property 'MessageChannel' doesn't exist` | Throttle the source signal, or derive from a signal you update on your own cadence |
|
|
73
|
+
| `lazy` | Dynamic `import()` against a static module system | Import statically; there is no split bundle to defer |
|
|
74
|
+
| `SuspenseList` | Coordinates several `<Suspense>` boundaries through the concurrent scheduler | Order the loading states yourself with ordinary signals |
|
|
75
|
+
| `enableScheduling` | Platform-specific scheduler wiring that has no Roku implementation | Nothing to enable |
|
|
76
|
+
| `enableHydration` | Server-side rendering, which does not apply here | Nothing to hydrate |
|
|
77
|
+
| `enableExternalSource` | A hook for bridging external reactive libraries; no current demand | Adapt the external source into a signal by hand |
|
|
78
|
+
| `onError` | Deprecated upstream | `catchError` — and for containing a throw at all, the `logging-and-errors` skill |
|
|
79
|
+
|
|
80
|
+
### Withheld as internals
|
|
81
|
+
|
|
82
|
+
Not part of the public API in `solid-js` either. If one of these looks necessary, the design
|
|
83
|
+
usually wants revisiting rather than the symbol.
|
|
84
|
+
|
|
85
|
+
| Export | Source |
|
|
86
|
+
|---|---|
|
|
87
|
+
| `equalFn` | `solid-js` |
|
|
88
|
+
| `getListener` | `solid-js` |
|
|
89
|
+
| `requestCallback` | `solid-js` |
|
|
90
|
+
| `cancelCallback` | `solid-js` |
|
|
91
|
+
| `sharedConfig` | `solid-js` |
|
|
92
|
+
| `createComponent` | `solid-js` |
|
|
93
|
+
| `$PROXY`, `$TRACK`, `$DEVCOMP` | `solid-js` |
|
|
94
|
+
| `isWrappable` | `solid-js/store` |
|
|
95
|
+
| `setProperty` | `solid-js/store` |
|
|
96
|
+
| `updatePath` | `solid-js/store` |
|
|
97
|
+
| `$RAW` | `solid-js/store` |
|
|
98
|
+
|
|
99
|
+
## What a development build substitutes
|
|
100
|
+
|
|
101
|
+
A development build sets `__DEV__`, and re-exports `createSignal`, `createMemo` and
|
|
102
|
+
`createEffect` from an instrumented module rather than from the reactive core. The wrappers record
|
|
103
|
+
reactive activity for tooling; semantics match, timing does not.
|
|
104
|
+
|
|
105
|
+
The consequences worth acting on:
|
|
106
|
+
|
|
107
|
+
- **Never measure performance on it.** Every signal read and write carries bookkeeping the
|
|
108
|
+
shipped build does not.
|
|
109
|
+
- **Never depend on the instrumentation existing.** Code that reads what the wrappers record
|
|
110
|
+
compiles fine and does nothing in the build you ship.
|
|
111
|
+
- **A behavioural difference between the two builds is worth reporting**, since the wrappers are
|
|
112
|
+
meant to be transparent.
|
|
113
|
+
|
|
114
|
+
## The placeholder node behind a fallback-less conditional
|
|
115
|
+
|
|
116
|
+
SceneGraph has no text nodes, but the universal renderer asks for one to hold a position. The
|
|
117
|
+
SDK's renderer answers that by creating an empty `group`: childless, zero-size, no layout effect.
|
|
118
|
+
The warning path is only for *non-empty* text, so a positional placeholder is silent.
|
|
119
|
+
|
|
120
|
+
It appears in one situation: a conditional with no `fallback`, rendering falsy, as a direct child
|
|
121
|
+
of a real element **with siblings** — the compiler needs a marker to know where the content goes
|
|
122
|
+
when it comes back. Inside a fragment there is no marker and no placeholder, because a fragment
|
|
123
|
+
compiles to an array.
|
|
124
|
+
|
|
125
|
+
Each placeholder is its own node, and that is deliberate — the renderer tracks children by object
|
|
126
|
+
identity and a node has one parent, so a shared placeholder would corrupt positional tracking for
|
|
127
|
+
every conditional after the first. The cost is one node creation and one append, against the
|
|
128
|
+
hundreds a screen makes for real content.
|
|
129
|
+
|
|
130
|
+
So: leave `fallback` off when there is nothing to show. Add one when there is.
|