expo-tvos-search 1.4.0 → 1.5.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/CHANGELOG.md +11 -0
- package/README.md +179 -237
- package/build/index.d.ts +26 -0
- package/build/index.d.ts.map +1 -1
- package/ios/ExpoTvosSearch.podspec +1 -1
- package/ios/ExpoTvosSearchModule.swift +4 -0
- package/ios/ExpoTvosSearchView.swift +23 -0
- package/ios/TvosSearchContentView.swift +1 -0
- package/package.json +4 -4
- package/src/index.tsx +27 -0
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,17 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [1.5.0] - 2026-02-01
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
- `colorScheme` prop — override the system color scheme for the search view (`'dark'`, `'light'`, or `'system'`)
|
|
12
|
+
- Fixes illegible search bar text when app has a dark background and system is in light mode
|
|
13
|
+
- Maps to `UIHostingController.overrideUserInterfaceStyle` on the native side
|
|
14
|
+
- Default `"system"` preserves existing behavior (no breaking change)
|
|
15
|
+
|
|
16
|
+
### Changed
|
|
17
|
+
- Search bar interactive controls (cursor, highlights) now tint to match `accentColor`
|
|
18
|
+
|
|
8
19
|
## [1.3.2] - 2026-01-25
|
|
9
20
|
|
|
10
21
|
### Added
|
package/README.md
CHANGED
|
@@ -1,18 +1,57 @@
|
|
|
1
|
-
# expo-tvos-search
|
|
1
|
+
# expo-tvos-search — Native tvOS Search for React Native tvOS (Expo)
|
|
2
2
|
|
|
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
6
|
|
|
7
|
-
A native
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
-
|
|
12
|
-
-
|
|
7
|
+
A native Apple TV search component built with SwiftUI's `.searchable` modifier. Drop it into your Expo tvOS app and get the system search experience — keyboard, Siri Remote, focus handling — out of the box.
|
|
8
|
+
|
|
9
|
+
## Features
|
|
10
|
+
|
|
11
|
+
- **Native SwiftUI** — uses `.searchable` for the real tvOS search experience, not a web imitation
|
|
12
|
+
- **Siri Remote support** — full keyboard navigation, swipe, tap, and long-press handling on real hardware
|
|
13
|
+
- **Configurable grid** — portrait, landscape, square, or mini cards with adjustable columns, spacing, and padding
|
|
14
|
+
- **Marquee titles** — long titles auto-scroll on focus with configurable delay
|
|
15
|
+
- **Image caching** — async image loading with NSCache-backed caching
|
|
16
|
+
- **Title overlay** — gradient overlay with blur effect on card images, toggleable
|
|
17
|
+
- **External titles** — show title and subtitle below cards instead of (or alongside) the overlay
|
|
18
|
+
- **Customizable colors** — text color, accent/focus color, all via hex strings
|
|
19
|
+
- **Color scheme override** — force dark or light appearance regardless of system setting
|
|
20
|
+
- **Controlled search text** — set search field text programmatically for deep links, state restore, or "search for similar" flows
|
|
21
|
+
- **Error & validation callbacks** — structured error events and non-fatal validation warnings
|
|
22
|
+
- **Focus callbacks** — `onSearchFieldFocused` / `onSearchFieldBlurred` for gesture handler coordination
|
|
23
|
+
- **Platform-safe** — renders `null` on non-tvOS platforms; use `isNativeSearchAvailable()` to gate rendering
|
|
24
|
+
|
|
25
|
+
## Table of Contents
|
|
26
|
+
|
|
27
|
+
- [Installation](#installation)
|
|
28
|
+
- [Prerequisites](#prerequisites-for-tvos-builds)
|
|
29
|
+
- [Quick Start](#quick-start)
|
|
30
|
+
- [Usage Examples](#usage-examples)
|
|
31
|
+
- [Portrait Cards](#portrait-cards)
|
|
32
|
+
- [Landscape Cards](#landscape-cards)
|
|
33
|
+
- [Mini Grid](#mini-grid)
|
|
34
|
+
- [External Titles](#external-titles)
|
|
35
|
+
- [Title Overlay Customization](#title-overlay-customization)
|
|
36
|
+
- [Layout Spacing](#layout-spacing)
|
|
37
|
+
- [Image Display Mode](#image-display-mode)
|
|
38
|
+
- [Color Scheme Override](#color-scheme-override)
|
|
39
|
+
- [Colors and Dimensions](#colors-and-dimensions)
|
|
40
|
+
- [Error Handling](#error-handling)
|
|
41
|
+
- [Apple TV Hardware Keyboard](#apple-tv-hardware-keyboard-support)
|
|
42
|
+
- [API Reference](#api-reference)
|
|
43
|
+
- [Props](#props)
|
|
44
|
+
- [SearchResult](#searchresult)
|
|
45
|
+
- [Event Types](#event-types)
|
|
46
|
+
- [isNativeSearchAvailable()](#isnativesearchavailable)
|
|
47
|
+
- [Result Validation](#result-validation)
|
|
48
|
+
- [Demo App](#demo-app)
|
|
49
|
+
- [Testing](#testing)
|
|
50
|
+
- [Contributing](#contributing)
|
|
51
|
+
- [License](#license)
|
|
13
52
|
|
|
14
53
|
<p align="center">
|
|
15
|
-
<img src="screenshots/
|
|
54
|
+
<img src="screenshots/demo-05.webp" alt="Native tvOS search with portrait card grid showing planet results with gold accent and marquee titles" width="100%"/>
|
|
16
55
|
</p>
|
|
17
56
|
|
|
18
57
|
## Installation
|
|
@@ -29,7 +68,9 @@ npx expo install github:keiver/expo-tvos-search
|
|
|
29
68
|
|
|
30
69
|
## Prerequisites for tvOS Builds
|
|
31
70
|
|
|
32
|
-
Your project must be configured for React Native tvOS
|
|
71
|
+
Your project must be configured for React Native tvOS.
|
|
72
|
+
|
|
73
|
+
**Platform requirements:** tvOS 15.0+, Expo SDK 51+, React Native tvOS 0.71+
|
|
33
74
|
|
|
34
75
|
### 1. Install react-native-tvos
|
|
35
76
|
|
|
@@ -53,167 +94,50 @@ Then add the plugin in `app.json` / `app.config.js`:
|
|
|
53
94
|
}
|
|
54
95
|
```
|
|
55
96
|
|
|
56
|
-
##
|
|
57
|
-
|
|
58
|
-
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:
|
|
97
|
+
## Quick Start
|
|
59
98
|
|
|
60
99
|
```tsx
|
|
61
|
-
import {
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
import { LinearGradient } from 'expo-linear-gradient';
|
|
65
|
-
import {
|
|
66
|
-
TvosSearchView,
|
|
67
|
-
isNativeSearchAvailable,
|
|
68
|
-
type SearchResult,
|
|
69
|
-
} from 'expo-tvos-search';
|
|
70
|
-
|
|
71
|
-
const PLANETS: SearchResult[] = [
|
|
100
|
+
import { TvosSearchView, type SearchResult } from 'expo-tvos-search';
|
|
101
|
+
|
|
102
|
+
const results: SearchResult[] = [
|
|
72
103
|
{
|
|
73
104
|
id: 'earth',
|
|
74
|
-
title: 'Earth
|
|
75
|
-
subtitle: '
|
|
76
|
-
imageUrl:
|
|
105
|
+
title: 'Earth',
|
|
106
|
+
subtitle: 'The Blue Marble',
|
|
107
|
+
imageUrl: 'https://example.com/earth.jpg',
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
id: 'mars',
|
|
111
|
+
title: 'Mars',
|
|
112
|
+
subtitle: 'The Red Planet',
|
|
113
|
+
imageUrl: 'https://example.com/mars.jpg',
|
|
77
114
|
},
|
|
78
|
-
// ... all planets
|
|
79
115
|
];
|
|
80
116
|
|
|
81
117
|
export default function SearchScreen() {
|
|
82
|
-
const [results, setResults] = useState<SearchResult[]>([]);
|
|
83
|
-
const [isLoading, setIsLoading] = useState(false);
|
|
84
|
-
const insets = useSafeAreaInsets();
|
|
85
|
-
|
|
86
|
-
const handleSearch = (event: { nativeEvent: { query: string } }) => {
|
|
87
|
-
const { query } = event.nativeEvent;
|
|
88
|
-
|
|
89
|
-
if (!query.trim()) {
|
|
90
|
-
setResults([]);
|
|
91
|
-
return;
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
setIsLoading(true);
|
|
95
|
-
|
|
96
|
-
// Debounce dummy search (300ms)
|
|
97
|
-
setTimeout(() => {
|
|
98
|
-
const filtered = PLANETS.filter(
|
|
99
|
-
planet =>
|
|
100
|
-
planet.title.toLowerCase().includes(query.toLowerCase()) ||
|
|
101
|
-
planet.subtitle?.toLowerCase().includes(query.toLowerCase())
|
|
102
|
-
);
|
|
103
|
-
setResults(filtered);
|
|
104
|
-
setIsLoading(false);
|
|
105
|
-
}, 300);
|
|
106
|
-
};
|
|
107
|
-
|
|
108
|
-
const handleSelect = (event: { nativeEvent: { id: string } }) => {
|
|
109
|
-
const planet = PLANETS.find(p => p.id === event.nativeEvent.id);
|
|
110
|
-
if (planet) {
|
|
111
|
-
Alert.alert(planet.title, planet.subtitle);
|
|
112
|
-
}
|
|
113
|
-
};
|
|
114
|
-
|
|
115
|
-
if (!isNativeSearchAvailable()) {
|
|
116
|
-
return null; // Or show web fallback
|
|
117
|
-
}
|
|
118
|
-
|
|
119
118
|
return (
|
|
120
|
-
<
|
|
121
|
-
|
|
119
|
+
<TvosSearchView
|
|
120
|
+
results={results}
|
|
121
|
+
columns={4}
|
|
122
|
+
placeholder="Search planets..."
|
|
123
|
+
isLoading={false}
|
|
124
|
+
topInset={80}
|
|
125
|
+
onSearch={(e) => console.log('Search:', e.nativeEvent.query)}
|
|
126
|
+
onSelectItem={(e) => console.log('Selected:', e.nativeEvent.id)}
|
|
127
|
+
textColor="#E5E5E5"
|
|
128
|
+
accentColor="#E50914"
|
|
129
|
+
cardWidth={280}
|
|
130
|
+
cardHeight={420}
|
|
131
|
+
overlayTitleSize={18}
|
|
122
132
|
style={{ flex: 1 }}
|
|
123
|
-
|
|
124
|
-
<TvosSearchView
|
|
125
|
-
results={results}
|
|
126
|
-
columns={4}
|
|
127
|
-
placeholder="Search planets..."
|
|
128
|
-
isLoading={isLoading}
|
|
129
|
-
topInset={insets.top + 80}
|
|
130
|
-
onSearch={handleSearch}
|
|
131
|
-
onSelectItem={handleSelect}
|
|
132
|
-
textColor="#E5E5E5"
|
|
133
|
-
accentColor="#E50914"
|
|
134
|
-
cardWidth={280}
|
|
135
|
-
cardHeight={420}
|
|
136
|
-
overlayTitleSize={18}
|
|
137
|
-
style={{ flex: 1 }}
|
|
138
|
-
/>
|
|
139
|
-
</LinearGradient>
|
|
133
|
+
/>
|
|
140
134
|
);
|
|
141
135
|
}
|
|
142
136
|
```
|
|
143
137
|
|
|
144
|
-
|
|
145
|
-
<img src="screenshots/no-results.png" alt="No results for search screen using expo-tvos-search" style="border-radius: 16px;max-width: 100%;"/><br/>
|
|
146
|
-
</p>
|
|
147
|
-
|
|
148
|
-
## Demo App and Common Configurations
|
|
149
|
-
|
|
150
|
-
Explore all configurations in the [demo app](https://github.com/keiver/expo-tvos-search-demo).
|
|
151
|
-
|
|
152
|
-
### Portrait Cards
|
|
153
|
-
|
|
154
|
-
```tsx
|
|
155
|
-
<TvosSearchView
|
|
156
|
-
columns={4}
|
|
157
|
-
cardWidth={280}
|
|
158
|
-
cardHeight={420}
|
|
159
|
-
overlayTitleSize={18}
|
|
160
|
-
// ... other props
|
|
161
|
-
/>
|
|
162
|
-
```
|
|
163
|
-
|
|
164
|
-
### Landscape Cards
|
|
165
|
-
|
|
166
|
-
```tsx
|
|
167
|
-
<TvosSearchView
|
|
168
|
-
columns={3}
|
|
169
|
-
cardWidth={500}
|
|
170
|
-
cardHeight={280}
|
|
171
|
-
// ... other props
|
|
172
|
-
/>
|
|
173
|
-
```
|
|
174
|
-
|
|
175
|
-
### Mini Grid
|
|
176
|
-
|
|
177
|
-
```tsx
|
|
178
|
-
<TvosSearchView
|
|
179
|
-
columns={5}
|
|
180
|
-
cardWidth={240}
|
|
181
|
-
cardHeight={360}
|
|
182
|
-
cardMargin={60} // v1.3.0 - extra spacing
|
|
183
|
-
// ... other props
|
|
184
|
-
/>
|
|
185
|
-
```
|
|
186
|
-
|
|
187
|
-
### External Titles
|
|
188
|
-
|
|
189
|
-
```tsx
|
|
190
|
-
<TvosSearchView
|
|
191
|
-
showTitle={true}
|
|
192
|
-
showSubtitle={true}
|
|
193
|
-
showTitleOverlay={false}
|
|
194
|
-
// ... other props
|
|
195
|
-
/>
|
|
196
|
-
```
|
|
197
|
-
|
|
198
|
-
### Error Handling
|
|
199
|
-
|
|
200
|
-
```tsx
|
|
201
|
-
<TvosSearchView
|
|
202
|
-
onError={(e) => {
|
|
203
|
-
const { category, message, context } = e.nativeEvent;
|
|
204
|
-
console.error(`[Search Error] ${category}: ${message}`, context);
|
|
205
|
-
}}
|
|
206
|
-
onValidationWarning={(e) => {
|
|
207
|
-
const { type, message, context } = e.nativeEvent;
|
|
208
|
-
console.warn(`[Validation] ${type}: ${message}`, context);
|
|
209
|
-
}}
|
|
210
|
-
// ... other props
|
|
211
|
-
/>
|
|
212
|
-
```
|
|
213
|
-
|
|
214
|
-
### Apple TV Hardware Keyboard Support (v1.3.2+)
|
|
138
|
+
### Apple TV Hardware Keyboard Support
|
|
215
139
|
|
|
216
|
-
On real Apple TV hardware, React Native's `RCTTVRemoteHandler` installs gesture recognizers that consume Siri Remote presses before they reach SwiftUI's `.searchable` text field, which prevents keyboard input. When the search field gains focus, this component temporarily disables touch cancellation using the official `react-native-tvos` notification API, and also disables tap/long-press recognizers from parent views (to cover cases like `react-native-gesture-handler`). Swipe and pan recognizers stay active for keyboard navigation. Everything is restored when focus leaves the field. This only applies to physical devices
|
|
140
|
+
On real Apple TV hardware, React Native's `RCTTVRemoteHandler` installs gesture recognizers that consume Siri Remote presses before they reach SwiftUI's `.searchable` text field, which prevents keyboard input. When the search field gains focus, this component temporarily disables touch cancellation using the official `react-native-tvos` notification API, and also disables tap/long-press recognizers from parent views (to cover cases like `react-native-gesture-handler`). Swipe and pan recognizers stay active for keyboard navigation. Everything is restored when focus leaves the field. This only applies to physical devices — the Simulator doesn't need it.
|
|
217
141
|
|
|
218
142
|
If this interferes with gesture handling in your app, please [open an issue](https://github.com/keiver/expo-tvos-search/issues) so we can sort it out.
|
|
219
143
|
|
|
@@ -233,137 +157,140 @@ import { TVEventControl } from 'react-native';
|
|
|
233
157
|
/>
|
|
234
158
|
```
|
|
235
159
|
|
|
236
|
-
### Customizing Colors and Card Dimensions
|
|
237
|
-
|
|
238
|
-
```tsx
|
|
239
|
-
<TvosSearchView
|
|
240
|
-
textColor="#E5E5E5"
|
|
241
|
-
accentColor="#E50914"
|
|
242
|
-
cardWidth={420}
|
|
243
|
-
cardHeight={240}
|
|
244
|
-
// ... other props
|
|
245
|
-
/>
|
|
246
|
-
```
|
|
247
|
-
|
|
248
|
-
### Title Overlay Customization (v1.3.0+)
|
|
249
160
|
|
|
250
|
-
|
|
251
|
-
<TvosSearchView
|
|
252
|
-
overlayTitleSize={22}
|
|
253
|
-
enableMarquee={true}
|
|
254
|
-
marqueeDelay={1.5}
|
|
255
|
-
// ... other props
|
|
256
|
-
/>
|
|
257
|
-
```
|
|
161
|
+
### Color Scheme Override
|
|
258
162
|
|
|
259
|
-
|
|
163
|
+
Apps with a fixed dark background can get illegible search bar text when the system is in light mode. Use `colorScheme` to force a specific appearance:
|
|
260
164
|
|
|
261
165
|
```tsx
|
|
262
166
|
<TvosSearchView
|
|
263
|
-
|
|
264
|
-
cardPadding={25}
|
|
167
|
+
colorScheme="dark"
|
|
265
168
|
// ... other props
|
|
266
169
|
/>
|
|
267
170
|
```
|
|
268
171
|
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
<TvosSearchView
|
|
273
|
-
imageContentMode="fit" // 'fill' (crop), 'fit'/'contain' (letterbox)
|
|
274
|
-
// ... other props
|
|
275
|
-
/>
|
|
276
|
-
```
|
|
172
|
+
- `"dark"` — white text, dark UI elements (good for dark-background apps)
|
|
173
|
+
- `"light"` — black text, light UI elements
|
|
174
|
+
- `"system"` — follows the device setting (default, no override)
|
|
277
175
|
|
|
278
|
-
|
|
279
|
-
<img src="screenshots/results.png" alt="Results for search screen using expo-tvos-search" style="border-radius: 16px;max-width: 100%;"/><br/>
|
|
280
|
-
</p>
|
|
176
|
+
## API Reference
|
|
281
177
|
|
|
282
|
-
|
|
178
|
+
### Props
|
|
283
179
|
|
|
284
|
-
|
|
180
|
+
#### Core
|
|
285
181
|
|
|
286
182
|
| Prop | Type | Default | Description |
|
|
287
183
|
|------|------|---------|-------------|
|
|
288
|
-
| `results` | `SearchResult[]` | `[]` | Array of search results |
|
|
289
|
-
| `columns` | `number` | `5` | Number of columns
|
|
290
|
-
| `placeholder` | `string` | `"Search..."` | Search field placeholder |
|
|
291
|
-
| `searchText` | `string` | — | Programmatically set search field text (
|
|
292
|
-
| `isLoading` | `boolean` | `false` | Shows loading indicator |
|
|
184
|
+
| `results` | `SearchResult[]` | `[]` | Array of search results to display. Capped at 500 items. |
|
|
185
|
+
| `columns` | `number` | `5` | Number of grid columns (clamped 1–10) |
|
|
186
|
+
| `placeholder` | `string` | `"Search..."` | Search field placeholder text |
|
|
187
|
+
| `searchText` | `string` | — | Programmatically set search field text (for deep links, state restore) |
|
|
188
|
+
| `isLoading` | `boolean` | `false` | Shows a loading indicator |
|
|
293
189
|
|
|
294
|
-
|
|
190
|
+
#### Card Dimensions & Spacing
|
|
295
191
|
|
|
296
192
|
| Prop | Type | Default | Description |
|
|
297
193
|
|------|------|---------|-------------|
|
|
298
|
-
| `cardWidth` | `number` | `280` | Width of each result card in points |
|
|
299
|
-
| `cardHeight` | `number` | `420` | Height of each result card in points |
|
|
300
|
-
| `cardMargin` | `number` | `40` |
|
|
301
|
-
| `cardPadding` | `number` | `16` |
|
|
302
|
-
| `topInset` | `number` | `0` | Top padding
|
|
194
|
+
| `cardWidth` | `number` | `280` | Width of each result card in points (clamped 50–1000) |
|
|
195
|
+
| `cardHeight` | `number` | `420` | Height of each result card in points (clamped 50–1000) |
|
|
196
|
+
| `cardMargin` | `number` | `40` | Spacing between cards (horizontal and vertical, clamped 0–200) |
|
|
197
|
+
| `cardPadding` | `number` | `16` | Padding inside the card for overlay content (clamped 0–100) |
|
|
198
|
+
| `topInset` | `number` | `0` | Top padding for tab bar clearance (clamped 0–500) |
|
|
303
199
|
|
|
304
|
-
|
|
200
|
+
#### Display Options
|
|
305
201
|
|
|
306
202
|
| Prop | Type | Default | Description |
|
|
307
203
|
|------|------|---------|-------------|
|
|
308
|
-
| `showTitle` | `boolean` | `false` | Show title below each result |
|
|
204
|
+
| `showTitle` | `boolean` | `false` | Show title below each result card |
|
|
309
205
|
| `showSubtitle` | `boolean` | `false` | Show subtitle below title |
|
|
310
206
|
| `showTitleOverlay` | `boolean` | `true` | Show title overlay with gradient at bottom of card |
|
|
311
207
|
| `showFocusBorder` | `boolean` | `false` | Show border on focused item |
|
|
312
|
-
| `imageContentMode` | `'fill' \| 'fit' \| 'contain'` | `'fill'` | How images fill the card: `fill`
|
|
208
|
+
| `imageContentMode` | `'fill' \| 'fit' \| 'contain'` | `'fill'` | How images fill the card: `fill` crops, `fit`/`contain` letterbox |
|
|
313
209
|
|
|
314
|
-
|
|
210
|
+
#### Styling & Colors
|
|
315
211
|
|
|
316
212
|
| Prop | Type | Default | Description |
|
|
317
213
|
|------|------|---------|-------------|
|
|
318
|
-
| `textColor` | `string` | system default | Color for text and UI elements (hex
|
|
319
|
-
| `accentColor` | `string` | `"#FFC312"` | Accent color for focused elements (hex
|
|
320
|
-
| `
|
|
214
|
+
| `textColor` | `string` | system default | Color for text and UI elements (hex, e.g. `"#FFFFFF"`) |
|
|
215
|
+
| `accentColor` | `string` | `"#FFC312"` | Accent color for focused elements (hex, e.g. `"#E50914"`) |
|
|
216
|
+
| `colorScheme` | `'light' \| 'dark' \| 'system'` | `"system"` | Override the system color scheme for the search view |
|
|
217
|
+
| `overlayTitleSize` | `number` | `20` | Font size for title text in the blur overlay (clamped 8–72) |
|
|
321
218
|
|
|
322
|
-
|
|
219
|
+
#### Animation
|
|
323
220
|
|
|
324
221
|
| Prop | Type | Default | Description |
|
|
325
222
|
|------|------|---------|-------------|
|
|
326
223
|
| `enableMarquee` | `boolean` | `true` | Enable marquee scrolling for long titles |
|
|
327
|
-
| `marqueeDelay` | `number` | `1.5` | Delay in seconds before marquee starts |
|
|
224
|
+
| `marqueeDelay` | `number` | `1.5` | Delay in seconds before marquee starts (clamped 0–60) |
|
|
328
225
|
|
|
329
|
-
|
|
226
|
+
#### Text Customization
|
|
330
227
|
|
|
331
228
|
| Prop | Type | Default | Description |
|
|
332
229
|
|------|------|---------|-------------|
|
|
333
230
|
| `emptyStateText` | `string` | `"Search your library"` | Text shown when search field is empty |
|
|
334
231
|
| `searchingText` | `string` | `"Searching..."` | Text shown during search |
|
|
335
|
-
| `noResultsText` | `string` | `"No results found"` | Text shown when no results
|
|
232
|
+
| `noResultsText` | `string` | `"No results found"` | Text shown when no results match |
|
|
336
233
|
| `noResultsHintText` | `string` | `"Try a different search term"` | Hint text below no results message |
|
|
337
234
|
|
|
338
|
-
|
|
235
|
+
#### Event Handlers
|
|
339
236
|
|
|
340
|
-
| Prop | Type |
|
|
341
|
-
|
|
342
|
-
| `onSearch` | `
|
|
343
|
-
| `onSelectItem` | `
|
|
344
|
-
| `onError` | `
|
|
345
|
-
| `onValidationWarning` | `
|
|
346
|
-
| `onSearchFieldFocused` | `
|
|
347
|
-
| `onSearchFieldBlurred` | `
|
|
237
|
+
| Prop | Type | Required | Description |
|
|
238
|
+
|------|------|----------|-------------|
|
|
239
|
+
| `onSearch` | `(event: SearchEvent) => void` | Yes | Called when search text changes |
|
|
240
|
+
| `onSelectItem` | `(event: SelectItemEvent) => void` | Yes | Called when a result is selected |
|
|
241
|
+
| `onError` | `(event: SearchViewErrorEvent) => void` | No | Called on errors (image loading, validation) |
|
|
242
|
+
| `onValidationWarning` | `(event: ValidationWarningEvent) => void` | No | Called for non-fatal warnings (truncated fields, clamped values) |
|
|
243
|
+
| `onSearchFieldFocused` | `(event: SearchFieldFocusEvent) => void` | No | Called when native search field gains focus |
|
|
244
|
+
| `onSearchFieldBlurred` | `(event: SearchFieldFocusEvent) => void` | No | Called when native search field loses focus |
|
|
348
245
|
|
|
349
|
-
|
|
246
|
+
#### Other
|
|
350
247
|
|
|
351
248
|
| Prop | Type | Default | Description |
|
|
352
249
|
|------|------|---------|-------------|
|
|
353
|
-
| `style` | `ViewStyle` |
|
|
250
|
+
| `style` | `ViewStyle` | — | Style object for the view container |
|
|
354
251
|
|
|
355
|
-
|
|
252
|
+
### SearchResult
|
|
356
253
|
|
|
357
|
-
|
|
254
|
+
```ts
|
|
255
|
+
interface SearchResult {
|
|
256
|
+
id: string; // Unique identifier (used in onSelectItem)
|
|
257
|
+
title: string; // Primary display text
|
|
258
|
+
subtitle?: string; // Optional secondary text
|
|
259
|
+
imageUrl?: string; // Optional poster/thumbnail URL (HTTPS, HTTP, or data: URI)
|
|
260
|
+
}
|
|
261
|
+
```
|
|
358
262
|
|
|
359
|
-
|
|
360
|
-
- **Required fields**: Results with empty `id` or `title` are automatically filtered out and not displayed.
|
|
361
|
-
- **Image URL schemes**: HTTP, HTTPS, and `data:` URIs are accepted for `imageUrl`. Other URL schemes (e.g., `file://`) are rejected.
|
|
362
|
-
- **HTTPS recommended**: HTTP URLs may be blocked by App Transport Security on tvOS unless explicitly allowed in Info.plist.
|
|
263
|
+
### isNativeSearchAvailable()
|
|
363
264
|
|
|
364
|
-
|
|
265
|
+
```ts
|
|
266
|
+
function isNativeSearchAvailable(): boolean
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
Returns `true` when running on tvOS with the native module properly built. Use this to conditionally render a fallback on non-tvOS platforms.
|
|
365
270
|
|
|
366
|
-
|
|
271
|
+
```tsx
|
|
272
|
+
import { TvosSearchView, isNativeSearchAvailable } from 'expo-tvos-search';
|
|
273
|
+
|
|
274
|
+
if (!isNativeSearchAvailable()) {
|
|
275
|
+
return <FallbackSearch />;
|
|
276
|
+
}
|
|
277
|
+
return <TvosSearchView {...props} />;
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
## Result Validation
|
|
281
|
+
|
|
282
|
+
The native implementation applies the following constraints:
|
|
283
|
+
|
|
284
|
+
- **Maximum results** — the array is capped at 500 items; extras are silently ignored
|
|
285
|
+
- **Required fields** — results with empty `id` or `title` are filtered out
|
|
286
|
+
- **Image URL schemes** — HTTP, HTTPS, and `data:` URIs are accepted; other schemes (e.g. `file://`) are rejected
|
|
287
|
+
- **HTTPS recommended** — HTTP URLs may be blocked by App Transport Security unless explicitly allowed in Info.plist
|
|
288
|
+
|
|
289
|
+
## Demo App
|
|
290
|
+
|
|
291
|
+
Explore all configurations in the [expo-tvos-search-demo](https://github.com/keiver/expo-tvos-search-demo) repository.
|
|
292
|
+
|
|
293
|
+
## Testing
|
|
367
294
|
|
|
368
295
|
```bash
|
|
369
296
|
npm test # Run tests once
|
|
@@ -373,7 +300,7 @@ npm run test:coverage # Generate coverage report
|
|
|
373
300
|
|
|
374
301
|
Tests cover:
|
|
375
302
|
|
|
376
|
-
-
|
|
303
|
+
- Behavior across platforms
|
|
377
304
|
- Component rendering when native module is unavailable
|
|
378
305
|
- Event structure validation
|
|
379
306
|
|
|
@@ -387,10 +314,25 @@ We welcome contributions! Please see [CONTRIBUTING.md](CONTRIBUTING.md) for guid
|
|
|
387
314
|
- Commit message conventions
|
|
388
315
|
- Pull request process
|
|
389
316
|
|
|
390
|
-
|
|
317
|
+
<table>
|
|
318
|
+
<tr>
|
|
319
|
+
<td><img src="screenshots/default.webp" alt="Default empty search state with tvOS keyboard and tab bar" width="100%"/></td>
|
|
320
|
+
<td><img src="screenshots/demo-01.webp" alt="Square card grid layout with planet images and no title overlay" width="100%"/></td>
|
|
321
|
+
<td><img src="screenshots/demo-02.webp" alt="Four-column grid with title overlays and hardware keyboard active indicator" width="100%"/></td>
|
|
322
|
+
</tr>
|
|
323
|
+
<tr>
|
|
324
|
+
<td><img src="screenshots/demo-03.webp" alt="Five-column compact grid with short overlay titles" width="100%"/></td>
|
|
325
|
+
<td><img src="screenshots/demo-04.webp" alt="Mini card grid with truncated overlay titles on planet cards" width="100%"/></td>
|
|
326
|
+
<td><img src="screenshots/demo-06.webp" alt="Demo app home screen with feature badges and configuration menu" width="100%"/></td>
|
|
327
|
+
</tr>
|
|
328
|
+
<tr>
|
|
329
|
+
<td><img src="screenshots/no-results.webp" alt="No results found state after searching for a term with no matches" width="100%"/></td>
|
|
330
|
+
<td><img src="screenshots/results-jungle-book.webp" alt="Single search result for Jungle Book with poster card and title overlay" width="100%"/></td>
|
|
331
|
+
<td><img src="screenshots/results.webp" alt="Focused search result card for Caminandes with accent color border" width="100%"/></td>
|
|
332
|
+
</tr>
|
|
333
|
+
</table>
|
|
391
334
|
|
|
392
|
-
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.
|
|
393
335
|
|
|
394
336
|
## License
|
|
395
337
|
|
|
396
|
-
|
|
338
|
+
MIT — see [LICENSE](LICENSE) for details.
|
package/build/index.d.ts
CHANGED
|
@@ -206,17 +206,34 @@ export interface TvosSearchViewProps {
|
|
|
206
206
|
* @example "#E50914" for Netflix red
|
|
207
207
|
*/
|
|
208
208
|
accentColor?: string;
|
|
209
|
+
/**
|
|
210
|
+
* Override the system color scheme for the search view.
|
|
211
|
+
* - `'dark'`: Force dark appearance (white text, dark UI elements)
|
|
212
|
+
* - `'light'`: Force light appearance (black text, light UI elements)
|
|
213
|
+
* - `'system'`: Follow the system appearance (default)
|
|
214
|
+
*
|
|
215
|
+
* Useful when your app has a fixed dark background and the system is in
|
|
216
|
+
* light mode, which would make search bar text illegible.
|
|
217
|
+
* @default "system"
|
|
218
|
+
*/
|
|
219
|
+
colorScheme?: 'light' | 'dark' | 'system';
|
|
209
220
|
/**
|
|
210
221
|
* Width of each result card in points.
|
|
211
222
|
* Allows customization for portrait, landscape, or square layouts.
|
|
223
|
+
* Values outside 50-1000 range are clamped.
|
|
212
224
|
* @default 280
|
|
225
|
+
* @minimum 50
|
|
226
|
+
* @maximum 1000
|
|
213
227
|
* @example 420 for landscape cards
|
|
214
228
|
*/
|
|
215
229
|
cardWidth?: number;
|
|
216
230
|
/**
|
|
217
231
|
* Height of each result card in points.
|
|
218
232
|
* Allows customization for portrait, landscape, or square layouts.
|
|
233
|
+
* Values outside 50-1000 range are clamped.
|
|
219
234
|
* @default 420
|
|
235
|
+
* @minimum 50
|
|
236
|
+
* @maximum 1000
|
|
220
237
|
* @example 240 for landscape cards (16:9 ratio with width=420)
|
|
221
238
|
*/
|
|
222
239
|
cardHeight?: number;
|
|
@@ -230,20 +247,29 @@ export interface TvosSearchViewProps {
|
|
|
230
247
|
imageContentMode?: 'fill' | 'fit' | 'contain';
|
|
231
248
|
/**
|
|
232
249
|
* Spacing between cards in the grid layout (both horizontal and vertical).
|
|
250
|
+
* Values outside 0-200 range are clamped.
|
|
233
251
|
* @default 40
|
|
252
|
+
* @minimum 0
|
|
253
|
+
* @maximum 200
|
|
234
254
|
* @example 60 for spacious layouts, 20 for compact grids
|
|
235
255
|
*/
|
|
236
256
|
cardMargin?: number;
|
|
237
257
|
/**
|
|
238
258
|
* Padding inside the card for overlay content (title, subtitle).
|
|
259
|
+
* Values outside 0-100 range are clamped.
|
|
239
260
|
* @default 16
|
|
261
|
+
* @minimum 0
|
|
262
|
+
* @maximum 100
|
|
240
263
|
* @example 20 for more breathing room, 12 for compact cards
|
|
241
264
|
*/
|
|
242
265
|
cardPadding?: number;
|
|
243
266
|
/**
|
|
244
267
|
* Font size for title in the blur overlay (when showTitleOverlay is true).
|
|
245
268
|
* Allows customization of overlay text size for different card layouts.
|
|
269
|
+
* Values outside 8-72 range are clamped.
|
|
246
270
|
* @default 20
|
|
271
|
+
* @minimum 8
|
|
272
|
+
* @maximum 72
|
|
247
273
|
* @example 18 for smaller cards, 24 for larger cards
|
|
248
274
|
*/
|
|
249
275
|
overlayTitleSize?: number;
|
package/build/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.tsx"],"names":[],"mappings":";AACA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAG9C;;;GAGG;AACH,MAAM,WAAW,WAAW;IAC1B,WAAW,EAAE;QACX,0DAA0D;QAC1D,KAAK,EAAE,MAAM,CAAC;KACf,CAAC;CACH;AAED;;;GAGG;AACH,MAAM,WAAW,eAAe;IAC9B,WAAW,EAAE;QACX,0DAA0D;QAC1D,EAAE,EAAE,MAAM,CAAC;KACZ,CAAC;CACH;AAED;;GAEG;AACH,MAAM,MAAM,uBAAuB,GAC/B,oBAAoB,GACpB,mBAAmB,GACnB,mBAAmB,GACnB,SAAS,CAAC;AAEd;;;GAGG;AACH,MAAM,WAAW,oBAAoB;IACnC,WAAW,EAAE;QACX,sDAAsD;QACtD,QAAQ,EAAE,uBAAuB,CAAC;QAClC,mCAAmC;QACnC,OAAO,EAAE,MAAM,CAAC;QAChB,yDAAyD;QACzD,OAAO,CAAC,EAAE,MAAM,CAAC;KAClB,CAAC;CACH;AAED;;;GAGG;AACH,MAAM,WAAW,sBAAsB;IACrC,WAAW,EAAE;QACX,iCAAiC;QACjC,IAAI,EAAE,iBAAiB,GAAG,eAAe,GAAG,iBAAiB,GAAG,mBAAmB,GAAG,aAAa,GAAG,cAAc,GAAG,mBAAmB,CAAC;QAC3I,qCAAqC;QACrC,OAAO,EAAE,MAAM,CAAC;QAChB,kCAAkC;QAClC,OAAO,CAAC,EAAE,MAAM,CAAC;KAClB,CAAC;CACH;AAED;;;;GAIG;AACH,MAAM,WAAW,qBAAqB;IACpC,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,KAAK,CAAC,CAAC;CACpC;AAED;;GAEG;AACH,MAAM,WAAW,YAAY;IAC3B,uEAAuE;IACvE,EAAE,EAAE,MAAM,CAAC;IACX,0CAA0C;IAC1C,KAAK,EAAE,MAAM,CAAC;IACd,wDAAwD;IACxD,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,+FAA+F;IAC/F,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,WAAW,mBAAmB;IAClC;;;;;;OAMG;IACH,OAAO,EAAE,YAAY,EAAE,CAAC;IAExB;;;;;;OAMG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;IAEjB;;;OAGG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;IAErB;;;;;;;;;OASG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;IAEpB;;;OAGG;IACH,SAAS,CAAC,EAAE,OAAO,CAAC;IAEpB;;;OAGG;IACH,SAAS,CAAC,EAAE,OAAO,CAAC;IAEpB;;;;OAIG;IACH,YAAY,CAAC,EAAE,OAAO,CAAC;IAEvB;;;OAGG;IACH,eAAe,CAAC,EAAE,OAAO,CAAC;IAE1B;;;;;;;OAOG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC;IAElB;;;;OAIG;IACH,gBAAgB,CAAC,EAAE,OAAO,CAAC;IAE3B;;;OAGG;IACH,aAAa,CAAC,EAAE,OAAO,CAAC;IAExB;;;;;;OAMG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC;IAEtB;;;OAGG;IACH,cAAc,CAAC,EAAE,MAAM,CAAC;IAExB;;;OAGG;IACH,aAAa,CAAC,EAAE,MAAM,CAAC;IAEvB;;;OAGG;IACH,aAAa,CAAC,EAAE,MAAM,CAAC;IAEvB;;;OAGG;IACH,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAE3B;;;;;OAKG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;IAEnB;;;;;OAKG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;IAErB
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.tsx"],"names":[],"mappings":"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"}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Pod::Spec.new do |s|
|
|
2
2
|
s.name = 'ExpoTvosSearch'
|
|
3
|
-
s.version = '1.
|
|
3
|
+
s.version = '1.5.0'
|
|
4
4
|
s.summary = 'Native tvOS search view with SwiftUI .searchable modifier'
|
|
5
5
|
s.description = 'Provides a native tvOS search experience using SwiftUI searchable modifier for proper focus and keyboard navigation'
|
|
6
6
|
s.author = 'Keiver Hernandez'
|
|
@@ -143,6 +143,10 @@ public class ExpoTvosSearchModule: Module {
|
|
|
143
143
|
view.accentColor = colorHex
|
|
144
144
|
}
|
|
145
145
|
|
|
146
|
+
Prop("colorScheme") { (view: ExpoTvosSearchView, scheme: String) in
|
|
147
|
+
view.colorScheme = scheme
|
|
148
|
+
}
|
|
149
|
+
|
|
146
150
|
Prop("cardWidth") { (view: ExpoTvosSearchView, width: Double) in
|
|
147
151
|
view.cardWidth = CGFloat(max(50, min(1000, width))) // Clamp to reasonable range
|
|
148
152
|
}
|
|
@@ -178,6 +178,12 @@ class ExpoTvosSearchView: ExpoView {
|
|
|
178
178
|
}
|
|
179
179
|
}
|
|
180
180
|
|
|
181
|
+
var colorScheme: String = "system" {
|
|
182
|
+
didSet {
|
|
183
|
+
applyColorScheme()
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
181
187
|
var cardWidth: CGFloat = 280 {
|
|
182
188
|
didSet {
|
|
183
189
|
viewModel.cardWidth = cardWidth
|
|
@@ -249,12 +255,28 @@ class ExpoTvosSearchView: ExpoView {
|
|
|
249
255
|
}
|
|
250
256
|
}
|
|
251
257
|
|
|
258
|
+
private func applyColorScheme() {
|
|
259
|
+
let style: UIUserInterfaceStyle
|
|
260
|
+
switch colorScheme.lowercased() {
|
|
261
|
+
case "dark":
|
|
262
|
+
style = .dark
|
|
263
|
+
case "light":
|
|
264
|
+
style = .light
|
|
265
|
+
default:
|
|
266
|
+
style = .unspecified
|
|
267
|
+
}
|
|
268
|
+
hostingController?.overrideUserInterfaceStyle = style
|
|
269
|
+
}
|
|
270
|
+
|
|
252
271
|
private func setupView() {
|
|
253
272
|
let contentView = TvosSearchContentView(viewModel: viewModel)
|
|
254
273
|
let controller = UIHostingController(rootView: contentView)
|
|
255
274
|
controller.view.backgroundColor = .clear
|
|
256
275
|
hostingController = controller
|
|
257
276
|
|
|
277
|
+
// Apply initial color scheme (default "system" → .unspecified)
|
|
278
|
+
applyColorScheme()
|
|
279
|
+
|
|
258
280
|
// Configure viewModel callbacks
|
|
259
281
|
viewModel.onSearch = { [weak self] query in
|
|
260
282
|
self?.onSearch(["query": query])
|
|
@@ -557,6 +579,7 @@ class ExpoTvosSearchView: ExpoView {
|
|
|
557
579
|
var noResultsText: String = "No results found"
|
|
558
580
|
var noResultsHintText: String = "Try a different search term"
|
|
559
581
|
var textColor: String? = nil
|
|
582
|
+
var colorScheme: String = "system"
|
|
560
583
|
var accentColor: String = "#FFC312"
|
|
561
584
|
var cardWidth: CGFloat = 280
|
|
562
585
|
var cardHeight: CGFloat = 420
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "expo-tvos-search",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.5.0",
|
|
4
4
|
"description": "Native tvOS search view using SwiftUI .searchable modifier for Expo/React Native",
|
|
5
5
|
"main": "build/index.js",
|
|
6
6
|
"types": "build/index.d.ts",
|
|
@@ -46,9 +46,9 @@
|
|
|
46
46
|
"react-native-tvos"
|
|
47
47
|
],
|
|
48
48
|
"peerDependencies": {
|
|
49
|
-
"expo": "
|
|
50
|
-
"expo-modules-core": "
|
|
51
|
-
"react": "
|
|
49
|
+
"expo": "*",
|
|
50
|
+
"expo-modules-core": "*",
|
|
51
|
+
"react": "*",
|
|
52
52
|
"react-native": "*"
|
|
53
53
|
},
|
|
54
54
|
"peerDependenciesMeta": {
|
package/src/index.tsx
CHANGED
|
@@ -237,10 +237,25 @@ export interface TvosSearchViewProps {
|
|
|
237
237
|
*/
|
|
238
238
|
accentColor?: string;
|
|
239
239
|
|
|
240
|
+
/**
|
|
241
|
+
* Override the system color scheme for the search view.
|
|
242
|
+
* - `'dark'`: Force dark appearance (white text, dark UI elements)
|
|
243
|
+
* - `'light'`: Force light appearance (black text, light UI elements)
|
|
244
|
+
* - `'system'`: Follow the system appearance (default)
|
|
245
|
+
*
|
|
246
|
+
* Useful when your app has a fixed dark background and the system is in
|
|
247
|
+
* light mode, which would make search bar text illegible.
|
|
248
|
+
* @default "system"
|
|
249
|
+
*/
|
|
250
|
+
colorScheme?: 'light' | 'dark' | 'system';
|
|
251
|
+
|
|
240
252
|
/**
|
|
241
253
|
* Width of each result card in points.
|
|
242
254
|
* Allows customization for portrait, landscape, or square layouts.
|
|
255
|
+
* Values outside 50-1000 range are clamped.
|
|
243
256
|
* @default 280
|
|
257
|
+
* @minimum 50
|
|
258
|
+
* @maximum 1000
|
|
244
259
|
* @example 420 for landscape cards
|
|
245
260
|
*/
|
|
246
261
|
cardWidth?: number;
|
|
@@ -248,7 +263,10 @@ export interface TvosSearchViewProps {
|
|
|
248
263
|
/**
|
|
249
264
|
* Height of each result card in points.
|
|
250
265
|
* Allows customization for portrait, landscape, or square layouts.
|
|
266
|
+
* Values outside 50-1000 range are clamped.
|
|
251
267
|
* @default 420
|
|
268
|
+
* @minimum 50
|
|
269
|
+
* @maximum 1000
|
|
252
270
|
* @example 240 for landscape cards (16:9 ratio with width=420)
|
|
253
271
|
*/
|
|
254
272
|
cardHeight?: number;
|
|
@@ -264,14 +282,20 @@ export interface TvosSearchViewProps {
|
|
|
264
282
|
|
|
265
283
|
/**
|
|
266
284
|
* Spacing between cards in the grid layout (both horizontal and vertical).
|
|
285
|
+
* Values outside 0-200 range are clamped.
|
|
267
286
|
* @default 40
|
|
287
|
+
* @minimum 0
|
|
288
|
+
* @maximum 200
|
|
268
289
|
* @example 60 for spacious layouts, 20 for compact grids
|
|
269
290
|
*/
|
|
270
291
|
cardMargin?: number;
|
|
271
292
|
|
|
272
293
|
/**
|
|
273
294
|
* Padding inside the card for overlay content (title, subtitle).
|
|
295
|
+
* Values outside 0-100 range are clamped.
|
|
274
296
|
* @default 16
|
|
297
|
+
* @minimum 0
|
|
298
|
+
* @maximum 100
|
|
275
299
|
* @example 20 for more breathing room, 12 for compact cards
|
|
276
300
|
*/
|
|
277
301
|
cardPadding?: number;
|
|
@@ -279,7 +303,10 @@ export interface TvosSearchViewProps {
|
|
|
279
303
|
/**
|
|
280
304
|
* Font size for title in the blur overlay (when showTitleOverlay is true).
|
|
281
305
|
* Allows customization of overlay text size for different card layouts.
|
|
306
|
+
* Values outside 8-72 range are clamped.
|
|
282
307
|
* @default 20
|
|
308
|
+
* @minimum 8
|
|
309
|
+
* @maximum 72
|
|
283
310
|
* @example 18 for smaller cards, 24 for larger cards
|
|
284
311
|
*/
|
|
285
312
|
overlayTitleSize?: number;
|