@lightningtv/solid 3.0.0-1 → 3.0.0-11

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 (87) hide show
  1. package/LICENSE +1 -1
  2. package/dist/src/devtools/index.d.ts +6 -0
  3. package/dist/src/devtools/index.js +65 -0
  4. package/dist/src/devtools/index.js.map +1 -0
  5. package/dist/src/index.d.ts +1 -1
  6. package/dist/src/index.js +1 -1
  7. package/dist/src/index.js.map +1 -1
  8. package/dist/src/jsx-runtime.d.ts +1 -3
  9. package/dist/src/primitives/Column.jsx +2 -2
  10. package/dist/src/primitives/Column.jsx.map +1 -1
  11. package/dist/src/primitives/FPSCounter.jsx +60 -61
  12. package/dist/src/primitives/FPSCounter.jsx.map +1 -1
  13. package/dist/src/primitives/Grid.d.ts +15 -6
  14. package/dist/src/primitives/Grid.jsx +36 -23
  15. package/dist/src/primitives/Grid.jsx.map +1 -1
  16. package/dist/src/primitives/Lazy.jsx +15 -11
  17. package/dist/src/primitives/Lazy.jsx.map +1 -1
  18. package/dist/src/primitives/LazyUp.jsx +1 -0
  19. package/dist/src/primitives/LazyUp.jsx.map +1 -1
  20. package/dist/src/primitives/Marquee.d.ts +64 -0
  21. package/dist/src/primitives/Marquee.jsx +86 -0
  22. package/dist/src/primitives/Marquee.jsx.map +1 -0
  23. package/dist/src/primitives/Preserve.d.ts +4 -0
  24. package/dist/src/primitives/Preserve.jsx +11 -0
  25. package/dist/src/primitives/Preserve.jsx.map +1 -0
  26. package/dist/src/primitives/Row.jsx +2 -2
  27. package/dist/src/primitives/Row.jsx.map +1 -1
  28. package/dist/src/primitives/Suspense.d.ts +23 -0
  29. package/dist/src/primitives/Suspense.jsx +34 -0
  30. package/dist/src/primitives/Suspense.jsx.map +1 -0
  31. package/dist/src/primitives/announcer/announcer.d.ts +1 -0
  32. package/dist/src/primitives/announcer/announcer.js +6 -5
  33. package/dist/src/primitives/announcer/announcer.js.map +1 -1
  34. package/dist/src/primitives/announcer/index.d.ts +5 -1
  35. package/dist/src/primitives/announcer/index.js +8 -2
  36. package/dist/src/primitives/announcer/index.js.map +1 -1
  37. package/dist/src/primitives/announcer/speech.d.ts +2 -2
  38. package/dist/src/primitives/announcer/speech.js +67 -28
  39. package/dist/src/primitives/announcer/speech.js.map +1 -1
  40. package/dist/src/primitives/index.d.ts +5 -1
  41. package/dist/src/primitives/index.js +5 -1
  42. package/dist/src/primitives/index.js.map +1 -1
  43. package/dist/src/primitives/types.d.ts +2 -0
  44. package/dist/src/primitives/useHold.d.ts +27 -0
  45. package/dist/src/primitives/useHold.js +54 -0
  46. package/dist/src/primitives/useHold.js.map +1 -0
  47. package/dist/src/primitives/utils/chainFunctions.d.ts +30 -4
  48. package/dist/src/primitives/utils/chainFunctions.js +14 -3
  49. package/dist/src/primitives/utils/chainFunctions.js.map +1 -1
  50. package/dist/src/primitives/utils/createSpriteMap.js.map +1 -1
  51. package/dist/src/primitives/utils/handleNavigation.js +11 -2
  52. package/dist/src/primitives/utils/handleNavigation.js.map +1 -1
  53. package/dist/src/primitives/utils/withScrolling.d.ts +3 -0
  54. package/dist/src/primitives/utils/withScrolling.js +14 -1
  55. package/dist/src/primitives/utils/withScrolling.js.map +1 -1
  56. package/dist/src/render.d.ts +5 -5
  57. package/dist/src/render.js +8 -6
  58. package/dist/src/render.js.map +1 -1
  59. package/dist/src/solidOpts.d.ts +7 -0
  60. package/dist/src/solidOpts.js +39 -7
  61. package/dist/src/solidOpts.js.map +1 -1
  62. package/dist/tsconfig.tsbuildinfo +1 -1
  63. package/{src/jsx-runtime.ts → jsx-runtime.d.ts} +3 -3
  64. package/package.json +23 -8
  65. package/src/devtools/index.ts +77 -0
  66. package/src/index.ts +1 -1
  67. package/src/primitives/Column.tsx +2 -2
  68. package/src/primitives/FPSCounter.tsx +61 -61
  69. package/src/primitives/Grid.tsx +59 -35
  70. package/src/primitives/Lazy.tsx +21 -12
  71. package/src/primitives/Marquee.tsx +149 -0
  72. package/src/primitives/Preserve.tsx +18 -0
  73. package/src/primitives/Row.tsx +1 -1
  74. package/src/primitives/Suspense.tsx +41 -0
  75. package/src/primitives/announcer/announcer.ts +9 -10
  76. package/src/primitives/announcer/index.ts +12 -2
  77. package/src/primitives/announcer/speech.ts +82 -28
  78. package/src/primitives/index.ts +9 -1
  79. package/src/primitives/types.ts +9 -0
  80. package/src/primitives/useHold.ts +69 -0
  81. package/src/primitives/utils/chainFunctions.ts +40 -9
  82. package/src/primitives/utils/createSpriteMap.ts +2 -2
  83. package/src/primitives/utils/handleNavigation.ts +11 -2
  84. package/src/primitives/utils/withScrolling.ts +25 -2
  85. package/src/render.ts +12 -12
  86. package/src/solidOpts.ts +51 -7
  87. package/src/primitives/jsx-runtime.d.ts +0 -8
