@shortkitsdk/web 0.3.0 → 0.3.2

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.
@@ -599,16 +599,35 @@ class PlayerPool {
599
599
  player._skCurrentUrl = streamingUrl;
600
600
  }
601
601
  /**
602
- * Promote a preload player to active: raise buffer limits and uncap ABR.
603
- * Safe to call even if the HLS instance was already created with active config.
602
+ * Promote a preload player to active: raise buffer limits, uncap ABR, and
603
+ * force an immediate quality upgrade.
604
+ *
605
+ * Preload streams start at level 0 (lowest rendition) to conserve bandwidth.
606
+ * When attachStream() is called again with isActive=true for the same URL it
607
+ * early-returns, so the HLS instance keeps its startLevel:0 config. The
608
+ * already-buffered low-quality segments play through before any higher-quality
609
+ * segments arrive — causing a visible low-res start even on fast connections.
610
+ *
611
+ * Fix: lock HLS to the highest level, then seek-in-place to flush the
612
+ * low-quality buffer so the player immediately re-fetches at high quality.
613
+ * Re-enable ABR after a few seconds so slow connections self-correct.
604
614
  */
605
615
  promoteToActive(itemId) {
606
616
  const hls = this.hlsInstances.get(itemId);
607
- if (hls) {
608
- hls.config.maxBufferLength = PlayerPool.ACTIVE_HLS_CONFIG.maxBufferLength;
609
- hls.config.maxMaxBufferLength = PlayerPool.ACTIVE_HLS_CONFIG.maxMaxBufferLength;
610
- hls.autoLevelCapping = -1;
611
- hls.nextAutoLevel = -1;
617
+ if (!hls) return;
618
+ hls.config.maxBufferLength = PlayerPool.ACTIVE_HLS_CONFIG.maxBufferLength;
619
+ hls.config.maxMaxBufferLength = PlayerPool.ACTIVE_HLS_CONFIG.maxMaxBufferLength;
620
+ hls.autoLevelCapping = -1;
621
+ const top = hls.levels ? hls.levels.length - 1 : -1;
622
+ if (top > 0 && hls.currentLevel < top) {
623
+ hls.currentLevel = top;
624
+ const player = this.assignments.get(itemId);
625
+ if (player) {
626
+ player.currentTime = player.currentTime;
627
+ }
628
+ setTimeout(() => {
629
+ if (!hls.destroyed) hls.currentLevel = -1;
630
+ }, 4e3);
612
631
  }
613
632
  }
614
633
  /** Pause, destroy HLS, and remove player from DOM and pool tracking. */
@@ -904,7 +923,7 @@ class FeedManager {
904
923
  }
905
924
  setMuted(muted) {
906
925
  this.isMuted = muted;
907
- for (const [, player] of this.pool._players) {
926
+ for (const [, player] of this.pool.assignments) {
908
927
  player.muted = muted;
909
928
  }
910
929
  this._pushPlayerState({ isMuted: muted });
@@ -917,7 +936,7 @@ class FeedManager {
917
936
  }
918
937
  setPlaybackRate(rate) {
919
938
  this.playbackRate = rate;
920
- for (const [, player] of this.pool._players) {
939
+ for (const [, player] of this.pool.assignments) {
921
940
  player.playbackRate = rate;
922
941
  }
923
942
  this._pushPlayerState({ playbackRate: rate });
@@ -1394,11 +1413,10 @@ class FeedManager {
1394
1413
  const player = this.pool.getPlayer(id);
1395
1414
  if (player) {
1396
1415
  player.pause();
1397
- player.style.opacity = "0";
1398
1416
  player._skRevealedFor = null;
1399
1417
  }
1400
1418
  this._stopTimeLoop(id);
1401
- this._clearOverlay(id);
1419
+ this._detachOverlay(id);
1402
1420
  this._sk._tracker.deactivateContent();
1403
1421
  }
1404
1422
  // --- Time loop ---
@@ -1580,14 +1598,20 @@ class FeedManager {
1580
1598
  }
1581
1599
  }
1582
1600
  /** Clear the overlay container's DOM and unsubscribe tracked listeners. */
1583
- _clearOverlay(itemId) {
1601
+ /** Unsubscribe overlay event listeners without clearing DOM. */
1602
+ _detachOverlay(itemId) {
1584
1603
  const entry = this._overlayContainers.get(itemId);
1585
1604
  if (!entry) return;
1586
1605
  if (entry.unsub) {
1587
1606
  entry.unsub();
1588
1607
  entry.unsub = null;
1589
1608
  }
1590
- entry.el.innerHTML = "";
1609
+ }
1610
+ /** Unsubscribe and clear overlay DOM (used on destroy). */
1611
+ _clearOverlay(itemId) {
1612
+ this._detachOverlay(itemId);
1613
+ const entry = this._overlayContainers.get(itemId);
1614
+ if (entry) entry.el.innerHTML = "";
1591
1615
  }
1592
1616
  }
1593
1617
  class EmbeddedFeedManager {
@@ -1636,6 +1660,7 @@ class EmbeddedFeedManager {
1636
1660
  this._setupScrollEnd();
1637
1661
  this._setupVisibilityHandler();
1638
1662
  this._setupCellHeightObserver();
1663
+ this._setupDebugPanel();
1639
1664
  if (startIndex > 0 && startIndex < items.length) {
1640
1665
  const targetEl = this.itemEls.get(items[startIndex].id);
1641
1666
  if (targetEl) targetEl.scrollIntoView({ behavior: "instant", block: "start" });
@@ -1658,6 +1683,11 @@ class EmbeddedFeedManager {
1658
1683
  this._resizeObserver = null;
1659
1684
  }
1660
1685
  if (this._visHandler) document.removeEventListener("visibilitychange", this._visHandler);
1686
+ if (this._debugKeyHandler) document.removeEventListener("keydown", this._debugKeyHandler);
1687
+ if (this._debugPanel) {
1688
+ this._debugPanel.remove();
1689
+ this._debugPanel = null;
1690
+ }
1661
1691
  this.container.innerHTML = "";
1662
1692
  this.itemEls.clear();
1663
1693
  this.items = [];
@@ -1701,7 +1731,7 @@ class EmbeddedFeedManager {
1701
1731
  }
1702
1732
  setMuted(muted) {
1703
1733
  this.isMuted = muted;
1704
- for (const [, player] of this.pool._players) {
1734
+ for (const [, player] of this.pool.assignments) {
1705
1735
  player.muted = muted;
1706
1736
  }
1707
1737
  this._pushPlayerState({ isMuted: muted });
@@ -1714,7 +1744,7 @@ class EmbeddedFeedManager {
1714
1744
  }
1715
1745
  setPlaybackRate(rate) {
1716
1746
  this.playbackRate = rate;
1717
- for (const [, player] of this.pool._players) {
1747
+ for (const [, player] of this.pool.assignments) {
1718
1748
  player.playbackRate = rate;
1719
1749
  }
1720
1750
  this._pushPlayerState({ playbackRate: rate });
@@ -1986,11 +2016,52 @@ class EmbeddedFeedManager {
1986
2016
  const p = this.pool.getPlayer(id);
1987
2017
  if (p) {
1988
2018
  p.pause();
1989
- p.style.opacity = "0";
1990
2019
  p._skRevealedFor = null;
1991
2020
  }
1992
2021
  this._stopTimeLoop(id);
1993
- this._clearOverlay(id);
2022
+ this._detachOverlay(id);
2023
+ }
2024
+ // --- Debug panel (toggle with 'D' key) ---
2025
+ _setupDebugPanel() {
2026
+ const panel = document.createElement("div");
2027
+ panel.id = "sk-debug-panel";
2028
+ panel.style.cssText = "position:fixed;top:12px;right:12px;z-index:99999;background:rgba(0,0,0,.85);color:#0f0;font:12px/1.5 monospace;padding:10px 14px;border-radius:8px;pointer-events:none;display:none;min-width:220px;";
2029
+ document.body.appendChild(panel);
2030
+ this._debugPanel = panel;
2031
+ this._debugVisible = false;
2032
+ this._debugKeyHandler = (e) => {
2033
+ if (e.key === "d" || e.key === "D") {
2034
+ this._debugVisible = !this._debugVisible;
2035
+ panel.style.display = this._debugVisible ? "block" : "none";
2036
+ }
2037
+ };
2038
+ document.addEventListener("keydown", this._debugKeyHandler);
2039
+ }
2040
+ _updateDebugPanel() {
2041
+ if (!this._debugVisible || !this._debugPanel) return;
2042
+ const hls = this.pool.hlsInstances.get(this.activeItemId);
2043
+ if (!hls || !hls.levels || !hls.levels.length) {
2044
+ this._debugPanel.innerHTML = "HLS: no levels loaded";
2045
+ return;
2046
+ }
2047
+ const level = hls.levels[hls.currentLevel] || {};
2048
+ const bw = hls.bandwidthEstimate ? (hls.bandwidthEstimate / 1e6).toFixed(2) : "?";
2049
+ const lines = [
2050
+ `<b style="color:#fff">HLS Debug</b>`,
2051
+ `Level: ${hls.currentLevel} / ${hls.levels.length - 1}`,
2052
+ `Resolution: ${level.width || "?"}x${level.height || "?"}`,
2053
+ `Bitrate: ${level.bitrate ? (level.bitrate / 1e3).toFixed(0) + " kbps" : "?"}`,
2054
+ `BW estimate: ${bw} Mbps`,
2055
+ `Auto cap: ${hls.autoLevelCapping}`,
2056
+ `Next level: ${hls.nextLevel}`,
2057
+ `<span style="color:#888">Levels:</span>`
2058
+ ];
2059
+ for (let i = 0; i < hls.levels.length; i++) {
2060
+ const l = hls.levels[i];
2061
+ const marker = i === hls.currentLevel ? ' <b style="color:#0ff">◀</b>' : "";
2062
+ lines.push(` ${i}: ${l.width}x${l.height} @ ${(l.bitrate / 1e3).toFixed(0)}k${marker}`);
2063
+ }
2064
+ this._debugPanel.innerHTML = lines.join("<br>");
1994
2065
  }
1995
2066
  // --- Time loop (state emission only, no DOM updates) ---
1996
2067
  _startTimeLoop(id, el, player) {
@@ -1998,6 +2069,7 @@ class EmbeddedFeedManager {
1998
2069
  const tick = () => {
1999
2070
  if (this._destroyed) return;
2000
2071
  if (!player || player.paused) {
2072
+ this._updateDebugPanel();
2001
2073
  this.rafIds.set(id, requestAnimationFrame(tick));
2002
2074
  return;
2003
2075
  }
@@ -2023,6 +2095,7 @@ class EmbeddedFeedManager {
2023
2095
  player.play().catch(() => {
2024
2096
  });
2025
2097
  }
2098
+ this._updateDebugPanel();
2026
2099
  this.rafIds.set(id, requestAnimationFrame(tick));
2027
2100
  };
2028
2101
  this.rafIds.set(id, requestAnimationFrame(tick));
@@ -2189,14 +2262,20 @@ class EmbeddedFeedManager {
2189
2262
  } catch (e) {
2190
2263
  }
2191
2264
  }
2192
- _clearOverlay(itemId) {
2265
+ /** Unsubscribe overlay event listeners without clearing DOM. */
2266
+ _detachOverlay(itemId) {
2193
2267
  const entry = this._overlayContainers.get(itemId);
2194
2268
  if (!entry) return;
2195
2269
  if (entry.unsub) {
2196
2270
  entry.unsub();
2197
2271
  entry.unsub = null;
2198
2272
  }
2199
- entry.el.innerHTML = "";
2273
+ }
2274
+ /** Unsubscribe and clear overlay DOM (used on destroy). */
2275
+ _clearOverlay(itemId) {
2276
+ this._detachOverlay(itemId);
2277
+ const entry = this._overlayContainers.get(itemId);
2278
+ if (entry) entry.el.innerHTML = "";
2200
2279
  }
2201
2280
  }
2202
2281
  const MuteOnSvg = '<svg viewBox="0 0 24 24"><path d="M16.5 12c0-1.77-1.02-3.29-2.5-4.03v2.21l2.45 2.45c.03-.2.05-.41.05-.63zm2.5 0c0 .94-.2 1.82-.54 2.64l1.51 1.51C20.63 14.91 21 13.5 21 12c0-4.28-2.99-7.86-7-8.77v2.06c2.89.86 5 3.54 5 6.71zM4.27 3L3 4.27 7.73 9H3v6h4l5 5v-6.73l4.25 4.25c-.67.52-1.42.93-2.25 1.18v2.06c1.38-.31 2.63-.95 3.69-1.81L19.73 21 21 19.73l-9-9L4.27 3zM12 4L9.91 6.09 12 8.18V4z"/></svg>';
@@ -2546,6 +2625,7 @@ class PlayerFeedView {
2546
2625
  this._isOpen = true;
2547
2626
  this._onClose = onClose;
2548
2627
  this._openItemId = item.id;
2628
+ this._openThumbnailUrl = item.thumbnailUrl || null;
2549
2629
  const sourceRect = sourceEl.getBoundingClientRect();
2550
2630
  const sourceRadius = parseFloat(getComputedStyle(sourceEl).borderRadius) || 12;
2551
2631
  this.overlayEl.classList.add("skp-active");
@@ -2590,6 +2670,21 @@ class PlayerFeedView {
2590
2670
  feedEl.style.transform = "none";
2591
2671
  feedEl.style.borderRadius = `${targetRadius}px`;
2592
2672
  });
2673
+ this.feedManager.startObserver();
2674
+ this.feedManager._activateItem(item.id);
2675
+ this.overlayEl.classList.add("skp-feed-ready");
2676
+ const itemOverlay = feedItemEl?.querySelector('[data-ref="overlay"]');
2677
+ const chromeEls = [...this.overlayEl.querySelectorAll(".skp-feed-close, .sk-sidebar")];
2678
+ if (itemOverlay) chromeEls.push(itemOverlay);
2679
+ chromeEls.forEach((el) => {
2680
+ el.style.opacity = "0";
2681
+ el.style.transition = "none";
2682
+ });
2683
+ this.overlayEl.getBoundingClientRect();
2684
+ chromeEls.forEach((el) => {
2685
+ el.style.transition = "opacity .35s cubic-bezier(.32,.72,0,1)";
2686
+ el.style.opacity = "1";
2687
+ });
2593
2688
  await this._waitForTransitionEnd(feedEl, 450);
2594
2689
  feedEl.classList.remove("skp-flip-animating");
2595
2690
  feedEl.style.transformOrigin = "";
@@ -2597,9 +2692,10 @@ class PlayerFeedView {
2597
2692
  feedEl.style.scrollSnapType = "";
2598
2693
  feedEl.style.transform = "";
2599
2694
  feedEl.style.borderRadius = "";
2600
- this.feedManager.startObserver();
2601
- this.feedManager._activateItem(item.id);
2602
- this.overlayEl.classList.add("skp-feed-ready");
2695
+ chromeEls.forEach((el) => {
2696
+ el.style.opacity = "";
2697
+ el.style.transition = "";
2698
+ });
2603
2699
  this._escHandler = (e) => {
2604
2700
  if (e.key === "Escape") this.close();
2605
2701
  };
@@ -2612,33 +2708,44 @@ class PlayerFeedView {
2612
2708
  const feedWrapper = feedEl.parentNode;
2613
2709
  const feedRect = feedEl.getBoundingClientRect();
2614
2710
  const activeItemId = this.feedManager?.activeItemId;
2615
- let transferBack = false, transferVideo = null, transferHls = null;
2616
- if (activeItemId === this._openItemId) {
2617
- const ejected = this.feedManager?.pool.ejectPlayer(activeItemId);
2618
- if (ejected) {
2619
- transferBack = true;
2620
- transferVideo = ejected.video;
2621
- transferHls = ejected.hls;
2622
- }
2623
- }
2711
+ const canTransfer = activeItemId === this._openItemId;
2624
2712
  let targetRect = null;
2625
2713
  if (this._onClose) targetRect = this._onClose("getSourceRect");
2626
2714
  if (feedRect && targetRect) {
2627
- if (this.feedManager?.activeItemId && !transferBack) {
2715
+ if (this.feedManager?.activeItemId && !canTransfer) {
2628
2716
  this.feedManager._deactivateItem(this.feedManager.activeItemId);
2629
2717
  }
2718
+ const targetItemId = canTransfer ? this._openItemId : activeItemId;
2719
+ if (targetItemId && this.feedManager) {
2720
+ const targetEl = this.feedManager.itemEls.get(targetItemId);
2721
+ if (targetEl) targetEl.scrollIntoView({ behavior: "instant", block: "start" });
2722
+ }
2723
+ const savedScrollTop = feedEl.scrollTop;
2630
2724
  feedEl.style.position = "fixed";
2631
2725
  feedEl.style.left = `${feedRect.left}px`;
2632
2726
  feedEl.style.top = `${feedRect.top}px`;
2633
2727
  feedEl.style.width = `${feedRect.width}px`;
2634
2728
  feedEl.style.height = `${feedRect.height}px`;
2635
- feedEl.style.zIndex = "9999";
2729
+ feedEl.style.zIndex = String(getComputedStyle(this.overlayEl).zIndex || 9999);
2636
2730
  feedEl.style.margin = "0";
2637
2731
  feedEl.style.flex = "none";
2638
2732
  feedEl.style.aspectRatio = "unset";
2639
2733
  feedEl.style.maxHeight = "none";
2640
2734
  document.body.appendChild(feedEl);
2735
+ feedEl.scrollTop = savedScrollTop;
2641
2736
  this.overlayEl.classList.remove("skp-active", "skp-feed-ready");
2737
+ let thumbOverlay = null;
2738
+ if (this._openThumbnailUrl) {
2739
+ const crossfadeItemId = canTransfer ? this._openItemId : activeItemId;
2740
+ const crossfadeEl = crossfadeItemId && this.feedManager?.itemEls.get(crossfadeItemId);
2741
+ const videoContainer = crossfadeEl?.querySelector('[data-ref="videoContainer"]');
2742
+ if (videoContainer) {
2743
+ thumbOverlay = document.createElement("img");
2744
+ thumbOverlay.src = this._openThumbnailUrl;
2745
+ thumbOverlay.style.cssText = "position:absolute;inset:0;width:100%;height:100%;object-fit:cover;opacity:0;transition:opacity .4s cubic-bezier(.32,.72,0,1);pointer-events:none;z-index:2;";
2746
+ videoContainer.appendChild(thumbOverlay);
2747
+ }
2748
+ }
2642
2749
  const scaleX = targetRect.width / feedRect.width;
2643
2750
  const scaleY = targetRect.height / feedRect.height;
2644
2751
  const translateX = targetRect.left - feedRect.left;
@@ -2648,29 +2755,58 @@ class PlayerFeedView {
2648
2755
  feedEl.style.transformOrigin = "0 0";
2649
2756
  feedEl.style.overflow = "hidden";
2650
2757
  feedEl.style.scrollSnapType = "none";
2758
+ feedEl.style.pointerEvents = "none";
2651
2759
  feedEl.getBoundingClientRect();
2652
2760
  feedEl.classList.add("skp-flip-animating");
2653
2761
  requestAnimationFrame(() => {
2654
2762
  feedEl.style.transform = `translate(${translateX}px, ${translateY}px) scale(${scaleX}, ${scaleY})`;
2655
2763
  feedEl.style.borderRadius = `${endRadius}px`;
2764
+ if (thumbOverlay) thumbOverlay.style.opacity = "1";
2656
2765
  });
2657
2766
  await this._waitForTransitionEnd(feedEl, 450);
2767
+ if (thumbOverlay) thumbOverlay.remove();
2768
+ let transferVideo = null;
2769
+ let transferHls = null;
2770
+ if (canTransfer) {
2771
+ const ejected = this.feedManager?.pool.ejectPlayer(this._openItemId);
2772
+ if (ejected) {
2773
+ transferVideo = ejected.video;
2774
+ transferHls = ejected.hls;
2775
+ }
2776
+ }
2658
2777
  feedEl.style.visibility = "hidden";
2659
2778
  feedEl.classList.remove("skp-flip-animating");
2660
- ["position", "left", "top", "width", "height", "zIndex", "margin", "flex", "aspectRatio", "maxHeight", "transformOrigin", "overflow", "scrollSnapType", "transform", "borderRadius"].forEach((p) => feedEl.style[p] = "");
2779
+ ["position", "left", "top", "width", "height", "zIndex", "margin", "flex", "aspectRatio", "maxHeight", "transformOrigin", "overflow", "scrollSnapType", "pointerEvents", "transform", "borderRadius"].forEach((p) => feedEl.style[p] = "");
2661
2780
  const sidebarEl = feedWrapper.querySelector(".sk-sidebar");
2662
2781
  feedWrapper.insertBefore(feedEl, sidebarEl);
2663
2782
  feedEl.style.visibility = "";
2783
+ if (this._onClose) {
2784
+ this._onClose("closed", {
2785
+ transferVideo,
2786
+ transferHls,
2787
+ transferItemId: transferVideo ? this._openItemId : null
2788
+ });
2789
+ this._onClose = null;
2790
+ }
2664
2791
  } else {
2665
2792
  this.overlayEl.classList.remove("skp-active", "skp-feed-ready");
2666
- }
2667
- if (this._onClose) {
2668
- this._onClose("closed", {
2669
- transferVideo: transferBack ? transferVideo : null,
2670
- transferHls: transferBack ? transferHls : null,
2671
- transferItemId: transferBack ? this._openItemId : null
2672
- });
2673
- this._onClose = null;
2793
+ if (this._onClose) {
2794
+ let transferVideo = null;
2795
+ let transferHls = null;
2796
+ if (canTransfer) {
2797
+ const ejected = this.feedManager?.pool.ejectPlayer(this._openItemId);
2798
+ if (ejected) {
2799
+ transferVideo = ejected.video;
2800
+ transferHls = ejected.hls;
2801
+ }
2802
+ }
2803
+ this._onClose("closed", {
2804
+ transferVideo,
2805
+ transferHls,
2806
+ transferItemId: transferVideo ? this._openItemId : null
2807
+ });
2808
+ this._onClose = null;
2809
+ }
2674
2810
  }
2675
2811
  if (this.feedManager) {
2676
2812
  this.feedManager.destroy();
@@ -2722,6 +2858,7 @@ class FeedView {
2722
2858
  this._onClose = onClose;
2723
2859
  this._openSlotIndex = startIndex;
2724
2860
  this._openItemId = item.id;
2861
+ this._openThumbnailUrl = item.thumbnailUrl || null;
2725
2862
  const slotRect = slotEl.getBoundingClientRect();
2726
2863
  const slotRadius = parseFloat(getComputedStyle(slotEl).borderRadius) || 12;
2727
2864
  if (_isPreview) {
@@ -2768,6 +2905,21 @@ class FeedView {
2768
2905
  feedEl.style.transform = "none";
2769
2906
  feedEl.style.borderRadius = `${targetRadius}px`;
2770
2907
  });
2908
+ this.feedManager.startObserver();
2909
+ this.feedManager._activateItem(item.id);
2910
+ this.overlayEl.classList.add("skw-feed-ready");
2911
+ const itemOverlay = feedItemEl?.querySelector('[data-ref="overlay"]');
2912
+ const chromeEls = [...this.overlayEl.querySelectorAll(".skw-feed-close, .sk-sidebar")];
2913
+ if (itemOverlay) chromeEls.push(itemOverlay);
2914
+ chromeEls.forEach((el) => {
2915
+ el.style.opacity = "0";
2916
+ el.style.transition = "none";
2917
+ });
2918
+ this.overlayEl.getBoundingClientRect();
2919
+ chromeEls.forEach((el) => {
2920
+ el.style.transition = "opacity .35s cubic-bezier(.32,.72,0,1)";
2921
+ el.style.opacity = "1";
2922
+ });
2771
2923
  await this._waitForTransitionEnd(feedEl, 450);
2772
2924
  feedEl.classList.remove("skw-flip-animating");
2773
2925
  feedEl.style.transformOrigin = "";
@@ -2775,9 +2927,10 @@ class FeedView {
2775
2927
  feedEl.style.scrollSnapType = "";
2776
2928
  feedEl.style.transform = "";
2777
2929
  feedEl.style.borderRadius = "";
2778
- this.feedManager.startObserver();
2779
- this.feedManager._activateItem(item.id);
2780
- this.overlayEl.classList.add("skw-feed-ready");
2930
+ chromeEls.forEach((el) => {
2931
+ el.style.opacity = "";
2932
+ el.style.transition = "";
2933
+ });
2781
2934
  this._escHandler = (e) => {
2782
2935
  if (e.key === "Escape") this.close();
2783
2936
  };
@@ -2804,6 +2957,12 @@ class FeedView {
2804
2957
  if (this.feedManager?.activeItemId && !canTransfer) {
2805
2958
  this.feedManager._deactivateItem(this.feedManager.activeItemId);
2806
2959
  }
2960
+ const targetItemId = canTransfer ? this._openItemId : activeItemId;
2961
+ if (targetItemId && this.feedManager) {
2962
+ const targetEl = this.feedManager.itemEls.get(targetItemId);
2963
+ if (targetEl) targetEl.scrollIntoView({ behavior: "instant", block: "start" });
2964
+ }
2965
+ const savedScrollTop = feedEl.scrollTop;
2807
2966
  feedEl.style.position = "fixed";
2808
2967
  feedEl.style.left = `${feedRect.left}px`;
2809
2968
  feedEl.style.top = `${feedRect.top}px`;
@@ -2815,7 +2974,20 @@ class FeedView {
2815
2974
  feedEl.style.aspectRatio = "unset";
2816
2975
  feedEl.style.maxHeight = "none";
2817
2976
  document.body.appendChild(feedEl);
2977
+ feedEl.scrollTop = savedScrollTop;
2818
2978
  this.overlayEl.classList.remove("skw-active", "skw-feed-ready");
2979
+ let thumbOverlay = null;
2980
+ if (this._openThumbnailUrl) {
2981
+ const targetItemId2 = canTransfer ? this._openItemId : activeItemId;
2982
+ const targetEl = targetItemId2 && this.feedManager?.itemEls.get(targetItemId2);
2983
+ const videoContainer = targetEl?.querySelector('[data-ref="videoContainer"]');
2984
+ if (videoContainer) {
2985
+ thumbOverlay = document.createElement("img");
2986
+ thumbOverlay.src = this._openThumbnailUrl;
2987
+ thumbOverlay.style.cssText = "position:absolute;inset:0;width:100%;height:100%;object-fit:cover;opacity:0;transition:opacity .4s cubic-bezier(.32,.72,0,1);pointer-events:none;z-index:2;";
2988
+ videoContainer.appendChild(thumbOverlay);
2989
+ }
2990
+ }
2819
2991
  const slotRadius = parseFloat(getComputedStyle(document.documentElement).getPropertyValue("--skw-radius").trim()) || 12;
2820
2992
  const scaleX = targetSlotRect.width / feedRect.width;
2821
2993
  const scaleY = targetSlotRect.height / feedRect.height;
@@ -2825,13 +2997,16 @@ class FeedView {
2825
2997
  feedEl.style.transformOrigin = "0 0";
2826
2998
  feedEl.style.overflow = "hidden";
2827
2999
  feedEl.style.scrollSnapType = "none";
3000
+ feedEl.style.pointerEvents = "none";
2828
3001
  feedEl.getBoundingClientRect();
2829
3002
  feedEl.classList.add("skw-flip-animating");
2830
3003
  requestAnimationFrame(() => {
2831
3004
  feedEl.style.transform = `translate(${translateX}px, ${translateY}px) scale(${scaleX}, ${scaleY})`;
2832
3005
  feedEl.style.borderRadius = `${endRadius}px`;
3006
+ if (thumbOverlay) thumbOverlay.style.opacity = "1";
2833
3007
  });
2834
3008
  await this._waitForTransitionEnd(feedEl, 450);
3009
+ if (thumbOverlay) thumbOverlay.remove();
2835
3010
  let transferVideo = null;
2836
3011
  let transferHls = null;
2837
3012
  if (canTransfer) {
@@ -2856,6 +3031,7 @@ class FeedView {
2856
3031
  feedEl.style.transformOrigin = "";
2857
3032
  feedEl.style.overflow = "";
2858
3033
  feedEl.style.scrollSnapType = "";
3034
+ feedEl.style.pointerEvents = "";
2859
3035
  feedEl.style.transform = "";
2860
3036
  feedEl.style.borderRadius = "";
2861
3037
  feedWrapper.insertBefore(feedEl, feedWrapper.firstChild);
@@ -2985,11 +3161,20 @@ class WidgetPlayerPool {
2985
3161
  }
2986
3162
  promoteToActive(itemId) {
2987
3163
  const hls = this.hlsInstances.get(itemId);
2988
- if (hls) {
2989
- hls.config.maxBufferLength = WidgetPlayerPool.ACTIVE_HLS_CONFIG.maxBufferLength;
2990
- hls.config.maxMaxBufferLength = WidgetPlayerPool.ACTIVE_HLS_CONFIG.maxMaxBufferLength;
2991
- hls.autoLevelCapping = -1;
2992
- hls.nextAutoLevel = -1;
3164
+ if (!hls) return;
3165
+ hls.config.maxBufferLength = WidgetPlayerPool.ACTIVE_HLS_CONFIG.maxBufferLength;
3166
+ hls.config.maxMaxBufferLength = WidgetPlayerPool.ACTIVE_HLS_CONFIG.maxMaxBufferLength;
3167
+ hls.autoLevelCapping = -1;
3168
+ const top = hls.levels ? hls.levels.length - 1 : -1;
3169
+ if (top > 0 && hls.currentLevel < top) {
3170
+ hls.currentLevel = top;
3171
+ const player = this.assignments.get(itemId);
3172
+ if (player) {
3173
+ player.currentTime = player.currentTime;
3174
+ }
3175
+ setTimeout(() => {
3176
+ if (!hls.destroyed) hls.currentLevel = -1;
3177
+ }, 4e3);
2993
3178
  }
2994
3179
  }
2995
3180
  _destroyHls(itemId) {
@@ -3594,7 +3779,7 @@ const CSS = `
3594
3779
 
3595
3780
  /* Video container */
3596
3781
  .sk-video-container{position:absolute;inset:0;width:100%;height:100%;overflow:hidden;background-size:cover;background-position:center}
3597
- .sk-video-container video{position:absolute;inset:0;width:100%;height:100%;object-fit:cover;opacity:0;transition:opacity .15s ease}
3782
+ .sk-video-container video{position:absolute;inset:0;width:100%;height:100%;object-fit:cover;opacity:0;transition:opacity .35s ease}
3598
3783
 
3599
3784
  /* Tap zone */
3600
3785
  .sk-tap-zone{position:absolute;inset:0;z-index:3;cursor:pointer}
@@ -3610,7 +3795,7 @@ const CSS = `
3610
3795
 
3611
3796
  /* Widget slot */
3612
3797
  .skw-slot{position:relative;overflow:hidden;cursor:pointer}
3613
- .skw-slot video{position:absolute;inset:0;width:100%;height:100%;object-fit:cover;opacity:0;transition:opacity .2s ease}
3798
+ .skw-slot video{position:absolute;inset:0;width:100%;height:100%;object-fit:cover;opacity:0;transition:opacity .4s ease}
3614
3799
  .skw-slot-thumb{position:absolute;inset:0;background-size:cover;background-position:center}
3615
3800
  .skw-slot-overlay{position:absolute;inset:0;z-index:4;pointer-events:none}
3616
3801
  .skw-slot-overlay>*{pointer-events:auto}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shortkitsdk/web",
3
- "version": "0.3.0",
3
+ "version": "0.3.2",
4
4
  "description": "ShortKit Web SDK — embeddable short-form video feed, player, and widget",
5
5
  "type": "module",
6
6
  "main": "dist/shortkit.slim.cjs",