@lightningtv/solid 2.7.19 → 2.8.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 (49) hide show
  1. package/dist/src/index.d.ts +1 -1
  2. package/dist/src/index.js +1 -1
  3. package/dist/src/index.js.map +1 -1
  4. package/dist/src/primitives/Column.jsx +2 -2
  5. package/dist/src/primitives/Column.jsx.map +1 -1
  6. package/dist/src/primitives/FPSCounter.jsx +60 -61
  7. package/dist/src/primitives/FPSCounter.jsx.map +1 -1
  8. package/dist/src/primitives/Grid.d.ts +15 -6
  9. package/dist/src/primitives/Grid.jsx +36 -23
  10. package/dist/src/primitives/Grid.jsx.map +1 -1
  11. package/dist/src/primitives/Lazy.jsx +1 -1
  12. package/dist/src/primitives/Lazy.jsx.map +1 -1
  13. package/dist/src/primitives/Marquee.d.ts +2 -3
  14. package/dist/src/primitives/Row.jsx +2 -2
  15. package/dist/src/primitives/Row.jsx.map +1 -1
  16. package/dist/src/primitives/index.d.ts +2 -2
  17. package/dist/src/primitives/index.js +2 -2
  18. package/dist/src/primitives/index.js.map +1 -1
  19. package/dist/src/primitives/utils/chainFunctions.d.ts +30 -4
  20. package/dist/src/primitives/utils/chainFunctions.js +14 -3
  21. package/dist/src/primitives/utils/chainFunctions.js.map +1 -1
  22. package/dist/src/primitives/utils/createSpriteMap.js.map +1 -1
  23. package/dist/src/primitives/utils/withScrolling.js +1 -1
  24. package/dist/src/primitives/utils/withScrolling.js.map +1 -1
  25. package/dist/src/render.d.ts +5 -5
  26. package/dist/src/render.js +8 -6
  27. package/dist/src/render.js.map +1 -1
  28. package/dist/src/solidOpts.d.ts +6 -0
  29. package/dist/src/solidOpts.js +31 -7
  30. package/dist/src/solidOpts.js.map +1 -1
  31. package/dist/tsconfig.tsbuildinfo +1 -1
  32. package/{src/jsx-runtime.ts → jsx-runtime.d.ts} +2 -0
  33. package/package.json +21 -6
  34. package/src/devtools/index.ts +77 -0
  35. package/src/index.ts +1 -1
  36. package/src/primitives/Column.tsx +2 -2
  37. package/src/primitives/FPSCounter.tsx +61 -61
  38. package/src/primitives/FadeInOut.tsx +34 -0
  39. package/src/primitives/Grid.tsx +59 -35
  40. package/src/primitives/Lazy.tsx +1 -1
  41. package/src/primitives/Row.tsx +1 -1
  42. package/src/primitives/index.ts +6 -2
  43. package/src/primitives/utils/chainFunctions.ts +40 -9
  44. package/src/primitives/utils/createSpriteMap.ts +1 -1
  45. package/src/primitives/utils/withScrolling.ts +4 -3
  46. package/src/render.ts +13 -12
  47. package/src/solidOpts.ts +41 -7
  48. package/src/primitives/LazyUp.tsx +0 -72
  49. package/src/primitives/jsx-runtime.d.ts +0 -8