@@ -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
  };
@@ -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
-
@@ -8,8 +8,8 @@ import {
8
8
  type ValidComponent,
9
9
  untrack,
10
10
  type Accessor,
11
- } from 'solid-js';
12
- import { Dynamic, type NewOmit, scheduleTask, type NodeProps } from '@lightningtv/solid';
11
+ } from 'solid-js'; // Dynamic removed
12
+ import { type NewOmit, scheduleTask, type NodeProps, Dynamic } from '@lightningtv/solid'; // Dynamic removed from imports
13
13
  import { Row, Column } from '@lightningtv/solid/primitives';
14
14
 
15
15
  type LazyProps<T extends readonly any[]> = NewOmit<NodeProps, 'children'> & {
@@ -28,26 +28,23 @@ function createLazy<T>(
28
28
  keyHandler: (updateOffset: () => void) => Record<string, () => void>
29
29
  ) {
30
30
  // Need at least one item so it can be focused
31
- const [offset, setOffset] = createSignal(1);
31
+ const [offset, setOffset] = createSignal<number>(props.sync ? props.upCount : 1);
32
32
  let timeoutId: ReturnType<typeof setTimeout> | null = null;
33
33
 
34
- createEffect(() => setOffset(props.selected || 1));
34
+ createEffect(() => setOffset(offset => Math.max(offset, (props.selected || 0) + 1)));
35
35
 
36
- if (props.sync) {
37
- setOffset(props.upCount);
38
- } else {
36
+ if (!props.sync || props.eaglerLoad) {
39
37
  createEffect(() => {
40
38
  if (props.each) {
41
- let count = untrack(offset);
42
-
43
39
  const loadItems = () => {
40
+ let count = untrack(offset);
44
41
  if (count < props.upCount) {
45
42
  setOffset(count + 1);
46
43
  timeoutId = setTimeout(loadItems, 16); // ~60fps
47
44
  count++;
48
45
  } else if (props.eagerLoad) {
49
46
  const maxOffset = props.each ? props.each.length : 0;
50
- if (offset() > maxOffset) return;
47
+ if (offset() >= maxOffset) return;
51
48
  setOffset((prev) => Math.min(prev + 1, maxOffset));
52
49
  scheduleTask(loadItems);
53
50
  }
@@ -57,13 +54,25 @@ function createLazy<T>(
57
54
  });
58
55
  }
59
56
 
60
- const items = createMemo(() => (Array.isArray(props.each) ? props.each.slice(0, offset()) : []));
57
+ const items = createMemo(() => (
58
+ Array.isArray(props.each) ? props.each.slice(0, offset()) : [])
59
+ );
61
60
 
62
61
  const updateOffset = () => {
63
62
  const maxOffset = props.each ? props.each.length : 0;
64
63
  if (offset() >= maxOffset) return;
65
64
 
66
- if (timeoutId) clearTimeout(timeoutId);
65
+ if (!props.delay) {
66
+ setOffset((prev) => Math.min(prev + 1, maxOffset));
67
+ return;
68
+ }
69
+
70
+ if (timeoutId) {
71
+ clearTimeout(timeoutId);
72
+ //Moving faster than the delay so need to go sync
73
+ setOffset((prev) => Math.min(prev + 1, maxOffset));
74
+ }
75
+
67
76
  timeoutId = setTimeout(() => {
68
77
  setOffset((prev) => Math.min(prev + 1, maxOffset));
69
78
  timeoutId = null;
@@ -0,0 +1,149 @@
1
+ import * as s from 'solid-js'
2
+ import * as lng from '@lightningtv/solid'
3
+ import { chainFunctions } from './utils/chainFunctions.js'
4
+
5
+ export interface MarqueeAnimationProps {
6
+ /** delay in ms between animations, @default 1000 */
7
+ delay?: number
8
+ /** pixels per second, @default 200 */
9
+ speed?: number
10
+ /**
11
+ * distance between the end of the text and the start of the next animation
12
+ * @default `0.5 * clipWidth`
13
+ */
14
+ scrollGap?: number
15
+ /** easing function, @default 'linear' */
16
+ easing?: string
17
+ }
18
+
19
+ export interface MarqueeControlProps {
20
+ /** whether to scroll the text or show it contained */
21
+ marquee: boolean
22
+ }
23
+
24
+ export interface MarqueeTextProps
25
+ extends lng.TextProps, MarqueeControlProps, MarqueeAnimationProps {
26
+ /** width of the container */
27
+ clipWidth: number
28
+ }
29
+
30
+ export interface MarqueeProps
31
+ extends lng.NewOmit<lng.NodeProps, 'children'>, MarqueeControlProps, MarqueeAnimationProps {
32
+ textProps?: lng.TextProps
33
+ children: lng.TextProps['children']
34
+ }
35
+
36
+ const SAFETY_MARGIN = 10
37
+
38
+ /**
39
+ * MarqueeText is a component that scrolls text when it overflows the container.
40
+ *
41
+ * @example
42
+ * ```tsx
43
+ * <view width={400} height={28} clipping>
44
+ * <MarqueeText
45
+ * clipWidth={400}
46
+ * marquee={inFocus()}
47
+ * speed={200}
48
+ * delay={1000}
49
+ * scrollGap={24}
50
+ * easing='ease-in-out'
51
+ * >
52
+ * This is a long text that will scroll when it overflows the container.
53
+ * </MarqueeText>
54
+ * </view>
55
+ */
56
+ export function MarqueeText(props: MarqueeTextProps) {
57
+
58
+ const speed = s.createMemo(() => props.speed || 200)
59
+ const delay = s.createMemo(() => props.delay ?? 1000)
60
+ const scrollGap = s.createMemo(() => props.scrollGap ?? (props.clipWidth * 0.5))
61
+
62
+ const [textWidth, setTextWidth] = s.createSignal(0)
63
+
64
+ const isTextOverflowing = s.createMemo(() => textWidth() > props.clipWidth - SAFETY_MARGIN)
65
+ const shouldScroll = s.createMemo(() => props.marquee && isTextOverflowing())
66
+
67
+ const wasFocusedBefore = s.createMemo<boolean>(p => p || props.marquee, false)
68
+
69
+ s.createEffect(() => {
70
+ if (shouldScroll()) {
71
+
72
+ let options: lng.AnimationSettings = {
73
+ duration: (textWidth() + scrollGap()) / speed() * 1000,
74
+ delay: delay(),
75
+ loop: true,
76
+ easing: props.easing,
77
+ }
78
+
79
+ text1.lng.x = 0
80
+ text2.lng.x = textWidth() + scrollGap()
81
+
82
+ let a1 = text1.lng.animate!({x: -textWidth() -scrollGap()}, options).start()
83
+ let a2 = text2.lng.animate!({x: 0}, options).start()
84
+
85
+ s.onCleanup(() => {
86
+ a1.stop()
87
+ a2.stop()
88
+ })
89
+ }
90
+ })
91
+
92
+ const events = {loaded(el: lng.ElementNode) {setTextWidth(el.width)}}
93
+
94
+ let text1!: lng.ElementNode
95
+ let text2!: lng.ElementNode
96
+ return (
97
+ <>
98
+ {wasFocusedBefore() && <>
99
+ <text {...props} ref={text1} hidden={!shouldScroll()} rtt maxLines={1} onEvent={events} />
100
+ <text {...props} ref={text2} hidden={!shouldScroll()} rtt maxLines={1} />
101
+ </>}
102
+ <text {...props} maxLines={1} hidden={shouldScroll()} contain='width' />
103
+ </>
104
+ )
105
+ }
106
+
107
+ /**
108
+ * Marquee is a component that scrolls text when it overflows the container.
109
+ * It uses the {@link MarqueeText} component to do the actual scrolling.
110
+ *
111
+ * @example
112
+ * ```tsx
113
+ * <Marquee
114
+ * width={400}
115
+ * marquee={inFocus()}
116
+ * easing='ease-in-out'
117
+ * textProps={{
118
+ * fontSize: 28,
119
+ * }}
120
+ * >
121
+ * This is a long text that will scroll when it overflows the container.
122
+ * </Marquee>
123
+ * ```
124
+ */
125
+ export function Marquee(props: MarqueeProps) {
126
+ const [clipWidth, setClipWidth] = s.createSignal(props.width || 0);
127
+ const clipHeight = s.createMemo(() => props.height || props.textProps?.lineHeight || ((props.textProps?.fontSize || 16) * 1.5));
128
+
129
+ return (
130
+ <view
131
+ {...props}
132
+ height={clipHeight()}
133
+ onLayout={/* @once */ chainFunctions(props.onLayout, (e: lng.ElementNode) => setClipWidth(e.width))}
134
+ clipping={props.marquee}
135
+ >
136
+ <MarqueeText
137
+ {...props.textProps}
138
+ marquee={props.marquee}
139
+ clipWidth={clipWidth()}
140
+ speed={props.speed}
141
+ delay={props.delay}
142
+ scrollGap={props.scrollGap}
143
+ easing={props.easing}
144
+ >
145
+ {props.children}
146
+ </MarqueeText>
147
+ </view>
148
+ )
149
+ }
@@ -0,0 +1,18 @@
1
+ import * as s from 'solid-js'
2
+ import * as lng from '@lightningtv/solid'
3
+
4
+ function Preserve(props: lng.NodeProps): s.JSX.Element {
5
+
6
+ let view = <view {...props} /> as any as lng.ElementNode
7
+
8
+ view.preserve = true;
9
+
10
+ view.onRender ??= () => {view.hidden = false}
11
+ view.onRemove ??= () => {view.hidden = true}
12
+
13
+ s.onCleanup(() => {view.destroy()})
14
+
15
+ return view as any as s.JSX.Element
16
+ }
17
+
18
+ export default Preserve;
@@ -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)}
@@ -0,0 +1,41 @@
1
+ import * as s from 'solid-js';
2
+
3
+ /**
4
+ * Tracks all resources inside a component and renders a fallback until they are all resolved.
5
+ *
6
+ * ```tsx
7
+ * const [data] = createResource(async () => ...);
8
+ *
9
+ * <Suspense fallback={<LoadingIndicator />}>
10
+ * <view>
11
+ * <text>{data()}</text>
12
+ * </view>
13
+ * </Suspense>
14
+ * ```
15
+ *
16
+ * This is a modified version of the SolidJS Suspense component that works with Lightning.
17
+ *
18
+ * @see https://docs.solidjs.com/reference/components/suspense
19
+ */
20
+ function Suspense(props: {
21
+ fallback?: s.JSX.Element;
22
+ children: s.JSX.Element;
23
+ }): s.JSX.Element {
24
+
25
+ let children: s.JSX.Element;
26
+
27
+ let suspense = s.Suspense({
28
+ get children() {
29
+ return [children = s.children(() => props.children) as any];
30
+ },
31
+ }) as any as () => s.JSX.Element;
32
+
33
+ return <>
34
+ {suspense() ?? props.fallback}
35
+ <view hidden>
36
+ {suspense() ? null : children}
37
+ </view>
38
+ </>
39
+ }
40
+
41
+ export default Suspense;
@@ -73,8 +73,9 @@ function onFocusChangeCore(focusPath: ElementNode[] = []) {
73
73
  prevFocusPath = focusPath.slice(0);
74
74
 
75
75
  const toAnnounceText: SpeechType[] = [];
76
- const toAnnounce = focusDiff.reduce(
77
- (acc: [string, string, SpeechType][], elm) => {
76
+ const toAnnounce = focusDiff
77
+ .reverse()
78
+ .reduce((acc: [string, string, SpeechType][], elm) => {
78
79
  if (elm.announce) {
79
80
  acc.push([getElmName(elm), 'Announce', elm.announce]);
80
81
  toAnnounceText.push(elm.announce);
@@ -85,9 +86,7 @@ function onFocusChangeCore(focusPath: ElementNode[] = []) {
85
86
  acc.push([getElmName(elm), 'No Announce', '']);
86
87
  }
87
88
  return acc;
88
- },
89
- [],
90
- );
89
+ }, []);
91
90
 
92
91
  focusDiff.reverse().reduce((acc, elm) => {
93
92
  if (elm.announceContext) {
@@ -110,18 +109,19 @@ function onFocusChangeCore(focusPath: ElementNode[] = []) {
110
109
  }
111
110
  }
112
111
 
113
- function textToSpeech(toSpeak: SpeechType, lang: string) {
112
+ function textToSpeech(toSpeak: SpeechType, lang: string, voice?: string) {
114
113
  if (voiceOutDisabled) {
115
114
  return;
116
115
  }
117
116
 
118
- return (currentlySpeaking = SpeechEngine(toSpeak, lang));
117
+ return (currentlySpeaking = SpeechEngine(toSpeak, lang, voice));
119
118
  }
120
119
 
121
120
  export interface Announcer {
122
121
  debug: boolean;
123
122
  enabled: boolean;
124
123
  lang: string;
124
+ voice?: string;
125
125
  cancel: VoidFunction;
126
126
  clearPrevFocus: (depth?: number) => void;
127
127
  speak: (
@@ -147,14 +147,13 @@ export const Announcer: Announcer = {
147
147
  prevFocusPath = prevFocusPath.slice(0, depth);
148
148
  resetFocusPathTimer();
149
149
  },
150
- speak: function (text, { append = false, notification = false} = {}) {
150
+ speak: function (text, { append = false, notification = false } = {}) {
151
151
  if (Announcer.onFocusChange && Announcer.enabled) {
152
- Announcer.onFocusChange.flush();
153
152
  if (append && currentlySpeaking && currentlySpeaking.active) {
154
153
  currentlySpeaking.append(text);
155
154
  } else {
156
155
  Announcer.cancel();
157
- textToSpeech(text, Announcer.lang);
156
+ textToSpeech(text, Announcer.lang, Announcer.voice);
158
157
  }
159
158
 
160
159
  if (notification) {
@@ -2,9 +2,19 @@ import { createEffect, on } from 'solid-js';
2
2
  import { Announcer } from './announcer.js';
3
3
  import { focusPath } from '../useFocusManager.js';
4
4
 
5
- export const useAnnouncer = () => {
6
- Announcer.setupTimers();
5
+ let doOnce = false;
6
+ export const useAnnouncer = (options?: {
7
+ focusDebounce?: number;
8
+ focusChangeTimeout?: number;
9
+ }) => {
10
+ if (doOnce) {
11
+ return Announcer;
12
+ }
13
+ doOnce = true;
14
+ Announcer.setupTimers(options);
7
15
  createEffect(on(focusPath, Announcer.onFocusChange!, { defer: true }));
8
16
 
9
17
  return Announcer;
10
18
  };
19
+
20
+ export { Announcer };