@lightningtv/solid 3.0.0-19 → 3.0.0-20

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 (36) hide show
  1. package/dist/src/primitives/Lazy.d.ts +1 -0
  2. package/dist/src/primitives/Lazy.jsx +14 -1
  3. package/dist/src/primitives/Lazy.jsx.map +1 -1
  4. package/dist/src/primitives/Virtual.jsx +7 -1
  5. package/dist/src/primitives/Virtual.jsx.map +1 -1
  6. package/dist/src/primitives/announcer/announcer.d.ts +1 -0
  7. package/dist/src/primitives/announcer/announcer.js +4 -3
  8. package/dist/src/primitives/announcer/announcer.js.map +1 -1
  9. package/dist/src/primitives/announcer/speech.d.ts +1 -1
  10. package/dist/src/primitives/announcer/speech.js +98 -8
  11. package/dist/src/primitives/announcer/speech.js.map +1 -1
  12. package/dist/src/primitives/createTag.d.ts +8 -0
  13. package/dist/src/primitives/createTag.jsx +20 -0
  14. package/dist/src/primitives/createTag.jsx.map +1 -0
  15. package/dist/src/primitives/index.d.ts +2 -0
  16. package/dist/src/primitives/index.js +2 -0
  17. package/dist/src/primitives/index.js.map +1 -1
  18. package/dist/src/primitives/useMouse.d.ts +18 -1
  19. package/dist/src/primitives/useMouse.js +142 -59
  20. package/dist/src/primitives/useMouse.js.map +1 -1
  21. package/dist/src/primitives/utils/createBlurredImage.d.ts +56 -0
  22. package/dist/src/primitives/utils/createBlurredImage.js +223 -0
  23. package/dist/src/primitives/utils/createBlurredImage.js.map +1 -0
  24. package/dist/src/primitives/utils/handleNavigation.js +7 -13
  25. package/dist/src/primitives/utils/handleNavigation.js.map +1 -1
  26. package/dist/tsconfig.tsbuildinfo +1 -1
  27. package/package.json +8 -5
  28. package/src/primitives/Lazy.tsx +15 -3
  29. package/src/primitives/Virtual.tsx +8 -1
  30. package/src/primitives/announcer/announcer.ts +10 -3
  31. package/src/primitives/announcer/speech.ts +113 -6
  32. package/src/primitives/createTag.tsx +31 -0
  33. package/src/primitives/index.ts +2 -0
  34. package/src/primitives/useMouse.ts +253 -81
  35. package/src/primitives/utils/createBlurredImage.ts +366 -0
  36. package/src/primitives/utils/handleNavigation.ts +9 -14