@@ -0,0 +1,77 @@
1
+ import * as debug from '@solid-devtools/debugger/types';
2
+ import * as lng from '@lightningtv/core';
3
+
4
+ const EMPTY_CHILDREN: (lng.ElementNode | lng.ElementText)[] = [];
5
+
6
+ /**
7
+ * Implementation of the solid-devtools element interface for Lightning elements
8
+ */
9
+ export const elementInterface: debug.ElementInterface<
10
+ lng.ElementNode | lng.ElementText
11
+ > = {
12
+ isElement(node): node is lng.ElementNode | lng.ElementText {
13
+ return (
14
+ '_type' in node &&
15
+ (node._type === lng.NodeType.Element ||
16
+ node._type === lng.NodeType.TextNode)
17
+ );
18
+ },
19
+ getChildren(node) {
20
+ return node instanceof lng.ElementNode ? node.children : EMPTY_CHILDREN;
21
+ },
22
+ getName(node) {
23
+ return node._type === lng.NodeType.Element ? 'view' : 'text';
24
+ },
25
+ getParent(node) {
26
+ return node.parent ?? null;
27
+ },
28
+ getRect(node) {
29
+ let { width, height } = node;
30
+ let x = 0,
31
+ y = 0;
32
+
33
+ if (node.scaleX != null) width *= node.scaleX;
34
+ if (node.scaleY != null) height *= node.scaleY;
35
+
36
+ let curr = node as lng.ElementNode | undefined | null;
37
+ while (curr != null) {
38
+ x += curr.x;
39
+ y += curr.y;
40
+
41
+ if (curr.scaleX != null) {
42
+ x += (curr.width / 2) * (1 - curr.scaleX);
43
+ }
44
+ if (curr.scaleY != null) {
45
+ y += (curr.height / 2) * (1 - curr.scaleY);
46
+ }
47
+
48
+ curr = curr.parent;
49
+ }
50
+
51
+ if (lng.Config.rendererOptions != null) {
52
+ let dpr = lng.Config.rendererOptions.deviceLogicalPixelRatio;
53
+ if (dpr != null) {
54
+ x *= dpr;
55
+ y *= dpr;
56
+ width *= dpr;
57
+ height *= dpr;
58
+ }
59
+ }
60
+
61
+ return { x, y, width, height };
62
+ },
63
+ getElementAt(e) {
64
+ let target = e.target as any;
65
+ return target != null && target.element instanceof lng.ElementNode
66
+ ? target.element
67
+ : null;
68
+ },
69
+ getLocation(node) {
70
+ if (typeof node[debug.LOCATION_ATTRIBUTE_NAME] === 'string') {
71
+ return (
72
+ debug.parseLocationString(node[debug.LOCATION_ATTRIBUTE_NAME]) ?? null
73
+ );
74
+ }
75
+ return null;
76
+ },
77
+ };
package/src/index.ts CHANGED
@@ -1,4 +1,4 @@
1
- import './jsx-runtime.js';
1
+ export * from '@lightningtv/solid/jsx-runtime';
2
2
  export * from '@lightningtv/core';
3
3
  export type * from '@lightningtv/core';
4
4
  export type { KeyHandler, KeyMap } from '@lightningtv/core/focusManager';
@@ -1,5 +1,5 @@
1
1
  import { type Component } from 'solid-js';
