expo-tvos-search 1.2.3 → 1.3.1
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/README.md +493 -50
- package/build/index.d.ts +115 -4
- package/build/index.d.ts.map +1 -1
- package/build/index.js +54 -6
- package/ios/ExpoTvosSearchModule.swift +67 -4
- package/ios/ExpoTvosSearchView.swift +325 -61
- package/ios/MarqueeText.swift +2 -2
- package/package.json +9 -1
- package/src/__tests__/index.test.tsx +456 -1
- package/src/index.tsx +197 -9
package/README.md
CHANGED
|
@@ -3,31 +3,19 @@
|
|
|
3
3
|
[](https://www.npmjs.com/package/expo-tvos-search)
|
|
4
4
|
[](https://opensource.org/licenses/MIT)
|
|
5
5
|
[](https://github.com/keiver/expo-tvos-search/actions)
|
|
6
|
+
[](https://bundlephobia.com/package/expo-tvos-search)
|
|
6
7
|
|
|
7
|
-
A native tvOS search component for Expo and React Native using SwiftUI's `.searchable` modifier.
|
|
8
|
+
A native tvOS search component for Expo and React Native using SwiftUI's `.searchable` modifier. Provides the native tvOS search experience with automatic focus handling, remote control support, and flexible customization for media apps.
|
|
9
|
+
|
|
10
|
+
**Platform Support:**
|
|
11
|
+
- tvOS 15.0+
|
|
12
|
+
- Expo SDK 51+
|
|
13
|
+
- React Native tvOS 0.71+
|
|
8
14
|
|
|
9
15
|
<p align="center">
|
|
10
|
-
<img src="screenshots/
|
|
16
|
+
<img src="screenshots/demo-mini.png" width="80%" alt="Demo Mini screen for expo-tvos-search" style="border-radius: 16px;max-width: 100%;"/>
|
|
11
17
|
</p>
|
|
12
18
|
|
|
13
|
-
<table>
|
|
14
|
-
<tr>
|
|
15
|
-
<td align="center">
|
|
16
|
-
<img src="screenshots/default.png" width="280" alt="Search"/><br/>
|
|
17
|
-
<sub>Native Search</sub>
|
|
18
|
-
</td>
|
|
19
|
-
<td align="center">
|
|
20
|
-
<img src="screenshots/results.png" width="280" alt="Results"/><br/>
|
|
21
|
-
<sub>Results</sub>
|
|
22
|
-
</td>
|
|
23
|
-
<td align="center">
|
|
24
|
-
<img src="screenshots/no-results.png" width="280" alt="No Results"/><br/>
|
|
25
|
-
<sub>Empty State</sub>
|
|
26
|
-
</td>
|
|
27
|
-
</tr>
|
|
28
|
-
</table>
|
|
29
|
-
|
|
30
|
-
|
|
31
19
|
## Installation
|
|
32
20
|
|
|
33
21
|
```bash
|
|
@@ -40,72 +28,454 @@ Or install from GitHub:
|
|
|
40
28
|
npx expo install github:keiver/expo-tvos-search
|
|
41
29
|
```
|
|
42
30
|
|
|
43
|
-
Then rebuild your native project
|
|
31
|
+
Then follow the **tvOS prerequisites** below and rebuild your native project.
|
|
32
|
+
|
|
33
|
+
## Quick Start
|
|
34
|
+
|
|
35
|
+
### Try the Demo App
|
|
36
|
+
|
|
37
|
+
**[expo-tvos-search-demo](https://github.com/keiver/expo-tvos-search-demo)** - Comprehensive showcase with 7 tabs demonstrating all library features:
|
|
38
|
+
|
|
39
|
+
- **Default** - 4-column grid with custom colors
|
|
40
|
+
- **Portrait** - Netflix-style tall cards (280×420)
|
|
41
|
+
- **Landscape** - Wide 16:9 cards (500×280)
|
|
42
|
+
- **Mini** - Compact 5-column layout (240×360)
|
|
43
|
+
- **External Title** - Titles displayed below cards
|
|
44
|
+
- **Minimal** - Bare minimum setup (5 props)
|
|
45
|
+
- **Help** - Feature overview and usage guide
|
|
46
|
+
|
|
47
|
+
Clone and run:
|
|
48
|
+
```bash
|
|
49
|
+
git clone https://github.com/keiver/expo-tvos-search-demo.git
|
|
50
|
+
cd expo-tvos-search-demo
|
|
51
|
+
npm install
|
|
52
|
+
npm run prebuild
|
|
53
|
+
npm run tvos
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
The demo uses a planet search theme with 8 planets (Mercury to Neptune) and demonstrates all library features with real working code.
|
|
57
|
+
|
|
58
|
+
## Prerequisites for tvOS Builds (Expo)
|
|
59
|
+
|
|
60
|
+
Your project must be configured for React Native tvOS to build and run this module.
|
|
61
|
+
|
|
62
|
+
**Quick Checklist:**
|
|
63
|
+
|
|
64
|
+
- ✅ `react-native-tvos` in use
|
|
65
|
+
- ✅ `@react-native-tvos/config-tv` installed + added to Expo plugins
|
|
66
|
+
- ✅ Run prebuild with `EXPO_TV=1`
|
|
67
|
+
|
|
68
|
+
### 1. Swap to react-native-tvos
|
|
69
|
+
|
|
70
|
+
Replace `react-native` with the [tvOS fork](https://github.com/react-native-tvos/react-native-tvos):
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
npm remove react-native && npm install react-native-tvos@latest
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
### 2. Install the tvOS config plugin
|
|
77
|
+
|
|
78
|
+
Install:
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
npx expo install @react-native-tvos/config-tv
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
Then add the plugin in `app.json` / `app.config.js`:
|
|
85
|
+
|
|
86
|
+
```json
|
|
87
|
+
{
|
|
88
|
+
"expo": {
|
|
89
|
+
"plugins": ["@react-native-tvos/config-tv"]
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
### 3. Generate native projects with tvOS enabled
|
|
44
95
|
|
|
45
96
|
```bash
|
|
46
97
|
EXPO_TV=1 npx expo prebuild --clean
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
Then run:
|
|
101
|
+
|
|
102
|
+
```bash
|
|
47
103
|
npx expo run:ios
|
|
48
104
|
```
|
|
49
105
|
|
|
106
|
+
|
|
50
107
|
## Usage
|
|
51
108
|
|
|
52
|
-
|
|
53
|
-
import { TvosSearchView, isNativeSearchAvailable } from 'expo-tvos-search';
|
|
109
|
+
### Minimal Example
|
|
54
110
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
111
|
+
For the absolute minimum setup, see the [Minimal tab](https://github.com/keiver/expo-tvos-search-demo/blob/main/app/(tabs)/minimal.tsx) in the demo app.
|
|
112
|
+
|
|
113
|
+
<p align="center">
|
|
114
|
+
<img src="screenshots/demo-default.png" width="80%" alt="Minimal demo screen for expo-tvos-search" style="border-radius: 16px;max-width: 100%;"/><br/>
|
|
115
|
+
</p>
|
|
116
|
+
|
|
117
|
+
### Complete Example
|
|
118
|
+
|
|
119
|
+
This example from the demo's [Portrait tab](https://github.com/keiver/expo-tvos-search-demo/blob/main/app/(tabs)/portrait.tsx) shows a complete implementation with best practices:
|
|
58
120
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
121
|
+
```tsx
|
|
122
|
+
import { useState } from 'react';
|
|
123
|
+
import { Alert } from 'react-native';
|
|
124
|
+
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
|
125
|
+
import { LinearGradient } from 'expo-linear-gradient';
|
|
126
|
+
import {
|
|
127
|
+
TvosSearchView,
|
|
128
|
+
isNativeSearchAvailable,
|
|
129
|
+
type SearchResult,
|
|
130
|
+
} from 'expo-tvos-search';
|
|
131
|
+
|
|
132
|
+
const PLANETS: SearchResult[] = [
|
|
133
|
+
{
|
|
134
|
+
id: 'earth',
|
|
135
|
+
title: 'Earth - The Blue Marble of Life',
|
|
136
|
+
subtitle: 'Our home planet, the only known world to harbor life',
|
|
137
|
+
imageUrl: require('./assets/planets/earth.webp'),
|
|
138
|
+
},
|
|
139
|
+
// ... more planets
|
|
140
|
+
];
|
|
141
|
+
|
|
142
|
+
export default function SearchScreen() {
|
|
143
|
+
const [results, setResults] = useState<SearchResult[]>([]);
|
|
144
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
145
|
+
const insets = useSafeAreaInsets();
|
|
146
|
+
|
|
147
|
+
const handleSearch = (event: { nativeEvent: { query: string } }) => {
|
|
148
|
+
const { query } = event.nativeEvent;
|
|
149
|
+
|
|
150
|
+
if (!query.trim()) {
|
|
151
|
+
setResults([]);
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
setIsLoading(true);
|
|
156
|
+
|
|
157
|
+
// Debounce search (300ms)
|
|
158
|
+
setTimeout(() => {
|
|
159
|
+
const filtered = PLANETS.filter(
|
|
160
|
+
planet =>
|
|
161
|
+
planet.title.toLowerCase().includes(query.toLowerCase()) ||
|
|
162
|
+
planet.subtitle?.toLowerCase().includes(query.toLowerCase())
|
|
163
|
+
);
|
|
164
|
+
setResults(filtered);
|
|
165
|
+
setIsLoading(false);
|
|
166
|
+
}, 300);
|
|
62
167
|
};
|
|
63
168
|
|
|
64
|
-
const handleSelect = (event) => {
|
|
65
|
-
const
|
|
66
|
-
|
|
169
|
+
const handleSelect = (event: { nativeEvent: { id: string } }) => {
|
|
170
|
+
const planet = PLANETS.find(p => p.id === event.nativeEvent.id);
|
|
171
|
+
if (planet) {
|
|
172
|
+
Alert.alert(planet.title, planet.subtitle);
|
|
173
|
+
}
|
|
67
174
|
};
|
|
68
175
|
|
|
69
176
|
if (!isNativeSearchAvailable()) {
|
|
70
|
-
return
|
|
177
|
+
return null; // Or show web fallback
|
|
71
178
|
}
|
|
72
179
|
|
|
73
180
|
return (
|
|
74
|
-
<
|
|
75
|
-
|
|
76
|
-
columns={5}
|
|
77
|
-
placeholder="Search..."
|
|
78
|
-
isLoading={isLoading}
|
|
79
|
-
topInset={140}
|
|
80
|
-
onSearch={handleSearch}
|
|
81
|
-
onSelectItem={handleSelect}
|
|
181
|
+
<LinearGradient
|
|
182
|
+
colors={['#0f172a', '#1e293b', '#0f172a']}
|
|
82
183
|
style={{ flex: 1 }}
|
|
83
|
-
|
|
184
|
+
>
|
|
185
|
+
<TvosSearchView
|
|
186
|
+
results={results}
|
|
187
|
+
columns={4}
|
|
188
|
+
placeholder="Search planets..."
|
|
189
|
+
isLoading={isLoading}
|
|
190
|
+
topInset={insets.top + 80}
|
|
191
|
+
onSearch={handleSearch}
|
|
192
|
+
onSelectItem={handleSelect}
|
|
193
|
+
textColor="#E5E5E5"
|
|
194
|
+
accentColor="#E50914"
|
|
195
|
+
cardWidth={280}
|
|
196
|
+
cardHeight={420}
|
|
197
|
+
overlayTitleSize={18} // v1.3.0 - control title font size
|
|
198
|
+
style={{ flex: 1 }}
|
|
199
|
+
/>
|
|
200
|
+
</LinearGradient>
|
|
84
201
|
);
|
|
85
202
|
}
|
|
86
203
|
```
|
|
87
204
|
|
|
205
|
+
## Layout Styles
|
|
206
|
+
|
|
207
|
+
Explore all 7 configurations in the [demo app](https://github.com/keiver/expo-tvos-search-demo).
|
|
208
|
+
|
|
209
|
+
### Portrait Cards
|
|
210
|
+
|
|
211
|
+
```tsx
|
|
212
|
+
<TvosSearchView
|
|
213
|
+
columns={4}
|
|
214
|
+
cardWidth={280}
|
|
215
|
+
cardHeight={420}
|
|
216
|
+
overlayTitleSize={18}
|
|
217
|
+
// ... other props
|
|
218
|
+
/>
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
### Landscape Cards
|
|
222
|
+
|
|
223
|
+
```tsx
|
|
224
|
+
<TvosSearchView
|
|
225
|
+
columns={3}
|
|
226
|
+
cardWidth={500}
|
|
227
|
+
cardHeight={280}
|
|
228
|
+
// ... other props
|
|
229
|
+
/>
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
### Mini Grid
|
|
233
|
+
|
|
234
|
+
```tsx
|
|
235
|
+
<TvosSearchView
|
|
236
|
+
columns={5}
|
|
237
|
+
cardWidth={240}
|
|
238
|
+
cardHeight={360}
|
|
239
|
+
cardMargin={60} // v1.3.0 - extra spacing
|
|
240
|
+
// ... other props
|
|
241
|
+
/>
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
### External Titles
|
|
245
|
+
|
|
246
|
+
```tsx
|
|
247
|
+
<TvosSearchView
|
|
248
|
+
showTitle={true}
|
|
249
|
+
showSubtitle={true}
|
|
250
|
+
showTitleOverlay={false}
|
|
251
|
+
// ... other props
|
|
252
|
+
/>
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
### Error Handling
|
|
256
|
+
|
|
257
|
+
```tsx
|
|
258
|
+
<TvosSearchView
|
|
259
|
+
onError={(e) => {
|
|
260
|
+
const { category, message, context } = e.nativeEvent;
|
|
261
|
+
console.error(`[Search Error] ${category}: ${message}`, context);
|
|
262
|
+
}}
|
|
263
|
+
onValidationWarning={(e) => {
|
|
264
|
+
const { type, message, context } = e.nativeEvent;
|
|
265
|
+
console.warn(`[Validation] ${type}: ${message}`, context);
|
|
266
|
+
}}
|
|
267
|
+
// ... other props
|
|
268
|
+
/>
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
### Customizing Colors and Card Dimensions
|
|
272
|
+
|
|
273
|
+
```tsx
|
|
274
|
+
<TvosSearchView
|
|
275
|
+
textColor="#E5E5E5"
|
|
276
|
+
accentColor="#E50914"
|
|
277
|
+
cardWidth={420}
|
|
278
|
+
cardHeight={240}
|
|
279
|
+
// ... other props
|
|
280
|
+
/>
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
### Title Overlay Customization (v1.3.0+)
|
|
284
|
+
|
|
285
|
+
```tsx
|
|
286
|
+
<TvosSearchView
|
|
287
|
+
overlayTitleSize={22}
|
|
288
|
+
enableMarquee={true}
|
|
289
|
+
marqueeDelay={1.5}
|
|
290
|
+
// ... other props
|
|
291
|
+
/>
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
### Layout Spacing (v1.3.0+)
|
|
295
|
+
|
|
296
|
+
```tsx
|
|
297
|
+
<TvosSearchView
|
|
298
|
+
cardMargin={60}
|
|
299
|
+
cardPadding={25}
|
|
300
|
+
// ... other props
|
|
301
|
+
/>
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
### Image Display Mode
|
|
305
|
+
|
|
306
|
+
```tsx
|
|
307
|
+
<TvosSearchView
|
|
308
|
+
imageContentMode="fit" // 'fill' (crop), 'fit'/'contain' (letterbox)
|
|
309
|
+
// ... other props
|
|
310
|
+
/>
|
|
311
|
+
```
|
|
312
|
+
|
|
313
|
+
## TypeScript Support
|
|
314
|
+
|
|
315
|
+
The library provides comprehensive type definitions for all events and props.
|
|
316
|
+
|
|
317
|
+
### Event Types
|
|
318
|
+
|
|
319
|
+
```typescript
|
|
320
|
+
import type {
|
|
321
|
+
SearchEvent,
|
|
322
|
+
SelectItemEvent,
|
|
323
|
+
SearchViewErrorEvent,
|
|
324
|
+
ValidationWarningEvent,
|
|
325
|
+
SearchResult,
|
|
326
|
+
} from 'expo-tvos-search';
|
|
327
|
+
|
|
328
|
+
// Search event - fired on text change
|
|
329
|
+
interface SearchEvent {
|
|
330
|
+
nativeEvent: {
|
|
331
|
+
query: string;
|
|
332
|
+
};
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// Selection event - fired when result is selected
|
|
336
|
+
interface SelectItemEvent {
|
|
337
|
+
nativeEvent: {
|
|
338
|
+
id: string;
|
|
339
|
+
};
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
// Error event - fatal errors (v1.2.0+)
|
|
343
|
+
interface SearchViewErrorEvent {
|
|
344
|
+
nativeEvent: {
|
|
345
|
+
category: 'module_unavailable' | 'validation_failed' | 'image_load_failed' | 'unknown';
|
|
346
|
+
message: string;
|
|
347
|
+
context?: string;
|
|
348
|
+
};
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// Validation warning - non-fatal issues (v1.2.0+)
|
|
352
|
+
interface ValidationWarningEvent {
|
|
353
|
+
nativeEvent: {
|
|
354
|
+
type: 'field_truncated' | 'value_clamped' | 'url_invalid' | 'validation_failed';
|
|
355
|
+
message: string;
|
|
356
|
+
context?: string;
|
|
357
|
+
};
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
// Search result shape
|
|
361
|
+
interface SearchResult {
|
|
362
|
+
id: string; // Required, max 500 chars
|
|
363
|
+
title: string; // Required, max 500 chars
|
|
364
|
+
subtitle?: string; // Optional, max 500 chars
|
|
365
|
+
imageUrl?: string; // Optional, HTTPS recommended
|
|
366
|
+
}
|
|
367
|
+
```
|
|
368
|
+
|
|
369
|
+
### Typed Usage
|
|
370
|
+
|
|
371
|
+
```typescript
|
|
372
|
+
const handleSearch = (event: SearchEvent) => {
|
|
373
|
+
const query = event.nativeEvent.query;
|
|
374
|
+
// TypeScript knows query is a string
|
|
375
|
+
};
|
|
376
|
+
|
|
377
|
+
const handleSelect = (event: SelectItemEvent) => {
|
|
378
|
+
const id = event.nativeEvent.id;
|
|
379
|
+
// TypeScript knows id is a string
|
|
380
|
+
};
|
|
381
|
+
|
|
382
|
+
const handleError = (event: SearchViewErrorEvent) => {
|
|
383
|
+
const { category, message, context } = event.nativeEvent;
|
|
384
|
+
// Full autocomplete for category values
|
|
385
|
+
if (category === 'image_load_failed') {
|
|
386
|
+
logger.warn(`Image failed to load: ${message}`, { context });
|
|
387
|
+
}
|
|
388
|
+
};
|
|
389
|
+
```
|
|
390
|
+
|
|
391
|
+
## Demo Apps & Examples
|
|
392
|
+
|
|
393
|
+
### Official Demo App
|
|
394
|
+
|
|
395
|
+
**[expo-tvos-search-demo](https://github.com/keiver/expo-tvos-search-demo)** - Complete working examples with 7 different layout styles. Browse the [source code](https://github.com/keiver/expo-tvos-search-demo/tree/main/app/(tabs)) for each configuration.
|
|
396
|
+
|
|
397
|
+
### Apps Using This Library
|
|
398
|
+
|
|
399
|
+
**[Tomo TV](https://github.com/keiver/tomotv)** - Full-featured tvOS Jellyfin client
|
|
400
|
+
- Real-world integration with media library API
|
|
401
|
+
- Advanced search with live server calls
|
|
402
|
+
- Complete authentication and navigation flow
|
|
403
|
+
|
|
404
|
+
## See it in action:
|
|
405
|
+
|
|
406
|
+
<p align="center">
|
|
407
|
+
<img src="screenshots/expo-tvos-search.gif" width="700" alt="expo-tvos-search screen in action" loading="lazy" />
|
|
408
|
+
</p>
|
|
409
|
+
|
|
88
410
|
## Props
|
|
89
411
|
|
|
412
|
+
### Core Props
|
|
413
|
+
|
|
90
414
|
| Prop | Type | Default | Description |
|
|
91
415
|
|------|------|---------|-------------|
|
|
92
416
|
| `results` | `SearchResult[]` | `[]` | Array of search results |
|
|
93
417
|
| `columns` | `number` | `5` | Number of columns in the grid |
|
|
94
|
-
| `placeholder` | `string` | `"Search..."` | Search field placeholder |
|
|
418
|
+
| `placeholder` | `string` | `"Search movies and videos..."` | Search field placeholder |
|
|
95
419
|
| `isLoading` | `boolean` | `false` | Shows loading indicator |
|
|
420
|
+
|
|
421
|
+
### Card Dimensions & Spacing
|
|
422
|
+
|
|
423
|
+
| Prop | Type | Default | Description |
|
|
424
|
+
|------|------|---------|-------------|
|
|
425
|
+
| `cardWidth` | `number` | `280` | Width of each result card in points |
|
|
426
|
+
| `cardHeight` | `number` | `420` | Height of each result card in points |
|
|
427
|
+
| `cardMargin` | `number` | `40` | **(v1.3.0+)** Spacing between cards in the grid (horizontal and vertical) |
|
|
428
|
+
| `cardPadding` | `number` | `16` | **(v1.3.0+)** Padding inside the card for overlay content (title/subtitle) |
|
|
429
|
+
| `topInset` | `number` | `0` | Top padding (for tab bar clearance) |
|
|
430
|
+
|
|
431
|
+
### Display Options
|
|
432
|
+
|
|
433
|
+
| Prop | Type | Default | Description |
|
|
434
|
+
|------|------|---------|-------------|
|
|
96
435
|
| `showTitle` | `boolean` | `false` | Show title below each result |
|
|
97
436
|
| `showSubtitle` | `boolean` | `false` | Show subtitle below title |
|
|
98
|
-
| `showFocusBorder` | `boolean` | `false` | Show border on focused item |
|
|
99
|
-
| `topInset` | `number` | `0` | Top padding (for tab bar clearance) |
|
|
100
437
|
| `showTitleOverlay` | `boolean` | `true` | Show title overlay with gradient at bottom of card |
|
|
438
|
+
| `showFocusBorder` | `boolean` | `false` | Show border on focused item |
|
|
439
|
+
| `imageContentMode` | `'fill' \| 'fit' \| 'contain'` | `'fill'` | How images fill the card: `fill` (crop to fill), `fit`/`contain` (letterbox) |
|
|
440
|
+
|
|
441
|
+
### Styling & Colors
|
|
442
|
+
|
|
443
|
+
| Prop | Type | Default | Description |
|
|
444
|
+
|------|------|---------|-------------|
|
|
445
|
+
| `textColor` | `string` | system default | Color for text and UI elements (hex format, e.g., "#FFFFFF") |
|
|
446
|
+
| `accentColor` | `string` | `"#FFC312"` | Accent color for focused elements (hex format, e.g., "#FFC312") |
|
|
447
|
+
| `overlayTitleSize` | `number` | `20` | **(v1.3.0+)** Font size for title text in the blur overlay (when showTitleOverlay is true) |
|
|
448
|
+
|
|
449
|
+
### Animation
|
|
450
|
+
|
|
451
|
+
| Prop | Type | Default | Description |
|
|
452
|
+
|------|------|---------|-------------|
|
|
101
453
|
| `enableMarquee` | `boolean` | `true` | Enable marquee scrolling for long titles |
|
|
102
454
|
| `marqueeDelay` | `number` | `1.5` | Delay in seconds before marquee starts |
|
|
455
|
+
|
|
456
|
+
### Text Customization
|
|
457
|
+
|
|
458
|
+
| Prop | Type | Default | Description |
|
|
459
|
+
|------|------|---------|-------------|
|
|
103
460
|
| `emptyStateText` | `string` | `"Search for movies and videos"` | Text shown when search field is empty |
|
|
104
461
|
| `searchingText` | `string` | `"Searching..."` | Text shown during search |
|
|
105
462
|
| `noResultsText` | `string` | `"No results found"` | Text shown when no results found |
|
|
106
463
|
| `noResultsHintText` | `string` | `"Try a different search term"` | Hint text below no results message |
|
|
464
|
+
|
|
465
|
+
### Event Handlers
|
|
466
|
+
|
|
467
|
+
| Prop | Type | Default | Description |
|
|
468
|
+
|------|------|---------|-------------|
|
|
107
469
|
| `onSearch` | `function` | required | Called when search text changes |
|
|
108
470
|
| `onSelectItem` | `function` | required | Called when result is selected |
|
|
471
|
+
| `onError` | `function` | optional | **(v1.2.0+)** Called when errors occur (image loading failures, validation errors) |
|
|
472
|
+
| `onValidationWarning` | `function` | optional | **(v1.2.0+)** Called for non-fatal warnings (truncated fields, clamped values, invalid URLs) |
|
|
473
|
+
|
|
474
|
+
### Other
|
|
475
|
+
|
|
476
|
+
| Prop | Type | Default | Description |
|
|
477
|
+
|------|------|---------|-------------|
|
|
478
|
+
| `style` | `ViewStyle` | optional | Style object for the view container |
|
|
109
479
|
|
|
110
480
|
## SearchResult Type
|
|
111
481
|
|
|
@@ -127,12 +497,70 @@ The native implementation applies the following validation and constraints:
|
|
|
127
497
|
- **Image URL schemes**: Only HTTP and HTTPS URLs are accepted for `imageUrl`. Other URL schemes (e.g., `file://`, `data:`) are rejected.
|
|
128
498
|
- **HTTPS recommended**: HTTP URLs may be blocked by App Transport Security on tvOS unless explicitly allowed in Info.plist.
|
|
129
499
|
|
|
130
|
-
##
|
|
500
|
+
## Focus Handling - Do's and Don'ts
|
|
131
501
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
502
|
+
The native `.searchable` modifier manages focus automatically. Here's what to do and what to avoid:
|
|
503
|
+
|
|
504
|
+
### ✅ Do: Render directly in your screen
|
|
505
|
+
|
|
506
|
+
```tsx
|
|
507
|
+
function SearchScreen() {
|
|
508
|
+
return (
|
|
509
|
+
<TvosSearchView
|
|
510
|
+
results={results}
|
|
511
|
+
onSearch={handleSearch}
|
|
512
|
+
onSelectItem={handleSelect}
|
|
513
|
+
style={{ flex: 1 }}
|
|
514
|
+
/>
|
|
515
|
+
);
|
|
516
|
+
}
|
|
517
|
+
```
|
|
518
|
+
|
|
519
|
+
### ❌ Don't: Wrap in focusable containers
|
|
520
|
+
|
|
521
|
+
```tsx
|
|
522
|
+
// ❌ WRONG - breaks focus navigation
|
|
523
|
+
function SearchScreen() {
|
|
524
|
+
return (
|
|
525
|
+
<Pressable> {/* Don't wrap in Pressable */}
|
|
526
|
+
<TvosSearchView ... />
|
|
527
|
+
</Pressable>
|
|
528
|
+
);
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
// ❌ WRONG - interferes with native focus
|
|
532
|
+
function SearchScreen() {
|
|
533
|
+
return (
|
|
534
|
+
<TouchableOpacity> {/* Don't wrap in TouchableOpacity */}
|
|
535
|
+
<TvosSearchView ... />
|
|
536
|
+
</TouchableOpacity>
|
|
537
|
+
);
|
|
538
|
+
}
|
|
539
|
+
```
|
|
540
|
+
|
|
541
|
+
**Why this breaks**: Focusable wrappers steal focus from the native SwiftUI search container, which breaks directional navigation.
|
|
542
|
+
|
|
543
|
+
### ✅ Do: Use non-interactive containers
|
|
544
|
+
|
|
545
|
+
```tsx
|
|
546
|
+
// ✅ CORRECT - View is not focusable
|
|
547
|
+
function SearchScreen() {
|
|
548
|
+
return (
|
|
549
|
+
<View style={{ flex: 1, backgroundColor: '#000' }}>
|
|
550
|
+
<TvosSearchView ... />
|
|
551
|
+
</View>
|
|
552
|
+
);
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
// ✅ CORRECT - SafeAreaView is not focusable
|
|
556
|
+
function SearchScreen() {
|
|
557
|
+
return (
|
|
558
|
+
<SafeAreaView style={{ flex: 1 }}>
|
|
559
|
+
<TvosSearchView ... />
|
|
560
|
+
</SafeAreaView>
|
|
561
|
+
);
|
|
562
|
+
}
|
|
563
|
+
```
|
|
136
564
|
|
|
137
565
|
## Troubleshooting
|
|
138
566
|
|
|
@@ -146,6 +574,8 @@ EXPO_TV=1 npx expo prebuild --clean
|
|
|
146
574
|
npx expo run:ios
|
|
147
575
|
```
|
|
148
576
|
|
|
577
|
+
**Note:** Expo Go doesn't support this. Build a dev client or native build instead.
|
|
578
|
+
|
|
149
579
|
### Images not loading
|
|
150
580
|
|
|
151
581
|
1. Verify your image URLs are HTTPS (HTTP may be blocked by App Transport Security)
|
|
@@ -183,6 +613,19 @@ Tests cover:
|
|
|
183
613
|
- Component rendering when native module is unavailable
|
|
184
614
|
- Event structure validation
|
|
185
615
|
|
|
616
|
+
## Contributing
|
|
617
|
+
|
|
618
|
+
We welcome contributions! Please see [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines on:
|
|
619
|
+
- Code of conduct
|
|
620
|
+
- Development setup
|
|
621
|
+
- Testing requirements
|
|
622
|
+
- Commit message conventions
|
|
623
|
+
- Pull request process
|
|
624
|
+
|
|
625
|
+
### Adding New Props
|
|
626
|
+
|
|
627
|
+
If you're adding new props to the library, follow the comprehensive checklist in [CLAUDE-adding-new-props.md](./CLAUDE-adding-new-props.md). This memory bank provides a 9-step guide ensuring props are properly wired from TypeScript through to Swift rendering.
|
|
628
|
+
|
|
186
629
|
## License
|
|
187
630
|
|
|
188
631
|
MIT
|