@@ -0,0 +1,366 @@
1
+ import { type Accessor, type Resource, createResource } from 'solid-js';
2
+
3
+ /**
4
+ * Represents a valid image source that can be used for blurring
5
+ */
6
+ type ImageSource = string | URL;
7
+
8
+ /**
9
+ * Represents a valid image source or null/undefined
10
+ */
11
+ type NullableImageSource = ImageSource | null | undefined;
12
+
13
+ /**
14
+ * Configuration options for Gaussian blur operation
15
+ */
16
+ interface BlurOptions {
17
+ /**
18
+ * The blur radius in pixels
19
+ * @default 10
20
+ */
21
+ readonly radius?: number;
22
+ /**
23
+ * CORS setting for image loading
24
+ * @default 'anonymous'
25
+ */
26
+ readonly crossOrigin?: 'anonymous' | 'use-credentials' | '';
27
+ /**
28
+ * The resolution of the output image in pixels
29
+ * @default 1
30
+ */
31
+ readonly resolution?: number;
32
+ }
33
+
34
+ /**
35
+ * Default blur options
36
+ */
37
+ const DEFAULT_BLUR_OPTIONS: Required<
38
+ Pick<BlurOptions, 'radius' | 'crossOrigin' | 'resolution'>
39
+ > = {
40
+ radius: 10,
41
+ crossOrigin: 'anonymous',
42
+ resolution: 1,
43
+ } as const;
44
+
45
+ /**
46
+ * Type for Gaussian kernel array
47
+ * Represents a normalized array of weights
48
+ */
49
+ type GaussianKernel = readonly number[];
50
+
51
+ /**
52
+ * Type for image dimensions
53
+ */
54
+ interface ImageDimensions {
55
+ readonly width: number;
56
+ readonly height: number;
57
+ }
58
+
59
+ /**
60
+ * Type for the resource return value from createBlurredImage
61
+ */
62
+ type BlurredImageResource<T extends NullableImageSource> = Resource<
63
+ T extends null | undefined ? null : string
64
+ >;
65
+
66
+ /**
67
+ * Ensures a number is within valid range
68
+ */
69
+ type ValidRadius = number & { __brand: 'ValidRadius' };
70
+
71
+ /**
72
+ * Validates that radius is a positive number
73
+ */
74
+ function isValidRadius(radius: number): radius is ValidRadius {
75
+ return radius > 0 && Number.isFinite(radius);
76
+ }
77
+
78
+ /**
79
+ * Ensures a resolution is a positive number
80
+ */
81
+ function isValidResolution(resolution: number): boolean {
82
+ return resolution > 0 && resolution <= 1 && Number.isFinite(resolution);
83
+ }
84
+
85
+ /**
86
+ * Applies vertical Gaussian blur to image data
87
+ * @param input - Input pixel data
88
+ * @param output - Output pixel data buffer
89
+ * @param width - Image width
90
+ * @param height - Image height
91
+ * @param kernel - Gaussian kernel weights
92
+ * @param half - Half of kernel size
93
+ */
94
+ function applyVerticalBlur(
95
+ input: Readonly<Uint8ClampedArray>,
96
+ output: Uint8ClampedArray,
97
+ width: number,
98
+ height: number,
99
+ kernel: Readonly<GaussianKernel>,
100
+ half: number,
101
+ ): void {
102
+ for (let y = 0; y < height; y++) {
103
+ for (let x = 0; x < width; x++) {
104
+ let r = 0,
105
+ g = 0,
106
+ b = 0,
107
+ a = 0;
108
+ let weightSum = 0;
109
+
110
+ for (let ky = -half; ky <= half; ky++) {
111
+ const py = y + ky;
112
+ if (py >= 0 && py < height) {
113
+ const pixelIndex = (py * width + x) * 4;
114
+ const weight = kernel[ky + half]!;
115
+
116
+ r += input[pixelIndex]! * weight;
117
+ g += input[pixelIndex + 1]! * weight;
118
+ b += input[pixelIndex + 2]! * weight;
119
+ a += input[pixelIndex + 3]! * weight;
120
+ weightSum += weight;
121
+ }
122
+ }
123
+
124
+ const outputIndex = (y * width + x) * 4;
125
+ output[outputIndex] = r / weightSum;
126
+ output[outputIndex + 1] = g / weightSum;
127
+ output[outputIndex + 2] = b / weightSum;
128
+ output[outputIndex + 3] = a / weightSum;
129
+ }
130
+ }
131
+ }
132
+
133
+ /**
134
+ * Applies horizontal Gaussian blur to image data
135
+ * @param input - Input pixel data
136
+ * @param output - Output pixel data buffer
137
+ * @param width - Image width
138
+ * @param height - Image height
139
+ * @param kernel - Gaussian kernel weights
140
+ * @param half - Half of kernel size
141
+ */
142
+ function applyHorizontalBlur(
143
+ input: Readonly<Uint8ClampedArray>,
144
+ output: Uint8ClampedArray,
145
+ width: number,
146
+ height: number,
147
+ kernel: Readonly<GaussianKernel>,
148
+ half: number,
149
+ ): void {
150
+ for (let y = 0; y < height; y++) {
151
+ for (let x = 0; x < width; x++) {
152
+ let r = 0,
153
+ g = 0,
154
+ b = 0,
155
+ a = 0;
156
+ let weightSum = 0;
157
+
158
+ for (let kx = -half; kx <= half; kx++) {
159
+ const px = x + kx;
160
+ if (px >= 0 && px < width) {
161
+ const pixelIndex = (y * width + px) * 4;
162
+ const weight = kernel[kx + half]!;
163
+
164
+ r += input[pixelIndex]! * weight;
165
+ g += input[pixelIndex + 1]! * weight;
166
+ b += input[pixelIndex + 2]! * weight;
167
+ a += input[pixelIndex + 3]! * weight;
168
+ weightSum += weight;
169
+ }
170
+ }
171
+
172
+ const outputIndex = (y * width + x) * 4;
173
+ output[outputIndex] = r / weightSum;
174
+ output[outputIndex + 1] = g / weightSum;
175
+ output[outputIndex + 2] = b / weightSum;
176
+ output[outputIndex + 3] = a / weightSum;
177
+ }
178
+ }
179
+ }
180
+
181
+ /**
182
+ * Generates a normalized Gaussian kernel
183
+ * @param size - Kernel size (must be odd)
184
+ * @param sigma - Standard deviation
185
+ * @returns Normalized Gaussian kernel
186
+ */
187
+ function generateGaussianKernel(
188
+ size: number,
189
+ sigma: number,
190
+ ): Readonly<GaussianKernel> {
191
+ const kernel: number[] = [];
192
+ const half = Math.floor(size / 2);
193
+ let sum = 0;
194
+
195
+ for (let i = -half; i <= half; i++) {
196
+ const value = Math.exp(-(i * i) / (2 * sigma * sigma));
197
+ kernel.push(value);
198
+ sum += value;
199
+ }
200
+
201
+ return Object.freeze(kernel.map((value) => value / sum));
202
+ }
203
+
204
+ /**
205
+ * Applies Gaussian blur convolution to image data
206
+ * @param imageData - Source image data
207
+ * @param dimensions - Image dimensions
208
+ * @param radius - Blur radius
209
+ * @returns Blurred image data
210
+ */
211
+ function gaussianBlurConvolution(
212
+ imageData: Readonly<ImageData>,
213
+ dimensions: Readonly<ImageDimensions>,
214
+ radius: ValidRadius,
215
+ ): ImageData {
216
+ const { data } = imageData;
217
+ const { width, height } = dimensions;
218
+ const output = new Uint8ClampedArray(data.length);
219
+
220
+ const kernelSize = Math.ceil(radius * 2) * 2 + 1;
221
+ const kernel = generateGaussianKernel(kernelSize, radius);
222
+ const half = Math.floor(kernelSize / 2);
223
+
224
+ applyHorizontalBlur(data, output, width, height, kernel, half);
225
+
226
+ const tempData = new Uint8ClampedArray(output);
227
+ applyVerticalBlur(tempData, output, width, height, kernel, half);
228
+
229
+ return new ImageData(output, width, height);
230
+ }
231
+
232
+ /**
233
+ * Applies Gaussian blur to an image URL
234
+ * @param imageUrl - Image source (string or URL)
235
+ * @param options - Blur configuration options
236
+ * @returns Promise resolving to data URL of blurred image
237
+ * @throws {Error} If image fails to load or blur operation fails
238
+ */
239
+ export async function applyGaussianBlur<TSource extends ImageSource>(
240
+ imageUrl: TSource,
241
+ options?: Readonly<BlurOptions>,
242
+ ): Promise<string> {
243
+ const opts = { ...DEFAULT_BLUR_OPTIONS, ...options };
244
+ const radius = opts.radius;
245
+ const resolution = opts.resolution;
246
+
247
+ if (!isValidRadius(radius)) {
248
+ throw new Error(
249
+ `Invalid blur radius: ${radius}. Must be a positive number.`,
250
+ );
251
+ }
252
+
253
+ if (!isValidResolution(resolution)) {
254
+ throw new Error(
255
+ `Invalid resolution: ${resolution}. Must be a number between 0 and 1.`,
256
+ );
257
+ }
258
+
259
+ return new Promise<string>((resolve, reject) => {
260
+ const img = new Image();
261
+ img.crossOrigin = opts.crossOrigin;
262
+
263
+ img.onload = (): void => {
264
+ try {
265
+ const canvas = document.createElement('canvas');
266
+ const ctx = canvas.getContext('2d', {
267
+ willReadFrequently: true,
268
+ });
269
+
270
+ if (ctx === null) {
271
+ reject(new Error('Failed to get canvas context'));
272
+ return;
273
+ }
274
+
275
+ const scaledWidth = Math.max(1, Math.round(img.width * resolution));
276
+ const scaledHeight = Math.max(1, Math.round(img.height * resolution));
277
+
278
+ const dimensions: ImageDimensions = {
279
+ width: scaledWidth,
280
+ height: scaledHeight,
281
+ };
282
+
283
+ canvas.width = dimensions.width;
284
+ canvas.height = dimensions.height;
285
+
286
+ const hasFilterSupport = ctx.filter !== undefined;
287
+ if (hasFilterSupport) {
288
+ ctx.filter = `blur(${radius}px)`;
289
+ ctx.drawImage(img, 0, 0, scaledWidth, scaledHeight);
290
+ ctx.filter = 'none';
291
+ } else {
292
+ ctx.drawImage(img, 0, 0, scaledWidth, scaledHeight);
293
+ const imageData = ctx.getImageData(
294
+ 0,
295
+ 0,
296
+ dimensions.width,
297
+ dimensions.height,
298
+ );
299
+ const blurredData = gaussianBlurConvolution(
300
+ imageData,
301
+ dimensions,
302
+ radius,
303
+ );
304
+ ctx.putImageData(blurredData, 0, 0);
305
+ }
306
+
307
+ const dataUrl = canvas.toDataURL();
308
+
309
+ if (dataUrl) {
310
+ resolve(dataUrl);
311
+ } else {
312
+ reject(new Error('Failed to create image data URL'));
313
+ }
314
+ } catch (error) {
315
+ reject(
316
+ error instanceof Error
317
+ ? error
318
+ : new Error('Unknown error during blur operation'),
319
+ );
320
+ }
321
+ };
322
+
323
+ img.onerror = (): void => {
324
+ reject(new Error('Failed to load image'));
325
+ };
326
+
327
+ const srcString: string =
328
+ typeof imageUrl === 'string' ? imageUrl : imageUrl.toString();
329
+ img.src = srcString;
330
+ });
331
+ }
332
+
333
+ /**
334
+ * Hook to create a blurred image resource
335
+ * @param imageUrl - Accessor function returning image source or null/undefined
336
+ * @param options - Blur configuration options
337
+ * @returns Resource containing blurred image data URL or null
338
+ * @template TSource - Type of image source (string, URL, or null/undefined)
339
+ *
340
+ * @example
341
+ * ```ts
342
+ * const imageUrl = () => 'https://example.com/image.jpg';
343
+ * const blurred = createBlurredImage(imageUrl, { radius: 15 });
344
+ * ```
345
+ */
346
+ export function createBlurredImage<TSource extends NullableImageSource>(
347
+ imageUrl: Accessor<TSource>,
348
+ options?: Readonly<BlurOptions>,
349
+ ): BlurredImageResource<TSource> {
350
+ const imageUrlString: Accessor<string | null | undefined> = () => {
351
+ const url = imageUrl();
352
+ if (url === null || url === undefined) {
353
+ return url;
354
+ }
355
+ return typeof url === 'string' ? url : url.toString();
356
+ };
357
+
358
+ const [blurredImage] = createResource(
359
+ imageUrlString,
360
+ async (url: string): Promise<string> => {
361
+ return await applyGaussianBlur(url, options);
362
+ },
363
+ );
364
+
365
+ return blurredImage as BlurredImageResource<TSource>;
366
+ }
@@ -10,7 +10,7 @@ declare module '@lightningtv/core' {
10
10
  }
