@xhub-reels/sdk 0.2.12 → 0.2.13

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 CHANGED
@@ -86,16 +86,53 @@ function MyFeed() {
86
86
  }
87
87
  ```
88
88
 
89
- ## Thumbnail grid
89
+ ## Thumbnail grid → Modal
90
90
 
91
- `<ReelsFeedThumbnail>` is a headless grid component for the pre-drawer browsing UX
92
- (e.g. a community feed that opens the player drawer on tap). It must be mounted
93
- inside `<ReelsProvider>` and reads items from the same shared feed. The host
94
- provides card visuals via `renderThumbnail`.
91
+ `<ReelsFeedThumbnail>` is a grid component for the pre-player browsing UX (e.g. a
92
+ community feed that opens the player on tap). It must be mounted inside
93
+ `<ReelsProvider>` and reads items from the same shared feed. The host provides
94
+ card visuals via `renderThumbnail`.
95
+
96
+ ### SDK-owned modal (recommended)
97
+
98
+ Pair `<ReelsFeedThumbnail openOnClick>` with `<ReelsModal>` to let the SDK own the
99
+ open→play lifecycle end to end. This eliminates the "poster stuck for ~3s" stall:
100
+ on tap, the SDK **mounts the feed offscreen-but-painted, prewarms the focused
101
+ video and its neighbors synchronously, then slides the panel in** — so the first
102
+ frame is already decoded by the time the animation finishes.
95
103
 
96
104
  ```tsx
