@xhub-reels/sdk 0.1.21 → 0.2.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 CHANGED
@@ -111,6 +111,48 @@ function SwipeableFeed() {
111
111
  }
112
112
  ```
113
113
 
114
+ ## Thumbnail grid
115
+
116
+ Headless pre-drawer browsing UX. Mount inside `<ReelsProvider>`. Host provides the
117
+ card visuals via `renderThumbnail`; the SDK ships no default card.
118
+
119
+ ```tsx
120
+ import { ReelsFeedThumbnail } from 'xhub-reels-sdk';
121
+
122
+ <ReelsFeedThumbnail
123
+ renderThumbnail={(videoData) => (
124
+ <article className="aspect-[3/2] rounded-lg overflow-hidden">
125
+ <img src={videoData.poster} alt="" />
126
+ <p>@{videoData.author.name}</p>
127
+ </article>
128
+ )}
129
+ onThumbnailClick={(id) => openDrawer(id)}
130
+ />
131
+ ```
132
+
133
+ Opt-in hover prefetch (warms HLS manifest via `adapters.videoLoader.preloadMetadata`):
134
+
135
+ ```tsx
136
+ <ReelsFeedThumbnail prefetchOnHover renderThumbnail={...} />
137
+ ```
138
+
139
+ On click the SDK calls `ResourceGovernor.setFocusedIndexImmediate(index)` before
140
+ invoking `onThumbnailClick`, so opening `<ReelsFeed>` lands on the tapped item
141
+ without a scroll-to-index flash. Disable with `setFocusOnClick={false}`.
142
+
143
+ Custom slots for the zero-items branches:
144
+
145
+ ```tsx
146
+ <ReelsFeedThumbnail
147
+ renderThumbnail={(item) => <Card item={item} />}
148
+ renderLoading={() => <SkeletonGrid />}
149
+ renderEmpty={() => <EmptyState />}
150
+ renderError={({ message, retry }) => (
151
+ <ErrorBox message={message} onRetry={retry} />
152
+ )}
153
+ />
154
+ ```
155
+
114
156
  ## Architecture
115
157
 
116
158
  ```
