@xhub-short/ui 0.1.0-beta.1 → 0.1.0-beta.10

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.
Files changed (45) hide show
  1. package/dist/CommentSheet.css-BeCrEaUG.d.ts +221 -0
  2. package/dist/{chunk-2PTMP65P.js → chunk-2FSDVYER.js} +8 -9
  3. package/dist/{chunk-WKX2WBVO.js → chunk-3XPJHUYL.js} +1 -39
  4. package/dist/{chunk-ANGBSV7L.js → chunk-AC2IFAJR.js} +10 -5
  5. package/dist/{chunk-4YDIRPIN.js → chunk-ANCP53F3.js} +3 -3
  6. package/dist/chunk-AQHD6LPS.js +430 -0
  7. package/dist/{chunk-HW4LXTFT.js → chunk-CL6BS7GB.js} +7 -5
  8. package/dist/{chunk-YW23IBKF.js → chunk-ECR42RKK.js} +46 -5
  9. package/dist/chunk-EDWS2IPH.js +1 -0
  10. package/dist/chunk-FNXTPQ6L.js +2573 -0
  11. package/dist/{chunk-DHQJBXQW.js → chunk-KWHMZ6H5.js} +1 -1
  12. package/dist/{chunk-UXMA4KJZ.js → chunk-RMLTPW5S.js} +3 -2
  13. package/dist/{chunk-SSJDO24Q.js → chunk-SZXFH334.js} +1 -1
  14. package/dist/{chunk-4MN72OZH.js → chunk-UNV3NWN6.js} +4 -4
  15. package/dist/{chunk-ZZDQKP4R.js → chunk-WCRDTBCZ.js} +94 -155
  16. package/dist/{chunk-XAOEHLOX.js → chunk-XDIH66C4.js} +245 -52
  17. package/dist/components/ActionBar/index.js +1 -1
  18. package/dist/components/AuthorInfo/index.d.ts +5 -1
  19. package/dist/components/AuthorInfo/index.js +1 -1
  20. package/dist/components/BlurhashPlaceholder/index.d.ts +67 -0
  21. package/dist/components/BlurhashPlaceholder/index.js +150 -0
  22. package/dist/components/CommentSheet/index.d.ts +164 -0
  23. package/dist/components/CommentSheet/index.js +1 -0
  24. package/dist/components/ErrorBoundary/index.js +1 -1
  25. package/dist/components/OfflineIndicator/index.d.ts +56 -0
  26. package/dist/components/OfflineIndicator/index.js +151 -0
  27. package/dist/components/ProgressBar/index.d.ts +30 -2
  28. package/dist/components/ProgressBar/index.js +1 -1
  29. package/dist/components/Skeleton/index.js +1 -1
  30. package/dist/components/SubtitleDisplay/index.d.ts +94 -0
  31. package/dist/components/SubtitleDisplay/index.js +165 -0
  32. package/dist/components/VideoFeed/index.d.ts +11 -0
  33. package/dist/components/VideoFeed/index.js +1 -1
  34. package/dist/components/VideoInfo/index.js +1 -1
  35. package/dist/components/VideoPlayer/index.d.ts +14 -41
  36. package/dist/components/VideoPlayer/index.js +1 -1
  37. package/dist/components/VideoSlot/index.d.ts +124 -64
  38. package/dist/components/VideoSlot/index.js +1 -1
  39. package/dist/components/VirtualSlider/index.d.ts +339 -0
  40. package/dist/components/VirtualSlider/index.js +1 -0
  41. package/dist/components/icons/index.js +1 -1
  42. package/dist/index.d.ts +76 -93
  43. package/dist/index.js +75 -27
  44. package/package.json +53 -8
  45. package/dist/use-gesture-react.esm-3SV4QLEJ.js +0 -1893
@@ -1,7 +1,7 @@
1
- import { HeartFilledIcon } from './chunk-4YDIRPIN.js';
2
- import { cn } from './chunk-WKX2WBVO.js';
3
- import { injectComponentCSS } from './chunk-UXMA4KJZ.js';
4
- import { createContext, memo, useEffect, useRef, useCallback, useContext, useInsertionEffect, useState, useMemo } from 'react';
1
+ import { PauseIcon, PlayIcon, HeartFilledIcon } from './chunk-ANCP53F3.js';
2
+ import { clsx2 } from './chunk-EDWS2IPH.js';
3
+ import { injectComponentCSS } from './chunk-RMLTPW5S.js';
4
+ import { createContext, memo, useMemo, useState, useCallback, useEffect, useRef, useInsertionEffect, useContext } from 'react';
5
5
  import { jsx, jsxs } from 'react/jsx-runtime';
6
6
 
7
7
  var DEFAULT_DOUBLE_TAP_DELAY = 300;