11
11
 
12
12
  function idxInArray(idx: number, arr: readonly any[]): boolean {
13
- return idx >= 0 && idx < arr.length;
13
+ return idx === 0 || (idx >= 0 && idx < arr.length);
14
14
  }
15
15
 
16
16
  function findFirstFocusableChildIdx(
@@ -77,20 +77,15 @@ export function onGridFocus(
77
77
  export const navigableForwardFocus: lng.ForwardFocusHandler = function () {
78
78
  const navigable = this as lngp.NavigableElement;
79
79
 
80
- // Undo for now - We should only do this when setFocus is called rather than on forwardFocus
81
- // needs some more research
82
- // if (!lng.isFocused(this)) {
83
- // // if a child already has focus, assume that should be selected
84
- // for (let [i, child] of this.children.entries()) {
85
- // if (lng.isFocused(child)) {
86
- // this.selected = i;
87
- // break;
88
- // }
89
- // }
90
- // }
91
-
92
80
  let selected = navigable.selected;
93
- selected = idxInArray(selected, this.children) ? selected : 0;
81
+
82
+ if (selected !== 0) {
83
+ selected = lng.clamp(selected, 0, this.children.length - 1);
84
+ while (!idxInArray(selected, this.children)) {
85
+ selected--;
86
+ }
87
+ }
88
+
94
89
  selected = findFirstFocusableChildIdx(navigable, selected);
95
90
  // update selected as firstfocusable maybe different if first element has skipFocus
96
91
  navigable.selected = selected;