2
- import { ElementNode, View, combineStyles, type NodeStyles } from '@lightningtv/solid';
2
+ import { ElementNode, combineStyles, type NodeStyles } from '@lightningtv/solid';
3
3
  import {
4
4
  handleNavigation,
5
5
  onGridFocus,
@@ -32,7 +32,7 @@ function scrollToIndex(this: ElementNode, index: number) {
32
32
 
33
33
  export const Column: Component<ColumnProps> = (props) => {
34
34
  return (
35
- <View
35
+ <view
36
36
  {...props}
37
37
  onUp={/* @once */ chainFunctions(props.onUp, onUp)}
38
38
  onDown={/* @once */ chainFunctions(props.onDown, onDown)}
@@ -1,4 +1,4 @@
1
- import { View, Text, type Stage, type NodeProps, RendererMain } from '@lightningtv/solid';
1
+ import { type Stage, type NodeProps, RendererMain } from '@lightningtv/solid';
2
2
  import { createSignal } from 'solid-js';
3
3
 
4
4
  const fpsStyle = {
@@ -90,90 +90,90 @@ export function setupFPS(root: any) {
90
90
 
91
91
  export const FPSCounter = (props: NodeProps) => {
92
92
  return (
93
- <View {...props} style={fpsStyle}>
94
- <View y={6}>
95
- <Text style={fpsLabel}>FPS:</Text>
96
- <Text style={fpsValue} x={90}>
93
+ <view {...props} style={fpsStyle}>
94
+ <view y={6}>
95
+ <text style={fpsLabel}>FPS:</text>
96
+ <text style={fpsValue} x={90}>
97
97
  {fps().toString()}
98
- </Text>
99
- </View>
98
+ </text>
99
+ </view>
100
100
 
101
- <View y={6} x={160}>
102
- <Text style={fpsLabel}>AVG:</Text>
103
- <Text style={fpsValue} x={100}>
101
+ <view y={6} x={160}>
102
+ <text style={fpsLabel}>AVG:</text>
103
+ <text style={fpsValue} x={100}>
104
104
  {avgFps().toString()}
105
- </Text>
106
- </View>
105
+ </text>
106
+ </view>
107
107
 
108
- <View x={0} y={26}>
109
- <Text style={fpsLabel}>MIN:</Text>
110
- <Text style={fpsValue} x={90}>
108
+ <view x={0} y={26}>
109
+ <text style={fpsLabel}>MIN:</text>
110
+ <text style={fpsValue} x={90}>
111
111
  {minFps().toString()}
112
- </Text>
113
- </View>
112
+ </text>
113
+ </view>
114
114
 
115
- <View x={160} y={26}>
116
- <Text style={fpsLabel}>MAX:</Text>
117
- <Text style={fpsValue} x={100}>
115
+ <view x={160} y={26}>
116
+ <text style={fpsLabel}>MAX:</text>
117
+ <text style={fpsValue} x={100}>
118
118
  {maxFps().toString()}
119
- </Text>
120
- </View>
119
+ </text>
120
+ </view>
121
121
 
122
- <View display="flex" flexDirection="column" y={58} gap={4}>
123
- <View height={infoFontSize}>
124
- <Text fontSize={infoFontSize} style={fpsLabel}>
122
+ <view display="flex" flexDirection="column" y={58} gap={4}>
123
+ <view height={infoFontSize}>
124
+ <text fontSize={infoFontSize} style={fpsLabel}>
125
125
  criticalThreshold:
126
- </Text>
127
- <Text fontSize={infoFontSize} style={fpsLabel} x={230}>
126
+ </text>
127
+ <text fontSize={infoFontSize} style={fpsLabel} x={230}>
128
128
  {criticalThresholdSignal()}
129
- </Text>
130
- </View>
129
+ </text>
130
+ </view>
131
131
 
132
- <View height={infoFontSize}>
133
- <Text fontSize={infoFontSize} style={fpsLabel}>
132
+ <view height={infoFontSize}>
133
+ <text fontSize={infoFontSize} style={fpsLabel}>
134
134
  targetThreshold:
135
- </Text>
136
- <Text fontSize={infoFontSize} style={fpsLabel} x={230}>
135
+ </text>
136
+ <text fontSize={infoFontSize} style={fpsLabel} x={230}>
137
137
  {targetThresholdSignal()}
138
- </Text>
139
- </View>
138
+ </text>
139
+ </view>
140
140
 
141
- <View height={infoFontSize}>
142
- <Text fontSize={infoFontSize} style={fpsLabel}>
141
+ <view height={infoFontSize}>
142
+ <text fontSize={infoFontSize} style={fpsLabel}>
143
143
  renderableMemUsed:
144
- </Text>
145
- <Text fontSize={infoFontSize} style={fpsLabel} x={230}>
144
+ </text>
145
+ <text fontSize={infoFontSize} style={fpsLabel} x={230}>
146
146
  {renderableMemUsedSignal()}
147
- </Text>
148
- </View>
147
+ </text>
148
+ </view>
149
149
 
150
- <View height={infoFontSize}>
151
- <Text fontSize={infoFontSize} style={fpsLabel}>
150
+ <view height={infoFontSize}>
151
+ <text fontSize={infoFontSize} style={fpsLabel}>
152
152
  memUsed:
153
- </Text>
154
- <Text fontSize={infoFontSize} style={fpsLabel} x={230}>
153
+ </text>
154
+ <text fontSize={infoFontSize} style={fpsLabel} x={230}>
155
155
  {memUsedSignal()}
156
- </Text>
157
- </View>
156
+ </text>
157
+ </view>
158
158
 
159
- <View height={infoFontSize}>
160
- <Text fontSize={infoFontSize} style={fpsLabel}>
159
+ <view height={infoFontSize}>
160
+ <text fontSize={infoFontSize} style={fpsLabel}>
161
161
  renderableTexturesLoaded:
162
- </Text>
163
- <Text fontSize={infoFontSize} style={fpsLabel} x={230}>
162
+ </text>
163
+ <text fontSize={infoFontSize} style={fpsLabel} x={230}>
164
164
  {renderableTexturesLoadedSignal().toString()}
165
- </Text>
166
- </View>
165
+ </text>
166
+ </view>
167
167
 
168
- <View height={infoFontSize}>
169
- <Text fontSize={infoFontSize} style={fpsLabel}>
168
+ <view height={infoFontSize}>
169
+ <text fontSize={infoFontSize} style={fpsLabel}>
170
170
  loadedTextures:
171
- </Text>
172
- <Text fontSize={infoFontSize} style={fpsLabel} x={230}>
171
+ </text>
172
+ <text fontSize={infoFontSize} style={fpsLabel} x={230}>
173
173
  {loadedTexturesSignal().toString()}
174
- </Text>
175
- </View>
176
- </View>
177
- </View>
174
+ </text>
175
+ </view>
176
+ </view>
177
+ </view>
178
178
  );
179
179
  };
@@ -0,0 +1,34 @@
1
+ import { ElementNode, NodeProps, View } from '@lightningtv/solid';
2
+ import { Show } from 'solid-js';
3
+
4
+ interface Props {
5
+ transition?: {
6
+ duration?: number;
7
+ easing?: string;
8
+ };
9
+ when?: boolean;
10
+ }
11
+
12
+ const DEFAULT_PROPS = {
13
+ duration: 250,
14
+ easing: 'ease-in-out',
15
+ };
16
+
17
+ export default function FadeInOut(props: Props & NodeProps) {
18
+ const config = Object.assign({}, DEFAULT_PROPS, props.transition);
19
+ function onCreate(elm: ElementNode) {
20
+ elm.alpha = 0;
21
+ elm.animate({ alpha: 1 }, { duration: config.duration, easing: config.easing }).start();
22
+ }
23
+
24
+ function onDestroy(elm: ElementNode) {
25
+ elm.rtt = true;
26
+ return elm.animate({ alpha: 0 }, { duration: config.duration, easing: config.easing })
27
+ .start().waitUntilStopped();
28
+ }
29
+
30
+ return (
31
+ <Show when={props.when} keyed>
32
+ <View {...props} onDestroy={onDestroy} onCreate={onCreate} />
33
+ </Show>);
34
+ }
@@ -1,25 +1,51 @@
1
- import { ValidComponent, For, createSignal, createMemo } from "solid-js";
2
- import { View, Dynamic, type NodeProps, type ElementNode, isFunction } from "@lightningtv/solid";
1
+ import { For, createSignal, createMemo, createEffect, JSX } from "solid-js";
2
+ import { type NodeProps, ElementNode, NewOmit } from "@lightningtv/solid";
3
+ import { chainRefs } from "./utils/chainFunctions.js";
3
4
 
4
- export const Grid = <T,>(props: {
5
- item: ValidComponent;
5
+ export interface GridItemProps<T> {
6
+ item: T
7
+ index: number
8
+ width: number
9
+ height: number
10
+ x: number
11
+ y: number
12
+ }
13
+
14
+ export interface GridProps<T> extends NewOmit<NodeProps, 'children'> {
15
+ items: readonly T[];
16
+ children: (props: GridItemProps<T>) => JSX.Element,
6
17
  itemHeight?: number;
7
18
  itemWidth?: number;
8
19
  itemOffset?: number;
9
- items: T[];
10
20
  columns?: number;
11
21
  looping?: boolean;
12
22
  scroll?: "auto" | "none";
13
23
  onSelectedChanged?: (index: number, grid: ElementNode, elm?: ElementNode) => void;
14
- } & NodeProps) => {
24
+ }
25
+
26
+ export function Grid<T>(props: GridProps<T>): JSX.Element {
27
+
15
28
  const [focusedIndex, setFocusedIndex] = createSignal(0);
16
29
  const baseColumns = 4;
17
30
 
31
+ const itemWidth = () => props.itemWidth ?? 300
32
+ const itemHeight = () => props.itemHeight ?? 300
33
+
18
34
  const columns = createMemo(() => props.columns || baseColumns);
19
- const totalWidth = createMemo(() => (props.itemWidth ?? 300) + (props.itemOffset ?? 0));
20
- const totalHeight = createMemo(() => (props.itemHeight ?? 300) + (props.itemOffset ?? 0));
35
+ const totalWidth = createMemo(() => itemWidth() + (props.itemOffset ?? 0));
36
+ const totalHeight = createMemo(() => itemHeight() + (props.itemOffset ?? 0));
37
+
38
+ function focus() {
39
+ const focusedElm = gridRef.children[focusedIndex()];
40
+ if (focusedElm instanceof ElementNode && !focusedElm.states.has('$focus')) {
41
+ focusedElm.setFocus();
42
+ props.onSelectedChanged?.call(gridRef, focusedIndex(), gridRef, focusedElm);
43
+ return true;
44
+ }
45
+ return false;
46
+ }
21
47
 
22
- const moveFocus = (delta: number, elm: ElementNode) => {
48
+ function moveFocus(delta: number) {
23
49
  if (!props.items || props.items.length === 0) return false;
24
50
  const newIndex = focusedIndex() + delta;
25
51
 
@@ -37,13 +63,10 @@ export const Grid = <T,>(props: {
37
63
  } else {
38
64
  return false;
39
65
  }
40
- const focusedElm = elm.children[focusedIndex()] as ElementNode;
41
- focusedElm.setFocus();
42
- isFunction(props.onSelectedChanged) && props.onSelectedChanged.call(elm, focusedIndex(), elm, focusedElm);
43
- return true;
66
+ return focus();
44
67
  };
45
68
 
46
- const handleHorizontalFocus = (delta: number, elm: ElementNode) => {
69
+ function handleHorizontalFocus(delta: number) {
47
70
  if (!props.items || props.items.length === 0) return false;
48
71
  const newIndex = focusedIndex() + delta;
49
72
  const isWithinRow = Math.floor(newIndex / columns()) === Math.floor(focusedIndex() / columns());
@@ -57,45 +80,46 @@ export const Grid = <T,>(props: {
57
80
  } else {
58
81
  return false;
59
82
  }
60
- const focusedElm = elm.children[focusedIndex()] as ElementNode;
61
- focusedElm.setFocus();
62
- isFunction(props.onSelectedChanged) && props.onSelectedChanged.call(elm, focusedIndex(), elm, focusedElm);
63
- return true;
83
+ return focus();
64
84
  };
65
85
 
66
- function onFocus(this: ElementNode) {
67
- handleHorizontalFocus(0, this);
68
- }
86
+ // Handle focus when items change - important for autofocus
87
+ createEffect(() => {
88
+ if (props.items && props.items.length > 0 && gridRef && gridRef.states.has('$focus')) {
89
+ queueMicrotask(focus)
90
+ }
91
+ })
69
92
 
70
93
  const scrollY = createMemo(() =>
71
94
  props.scroll === "none" ? props.y ?? 0 : -Math.floor(focusedIndex() / columns()) * totalHeight() + (props.y || 0)
72
95
  );
73
96
 
97
+ let gridRef!: ElementNode;
74
98
  return (
75
- <View
76
- transition={{ y: true }}
99
+ <view
77
100
  {...props}
78
- onUp={(_e, elm) => moveFocus(-columns(), elm)}
79
- onDown={(_e, elm) => moveFocus(columns(), elm)}
80
- onLeft={(_e, elm) => handleHorizontalFocus(-1, elm)}
81
- onRight={(_e, elm) => handleHorizontalFocus(1, elm)}
82
- onFocus={onFocus}
101
+ ref={chainRefs(el => gridRef = el, props.ref)}
102
+ transition={{ y: true }}
103
+ onUp={() => moveFocus(-columns())}
104
+ onDown={() => moveFocus(columns())}
105
+ onLeft={() => handleHorizontalFocus(-1)}
106
+ onRight={() => handleHorizontalFocus(1)}
107
+ onFocus={() => handleHorizontalFocus(0)}
83
108
  strictBounds={false}
84
109
  y={scrollY()}
85
110
  >
86
111
  <For each={props.items}>
87
112
  {(item, index) => (
88
- <Dynamic
89
- {...item}
90
- component={props.item}
91
- width={props.itemWidth}
92
- height={props.itemHeight}
113
+ <props.children
114
+ item={item}
115
+ index={index()}
116
+ width={itemWidth()}
117
+ height={itemHeight()}
93
118
  x={(index() % columns()) * totalWidth()}
94
119
  y={Math.floor(index() / columns()) * totalHeight()}
95
120
  />
96
121
  )}
97
122
  </For>
98
- </View>
123
+ </view>
99
124
  );
100
125
  };
101
-
@@ -33,7 +33,7 @@ function createLazy<T>(
33
33
 
34
34
  createEffect(() => setOffset(offset => Math.max(offset, (props.selected || 0) + 1)));
35
35
 
36
- if (!props.sync) {
36
+ if (!props.sync || props.eaglerLoad) {
37
37
  createEffect(() => {
38
38
  if (props.each) {
39
39
  const loadItems = () => {
@@ -31,7 +31,7 @@ function scrollToIndex(this: ElementNode, index: number) {
31
31
 
32
32
  export const Row: Component<RowProps> = (props) => {
33
33
  return (
34
- <View
34
+ <view
35
35
  {...props}
36
36
  selected={props.selected || 0}
37
37
  onLeft={/* @once */ chainFunctions(props.onLeft, onLeft)}
@@ -3,7 +3,6 @@ export * from './announcer/index.js';
3
3
  export * from './createInfiniteItems.js';
4
4
  export * from './useMouse.js';
5
5
  export * from './portal.jsx';
6
- export * from './LazyUp.jsx';
7
6
  export * from './Lazy.jsx';
8
7
  export * from './Visible.jsx';
9
8
  export * from './router.js';
@@ -11,11 +10,16 @@ export * from './Column.jsx';
11
10
  export * from './Row.jsx';
12
11
  export * from './Grid.jsx';
13
12
  export * from './FPSCounter.jsx';
13
+ export * from './FadeInOut.jsx';
14
14
  export * from './createFocusStack.jsx';
15
15
  export * from './marquee.jsx';
16
16
  export * from './useHold.js';
17
17
  export { withScrolling } from './utils/withScrolling.js';
18
- export { chainFunctions } from './utils/chainFunctions.js';
18
+ export {
19
+ type AnyFunction,
20
+ chainFunctions,
21
+ chainRefs,
22
+ } from './utils/chainFunctions.js';
19
23
  export { handleNavigation, onGridFocus } from './utils/handleNavigation.js';
20
24
  export { createSpriteMap, type SpriteDef } from './utils/createSpriteMap.js';
21
25
 
@@ -1,13 +1,29 @@
1
- type ChainableFunction = (...args: unknown[]) => unknown;
1
+ import * as s from 'solid-js';
2
2
 
3
- export function chainFunctions(...args: ChainableFunction[]): ChainableFunction;
4
- export function chainFunctions<T>(...args: (ChainableFunction | T)[]): T;
3
+ export type AnyFunction = (this: any, ...args: any[]) => any;
5
4
 
6
- // take an array of functions and if you return true from a function, it will stop the chain
7
- export function chainFunctions<T extends ChainableFunction>(
8
- ...args: (ChainableFunction | T)[]
9
- ) {
10
- const onlyFunctions = args.filter((func) => typeof func === 'function');
5
+ /**
6
+ * take an array of functions and if you return `true` from a function, it will stop the chain
7
+ * @param fns list of functions to chain together, can be `undefined`, `null`, or `false` to skip them
8
+ * @returns a function that will call each function in the list until one returns `true` or all functions are called.
9
+ * If no functions are provided, it will return `undefined`.
10
+ *
11
+ * @example
12
+ * ```tsx
13
+ * function Button (props: NodeProps) {
14
+ * function onEnter (el: ElementNode) {...}
15
+ * return <view onEnter={chainFunctions(props.onEnter, onEnter)} />
16
+ * }
17
+ * ```
18
+ */
19
+ export function chainFunctions<T extends AnyFunction>(...fns: T[]): T;
20
+ export function chainFunctions<T extends AnyFunction>(
21
+ ...fns: (T | undefined | null | false)[]
22
+ ): T | undefined;
23
+ export function chainFunctions(
24
+ ...fns: (AnyFunction | undefined | null | false)[]
25
+ ): AnyFunction | undefined {
26
+ const onlyFunctions = fns.filter((func) => typeof func === 'function');
11
27
  if (onlyFunctions.length === 0) {
12
28
  return undefined;
13
29
  }
@@ -16,7 +32,7 @@ export function chainFunctions<T extends ChainableFunction>(
16
32
  return onlyFunctions[0];
17
33
  }
18
34
 
19
- return function (this: unknown | T, ...innerArgs: unknown[]) {
35
+ return function (...innerArgs) {
20
36
  let result;
21
37
  for (const func of onlyFunctions) {
22
38
  result = func.apply(this, innerArgs);
@@ -27,3 +43,18 @@ export function chainFunctions<T extends ChainableFunction>(
27
43
  return result;
28
44
  };
29
45
  }
46
+
47
+ /**
48
+ * Utility for chaining multiple `ref` assignments with `props.ref` forwarding.
49
+ * @param refs list of ref setters. Can be a `props.ref` prop for ref forwarding or a setter to a local variable (`el => ref = el`).
50
+ * @example
51
+ * ```tsx
52
+ * function Button (props: NodeProps) {
53
+ * let localRef: ElementNode | undefined
54
+ * return <view ref={chainRefs(props.ref, el => localRef = el)} />
55
+ * }
56
+ * ```
57
+ */
58
+ export const chainRefs = chainFunctions as <T>(
59
+ ...refs: (s.Ref<T> | undefined)[]
60
+ ) => (el: T) => void;
@@ -1,4 +1,4 @@
1
- import { renderer, type TextureMap } from '@lightningtv/core';
1
+ import { type TextureMap, renderer } from '@lightningtv/core';
2
2
 
3
3
  export interface SpriteDef {
4
4
  name: string | number;
@@ -82,9 +82,10 @@ export function withScrolling(isRow: boolean) {
82
82
 
83
83
  // Allows manual position control
84
84
  const targetPosition = componentRef._targetPosition ?? componentRef[axis];
85
- const rootPosition = isIncrementing
86
- ? Math.min(targetPosition, componentRef[axis])
87
- : Math.max(targetPosition, componentRef[axis]);
85
+ const rootPosition =
86
+ isIncrementing || scroll === 'auto'
87
+ ? Math.min(targetPosition, componentRef[axis])
88
+ : Math.max(targetPosition, componentRef[axis]);
88
89
  componentRef.offset = componentRef.offset ?? rootPosition;
89
90
  const offset = componentRef.offset;
90
91
  selectedElement =
package/src/render.ts CHANGED
@@ -4,8 +4,8 @@ import {
4
4
  type NodeProps,
5
5
  type TextProps,
6
6
  startLightningRenderer,
7
- type RendererMain,
8
7
  type RendererMainSettings,
8
+ type RendererMain as IRendererMain,
9
9
  } from '@lightningtv/core';
10
10
  import nodeOpts from './solidOpts.js';
11
11
  import {
@@ -14,14 +14,15 @@ import {
14
14
  createRenderEffect,
15
15
  untrack,
16
16
  type JSXElement,
17
- type ValidComponent,
17
+ createRoot,
18
+ type Component,
18
19
  } from 'solid-js';
19
20
  import type { SolidNode } from './types.js';
20
21
  import { activeElement, setActiveElement } from './activeElement.js';
21
22
 
22
23
  const solidRenderer = solidCreateRenderer<SolidNode>(nodeOpts);
23
24
 
24
- let renderer: RendererMain;
25
+ let renderer: IRendererMain;
25
26
  export const rootNode = nodeOpts.createElement('App');
26
27
 
27
28
  const render = function (code: () => JSXElement) {
@@ -71,11 +72,13 @@ type Task = () => void;
71
72
  const taskQueue: Task[] = [];
72
73
  let tasksEnabled = false;
73
74
 
74
- createRenderEffect(() => {
75
- // should change whenever a keypress occurs, so we disable the task queue
76
- // until the renderer is idle again.
77
- activeElement();
78
- tasksEnabled = false;
75
+ createRoot(() => {
76
+ createRenderEffect(() => {
77
+ // should change whenever a keypress occurs, so we disable the task queue
78
+ // until the renderer is idle again.
79
+ activeElement();
80
+ tasksEnabled = false;
81
+ });
79
82
  });
80
83
 
81
84
  export function setTasksEnabled(enabled: boolean): void {
@@ -117,10 +120,8 @@ function processTasks(): void {
117
120
  * ```
118
121
  * @description https://www.solidjs.com/docs/latest/api#dynamic
119
122
  */
120
- export function Dynamic<T>(
121
- props: T & {
122
- component?: ValidComponent;
123
- },
123
+ export function Dynamic<T extends Record<string, any>>(
124
+ props: T & { component?: Component<T> | undefined | null },
124
125
  ): JSXElement {
125
126
  const [p, others] = splitProps(props, ['component']);
126
127