@@ -21,6 +21,7 @@ function useDoubleTap({
21
21
  const lastPositionRef = useRef({ x: 50, y: 50 });
22
22
  const lastExecutionTimeRef = useRef(0);
23
23
  const pointerStartRef = useRef(null);
24
+ const cumulativeMovementRef = useRef(0);
24
25
  useEffect(() => {
25
26
  return () => {
26
27
  if (tapTimeoutRef.current) {
@@ -72,6 +73,9 @@ function useDoubleTap({
72
73
  }, [disabled, doubleTapDelay, onSingleTap, onDoubleTap]);
73
74
  const isSwipe = useCallback(
74
75
  (clientX, clientY) => {
76
+ if (cumulativeMovementRef.current > movementThreshold * 2) {
77
+ return true;
78
+ }
75
79
  const start = pointerStartRef.current;
76
80
  if (!start) return false;
77
81
  const dx = Math.abs(clientX - start.x);
@@ -101,6 +105,11 @@ function useDoubleTap({
101
105
  const handlePointerDown = useCallback((e) => {
102
106
  if (e.button !== 0) return;
103
107
  pointerStartRef.current = { x: e.clientX, y: e.clientY };
108
+ cumulativeMovementRef.current = 0;
109
+ }, []);
110
+ const handlePointerMove = useCallback((e) => {
111
+ if (!pointerStartRef.current) return;
112
+ cumulativeMovementRef.current += Math.abs(e.movementX) + Math.abs(e.movementY);
104
113
  }, []);
105
114
  const handlePointerUp = useCallback(
106
115
  (e) => {
@@ -120,6 +129,7 @@ function useDoubleTap({
120
129
  return {
121
130
  handlers: {
122
131
  onPointerDown: handlePointerDown,
132
+ onPointerMove: handlePointerMove,
123
133
  onPointerUp: handlePointerUp,
124
134
  onClick: handleClick
125
135
  },
@@ -133,17 +143,18 @@ var VIDEO_SLOT_CSS = `
133
143
  * Z-INDEX LAYERING CONTRACT
134
144
  *
135
145
  * Override these variables to customize layer stacking:
136
- * --sv-z-poster: Poster/Video layer (default: 1)
137
- * --sv-z-overlay: Overlay/Spinner layer (default: 2)
138
- * --sv-z-indicator: Error/Play indicator layer (default: 3)
139
- * --sv-z-progress: Progress bar layer (default: 4)
146
+ * --sv-z-poster: Poster/Thumbnail layer (default: 3)
147
+ * --sv-z-slot-spinner: Loading spinner layer (default: 2)
148
+ * --sv-z-indicator: Error/Play indicator layer (default: 4)
149
+ * --sv-z-progress: Progress bar layer (default: 5)
140
150
  *
141
151
  * Layer Stack (bottom to top):
142
152
  * \u250C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510
143
- * \u2502 z-index: 4 - Progress Bar (topmost) \u2502
144
- * \u2502 z-index: 3 - Error UI / Play Indicator \u2502
145
- * \u2502 z-index: 2 - Overlay / Spinner \u2502
146
- * \u2502 z-index: 1 - Poster / Video \u2502
153
+ * \u2502 z-index: 5 - Progress Bar (topmost) \u2502
154
+ * \u2502 z-index: 4 - Error UI / Play Indicator \u2502
155
+ * \u2502 z-index: 3 - Poster / Thumbnail \u2502
156
+ * \u2502 z-index: 2 - Loading Spinner \u2502
157
+ * \u2502 z-index: 1 - Video Element \u2502
147
158
  * \u2502 z-index: 0 - Slot Container (base) \u2502
148
159
  * \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518
149
160
  * \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 */
@@ -249,7 +260,8 @@ var VIDEO_SLOT_CSS = `
249
260
  align-items: center;
250
261
  justify-content: center;
251
262
  background-color: var(--sv-bg-primary, #000);
252
- z-index: var(--sv-z-poster, 1);
263
+ /* Poster should be ABOVE spinner (z-index: 2) so thumbnail shows instead of spinner */
264
+ z-index: var(--sv-z-poster, 3);
253
265
  /* Smooth fade transition */
254
266
  transition: opacity var(--sv-transition-duration, 300ms) ease-out;
255
267
  pointer-events: none;
@@ -453,7 +465,8 @@ var VIDEO_SLOT_CSS = `
453
465
  top: 50%;
454
466
  left: 50%;
455
467
  transform: translate(-50%, -50%);
456
- z-index: var(--sv-z-overlay, 2);
468
+ /* Use slot-specific z-index (not global --sv-z-overlay which may conflict) */
469
+ z-index: var(--sv-z-slot-spinner, 2);
457
470
  }
458
471
 
459
472
  .sv-video-slot__spinner-circle {
@@ -485,20 +498,29 @@ var VIDEO_SLOT_CSS = `
485
498
  display: flex;
486
499
  align-items: center;
487
500
  justify-content: center;
488
- background-color: rgba(0, 0, 0, 0.5);
489
501
  border-radius: 50%;
490
502
  z-index: var(--sv-z-indicator, 3);
491
503
  opacity: 0;
492
- transition: opacity 200ms ease;
504
+ transition: opacity 200ms ease-out;
493
505
  pointer-events: none;
494
506
  }
495
507
 
508
+ /* Show indicator */
496
509
  .sv-video-slot__play-indicator--visible {
497
- opacity: 1;
498
- animation: sv-slot-play-indicator 400ms ease-out forwards;
510
+ opacity: 0.65;
499
511
  }
500
512
 
501
- @keyframes sv-slot-play-indicator {
513
+ /* Persist mode: stay visible, no animation */
514
+ .sv-video-slot__play-indicator--persist {
515
+ opacity: 0.65;
516
+ }
517
+
518
+ /* Animating out: scale up + fade out */
519
+ .sv-video-slot__play-indicator--animating-out {
520
+ animation: sv-slot-indicator-out var(--sv-indicator-duration, 200ms) ease-out forwards;
521
+ }
522
+
523
+ @keyframes sv-slot-indicator-out {
502
524
  0% {
503
525
  opacity: 1;
504
526
  transform: translate(-50%, -50%) scale(1);
@@ -512,6 +534,11 @@ var VIDEO_SLOT_CSS = `
512
534
  .sv-video-slot__play-indicator-icon {
513
535
  color: #fff;
514
536
  font-size: 28px;
537
+ width: 100%;
538
+ height: 100%;
539
+ display: flex;
540
+ align-items: center;
541
+ justify-content: center;
515
542
  }
516
543
 
517
544
  /* \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
@@ -628,23 +655,25 @@ var SLOT_PAUSED_CLASS = "sv-video-slot--paused";
628
655
  var Z_INDEX = {
629
656
  /** Base layer - slot container */
630
657
  SLOT: 0,
631
- /** Poster image and video element */
632
- POSTER: 1,
633
- /** Overlay content and loading spinner */
634
- OVERLAY: 2,
658
+ /** Video element */
659
+ VIDEO: 1,
660
+ /** Loading spinner */
661
+ SPINNER: 2,
662
+ /** Poster/thumbnail (above spinner so thumbnail shows during loading) */
663
+ POSTER: 3,
635
664
  /** Error UI and play/pause indicator */
636
- INDICATOR: 3,
665
+ INDICATOR: 4,
637
666
  /** Progress bar (always on top) */
638
- PROGRESS: 4
667
+ PROGRESS: 5
639
668
  };
640
669
  var Z_INDEX_CSS_VARS = {
641
670
  POSTER: "--sv-z-poster",
642
- OVERLAY: "--sv-z-overlay",
671
+ SPINNER: "--sv-z-slot-spinner",
643
672
  INDICATOR: "--sv-z-indicator",
644
673
  PROGRESS: "--sv-z-progress"
645
674
  };
646
675
  var ANIMATION_SELECTOR = '[data-like-animation="true"]';
647
- function VideoSlotHeadless({
676
+ function VideoSlotHeadlessBase({
648
677
  video,
649
678
  resourceState,
650
679
  playerState,
@@ -660,7 +689,8 @@ function VideoSlotHeadless({
660
689
  onTap,
661
690
  onDoubleTap,
662
691
  renderError,
663
- renderLoading
692
+ renderLoading,
693
+ restoreFrame
664
694
  }) {
665
695
  useInsertionEffect(() => {
666
696
  return injectComponentCSS("video-slot", VIDEO_SLOT_CSS);
@@ -669,6 +699,7 @@ function VideoSlotHeadless({
669
699
  const animationElementRef = useRef(null);
670
700
  const prevVideoIdRef = useRef(video.id);
671
701
  const [firstFrame, setFirstFrame] = useState(null);
702
+ const [lastUserToggleTime, setLastUserToggleTime] = useState(0);
672
703
  if (prevVideoIdRef.current !== video.id) {
673
704
  prevVideoIdRef.current = video.id;
674
705
  animationElementRef.current = null;
@@ -702,6 +733,7 @@ function VideoSlotHeadless({
702
733
  return animationElement?.__showWhenAlreadyLiked ?? true;
703
734
  }, [getAnimationElement]);
704
735
  const handleSingleTap = useCallback(() => {
736
+ setLastUserToggleTime(Date.now());
705
737
  if (onTap) {
706
738
  onTap();
707
739
  } else {
@@ -732,9 +764,11 @@ function VideoSlotHeadless({
732
764
  playerState,
733
765
  playerControls,
734
766
  firstFrame,
735
- setFirstFrame
767
+ setFirstFrame,
768
+ lastUserToggleTime,
769
+ setLastUserToggleTime
736
770
  }),
737
- [video, resourceState, playerState, playerControls, firstFrame]
771
+ [video, resourceState, playerState, playerControls, firstFrame, lastUserToggleTime]
738
772
  );
739
773
  const stateClasses = useMemo(() => {
740
774
  const classes = [SLOT_CLASS];
@@ -764,7 +798,7 @@ function VideoSlotHeadless({
764
798
  "div",
765
799
  {
766
800
  ref: containerRef,
767
- className: cn(...stateClasses, className),
801
+ className: clsx2(...stateClasses, className),
768
802
  ...tapHandlers,
769
803
  ...{
770
804
  [VIDEO_ID_ATTR]: video.id,
@@ -773,8 +807,26 @@ function VideoSlotHeadless({
773
807
  [LOADING_ATTR]: playerState.isLoading ? "true" : "false"
774
808
  },
775
809
  children: [
810
+ restoreFrame && !playerState.isPlaying && !playerState.error && /* @__PURE__ */ jsx(
811
+ "div",
812
+ {
813
+ className: "sv-video-slot__restore-frame",
814
+ style: {
815
+ position: "absolute",
816
+ inset: 0,
817
+ zIndex: 5,
818
+ // Above video, below overlay
819
+ backgroundImage: `url(${restoreFrame})`,
820
+ backgroundSize: "contain",
821
+ backgroundPosition: "center",
822
+ backgroundRepeat: "no-repeat",
823
+ backgroundColor: "#000"
824
+ },
825
+ "aria-hidden": "true"
826
+ }
827
+ ),
776
828
  playerState.error && (renderError ? renderError(playerState.error, handleRetry) : /* @__PURE__ */ jsx(DefaultErrorUI, { error: playerState.error, onRetry: handleRetry })),
777
- playerState.isLoading && !playerState.error && (renderLoading ? renderLoading() : /* @__PURE__ */ jsx(DefaultLoadingUI, {})),
829
+ playerState.isLoading && !playerState.error && !restoreFrame && (renderLoading ? renderLoading() : /* @__PURE__ */ jsx(DefaultLoadingUI, {})),
778
830
  children
779
831
  ]
780
832
  }
@@ -830,23 +882,28 @@ function DefaultErrorUI({
830
882
  function DefaultLoadingUI() {
831
883
  return /* @__PURE__ */ jsx("div", { className: "sv-video-slot__spinner", children: /* @__PURE__ */ jsx("div", { className: "sv-video-slot__spinner-circle", "aria-label": "Loading video" }) });
832
884
  }
885
+ var VideoSlotHeadless = memo(VideoSlotHeadlessBase);
833
886
  VideoSlotHeadless.displayName = "VideoSlotHeadless";
834
- function VideoSlotPoster({
887
+ var VideoSlotPosterInner = memo(function VideoSlotPosterInner2({
835
888
  className,
836
889
  forceSkeleton = false,
837
890
  posterUrl,
838
891
  loading = "eager",
839
892
  alt,
840
- onError
893
+ onError,
894
+ videoPoster,
895
+ videoTitle,
896
+ firstFrame,
897
+ isLoading,
898
+ currentTime
841
899
  }) {
842
- const { video, firstFrame, playerState } = useVideoSlotContext();
843
900
  const imageSource = useMemo(() => {
844
901
  if (forceSkeleton) return null;
845
902
  if (posterUrl) return posterUrl;
846
903
  if (firstFrame) return firstFrame;
847
- if (video.poster) return video.poster;
904
+ if (videoPoster) return videoPoster;
848
905
  return null;
849
- }, [forceSkeleton, posterUrl, firstFrame, video.poster]);
906
+ }, [forceSkeleton, posterUrl, firstFrame, videoPoster]);
850
907
  const sourceKey = imageSource ?? "skeleton";
851
908
  const [errorForSource, setErrorForSource] = useState(null);
852
909
  const imageError = errorForSource === sourceKey;
@@ -860,12 +917,12 @@ function VideoSlotPoster({
860
917
  if (imageSource === firstFrame) return "first-frame";
861
918
  return "poster";
862
919
  }, [showSkeleton, imageSource, firstFrame]);
863
- const imageAlt = alt ?? video.title ?? "";
864
- const isVisible = playerState.isLoading && playerState.currentTime === 0;
920
+ const imageAlt = alt ?? videoTitle ?? "";
921
+ const isVisible = isLoading && currentTime === 0;
865
922
  return /* @__PURE__ */ jsx(
866
923
  "div",
867
924
  {
868
- className: cn(
925
+ className: clsx2(
869
926
  "sv-video-slot__poster",
870
927
  `sv-video-slot__poster--${posterType}`,
871
928
  !isVisible && "sv-video-slot__poster--hidden",
@@ -888,20 +945,35 @@ function VideoSlotPoster({
888
945
  )
889
946
  }
890
947
  );
948
+ });
949
+ VideoSlotPosterInner.displayName = "VideoSlotPosterInner";
950
+ function VideoSlotPoster(props) {
951
+ const { video, firstFrame, playerState } = useVideoSlotContext();
952
+ return /* @__PURE__ */ jsx(
953
+ VideoSlotPosterInner,
954
+ {
955
+ ...props,
956
+ videoPoster: video.poster,
957
+ videoTitle: video.title,
958
+ firstFrame,
959
+ isLoading: playerState.isLoading,
960
+ currentTime: playerState.currentTime
961
+ }
962
+ );
891
963
  }
964
+ VideoSlotPoster.displayName = "VideoSlotPoster";
892
965
  function VideoSlotSkeleton({
893
966
  className
894
967
  }) {
895
968
  return /* @__PURE__ */ jsx(
896
969
  "div",
897
970
  {
898
- className: cn("sv-video-slot__poster", "sv-video-slot__poster--skeleton", className),
971
+ className: clsx2("sv-video-slot__poster", "sv-video-slot__poster--skeleton", className),
899
972
  "aria-hidden": "true",
900
973
  children: /* @__PURE__ */ jsx("div", { className: "sv-video-slot__poster-skeleton" })
901
974
  }
902
975
  );
903
976
  }
904
- VideoSlotPoster.displayName = "VideoSlotPoster";
905
977
  VideoSlotSkeleton.displayName = "VideoSlotSkeleton";
906
978
  function VideoSlotOverlayRoot({
907
979
  children,
@@ -911,7 +983,7 @@ function VideoSlotOverlayRoot({
911
983
  return /* @__PURE__ */ jsx(
912
984
  "div",
913
985
  {
914
- className: cn(
986
+ className: clsx2(
915
987
  "sv-video-slot__overlay",
916
988
  noGradient && "sv-video-slot__overlay--no-gradient",
917
989
  className
@@ -924,37 +996,37 @@ function VideoSlotOverlayTop({
924
996
  children,
925
997
  className
926
998
  }) {
927
- return /* @__PURE__ */ jsx("div", { className: cn("sv-video-slot__overlay-top", className), children });
999
+ return /* @__PURE__ */ jsx("div", { className: clsx2("sv-video-slot__overlay-top", className), children });
928
1000
  }
929
1001
  function VideoSlotOverlayBottom({
930
1002
  children,
931
1003
  className
932
1004
  }) {
933
- return /* @__PURE__ */ jsx("div", { className: cn("sv-video-slot__overlay-bottom", className), children });
1005
+ return /* @__PURE__ */ jsx("div", { className: clsx2("sv-video-slot__overlay-bottom", className), children });
934
1006
  }
935
1007
  function VideoSlotOverlayLeft({
936
1008
  children,
937
1009
  className
938
1010
  }) {
939
- return /* @__PURE__ */ jsx("div", { className: cn("sv-video-slot__overlay-left", className), children });
1011
+ return /* @__PURE__ */ jsx("div", { className: clsx2("sv-video-slot__overlay-left", className), children });
940
1012
  }
941
1013
  function VideoSlotOverlayRight({
942
1014
  children,
943
1015
  className
944
1016
  }) {
945
- return /* @__PURE__ */ jsx("div", { className: cn("sv-video-slot__overlay-right", className), children });
1017
+ return /* @__PURE__ */ jsx("div", { className: clsx2("sv-video-slot__overlay-right", className), children });
946
1018
  }
947
1019
  function VideoSlotOverlayActions({
948
1020
  children,
949
1021
  className
950
1022
  }) {
951
- return /* @__PURE__ */ jsx("div", { className: cn("sv-video-slot__actions", className), children });
1023
+ return /* @__PURE__ */ jsx("div", { className: clsx2("sv-video-slot__actions", className), children });
952
1024
  }
953
1025
  function VideoSlotOverlayAuthor({
954
1026
  children,
955
1027
  className
956
1028
  }) {
957
- return /* @__PURE__ */ jsx("div", { className: cn("sv-video-slot__author", className), children });
1029
+ return /* @__PURE__ */ jsx("div", { className: clsx2("sv-video-slot__author", className), children });
958
1030
  }
959
1031
  VideoSlotOverlayRoot.displayName = "VideoSlotOverlay";
960
1032
  VideoSlotOverlayTop.displayName = "VideoSlotOverlay.Top";
@@ -1214,7 +1286,7 @@ var SingleHeart = memo(function SingleHeart2({
1214
1286
  return /* @__PURE__ */ jsx(
1215
1287
  "div",
1216
1288
  {
1217
- className: cn("sv-like-heart", `sv-like-heart--${animation}`),
1289
+ className: clsx2("sv-like-heart", `sv-like-heart--${animation}`),
1218
1290
  style: {
1219
1291
  "--sv-heart-x": `${heart.x}%`,
1220
1292
  "--sv-heart-y": `${heart.y}%`,
@@ -1303,7 +1375,7 @@ function VideoSlotLikeAnimation({
1303
1375
  "div",
1304
1376
  {
1305
1377
  ref: containerRef,
1306
- className: cn("sv-like-animation-container", className),
1378
+ className: clsx2("sv-like-animation-container", className),
1307
1379
  "data-like-animation": "true",
1308
1380
  "aria-hidden": "true",
1309
1381
  children: hearts.map((heart) => /* @__PURE__ */ jsx(
@@ -1322,5 +1394,126 @@ function VideoSlotLikeAnimation({
1322
1394
  );
1323
1395
  }
1324
1396
  VideoSlotLikeAnimation.displayName = "VideoSlotLikeAnimation";
1397
+ function DefaultPlayIcon({ size }) {
1398
+ return /* @__PURE__ */ jsx(PlayIcon, { size });
1399
+ }
1400
+ function DefaultPauseIcon({ size }) {
1401
+ return /* @__PURE__ */ jsx(PauseIcon, { size });
1402
+ }
1403
+ var VideoSlotPlayIndicatorInner = memo(function VideoSlotPlayIndicatorInner2({
1404
+ playIcon,
1405
+ pauseIcon,
1406
+ size = 48,
1407
+ duration = 600,
1408
+ persistWhenPaused = true,
1409
+ className,
1410
+ testId,
1411
+ lastUserToggleTime,
1412
+ isPlaying
1413
+ }) {
1414
+ const [showIndicator, setShowIndicator] = useState(false);
1415
+ const [isPersisting, setIsPersisting] = useState(false);
1416
+ const [isAnimatingOut, setIsAnimatingOut] = useState(false);
1417
+ const [iconType, setIconType] = useState("play");
1418
+ const hideTimeoutRef = useRef(null);
1419
+ const lastHandledToggleTimeRef = useRef(0);
1420
+ const isPlayingRef = useRef(isPlaying);
1421
+ isPlayingRef.current = isPlaying;
1422
+ const persistWhenPausedRef = useRef(persistWhenPaused);
1423
+ persistWhenPausedRef.current = persistWhenPaused;
1424
+ const prevIsPlayingRef = useRef(isPlaying);
1425
+ const clearHideTimeout = () => {
1426
+ if (hideTimeoutRef.current) {
1427
+ clearTimeout(hideTimeoutRef.current);
1428
+ hideTimeoutRef.current = null;
1429
+ }
1430
+ };
1431
+ useEffect(() => {
1432
+ const wasPlaying = prevIsPlayingRef.current;
1433
+ prevIsPlayingRef.current = isPlaying;
1434
+ if (!wasPlaying && isPlaying && isPersisting) {
1435
+ const timeSinceLastToggle = Date.now() - lastUserToggleTime;
1436
+ const isAutoResume = timeSinceLastToggle > 100;
1437
+ if (isAutoResume) {
1438
+ if (hideTimeoutRef.current) {
1439
+ clearTimeout(hideTimeoutRef.current);
1440
+ hideTimeoutRef.current = null;
1441
+ }
1442
+ setShowIndicator(false);
1443
+ setIsPersisting(false);
1444
+ setIsAnimatingOut(false);
1445
+ }
1446
+ }
1447
+ }, [isPlaying, isPersisting, lastUserToggleTime]);
1448
+ useEffect(() => {
1449
+ if (lastUserToggleTime === 0 || lastUserToggleTime === lastHandledToggleTimeRef.current) {
1450
+ return;
1451
+ }
1452
+ lastHandledToggleTimeRef.current = lastUserToggleTime;
1453
+ if (isPersisting && isPlaying) {
1454
+ setIconType("pause");
1455
+ setIsPersisting(false);
1456
+ setIsAnimatingOut(true);
1457
+ clearHideTimeout();
1458
+ hideTimeoutRef.current = setTimeout(() => {
1459
+ setShowIndicator(false);
1460
+ setIsAnimatingOut(false);
1461
+ hideTimeoutRef.current = null;
1462
+ }, duration);
1463
+ return;
1464
+ }
1465
+ setShowIndicator(true);
1466
+ setIsPersisting(false);
1467
+ setIsAnimatingOut(false);
1468
+ setIconType("play");
1469
+ clearHideTimeout();
1470
+ hideTimeoutRef.current = setTimeout(() => {
1471
+ const currentIsPlaying = isPlayingRef.current;
1472
+ const shouldPersist = persistWhenPausedRef.current;
1473
+ if (shouldPersist && !currentIsPlaying) {
1474
+ setIsPersisting(true);
1475
+ return;
1476
+ }
1477
+ setShowIndicator(false);
1478
+ hideTimeoutRef.current = null;
1479
+ }, duration);
1480
+ return clearHideTimeout;
1481
+ }, [lastUserToggleTime, duration, isPersisting, isPlaying]);
1482
+ if (!showIndicator && !isPersisting) return null;
1483
+ const iconToShow = iconType === "pause" ? pauseIcon ?? /* @__PURE__ */ jsx(DefaultPauseIcon, { size }) : playIcon ?? /* @__PURE__ */ jsx(DefaultPlayIcon, { size });
1484
+ const indicatorClass = [
1485
+ "sv-video-slot__play-indicator",
1486
+ showIndicator && "sv-video-slot__play-indicator--visible",
1487
+ isPersisting && "sv-video-slot__play-indicator--persist",
1488
+ isAnimatingOut && "sv-video-slot__play-indicator--animating-out",
1489
+ className
1490
+ ].filter(Boolean).join(" ");
1491
+ return /* @__PURE__ */ jsx(
1492
+ "div",
1493
+ {
1494
+ className: indicatorClass,
1495
+ style: { "--sv-indicator-duration": `${duration}ms` },
1496
+ "aria-hidden": "true",
1497
+ "data-testid": testId,
1498
+ children: /* @__PURE__ */ jsx("span", { className: "sv-video-slot__play-indicator-icon", children: iconToShow })
1499
+ }
1500
+ );
1501
+ });
1502
+ VideoSlotPlayIndicatorInner.displayName = "VideoSlotPlayIndicatorInner";
1503
+ function VideoSlotPlayIndicator(props) {
1504
+ const context = useOptionalVideoSlotContext();
1505
+ if (!context) return null;
1506
+ const lastUserToggleTime = context.lastUserToggleTime ?? 0;
1507
+ const isPlaying = context.playerState?.isPlaying ?? false;
1508
+ return /* @__PURE__ */ jsx(
1509
+ VideoSlotPlayIndicatorInner,
1510
+ {
1511
+ ...props,
1512
+ lastUserToggleTime,
1513
+ isPlaying
1514
+ }
1515
+ );
1516
+ }
1517
+ VideoSlotPlayIndicator.displayName = "VideoSlotPlayIndicator";
1325
1518
 
1326
- export { LOADING_ATTR, LOADING_DATASET_KEY, PLAYER_STATE_ATTR, PLAYER_STATE_DATASET_KEY, SLOT_ACTIVE_ATTR, SLOT_ACTIVE_CLASS, SLOT_ACTIVE_DATASET_KEY, SLOT_CLASS, SLOT_ERROR_CLASS, SLOT_LOADING_CLASS, SLOT_PAUSED_CLASS, SLOT_PLAYING_CLASS, VIDEO_ID_ATTR, VIDEO_ID_DATASET_KEY, VIDEO_SLOT_CSS, VIDEO_SLOT_LIKE_ANIMATION_CSS, VideoSlotContext, VideoSlotHeadless, VideoSlotLikeAnimation, VideoSlotOverlay, VideoSlotPoster, VideoSlotSkeleton, Z_INDEX, Z_INDEX_CSS_VARS, useDoubleTap, useOptionalVideoSlotContext, useVideoSlotContext, useVideoSlotIsActive, useVideoSlotIsPlaying, useVideoSlotPlayer, useVideoSlotResource, useVideoSlotVideo };
1519
+ export { LOADING_ATTR, LOADING_DATASET_KEY, PLAYER_STATE_ATTR, PLAYER_STATE_DATASET_KEY, SLOT_ACTIVE_ATTR, SLOT_ACTIVE_CLASS, SLOT_ACTIVE_DATASET_KEY, SLOT_CLASS, SLOT_ERROR_CLASS, SLOT_LOADING_CLASS, SLOT_PAUSED_CLASS, SLOT_PLAYING_CLASS, VIDEO_ID_ATTR, VIDEO_ID_DATASET_KEY, VIDEO_SLOT_CSS, VIDEO_SLOT_LIKE_ANIMATION_CSS, VideoSlotContext, VideoSlotHeadless, VideoSlotLikeAnimation, VideoSlotOverlay, VideoSlotPlayIndicator, VideoSlotPoster, VideoSlotSkeleton, Z_INDEX, Z_INDEX_CSS_VARS, useDoubleTap, useOptionalVideoSlotContext, useVideoSlotContext, useVideoSlotIsActive, useVideoSlotIsPlaying, useVideoSlotPlayer, useVideoSlotResource, useVideoSlotVideo };
@@ -1 +1 @@
1
- export { ACTION_BAR_CSS, CompoundBookmark as ActionBarBookmark, CompoundComment as ActionBarComment, ActionBarHeadless, CompoundLike as ActionBarLike, CompoundShare as ActionBarShare, ActionButton, BookmarkButton, CommentButton, LikeButton, ShareButton } from '../../chunk-ANGBSV7L.js';
1
+ export { ACTION_BAR_CSS, CompoundBookmark as ActionBarBookmark, CompoundComment as ActionBarComment, ActionBarHeadless, CompoundLike as ActionBarLike, CompoundShare as ActionBarShare, ActionButton, BookmarkButton, CommentButton, LikeButton, ShareButton } from '../../chunk-AC2IFAJR.js';
@@ -137,6 +137,10 @@ interface FollowButtonProps {
137
137
  className?: string;
138
138
  /** Children (for fully custom content) */
139
139
  children?: ReactNode;
140
+ /** Aria-label for follow state (default: "Follow") */
141
+ followAriaLabel?: string;
142
+ /** Aria-label for unfollow state (default: "Unfollow") */
143
+ unfollowAriaLabel?: string;
140
144
  }
141
145
  /**
142
146
  * FollowButton - Follow/unfollow toggle button
@@ -155,7 +159,7 @@ interface FollowButtonProps {
155
159
  * <AuthorInfoHeadless.FollowButton size="small" />
156
160
  * </AuthorInfoHeadless>
157
161
  */
158
- declare function FollowButton({ isFollowing: isFollowingProp, isPending: isPendingProp, onClick, size, iconOnly, followIcon, followingIcon, followText, followingText, disabled, className, children, }: FollowButtonProps): react_jsx_runtime.JSX.Element;
162
+ declare function FollowButton({ isFollowing: isFollowingProp, isPending: isPendingProp, onClick, size, iconOnly, followIcon, followingIcon, followText, followingText, disabled, className, children, followAriaLabel, unfollowAriaLabel, }: FollowButtonProps): react_jsx_runtime.JSX.Element;
159
163
 
160
164
  /**
161
165
  * Layout variant for AuthorInfo
@@ -1 +1 @@
1
- export { AUTHOR_INFO_CSS, AuthorAvatar, AuthorDescription, AuthorInfoContext, AuthorInfoHeadless, AuthorName, FollowButton, useAuthorInfoContext, useOptionalAuthorInfoContext } from '../../chunk-2PTMP65P.js';
1
+ export { AUTHOR_INFO_CSS, AuthorAvatar, AuthorDescription, AuthorInfoContext, AuthorInfoHeadless, AuthorName, FollowButton, useAuthorInfoContext, useOptionalAuthorInfoContext } from '../../chunk-2FSDVYER.js';
@@ -0,0 +1,67 @@
1
+ /**
2
+ * BlurhashPlaceholderHeadless - Blur placeholder from blurhash string
3
+ *
4
+ * Lazy loads the blurhash decoder and caches decoded images.
5
+ *
6
+ * @packageDocumentation
7
+ */
8
+ /**
9
+ * Props for BlurhashPlaceholderHeadless
10
+ */
11
+ interface BlurhashPlaceholderHeadlessProps {
12
+ /** Blurhash string to decode */
13
+ blurhash: string;
14
+ /** Width of decoded image (default: 32) */
15
+ width?: number;
16
+ /** Height of decoded image (default: 32) */
17
+ height?: number;
18
+ /** Punch factor for color intensity (default: 1) */
19
+ punch?: number;
20
+ /** Whether to hide (fade out) */
21
+ isHidden?: boolean;
22
+ /** Additional CSS class */
23
+ className?: string;
24
+ /** Test ID */
25
+ testId?: string;
26
+ }
27
+ /**
28
+ * BlurhashPlaceholderHeadless - Display decoded blurhash as placeholder
29
+ *
30
+ * @example
31
+ * ```tsx
32
+ * <BlurhashPlaceholderHeadless
33
+ * blurhash="LEHV6nWB2yk8pyo0adR*.7kCMdnj"
34
+ * width={32}
35
+ * height={32}
36
+ * isHidden={isVideoReady}
37
+ * />
38
+ * ```
39
+ */
40
+ declare function BlurhashPlaceholderHeadless({ blurhash, width, height, punch, isHidden, className, testId, }: BlurhashPlaceholderHeadlessProps): React.ReactElement | null;
41
+ /**
42
+ * Clear the blurhash image cache
43
+ */
44
+ declare function clearBlurhashCache(): void;
45
+ /**
46
+ * Get cache size
47
+ */
48
+ declare function getBlurhashCacheSize(): number;
49
+ /**
50
+ * Preload a blurhash into cache
51
+ *
52
+ * @example
53
+ * ```ts
54
+ * // Preload blurhash before component renders
55
+ * await preloadBlurhash('LEHV6nWB2yk8pyo0adR*.7kCMdnj');
56
+ * ```
57
+ */
58
+ declare function preloadBlurhash(blurhash: string, width?: number, height?: number, punch?: number): Promise<string | null>;
59
+
60
+ /**
61
+ * BlurhashPlaceholder CSS
62
+ *
63
+ * Styles for blurhash placeholder images.
64
+ */
65
+ declare const BLURHASH_PLACEHOLDER_CSS = "\n/* \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n * BLURHASH PLACEHOLDER\n * \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 */\n\n.sv-blurhash-placeholder {\n position: absolute;\n inset: 0;\n z-index: var(--sv-z-placeholder, 1);\n background-size: cover;\n background-position: center;\n background-repeat: no-repeat;\n transition: opacity 200ms ease-out;\n}\n\n.sv-blurhash-placeholder--hidden {\n opacity: 0;\n pointer-events: none;\n}\n\n/* Canvas used for decoding - always hidden */\n.sv-blurhash-placeholder__canvas {\n display: none !important;\n}\n\n/* \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n * REDUCE MOTION\n * \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 */\n\n@media (prefers-reduced-motion: reduce) {\n .sv-blurhash-placeholder {\n transition: none;\n }\n}\n";
66
+
67
+ export { BLURHASH_PLACEHOLDER_CSS, BlurhashPlaceholderHeadless, type BlurhashPlaceholderHeadlessProps, clearBlurhashCache, getBlurhashCacheSize, preloadBlurhash };