@shortkitsdk/web 0.3.0 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -904,7 +904,7 @@ class FeedManager {
904
904
  }
905
905
  setMuted(muted) {
906
906
  this.isMuted = muted;
907
- for (const [, player] of this.pool._players) {
907
+ for (const [, player] of this.pool.assignments) {
908
908
  player.muted = muted;
909
909
  }
910
910
  this._pushPlayerState({ isMuted: muted });
@@ -917,7 +917,7 @@ class FeedManager {
917
917
  }
918
918
  setPlaybackRate(rate) {
919
919
  this.playbackRate = rate;
920
- for (const [, player] of this.pool._players) {
920
+ for (const [, player] of this.pool.assignments) {
921
921
  player.playbackRate = rate;
922
922
  }
923
923
  this._pushPlayerState({ playbackRate: rate });
@@ -1394,11 +1394,10 @@ class FeedManager {
1394
1394
  const player = this.pool.getPlayer(id);
1395
1395
  if (player) {
1396
1396
  player.pause();
1397
- player.style.opacity = "0";
1398
1397
  player._skRevealedFor = null;
1399
1398
  }
1400
1399
  this._stopTimeLoop(id);
1401
- this._clearOverlay(id);
1400
+ this._detachOverlay(id);
1402
1401
  this._sk._tracker.deactivateContent();
1403
1402
  }
1404
1403
  // --- Time loop ---
@@ -1580,14 +1579,20 @@ class FeedManager {
1580
1579
  }
1581
1580
  }
1582
1581
  /** Clear the overlay container's DOM and unsubscribe tracked listeners. */
1583
- _clearOverlay(itemId) {
1582
+ /** Unsubscribe overlay event listeners without clearing DOM. */
1583
+ _detachOverlay(itemId) {
1584
1584
  const entry = this._overlayContainers.get(itemId);
1585
1585
  if (!entry) return;
1586
1586
  if (entry.unsub) {
1587
1587
  entry.unsub();
1588
1588
  entry.unsub = null;
1589
1589
  }
1590
- entry.el.innerHTML = "";
1590
+ }
1591
+ /** Unsubscribe and clear overlay DOM (used on destroy). */
1592
+ _clearOverlay(itemId) {
1593
+ this._detachOverlay(itemId);
1594
+ const entry = this._overlayContainers.get(itemId);
1595
+ if (entry) entry.el.innerHTML = "";
1591
1596
  }
1592
1597
  }
1593
1598
  class EmbeddedFeedManager {
@@ -1701,7 +1706,7 @@ class EmbeddedFeedManager {
1701
1706
  }
1702
1707
  setMuted(muted) {
1703
1708
  this.isMuted = muted;
1704
- for (const [, player] of this.pool._players) {
1709
+ for (const [, player] of this.pool.assignments) {
1705
1710
  player.muted = muted;
1706
1711
  }
1707
1712
  this._pushPlayerState({ isMuted: muted });
@@ -1714,7 +1719,7 @@ class EmbeddedFeedManager {
1714
1719
  }
1715
1720
  setPlaybackRate(rate) {
1716
1721
  this.playbackRate = rate;
1717
- for (const [, player] of this.pool._players) {
1722
+ for (const [, player] of this.pool.assignments) {
1718
1723
  player.playbackRate = rate;
1719
1724
  }
1720
1725
  this._pushPlayerState({ playbackRate: rate });
@@ -1986,11 +1991,10 @@ class EmbeddedFeedManager {
1986
1991
  const p = this.pool.getPlayer(id);
1987
1992
  if (p) {
1988
1993
  p.pause();
1989
- p.style.opacity = "0";
1990
1994
  p._skRevealedFor = null;
1991
1995
  }
1992
1996
  this._stopTimeLoop(id);
1993
- this._clearOverlay(id);
1997
+ this._detachOverlay(id);
1994
1998
  }
1995
1999
  // --- Time loop (state emission only, no DOM updates) ---
1996
2000
  _startTimeLoop(id, el, player) {
@@ -2189,14 +2193,20 @@ class EmbeddedFeedManager {
2189
2193
  } catch (e) {
2190
2194
  }
2191
2195
  }
2192
- _clearOverlay(itemId) {
2196
+ /** Unsubscribe overlay event listeners without clearing DOM. */
2197
+ _detachOverlay(itemId) {
2193
2198
  const entry = this._overlayContainers.get(itemId);
2194
2199
  if (!entry) return;
2195
2200
  if (entry.unsub) {
2196
2201
  entry.unsub();
2197
2202
  entry.unsub = null;
2198
2203
  }
2199
- entry.el.innerHTML = "";
2204
+ }
2205
+ /** Unsubscribe and clear overlay DOM (used on destroy). */
2206
+ _clearOverlay(itemId) {
2207
+ this._detachOverlay(itemId);
2208
+ const entry = this._overlayContainers.get(itemId);
2209
+ if (entry) entry.el.innerHTML = "";
2200
2210
  }
2201
2211
  }
2202
2212
  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 +2556,7 @@ class PlayerFeedView {
2546
2556
  this._isOpen = true;
2547
2557
  this._onClose = onClose;
2548
2558
  this._openItemId = item.id;
2559
+ this._openThumbnailUrl = item.thumbnailUrl || null;
2549
2560
  const sourceRect = sourceEl.getBoundingClientRect();
2550
2561
  const sourceRadius = parseFloat(getComputedStyle(sourceEl).borderRadius) || 12;
2551
2562
  this.overlayEl.classList.add("skp-active");
@@ -2590,6 +2601,21 @@ class PlayerFeedView {
2590
2601
  feedEl.style.transform = "none";
2591
2602
  feedEl.style.borderRadius = `${targetRadius}px`;
2592
2603
  });
2604
+ this.feedManager.startObserver();
2605
+ this.feedManager._activateItem(item.id);
2606
+ this.overlayEl.classList.add("skp-feed-ready");
2607
+ const itemOverlay = feedItemEl?.querySelector('[data-ref="overlay"]');
2608
+ const chromeEls = [...this.overlayEl.querySelectorAll(".skp-feed-close, .sk-sidebar")];
2609
+ if (itemOverlay) chromeEls.push(itemOverlay);
2610
+ chromeEls.forEach((el) => {
2611
+ el.style.opacity = "0";
2612
+ el.style.transition = "none";
2613
+ });
2614
+ this.overlayEl.getBoundingClientRect();
2615
+ chromeEls.forEach((el) => {
2616
+ el.style.transition = "opacity .35s cubic-bezier(.32,.72,0,1)";
2617
+ el.style.opacity = "1";
2618
+ });
2593
2619
  await this._waitForTransitionEnd(feedEl, 450);
2594
2620
  feedEl.classList.remove("skp-flip-animating");
2595
2621
  feedEl.style.transformOrigin = "";
@@ -2597,9 +2623,10 @@ class PlayerFeedView {
2597
2623
  feedEl.style.scrollSnapType = "";
2598
2624
  feedEl.style.transform = "";
2599
2625
  feedEl.style.borderRadius = "";
2600
- this.feedManager.startObserver();
2601
- this.feedManager._activateItem(item.id);
2602
- this.overlayEl.classList.add("skp-feed-ready");
2626
+ chromeEls.forEach((el) => {
2627
+ el.style.opacity = "";
2628
+ el.style.transition = "";
2629
+ });
2603
2630
  this._escHandler = (e) => {
2604
2631
  if (e.key === "Escape") this.close();
2605
2632
  };
@@ -2612,33 +2639,44 @@ class PlayerFeedView {
2612
2639
  const feedWrapper = feedEl.parentNode;
2613
2640
  const feedRect = feedEl.getBoundingClientRect();
2614
2641
  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
- }
2642
+ const canTransfer = activeItemId === this._openItemId;
2624
2643
  let targetRect = null;
2625
2644
  if (this._onClose) targetRect = this._onClose("getSourceRect");
2626
2645
  if (feedRect && targetRect) {
2627
- if (this.feedManager?.activeItemId && !transferBack) {
2646
+ if (this.feedManager?.activeItemId && !canTransfer) {
2628
2647
  this.feedManager._deactivateItem(this.feedManager.activeItemId);
2629
2648
  }
2649
+ const targetItemId = canTransfer ? this._openItemId : activeItemId;
2650
+ if (targetItemId && this.feedManager) {
2651
+ const targetEl = this.feedManager.itemEls.get(targetItemId);
2652
+ if (targetEl) targetEl.scrollIntoView({ behavior: "instant", block: "start" });
2653
+ }
2654
+ const savedScrollTop = feedEl.scrollTop;
2630
2655
  feedEl.style.position = "fixed";
2631
2656
  feedEl.style.left = `${feedRect.left}px`;
2632
2657
  feedEl.style.top = `${feedRect.top}px`;
2633
2658
  feedEl.style.width = `${feedRect.width}px`;
2634
2659
  feedEl.style.height = `${feedRect.height}px`;
2635
- feedEl.style.zIndex = "9999";
2660
+ feedEl.style.zIndex = String(getComputedStyle(this.overlayEl).zIndex || 9999);
2636
2661
  feedEl.style.margin = "0";
2637
2662
  feedEl.style.flex = "none";
2638
2663
  feedEl.style.aspectRatio = "unset";
2639
2664
  feedEl.style.maxHeight = "none";
2640
2665
  document.body.appendChild(feedEl);
2666
+ feedEl.scrollTop = savedScrollTop;
2641
2667
  this.overlayEl.classList.remove("skp-active", "skp-feed-ready");
2668
+ let thumbOverlay = null;
2669
+ if (this._openThumbnailUrl) {
2670
+ const crossfadeItemId = canTransfer ? this._openItemId : activeItemId;
2671
+ const crossfadeEl = crossfadeItemId && this.feedManager?.itemEls.get(crossfadeItemId);
2672
+ const videoContainer = crossfadeEl?.querySelector('[data-ref="videoContainer"]');
2673
+ if (videoContainer) {
2674
+ thumbOverlay = document.createElement("img");
2675
+ thumbOverlay.src = this._openThumbnailUrl;
2676
+ 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;";
2677
+ videoContainer.appendChild(thumbOverlay);
2678
+ }
2679
+ }
2642
2680
  const scaleX = targetRect.width / feedRect.width;
2643
2681
  const scaleY = targetRect.height / feedRect.height;
2644
2682
  const translateX = targetRect.left - feedRect.left;
@@ -2648,29 +2686,58 @@ class PlayerFeedView {
2648
2686
  feedEl.style.transformOrigin = "0 0";
2649
2687
  feedEl.style.overflow = "hidden";
2650
2688
  feedEl.style.scrollSnapType = "none";
2689
+ feedEl.style.pointerEvents = "none";
2651
2690
  feedEl.getBoundingClientRect();
2652
2691
  feedEl.classList.add("skp-flip-animating");
2653
2692
  requestAnimationFrame(() => {
2654
2693
  feedEl.style.transform = `translate(${translateX}px, ${translateY}px) scale(${scaleX}, ${scaleY})`;
2655
2694
  feedEl.style.borderRadius = `${endRadius}px`;
2695
+ if (thumbOverlay) thumbOverlay.style.opacity = "1";
2656
2696
  });
2657
2697
  await this._waitForTransitionEnd(feedEl, 450);
2698
+ if (thumbOverlay) thumbOverlay.remove();
2699
+ let transferVideo = null;
2700
+ let transferHls = null;
2701
+ if (canTransfer) {
2702
+ const ejected = this.feedManager?.pool.ejectPlayer(this._openItemId);
2703
+ if (ejected) {
2704
+ transferVideo = ejected.video;
2705
+ transferHls = ejected.hls;
2706
+ }
2707
+ }
2658
2708
  feedEl.style.visibility = "hidden";
2659
2709
  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] = "");
2710
+ ["position", "left", "top", "width", "height", "zIndex", "margin", "flex", "aspectRatio", "maxHeight", "transformOrigin", "overflow", "scrollSnapType", "pointerEvents", "transform", "borderRadius"].forEach((p) => feedEl.style[p] = "");
2661
2711
  const sidebarEl = feedWrapper.querySelector(".sk-sidebar");
2662
2712
  feedWrapper.insertBefore(feedEl, sidebarEl);
2663
2713
  feedEl.style.visibility = "";
2714
+ if (this._onClose) {
2715
+ this._onClose("closed", {
2716
+ transferVideo,
2717
+ transferHls,
2718
+ transferItemId: transferVideo ? this._openItemId : null
2719
+ });
2720
+ this._onClose = null;
2721
+ }
2664
2722
  } else {
2665
2723
  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;
2724
+ if (this._onClose) {
2725
+ let transferVideo = null;
2726
+ let transferHls = null;
2727
+ if (canTransfer) {
2728
+ const ejected = this.feedManager?.pool.ejectPlayer(this._openItemId);
2729
+ if (ejected) {
2730
+ transferVideo = ejected.video;
2731
+ transferHls = ejected.hls;
2732
+ }
2733
+ }
2734
+ this._onClose("closed", {
2735
+ transferVideo,
2736
+ transferHls,
2737
+ transferItemId: transferVideo ? this._openItemId : null
2738
+ });
2739
+ this._onClose = null;
2740
+ }
2674
2741
  }
2675
2742
  if (this.feedManager) {
2676
2743
  this.feedManager.destroy();
@@ -2722,6 +2789,7 @@ class FeedView {
2722
2789
  this._onClose = onClose;
2723
2790
  this._openSlotIndex = startIndex;
2724
2791
  this._openItemId = item.id;
2792
+ this._openThumbnailUrl = item.thumbnailUrl || null;
2725
2793
  const slotRect = slotEl.getBoundingClientRect();
2726
2794
  const slotRadius = parseFloat(getComputedStyle(slotEl).borderRadius) || 12;
2727
2795
  if (_isPreview) {
@@ -2768,6 +2836,21 @@ class FeedView {
2768
2836
  feedEl.style.transform = "none";
2769
2837
  feedEl.style.borderRadius = `${targetRadius}px`;
2770
2838
  });
2839
+ this.feedManager.startObserver();
2840
+ this.feedManager._activateItem(item.id);
2841
+ this.overlayEl.classList.add("skw-feed-ready");
2842
+ const itemOverlay = feedItemEl?.querySelector('[data-ref="overlay"]');
2843
+ const chromeEls = [...this.overlayEl.querySelectorAll(".skw-feed-close, .sk-sidebar")];
2844
+ if (itemOverlay) chromeEls.push(itemOverlay);
2845
+ chromeEls.forEach((el) => {
2846
+ el.style.opacity = "0";
2847
+ el.style.transition = "none";
2848
+ });
2849
+ this.overlayEl.getBoundingClientRect();
2850
+ chromeEls.forEach((el) => {
2851
+ el.style.transition = "opacity .35s cubic-bezier(.32,.72,0,1)";
2852
+ el.style.opacity = "1";
2853
+ });
2771
2854
  await this._waitForTransitionEnd(feedEl, 450);
2772
2855
  feedEl.classList.remove("skw-flip-animating");
2773
2856
  feedEl.style.transformOrigin = "";
@@ -2775,9 +2858,10 @@ class FeedView {
2775
2858
  feedEl.style.scrollSnapType = "";
2776
2859
  feedEl.style.transform = "";
2777
2860
  feedEl.style.borderRadius = "";
2778
- this.feedManager.startObserver();
2779
- this.feedManager._activateItem(item.id);
2780
- this.overlayEl.classList.add("skw-feed-ready");
2861
+ chromeEls.forEach((el) => {
2862
+ el.style.opacity = "";
2863
+ el.style.transition = "";
2864
+ });
2781
2865
  this._escHandler = (e) => {
2782
2866
  if (e.key === "Escape") this.close();
2783
2867
  };
@@ -2804,6 +2888,12 @@ class FeedView {
2804
2888
  if (this.feedManager?.activeItemId && !canTransfer) {
2805
2889
  this.feedManager._deactivateItem(this.feedManager.activeItemId);
2806
2890
  }
2891
+ const targetItemId = canTransfer ? this._openItemId : activeItemId;
2892
+ if (targetItemId && this.feedManager) {
2893
+ const targetEl = this.feedManager.itemEls.get(targetItemId);
2894
+ if (targetEl) targetEl.scrollIntoView({ behavior: "instant", block: "start" });
2895
+ }
2896
+ const savedScrollTop = feedEl.scrollTop;
2807
2897
  feedEl.style.position = "fixed";
2808
2898
  feedEl.style.left = `${feedRect.left}px`;
2809
2899
  feedEl.style.top = `${feedRect.top}px`;
@@ -2815,7 +2905,20 @@ class FeedView {
2815
2905
  feedEl.style.aspectRatio = "unset";
2816
2906
  feedEl.style.maxHeight = "none";
2817
2907
  document.body.appendChild(feedEl);
2908
+ feedEl.scrollTop = savedScrollTop;
2818
2909
  this.overlayEl.classList.remove("skw-active", "skw-feed-ready");
2910
+ let thumbOverlay = null;
2911
+ if (this._openThumbnailUrl) {
2912
+ const targetItemId2 = canTransfer ? this._openItemId : activeItemId;
2913
+ const targetEl = targetItemId2 && this.feedManager?.itemEls.get(targetItemId2);
2914
+ const videoContainer = targetEl?.querySelector('[data-ref="videoContainer"]');
2915
+ if (videoContainer) {
2916
+ thumbOverlay = document.createElement("img");
2917
+ thumbOverlay.src = this._openThumbnailUrl;
2918
+ 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;";
2919
+ videoContainer.appendChild(thumbOverlay);
2920
+ }
2921
+ }
2819
2922
  const slotRadius = parseFloat(getComputedStyle(document.documentElement).getPropertyValue("--skw-radius").trim()) || 12;
2820
2923
  const scaleX = targetSlotRect.width / feedRect.width;
2821
2924
  const scaleY = targetSlotRect.height / feedRect.height;
@@ -2825,13 +2928,16 @@ class FeedView {
2825
2928
  feedEl.style.transformOrigin = "0 0";
2826
2929
  feedEl.style.overflow = "hidden";
2827
2930
  feedEl.style.scrollSnapType = "none";
2931
+ feedEl.style.pointerEvents = "none";
2828
2932
  feedEl.getBoundingClientRect();
2829
2933
  feedEl.classList.add("skw-flip-animating");
2830
2934
  requestAnimationFrame(() => {
2831
2935
  feedEl.style.transform = `translate(${translateX}px, ${translateY}px) scale(${scaleX}, ${scaleY})`;
2832
2936
  feedEl.style.borderRadius = `${endRadius}px`;
2937
+ if (thumbOverlay) thumbOverlay.style.opacity = "1";
2833
2938
  });
2834
2939
  await this._waitForTransitionEnd(feedEl, 450);
2940
+ if (thumbOverlay) thumbOverlay.remove();
2835
2941
  let transferVideo = null;
2836
2942
  let transferHls = null;
2837
2943
  if (canTransfer) {
@@ -2856,6 +2962,7 @@ class FeedView {
2856
2962
  feedEl.style.transformOrigin = "";
2857
2963
  feedEl.style.overflow = "";
2858
2964
  feedEl.style.scrollSnapType = "";
2965
+ feedEl.style.pointerEvents = "";
2859
2966
  feedEl.style.transform = "";
2860
2967
  feedEl.style.borderRadius = "";
2861
2968
  feedWrapper.insertBefore(feedEl, feedWrapper.firstChild);
@@ -3594,7 +3701,7 @@ const CSS = `
3594
3701
 
3595
3702
  /* Video container */
3596
3703
  .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}
3704
+ .sk-video-container video{position:absolute;inset:0;width:100%;height:100%;object-fit:cover;opacity:0;transition:opacity .35s ease}
3598
3705
 
3599
3706
  /* Tap zone */
3600
3707
  .sk-tap-zone{position:absolute;inset:0;z-index:3;cursor:pointer}
@@ -3610,7 +3717,7 @@ const CSS = `
3610
3717
 
3611
3718
  /* Widget slot */
3612
3719
  .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}
3720
+ .skw-slot video{position:absolute;inset:0;width:100%;height:100%;object-fit:cover;opacity:0;transition:opacity .4s ease}
3614
3721
  .skw-slot-thumb{position:absolute;inset:0;background-size:cover;background-position:center}
3615
3722
  .skw-slot-overlay{position:absolute;inset:0;z-index:4;pointer-events:none}
3616
3723
  .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.1",
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",