97
- import { ReelsProvider, ReelsFeedThumbnail } from 'xhub-reels-sdk';
105
+ import { ReelsProvider, ReelsFeedThumbnail, ReelsModal } from 'xhub-reels-sdk';
106
+
107
+ function CommunityPage() {
108
+ return (
109
+ <ReelsProvider adapters={{ dataSource, interaction }}>
110
+ <ReelsFeedThumbnail
111
+ openOnClick
112
+ renderThumbnail={(item) => (
113
+ <article className="aspect-[3/2] rounded-lg overflow-hidden">
114
+ <img src={item.poster} alt="" />
115
+ <p>@{item.author.name}</p>
116
+ </article>
117
+ )}
118
+ />
119
+ <ReelsModal feedProps={{ renderOverlay, renderActions, initialMuted: false }} />
120
+ </ReelsProvider>
121
+ );
122
+ }
123
+ ```
124
+
125
+ `openOnClick` calls `navigation.open(index)` on tap; `<ReelsModal>` subscribes to
126
+ the same navigation store and drives the rest. Because `play()` now starts inside
127
+ the tap's transient-activation window, **`initialMuted={false}` unmuted autoplay
128
+ is far more reliable** on mobile WebViews.
129
+
130
+ ### Headless callback (back-compat)
98
131
 
132
+ The original headless approach still works — drive your own drawer/modal via
133
+ `onThumbnailClick`:
134
+
135
+ ```tsx
99
136
  function CommunityPage() {
100
137
  const [drawerOpen, setDrawerOpen] = useState(false);
101
138
 
@@ -105,7 +142,6 @@ function CommunityPage() {
105
142
  renderThumbnail={(item) => (
106
143
  <article className="aspect-[3/2] rounded-lg overflow-hidden">
107
144
  <img src={item.poster} alt="" />
108
- <p>@{item.author.name}</p>
109
145
  </article>
110
146
  )}
111
147
  onThumbnailClick={(id) => {
@@ -113,7 +149,7 @@ function CommunityPage() {
113
149
  window.history.replaceState(null, '', `#reel_uuid=${id}`);
114
150
  }}
115
151
  />
116
- {drawerOpen && <ReelsDrawer onClose={() => setDrawerOpen(false)} />}
152
+ {drawerOpen && <YourDrawer onClose={() => setDrawerOpen(false)} />}
117
153
  </ReelsProvider>
118
154
  );
119
155
  }
@@ -124,21 +160,48 @@ By default, clicking a card calls `setFocusedIndexImmediate(index)` on the
124
160
  disable this glue (e.g. if the host manages focus separately), pass
125
161
  `setFocusOnClick={false}`.
126
162
 
127
- ### Props
163
+ ### `<ReelsFeedThumbnail>` props
128
164
 
129
165
  | Prop | Type | Default | Description |
130
166
  |---|---|---|---|
131
167
  | `renderThumbnail` | `(item, index) => ReactNode` | required | Card visual for a single item |
132
- | `onThumbnailClick` | `(id, item, index) => void` | | Click handler |
168
+ | `openOnClick` | `boolean` | `false` | Open the SDK-owned `<ReelsModal>` via `navigation.open(index)` on tap |
169
+ | `prewarmOnClick` | `boolean` | `true` | Fire HLS metadata prefetch on tap (mobile-friendly; runs even without hover) |
170
+ | `onThumbnailClick` | `(id, item, index) => void` | — | Click handler (fires alongside `open` for back-compat) |
171
+ | `setFocusOnClick` | `boolean` | `true` | Pre-focus the slot before `open`/`onThumbnailClick` fires |
172
+ | `prefetchOnHover` | `boolean` | `false` | Opt-in HLS metadata prefetch on `pointerenter` |
133
173
  | `renderLoading` | `() => ReactNode` | — | Shown while loading with no items |
134
174
  | `renderEmpty` | `() => ReactNode` | — | Shown when feed is empty |
135
175
  | `renderError` | `({ message, retry }) => ReactNode` | — | Shown on error with no items |
136
176
  | `className` | `string` | `'grid grid-cols-2 gap-3'` | Outer wrapper className |
137
177
  | `wrap` | `boolean` | `true` | Set `false` to render without an outer `<div>` |
138
- | `setFocusOnClick` | `boolean` | `true` | Pre-focus the slot before `onThumbnailClick` fires |
139
- | `prefetchOnHover` | `boolean` | `false` | Opt-in HLS metadata prefetch on `pointerenter` |
140
178
  | `getKey` | `(item, index) => string` | `item.id` | Key override for duplicate lists |
141
179
 
180
+ ### `<ReelsModal>` props
181
+
182
+ The modal is a **customizable shell**: the SDK owns timing and prewarm, while the
183
+ host can override the animation and chrome. Omit any prop to fall back to a
184
+ sensible default.
185
+
186
+ | Prop | Type | Default | Description |
187
+ |---|---|---|---|
188
+ | `feedProps` | `ReelsFeedProps` | — | Render props forwarded to the inner `<ReelsFeed>` (`renderOverlay`, `renderActions`, `initialMuted`, …) |
189
+ | `animationConfig` | `{ duration?, easing?, direction? }` | `{ 300, cubic-bezier(0.22, 1, 0.36, 1), 'up' }` | Slide-in tuning; `direction` is `'up' \| 'down' \| 'fade'` |
190
+ | `renderBackdrop` | `(state) => ReactNode` | dimmed overlay | Custom backdrop; `state` is `{ phase, openIndex, close }` |
191
+ | `renderCloseButton` | `(state) => ReactNode` | default ✕ button | Custom close affordance |
192
+ | `onOpen` | `(index) => void` | — | Fired when a modal session opens |
193
+ | `onClose` | `() => void` | — | Fired when the modal fully closes |
194
+ | `closeOnBackdropClick` | `boolean` | `true` | Close when the backdrop is clicked |
195
+ | `closeOnEscape` | `boolean` | `true` | Close on the `Escape` key |
196
+ | `lockBodyScroll` | `boolean` | `true` | Lock `<body>` scroll while open |
197
+ | `prewarmForward` | `number` | `2` | How many forward neighbors to prewarm on open |
198
+ | `className` | `string` | — | Class on the sliding panel |
199
+ | `zIndex` | `number` | `1000` | Stacking context for the portal |
200
+ | `portalTarget` | `HTMLElement \| null` | `document.body` | Portal mount node |
201
+
202
+ `<ReelsModal>` honors `prefers-reduced-motion` (skips the slide), traps focus,
203
+ restores focus to the trigger on close, and renders into a portal.
204
+
142
205
  ### Hover prefetch
143
206
 
144
207
  For hover-capable devices, opt into manifest prefetch so opening the player
@@ -150,7 +213,22 @@ feels instant:
150
213
 
151
214
  The SDK uses `adapters.videoLoader?.preloadMetadata?.(url)` and dedupes per item
152
215
  so a card only triggers one prefetch regardless of how many times it's hovered.
153
- Off by default to avoid surprising bandwidth on touch devices.
216
+ On touch devices there's no hover, so prefer `prewarmOnClick` (on by default).
217
+
218
+ ### Migrating from the headless callback
219
+
220
+ If you currently open your own drawer from `onThumbnailClick`:
221
+
222
+ 1. Add `openOnClick` to `<ReelsFeedThumbnail>` and mount `<ReelsModal>` as a
223
+ sibling inside `<ReelsProvider>`.
224
+ 2. Move your drawer's render props (`renderOverlay`, `renderActions`,
225
+ `initialMuted`, …) onto `<ReelsModal feedProps={{ … }}>`.
226
+ 3. Re-create your drawer's look with `animationConfig`, `renderBackdrop`, and
227
+ `renderCloseButton` — or drop them to use the defaults.
228
+ 4. Remove the host `drawerOpen` state and your `<ReelsDrawer>`/modal wrapper.
229
+
230
+ `onThumbnailClick` still fires alongside `open`, so you can migrate
231
+ incrementally (e.g. keep your URL-hash side effect) before deleting host state.
154
232
 
155
233
  ## Gesture Engine
156
234