package/dist/index.cjs CHANGED
@@ -2052,7 +2052,7 @@ function VideoSlotInner({
2052
2052
  inset: 0,
2053
2053
  backgroundImage: `url(${item.poster})`,
2054
2054
  backgroundSize: "cover",
2055
- backgroundPosition: "center",
2055
+ backgroundPosition: "center center",
2056
2056
  opacity: showPosterOverlay ? 1 : 0,
2057
2057
  transition: "opacity 0.15s ease",
2058
2058
  pointerEvents: "none"
@@ -2499,6 +2499,88 @@ function parsePxTranslateY(el) {
2499
2499
  if (!match || !match[1]) return 0;
2500
2500
  return Number.parseFloat(match[1]);
2501
2501
  }
2502
+ var DEFAULT_CLASSNAME = "grid grid-cols-2 gap-3";
2503
+ var defaultGetKey = (item) => item.id;
2504
+ var buttonResetStyle = {
2505
+ all: "unset",
2506
+ display: "block",
2507
+ cursor: "pointer",
2508
+ width: "100%",
2509
+ boxSizing: "border-box"
2510
+ };
2511
+ function ReelsFeedThumbnail({
2512
+ renderThumbnail,
2513
+ onThumbnailClick,
2514
+ renderLoading,
2515
+ renderEmpty,
2516
+ renderError,
2517
+ className = DEFAULT_CLASSNAME,
2518
+ wrap = true,
2519
+ setFocusOnClick = true,
2520
+ prefetchOnHover = false,
2521
+ getKey = defaultGetKey
2522
+ }) {
2523
+ const { items, loading, error, refresh } = useFeed();
2524
+ const { setFocusedIndexImmediate } = useResource();
2525
+ const { adapters } = useSDK();
2526
+ const prefetchedRef = react.useRef(/* @__PURE__ */ new Set());
2527
+ const handleClick = react.useCallback(
2528
+ (item, index) => {
2529
+ if (setFocusOnClick) {
2530
+ setFocusedIndexImmediate(index);
2531
+ }
2532
+ onThumbnailClick?.(item.id, item, index);
2533
+ },
2534
+ [onThumbnailClick, setFocusOnClick, setFocusedIndexImmediate]
2535
+ );
2536
+ const handlePointerEnter = react.useCallback(
2537
+ (item) => {
2538
+ if (!prefetchOnHover) return;
2539
+ if (!isVideoItem(item)) return;
2540
+ if (item.source.type !== "hls") return;
2541
+ if (prefetchedRef.current.has(item.id)) return;
2542
+ const loader = adapters.videoLoader;
2543
+ if (!loader?.preloadMetadata) return;
2544
+ if (loader.isPreloaded(item.id)) {
2545
+ prefetchedRef.current.add(item.id);
2546
+ return;
2547
+ }
2548
+ loader.preloadMetadata(item.source.url);
2549
+ prefetchedRef.current.add(item.id);
2550
+ },
2551
+ [prefetchOnHover, adapters.videoLoader]
2552
+ );
2553
+ const children = react.useMemo(
2554
+ () => items.map((item, index) => {
2555
+ const key = getKey(item, index);
2556
+ const onEnter = prefetchOnHover ? (_e) => handlePointerEnter(item) : void 0;
2557
+ return /* @__PURE__ */ jsxRuntime.jsx(
2558
+ "button",
2559
+ {
2560
+ type: "button",
2561
+ style: buttonResetStyle,
2562
+ "data-thumbnail-index": index,
2563
+ "data-thumbnail-id": item.id,
2564
+ onClick: () => handleClick(item, index),
2565
+ onPointerEnter: onEnter,
2566
+ children: renderThumbnail(item, index)
2567
+ },
2568
+ key
2569
+ );
2570
+ }),
2571
+ [items, getKey, prefetchOnHover, handlePointerEnter, handleClick, renderThumbnail]
2572
+ );
2573
+ if (items.length === 0) {
2574
+ if (loading && renderLoading) return /* @__PURE__ */ jsxRuntime.jsx(jsxRuntime.Fragment, { children: renderLoading() });
2575
+ if (error && renderError) {
2576
+ return /* @__PURE__ */ jsxRuntime.jsx(jsxRuntime.Fragment, { children: renderError({ message: error.message, retry: refresh }) });
2577
+ }
2578
+ if (!loading && !error && renderEmpty) return /* @__PURE__ */ jsxRuntime.jsx(jsxRuntime.Fragment, { children: renderEmpty() });
2579
+ return null;
2580
+ }
2581
+ if (!wrap) return /* @__PURE__ */ jsxRuntime.jsx(jsxRuntime.Fragment, { children });
2582
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { className, children });
2583
+ }
2502
2584
  function usePlayerSelector(selector) {
2503
2585
  const { playerEngine } = useSDK();
2504
2586
  const selectorRef = react.useRef(selector);
@@ -2919,16 +3001,12 @@ function transformVideoItem(raw) {
2919
3001
  const stats = transformStats(obj);
2920
3002
  const interaction = transformInteraction(obj);
2921
3003
  let poster;
2922
- const thumbnailObj = obj["thumbnail"];
2923
- if (thumbnailObj && thumbnailObj["url"]) {
2924
- poster = toStr(thumbnailObj["url"], "") || void 0;
2925
- }
2926
- if (!poster) {
2927
- const mediaArr2 = obj["media"];
2928
- if (Array.isArray(mediaArr2) && mediaArr2.length > 0) {
2929
- const first = mediaArr2[0];
2930
- poster = toStr(first["poster"], "") || void 0;
2931
- }
3004
+ let duration = 0;
3005
+ const mediaArr = obj["media"];
3006
+ if (Array.isArray(mediaArr) && mediaArr.length > 0) {
3007
+ const first = mediaArr[0];
3008
+ poster = toStr(first["poster"], "") || void 0;
3009
+ duration = toNum(first["duration"], 0);
2932
3010
  }
2933
3011
  if (!poster) {
2934
3012
  poster = toStr(
@@ -2936,11 +3014,11 @@ function transformVideoItem(raw) {
2936
3014
  void 0
2937
3015
  ) || void 0;
2938
3016
  }
2939
- let duration = 0;
2940
- const mediaArr = obj["media"];
2941
- if (Array.isArray(mediaArr) && mediaArr.length > 0) {
2942
- const first = mediaArr[0];
2943
- duration = toNum(first["duration"], 0);
3017
+ if (!poster) {
3018
+ const thumbnailObj = obj["thumbnail"];
3019
+ if (thumbnailObj && thumbnailObj["url"]) {
3020
+ poster = toStr(thumbnailObj["url"], "") || void 0;
3021
+ }
2944
3022
  }
2945
3023
  if (duration === 0) {
2946
3024
  duration = toNum(tryFields(obj, "duration", "duration_seconds", "length", "video_duration"), 0);
@@ -3135,6 +3213,7 @@ exports.OptimisticManager = OptimisticManager;
3135
3213
  exports.PlayerEngine = PlayerEngine;
3136
3214
  exports.PlayerStatus = PlayerStatus;
3137
3215
  exports.ReelsFeed = ReelsFeed;
3216
+ exports.ReelsFeedThumbnail = ReelsFeedThumbnail;
3138
3217
  exports.ReelsProvider = ReelsProvider;
3139
3218
  exports.ResourceGovernor = ResourceGovernor;
3140
3219
  exports.VALID_TRANSITIONS = VALID_TRANSITIONS;
package/dist/index.d.cts CHANGED
@@ -875,6 +875,41 @@ interface VideoSlotProps {
875
875
  }
876
876
  declare function VideoSlot({ item, index, isActive, isPrefetch, isPreloaded, bufferTier, isMuted, onToggleMute, onAutoplayBlocked, showFps, isDragging, renderOverlay, renderActions, renderPauseAction, }: VideoSlotProps): react_jsx_runtime.JSX.Element;
877
877
 
878
+ interface ReelsFeedThumbnailProps {
879
+ /** Render function for a single item's visual card. */
880
+ renderThumbnail: (videoData: ContentItem, index: number) => ReactNode;
881
+ /** Click handler — receives the item id, full data, and index in the feed. */
882
+ onThumbnailClick?: (id: string, videoData: ContentItem, index: number) => void;
883
+ /** Rendered when the feed is loading and has zero items yet. */
884
+ renderLoading?: () => ReactNode;
885
+ /** Rendered when the feed has loaded but is empty. */
886
+ renderEmpty?: () => ReactNode;
887
+ /** Rendered when the feed fails with no items — receives a retry callback. */
888
+ renderError?: (error: {
889
+ message: string;
890
+ retry: () => void;
891
+ }) => ReactNode;
892
+ /** Outer wrapper className. Defaults to `grid grid-cols-2 gap-3`. */
893
+ className?: string;
894
+ /** If the host wants to render its own wrapper, pass `false`. */
895
+ wrap?: boolean;
896
+ /**
897
+ * Update ResourceGovernor's focused index on click so the drawer/feed opens
898
+ * already pointing at the tapped item.
899
+ * @default true
900
+ */
901
+ setFocusOnClick?: boolean;
902
+ /**
903
+ * Prefetch HLS metadata for this slot on `pointerenter`. Off by default;
904
+ * only useful on hover-capable devices.
905
+ * @default false
906
+ */
907
+ prefetchOnHover?: boolean;
908
+ /** Key override — useful when host renders multiple lists from the same feed. */
909
+ getKey?: (item: ContentItem, index: number) => string;
910
+ }
911
+ declare function ReelsFeedThumbnail({ renderThumbnail, onThumbnailClick, renderLoading, renderEmpty, renderError, className, wrap, setFocusOnClick, prefetchOnHover, getKey, }: ReelsFeedThumbnailProps): react_jsx_runtime.JSX.Element | null;
912
+
878
913
  declare function DefaultOverlay({ item }: {
879
914
  item: ContentItem;
880
915
  }): react_jsx_runtime.JSX.Element;
@@ -1174,4 +1209,4 @@ declare class HttpError extends Error {
1174
1209
  constructor(status: number, message: string, body?: string | undefined);
1175
1210
  }
1176
1211
 
1177
- export { type Article, type ArticleImage, type Author, type BufferTier, type CommentItem, type CommentPage, type ContentItem, type ContentStats, DEFAULT_FEED_CONFIG, DEFAULT_PLAYER_CONFIG, DEFAULT_RESOURCE_CONFIG, DefaultActions, DefaultOverlay, DefaultPauseAction, DefaultSkeleton, type FeedConfig, type FeedError, FeedManager, type FeedPage, type FeedState, HttpDataSource, type HttpDataSourceConfig, HttpError, type IAnalytics, type ICommentAdapter, type IDataSource, type IInteraction, type ILogger, type INetworkAdapter, type ISessionStorage, type IVideoLoader, type InteractionState, type LogLevel, MockAnalytics, MockCommentAdapter, MockDataSource, MockInteraction, MockLogger, MockNetworkAdapter, MockSessionStorage, MockVideoLoader, type NetworkType, OptimisticManager, type PauseSlotActions, type PlayerConfig, PlayerEngine, type PlayerError, type PlayerEvent, type PlayerEventListener, type PlayerState, PlayerStatus, type PointerGestureConfig, type PreloadResult, type PreloadStatus, ReelsFeed, type ReelsFeedProps, ReelsProvider, type ReelsProviderProps, type ResourceConfig, ResourceGovernor, type ResourceState, type SDKAdapters, type SDKContextValue, type SlotActions, type SnapTarget, type UseHlsOptions, type UseHlsReturn, VALID_TRANSITIONS, type VideoItem, type VideoQuality, VideoSlot, type VideoSource, canPause, canPlay, canSeek, isArticle, isValidTransition, isVideoItem, useFeed, useFeedSelector, useHls, usePlayer, usePlayerSelector, usePointerGesture, useResource, useResourceSelector, useSDK, useSnapAnimation };
1212
+ export { type Article, type ArticleImage, type Author, type BufferTier, type CommentItem, type CommentPage, type ContentItem, type ContentStats, DEFAULT_FEED_CONFIG, DEFAULT_PLAYER_CONFIG, DEFAULT_RESOURCE_CONFIG, DefaultActions, DefaultOverlay, DefaultPauseAction, DefaultSkeleton, type FeedConfig, type FeedError, FeedManager, type FeedPage, type FeedState, HttpDataSource, type HttpDataSourceConfig, HttpError, type IAnalytics, type ICommentAdapter, type IDataSource, type IInteraction, type ILogger, type INetworkAdapter, type ISessionStorage, type IVideoLoader, type InteractionState, type LogLevel, MockAnalytics, MockCommentAdapter, MockDataSource, MockInteraction, MockLogger, MockNetworkAdapter, MockSessionStorage, MockVideoLoader, type NetworkType, OptimisticManager, type PauseSlotActions, type PlayerConfig, PlayerEngine, type PlayerError, type PlayerEvent, type PlayerEventListener, type PlayerState, PlayerStatus, type PointerGestureConfig, type PreloadResult, type PreloadStatus, ReelsFeed, type ReelsFeedProps, ReelsFeedThumbnail, type ReelsFeedThumbnailProps, ReelsProvider, type ReelsProviderProps, type ResourceConfig, ResourceGovernor, type ResourceState, type SDKAdapters, type SDKContextValue, type SlotActions, type SnapTarget, type UseHlsOptions, type UseHlsReturn, VALID_TRANSITIONS, type VideoItem, type VideoQuality, VideoSlot, type VideoSource, canPause, canPlay, canSeek, isArticle, isValidTransition, isVideoItem, useFeed, useFeedSelector, useHls, usePlayer, usePlayerSelector, usePointerGesture, useResource, useResourceSelector, useSDK, useSnapAnimation };
package/dist/index.d.ts CHANGED
@@ -875,6 +875,41 @@ interface VideoSlotProps {
875
875
  }
876
876
  declare function VideoSlot({ item, index, isActive, isPrefetch, isPreloaded, bufferTier, isMuted, onToggleMute, onAutoplayBlocked, showFps, isDragging, renderOverlay, renderActions, renderPauseAction, }: VideoSlotProps): react_jsx_runtime.JSX.Element;
877
877
 
878
+ interface ReelsFeedThumbnailProps {
879
+ /** Render function for a single item's visual card. */
880
+ renderThumbnail: (videoData: ContentItem, index: number) => ReactNode;
881
+ /** Click handler — receives the item id, full data, and index in the feed. */
882
+ onThumbnailClick?: (id: string, videoData: ContentItem, index: number) => void;
883
+ /** Rendered when the feed is loading and has zero items yet. */
884
+ renderLoading?: () => ReactNode;
885
+ /** Rendered when the feed has loaded but is empty. */
886
+ renderEmpty?: () => ReactNode;
887
+ /** Rendered when the feed fails with no items — receives a retry callback. */
888
+ renderError?: (error: {
889
+ message: string;
890
+ retry: () => void;
891
+ }) => ReactNode;
892
+ /** Outer wrapper className. Defaults to `grid grid-cols-2 gap-3`. */
893
+ className?: string;
894
+ /** If the host wants to render its own wrapper, pass `false`. */
895
+ wrap?: boolean;
896
+ /**
897
+ * Update ResourceGovernor's focused index on click so the drawer/feed opens
898
+ * already pointing at the tapped item.
899
+ * @default true
900
+ */
901
+ setFocusOnClick?: boolean;
902
+ /**
903
+ * Prefetch HLS metadata for this slot on `pointerenter`. Off by default;
904
+ * only useful on hover-capable devices.
905
+ * @default false
906
+ */
907
+ prefetchOnHover?: boolean;
908
+ /** Key override — useful when host renders multiple lists from the same feed. */
909
+ getKey?: (item: ContentItem, index: number) => string;
910
+ }
911
+ declare function ReelsFeedThumbnail({ renderThumbnail, onThumbnailClick, renderLoading, renderEmpty, renderError, className, wrap, setFocusOnClick, prefetchOnHover, getKey, }: ReelsFeedThumbnailProps): react_jsx_runtime.JSX.Element | null;
912
+
878
913
  declare function DefaultOverlay({ item }: {
879
914
  item: ContentItem;
880
915
  }): react_jsx_runtime.JSX.Element;
@@ -1174,4 +1209,4 @@ declare class HttpError extends Error {
1174
1209
  constructor(status: number, message: string, body?: string | undefined);
1175
1210
  }
1176
1211
 
1177
- export { type Article, type ArticleImage, type Author, type BufferTier, type CommentItem, type CommentPage, type ContentItem, type ContentStats, DEFAULT_FEED_CONFIG, DEFAULT_PLAYER_CONFIG, DEFAULT_RESOURCE_CONFIG, DefaultActions, DefaultOverlay, DefaultPauseAction, DefaultSkeleton, type FeedConfig, type FeedError, FeedManager, type FeedPage, type FeedState, HttpDataSource, type HttpDataSourceConfig, HttpError, type IAnalytics, type ICommentAdapter, type IDataSource, type IInteraction, type ILogger, type INetworkAdapter, type ISessionStorage, type IVideoLoader, type InteractionState, type LogLevel, MockAnalytics, MockCommentAdapter, MockDataSource, MockInteraction, MockLogger, MockNetworkAdapter, MockSessionStorage, MockVideoLoader, type NetworkType, OptimisticManager, type PauseSlotActions, type PlayerConfig, PlayerEngine, type PlayerError, type PlayerEvent, type PlayerEventListener, type PlayerState, PlayerStatus, type PointerGestureConfig, type PreloadResult, type PreloadStatus, ReelsFeed, type ReelsFeedProps, ReelsProvider, type ReelsProviderProps, type ResourceConfig, ResourceGovernor, type ResourceState, type SDKAdapters, type SDKContextValue, type SlotActions, type SnapTarget, type UseHlsOptions, type UseHlsReturn, VALID_TRANSITIONS, type VideoItem, type VideoQuality, VideoSlot, type VideoSource, canPause, canPlay, canSeek, isArticle, isValidTransition, isVideoItem, useFeed, useFeedSelector, useHls, usePlayer, usePlayerSelector, usePointerGesture, useResource, useResourceSelector, useSDK, useSnapAnimation };
1212
+ export { type Article, type ArticleImage, type Author, type BufferTier, type CommentItem, type CommentPage, type ContentItem, type ContentStats, DEFAULT_FEED_CONFIG, DEFAULT_PLAYER_CONFIG, DEFAULT_RESOURCE_CONFIG, DefaultActions, DefaultOverlay, DefaultPauseAction, DefaultSkeleton, type FeedConfig, type FeedError, FeedManager, type FeedPage, type FeedState, HttpDataSource, type HttpDataSourceConfig, HttpError, type IAnalytics, type ICommentAdapter, type IDataSource, type IInteraction, type ILogger, type INetworkAdapter, type ISessionStorage, type IVideoLoader, type InteractionState, type LogLevel, MockAnalytics, MockCommentAdapter, MockDataSource, MockInteraction, MockLogger, MockNetworkAdapter, MockSessionStorage, MockVideoLoader, type NetworkType, OptimisticManager, type PauseSlotActions, type PlayerConfig, PlayerEngine, type PlayerError, type PlayerEvent, type PlayerEventListener, type PlayerState, PlayerStatus, type PointerGestureConfig, type PreloadResult, type PreloadStatus, ReelsFeed, type ReelsFeedProps, ReelsFeedThumbnail, type ReelsFeedThumbnailProps, ReelsProvider, type ReelsProviderProps, type ResourceConfig, ResourceGovernor, type ResourceState, type SDKAdapters, type SDKContextValue, type SlotActions, type SnapTarget, type UseHlsOptions, type UseHlsReturn, VALID_TRANSITIONS, type VideoItem, type VideoQuality, VideoSlot, type VideoSource, canPause, canPlay, canSeek, isArticle, isValidTransition, isVideoItem, useFeed, useFeedSelector, useHls, usePlayer, usePlayerSelector, usePointerGesture, useResource, useResourceSelector, useSDK, useSnapAnimation };
package/dist/index.js CHANGED
@@ -2046,7 +2046,7 @@ function VideoSlotInner({
2046
2046
  inset: 0,
2047
2047
  backgroundImage: `url(${item.poster})`,
2048
2048
  backgroundSize: "cover",
2049
- backgroundPosition: "center",
2049
+ backgroundPosition: "center center",
2050
2050
  opacity: showPosterOverlay ? 1 : 0,
2051
2051
  transition: "opacity 0.15s ease",
2052
2052
  pointerEvents: "none"
@@ -2493,6 +2493,88 @@ function parsePxTranslateY(el) {
2493
2493
  if (!match || !match[1]) return 0;
2494
2494
  return Number.parseFloat(match[1]);
2495
2495
  }
2496
+ var DEFAULT_CLASSNAME = "grid grid-cols-2 gap-3";
2497
+ var defaultGetKey = (item) => item.id;
2498
+ var buttonResetStyle = {
2499
+ all: "unset",
2500
+ display: "block",
2501
+ cursor: "pointer",
2502
+ width: "100%",
2503
+ boxSizing: "border-box"
2504
+ };
2505
+ function ReelsFeedThumbnail({
2506
+ renderThumbnail,
2507
+ onThumbnailClick,
2508
+ renderLoading,
2509
+ renderEmpty,
2510
+ renderError,
2511
+ className = DEFAULT_CLASSNAME,
2512
+ wrap = true,
2513
+ setFocusOnClick = true,
2514
+ prefetchOnHover = false,
2515
+ getKey = defaultGetKey
2516
+ }) {
2517
+ const { items, loading, error, refresh } = useFeed();
2518
+ const { setFocusedIndexImmediate } = useResource();
2519
+ const { adapters } = useSDK();
2520
+ const prefetchedRef = useRef(/* @__PURE__ */ new Set());
2521
+ const handleClick = useCallback(
2522
+ (item, index) => {
2523
+ if (setFocusOnClick) {
2524
+ setFocusedIndexImmediate(index);
2525
+ }
2526
+ onThumbnailClick?.(item.id, item, index);
2527
+ },
2528
+ [onThumbnailClick, setFocusOnClick, setFocusedIndexImmediate]
2529
+ );
2530
+ const handlePointerEnter = useCallback(
2531
+ (item) => {
2532
+ if (!prefetchOnHover) return;
2533
+ if (!isVideoItem(item)) return;
2534
+ if (item.source.type !== "hls") return;
2535
+ if (prefetchedRef.current.has(item.id)) return;
2536
+ const loader = adapters.videoLoader;
2537
+ if (!loader?.preloadMetadata) return;
2538
+ if (loader.isPreloaded(item.id)) {
2539
+ prefetchedRef.current.add(item.id);
2540
+ return;
2541
+ }
2542
+ loader.preloadMetadata(item.source.url);
2543
+ prefetchedRef.current.add(item.id);
2544
+ },
2545
+ [prefetchOnHover, adapters.videoLoader]
2546
+ );
2547
+ const children = useMemo(
2548
+ () => items.map((item, index) => {
2549
+ const key = getKey(item, index);
2550
+ const onEnter = prefetchOnHover ? (_e) => handlePointerEnter(item) : void 0;
2551
+ return /* @__PURE__ */ jsx(
2552
+ "button",
2553
+ {
2554
+ type: "button",
2555
+ style: buttonResetStyle,
2556
+ "data-thumbnail-index": index,
2557
+ "data-thumbnail-id": item.id,
2558
+ onClick: () => handleClick(item, index),
2559
+ onPointerEnter: onEnter,
2560
+ children: renderThumbnail(item, index)
2561
+ },
2562
+ key
2563
+ );
2564
+ }),
2565
+ [items, getKey, prefetchOnHover, handlePointerEnter, handleClick, renderThumbnail]
2566
+ );
2567
+ if (items.length === 0) {
2568
+ if (loading && renderLoading) return /* @__PURE__ */ jsx(Fragment, { children: renderLoading() });
2569
+ if (error && renderError) {
2570
+ return /* @__PURE__ */ jsx(Fragment, { children: renderError({ message: error.message, retry: refresh }) });
2571
+ }
2572
+ if (!loading && !error && renderEmpty) return /* @__PURE__ */ jsx(Fragment, { children: renderEmpty() });
2573
+ return null;
2574
+ }
2575
+ if (!wrap) return /* @__PURE__ */ jsx(Fragment, { children });
2576
+ return /* @__PURE__ */ jsx("div", { className, children });
2577
+ }
2496
2578
  function usePlayerSelector(selector) {
2497
2579
  const { playerEngine } = useSDK();
2498
2580
  const selectorRef = useRef(selector);
@@ -2913,16 +2995,12 @@ function transformVideoItem(raw) {
2913
2995
  const stats = transformStats(obj);
2914
2996
  const interaction = transformInteraction(obj);
2915
2997
  let poster;
2916
- const thumbnailObj = obj["thumbnail"];
2917
- if (thumbnailObj && thumbnailObj["url"]) {
2918
- poster = toStr(thumbnailObj["url"], "") || void 0;
2919
- }
2920
- if (!poster) {
2921
- const mediaArr2 = obj["media"];
2922
- if (Array.isArray(mediaArr2) && mediaArr2.length > 0) {
2923
- const first = mediaArr2[0];
2924
- poster = toStr(first["poster"], "") || void 0;
2925
- }
2998
+ let duration = 0;
2999
+ const mediaArr = obj["media"];
3000
+ if (Array.isArray(mediaArr) && mediaArr.length > 0) {
3001
+ const first = mediaArr[0];
3002
+ poster = toStr(first["poster"], "") || void 0;
3003
+ duration = toNum(first["duration"], 0);
2926
3004
  }
2927
3005
  if (!poster) {
2928
3006
  poster = toStr(
@@ -2930,11 +3008,11 @@ function transformVideoItem(raw) {
2930
3008
  void 0
2931
3009
  ) || void 0;
2932
3010
  }
2933
- let duration = 0;
2934
- const mediaArr = obj["media"];
2935
- if (Array.isArray(mediaArr) && mediaArr.length > 0) {
2936
- const first = mediaArr[0];
2937
- duration = toNum(first["duration"], 0);
3011
+ if (!poster) {
3012
+ const thumbnailObj = obj["thumbnail"];
3013
+ if (thumbnailObj && thumbnailObj["url"]) {
3014
+ poster = toStr(thumbnailObj["url"], "") || void 0;
3015
+ }
2938
3016
  }
2939
3017
  if (duration === 0) {
2940
3018
  duration = toNum(tryFields(obj, "duration", "duration_seconds", "length", "video_duration"), 0);
@@ -3107,4 +3185,4 @@ var HttpError = class extends Error {
3107
3185
  }
3108
3186
  };
3109
3187
 
3110
- export { DEFAULT_FEED_CONFIG, DEFAULT_PLAYER_CONFIG, DEFAULT_RESOURCE_CONFIG, DefaultActions, DefaultOverlay, DefaultPauseAction, DefaultSkeleton, FeedManager, HttpDataSource, HttpError, MockAnalytics, MockCommentAdapter, MockDataSource, MockInteraction, MockLogger, MockNetworkAdapter, MockSessionStorage, MockVideoLoader, OptimisticManager, PlayerEngine, PlayerStatus, ReelsFeed, ReelsProvider, ResourceGovernor, VALID_TRANSITIONS, VideoSlot, canPause, canPlay, canSeek, isArticle, isValidTransition, isVideoItem, useFeed, useFeedSelector, useHls, usePlayer, usePlayerSelector, usePointerGesture, useResource, useResourceSelector, useSDK, useSnapAnimation };
3188
+ export { DEFAULT_FEED_CONFIG, DEFAULT_PLAYER_CONFIG, DEFAULT_RESOURCE_CONFIG, DefaultActions, DefaultOverlay, DefaultPauseAction, DefaultSkeleton, FeedManager, HttpDataSource, HttpError, MockAnalytics, MockCommentAdapter, MockDataSource, MockInteraction, MockLogger, MockNetworkAdapter, MockSessionStorage, MockVideoLoader, OptimisticManager, PlayerEngine, PlayerStatus, ReelsFeed, ReelsFeedThumbnail, ReelsProvider, ResourceGovernor, VALID_TRANSITIONS, VideoSlot, canPause, canPlay, canSeek, isArticle, isValidTransition, isVideoItem, useFeed, useFeedSelector, useHls, usePlayer, usePlayerSelector, usePointerGesture, useResource, useResourceSelector, useSDK, useSnapAnimation };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xhub-reels/sdk",
3
- "version": "0.1.21",
3
+ "version": "0.2.1",
4
4
  "description": "High-performance Short Video / Reels SDK for React — optimized for Flutter WebView",
5
5
  "license": "MIT",
6
6
  "type": "module",