@rxdrag/website-lib-react 0.0.7 → 0.0.8

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 (72) hide show
  1. package/package.json +22 -23
  2. package/forms.ts +0 -1
  3. package/index.ts +0 -1
  4. package/media.ts +0 -1
  5. package/richtext.ts +0 -1
  6. package/src/components/Analytics/eventHandlers.ts +0 -173
  7. package/src/components/Analytics/index.tsx +0 -21
  8. package/src/components/Analytics/singleton.ts +0 -214
  9. package/src/components/Analytics/tracking.ts +0 -221
  10. package/src/components/Analytics/types.ts +0 -60
  11. package/src/components/Analytics/utils.ts +0 -95
  12. package/src/components/AttachmentIcon/index.tsx +0 -53
  13. package/src/components/BackgroundHlsVideoPlayer.tsx +0 -97
  14. package/src/components/BackgroundVideoPlayer.tsx +0 -32
  15. package/src/components/Bulletin.tsx +0 -30
  16. package/src/components/ContactForm/ContactForm.tsx +0 -296
  17. package/src/components/ContactForm/FileUpload2.tsx +0 -423
  18. package/src/components/ContactForm/Input.tsx +0 -48
  19. package/src/components/ContactForm/Input2.tsx +0 -59
  20. package/src/components/ContactForm/Submit.tsx +0 -48
  21. package/src/components/ContactForm/TelInput.tsx +0 -215
  22. package/src/components/ContactForm/TelInput2.tsx +0 -213
  23. package/src/components/ContactForm/Textarea.tsx +0 -48
  24. package/src/components/ContactForm/Textarea2.tsx +0 -89
  25. package/src/components/ContactForm/countryDialCodes.ts +0 -243
  26. package/src/components/ContactForm/factory.tsx +0 -60
  27. package/src/components/ContactForm/funcs.ts +0 -64
  28. package/src/components/ContactForm/hooks/useInlineLabelPadding.ts +0 -43
  29. package/src/components/ContactForm/hooks/useTelControl.ts +0 -81
  30. package/src/components/ContactForm/index.ts +0 -7
  31. package/src/components/ContactForm/types.ts +0 -68
  32. package/src/components/Icon/index.tsx +0 -20
  33. package/src/components/Medias/MainMedia.tsx +0 -257
  34. package/src/components/Medias/Thumbnail.tsx +0 -62
  35. package/src/components/Medias/VideoPlayer.tsx +0 -114
  36. package/src/components/Medias/index.tsx +0 -271
  37. package/src/components/ProductCard/ProductCard.tsx +0 -24
  38. package/src/components/ProductCard/ProductCta/index.tsx +0 -28
  39. package/src/components/ProductCard/ProductCta/style.css +0 -4
  40. package/src/components/ProductCard/ProductDescription/index.tsx +0 -13
  41. package/src/components/ProductCard/ProductDescription/style.css +0 -6
  42. package/src/components/ProductCard/ProductMedia/index.tsx +0 -35
  43. package/src/components/ProductCard/ProductMedia/style.css +0 -6
  44. package/src/components/ProductCard/ProductTitle/index.tsx +0 -7
  45. package/src/components/ProductCard/ProductTitle/style.css +0 -4
  46. package/src/components/ProductCard/ProductView.tsx +0 -36
  47. package/src/components/ProductCard/index.ts +0 -5
  48. package/src/components/ProductCard/useQueryProduct.ts +0 -32
  49. package/src/components/ReactModalTrigger.tsx +0 -28
  50. package/src/components/ReactVideoPlayer.tsx +0 -332
  51. package/src/components/RichTextOutline/index.tsx +0 -74
  52. package/src/components/RichTextOutline/parseOutline.ts +0 -63
  53. package/src/components/RichTextOutline/useAcitviedHeading.ts +0 -142
  54. package/src/components/RichTextOutline/useAnchorScroll.ts +0 -24
  55. package/src/components/Scroller.tsx +0 -39
  56. package/src/components/SearchInput.tsx +0 -21
  57. package/src/components/Share/index.tsx +0 -86
  58. package/src/components/Share/socials.tsx +0 -80
  59. package/src/components/Share//350/265/204/346/226/231.md +0 -7
  60. package/src/components/ToTop.tsx +0 -72
  61. package/src/components/VideoPlayIcon.tsx +0 -43
  62. package/src/components/all.ts +0 -25
  63. package/src/components/index.ts +0 -12
  64. package/src/forms.ts +0 -1
  65. package/src/index.ts +0 -1
  66. package/src/media.ts +0 -1
  67. package/src/richtext.ts +0 -1
  68. package/src/types/view-model.ts +0 -37
  69. package/src/ui.ts +0 -10
  70. package/src/video.ts +0 -2
  71. package/ui.ts +0 -1
  72. package/video.ts +0 -1
@@ -1,257 +0,0 @@
1
- import clsx from "clsx";
2
- import { useRef, useState, useEffect, useCallback } from "react";
3
- import { TMedia } from "../../types/view-model";
4
- import { MediaType } from "@rxdrag/rxcms-models";
5
- import { VideoPlayer } from "./VideoPlayer";
6
-
7
- export type MainMediaProps = {
8
- value?: TMedia[];
9
- selectedId?: string | null;
10
- aspect?: string;
11
- enableZoom?: boolean;
12
- className?: string;
13
- arrowButtonClass?: string;
14
- arrowIconClass?: string;
15
- playButtonClass?: string;
16
- onPrevious: () => void;
17
- onNext: () => void;
18
- canPrevious: boolean;
19
- canNext: boolean;
20
- };
21
-
22
- export const MainMedia = ({
23
- value,
24
- selectedId,
25
- aspect,
26
- enableZoom,
27
- className,
28
- arrowButtonClass,
29
- arrowIconClass,
30
- playButtonClass,
31
- onPrevious,
32
- onNext,
33
- canPrevious,
34
- canNext,
35
- }: MainMediaProps) => {
36
- const mainAreaRef = useRef<HTMLDivElement>(null);
37
- const [isZoomed, setIsZoomed] = useState(false);
38
- const [position, setPosition] = useState({ x: 0, y: 0 });
39
- const hoverTimerRef = useRef<NodeJS.Timeout | null>(null);
40
- const lastMousePosRef = useRef({ x: 0, y: 0 });
41
-
42
- const selectedMedia = value?.find((media) => media.id === selectedId);
43
- const isVideo = selectedMedia?.mediaType === MediaType.video;
44
-
45
- // Reset zoom state when image changes
46
- useEffect(() => {
47
- setIsZoomed(false);
48
- setPosition({ x: 0, y: 0 });
49
- if (hoverTimerRef.current) {
50
- clearTimeout(hoverTimerRef.current);
51
- }
52
- }, [selectedId]);
53
-
54
- // Clean up timer on unmount
55
- useEffect(() => {
56
- return () => {
57
- if (hoverTimerRef.current) {
58
- clearTimeout(hoverTimerRef.current);
59
- }
60
- };
61
- }, []);
62
-
63
- const updateZoomPosition = useCallback((clientX: number, clientY: number) => {
64
- if (mainAreaRef.current) {
65
- const { left, top, width, height } =
66
- mainAreaRef.current.getBoundingClientRect();
67
- const x = clientX - left;
68
- const y = clientY - top;
69
-
70
- // Calculate percentage position (0 to 1)
71
- const ratioX = Math.max(0, Math.min(1, x / width));
72
- const ratioY = Math.max(0, Math.min(1, y / height));
73
-
74
- // Formula: Translate = Dimension * (0.5 - Ratio)
75
- // This shifts the image opposite to mouse movement relative to center
76
- const newX = width * (0.5 - ratioX);
77
- const newY = height * (0.5 - ratioY);
78
-
79
- setPosition({ x: newX, y: newY });
80
- }
81
- }, []);
82
-
83
- const handleMouseEnter = useCallback(
84
- (e: React.MouseEvent) => {
85
- if (!enableZoom || isVideo) return;
86
- lastMousePosRef.current = { x: e.clientX, y: e.clientY };
87
-
88
- hoverTimerRef.current = setTimeout(() => {
89
- setIsZoomed(true);
90
- updateZoomPosition(
91
- lastMousePosRef.current.x,
92
- lastMousePosRef.current.y
93
- );
94
- }, 1000);
95
- },
96
- [enableZoom, isVideo, updateZoomPosition]
97
- );
98
-
99
- const handleMouseMove = useCallback(
100
- (e: React.MouseEvent) => {
101
- if (!enableZoom || isVideo) return;
102
- lastMousePosRef.current = { x: e.clientX, y: e.clientY };
103
-
104
- if (isZoomed) {
105
- updateZoomPosition(e.clientX, e.clientY);
106
- }
107
- },
108
- [enableZoom, isVideo, isZoomed, updateZoomPosition]
109
- );
110
-
111
- const handleMouseLeave = useCallback(() => {
112
- if (hoverTimerRef.current) {
113
- clearTimeout(hoverTimerRef.current);
114
- }
115
- setIsZoomed(false);
116
- setPosition({ x: 0, y: 0 });
117
- }, []);
118
-
119
- const handleClick = useCallback(
120
- (e: React.MouseEvent) => {
121
- if (!enableZoom || isVideo) return;
122
-
123
- if (hoverTimerRef.current) {
124
- clearTimeout(hoverTimerRef.current);
125
- }
126
-
127
- if (isZoomed) {
128
- setIsZoomed(false);
129
- setPosition({ x: 0, y: 0 });
130
- } else {
131
- setIsZoomed(true);
132
- updateZoomPosition(e.clientX, e.clientY);
133
- }
134
- },
135
- [enableZoom, isVideo, isZoomed, updateZoomPosition]
136
- );
137
-
138
- return (
139
- <div
140
- ref={mainAreaRef}
141
- className={clsx(
142
- "relative group overflow-hidden rounded-xl bg-gray-100 dark:bg-gray-800 z-0",
143
- aspect,
144
- className,
145
- enableZoom && !isVideo && "cursor-crosshair"
146
- )}
147
- onMouseEnter={handleMouseEnter}
148
- onMouseMove={handleMouseMove}
149
- onMouseLeave={handleMouseLeave}
150
- onClick={handleClick}
151
- >
152
- {/* Navigation Arrows - Hide when zoomed to avoid misclick */}
153
- {!isZoomed && (
154
- <div className="absolute inset-0 flex items-center justify-between p-4 pointer-events-none z-20">
155
- <button
156
- onClick={(e) => {
157
- e.stopPropagation();
158
- onPrevious();
159
- }}
160
- disabled={!canPrevious}
161
- className={clsx(
162
- "pointer-events-auto transition-all duration-200 rounded-full p-2",
163
- "bg-white/80 dark:bg-black/50 backdrop-blur-sm shadow-sm",
164
- "hover:bg-white dark:hover:bg-black/70 hover:scale-105 active:scale-95",
165
- "text-gray-800 dark:text-white",
166
- !canPrevious
167
- ? "opacity-0 translate-x-[-10px]"
168
- : "opacity-0 group-hover:opacity-100 translate-x-0",
169
- arrowButtonClass
170
- )}
171
- aria-label="Previous slide"
172
- >
173
- <svg
174
- xmlns="http://www.w3.org/2000/svg"
175
- className={clsx("h-5 w-5", arrowIconClass)}
176
- fill="none"
177
- viewBox="0 0 24 24"
178
- stroke="currentColor"
179
- >
180
- <path
181
- strokeLinecap="round"
182
- strokeLinejoin="round"
183
- strokeWidth={2}
184
- d="M15 19l-7-7 7-7"
185
- />
186
- </svg>
187
- </button>
188
-
189
- <button
190
- onClick={(e) => {
191
- e.stopPropagation();
192
- onNext();
193
- }}
194
- disabled={!canNext}
195
- className={clsx(
196
- "pointer-events-auto transition-all duration-200 rounded-full p-2",
197
- "bg-white/80 dark:bg-black/50 backdrop-blur-sm shadow-sm",
198
- "hover:bg-white dark:hover:bg-black/70 hover:scale-105 active:scale-95",
199
- "text-gray-800 dark:text-white",
200
- !canNext
201
- ? "opacity-0 translate-x-[10px]"
202
- : "opacity-0 group-hover:opacity-100 translate-x-0",
203
- arrowButtonClass
204
- )}
205
- aria-label="Next slide"
206
- >
207
- <svg
208
- xmlns="http://www.w3.org/2000/svg"
209
- className={clsx("h-5 w-5", arrowIconClass)}
210
- fill="none"
211
- viewBox="0 0 24 24"
212
- stroke="currentColor"
213
- >
214
- <path
215
- strokeLinecap="round"
216
- strokeLinejoin="round"
217
- strokeWidth={2}
218
- d="M9 5l7 7-7 7"
219
- />
220
- </svg>
221
- </button>
222
- </div>
223
- )}
224
-
225
- {value?.map((media) => (
226
- <div
227
- key={media.id}
228
- className={clsx(
229
- "absolute inset-0 transition-opacity duration-500 ease-in-out",
230
- media.id === selectedId
231
- ? "opacity-100 z-10"
232
- : "opacity-0 z-0 pointer-events-none"
233
- )}
234
- >
235
- {media.mediaType === MediaType.video ? (
236
- <VideoPlayer media={media} playButtonClass={playButtonClass} />
237
- ) : (
238
- <img
239
- src={media?.file?.resize || media?.file?.url}
240
- alt={media?.alt}
241
- className="w-full h-full object-cover object-center origin-center"
242
- style={{
243
- transform:
244
- media.id === selectedId
245
- ? `translate(${position.x}px, ${position.y}px) scale(${
246
- isZoomed ? 2 : 1
247
- })`
248
- : undefined,
249
- transition: isZoomed ? "none" : "transform 0.3s ease-out",
250
- }}
251
- />
252
- )}
253
- </div>
254
- ))}
255
- </div>
256
- );
257
- };
@@ -1,62 +0,0 @@
1
- import clsx from "clsx";
2
- import { TMedia } from "../../types/view-model";
3
- import { MediaType } from "@rxdrag/rxcms-models";
4
- import { VideoPlayer } from "./VideoPlayer";
5
-
6
- export type ThumbnailProps = {
7
- media: TMedia;
8
- isSelected: boolean;
9
- onClick: () => void;
10
- aspect?: string;
11
- className?: string;
12
- imageClass?: string;
13
- playButtonClass?: string;
14
- };
15
-
16
- export const Thumbnail = ({
17
- media,
18
- isSelected,
19
- onClick,
20
- aspect,
21
- className,
22
- imageClass,
23
- playButtonClass,
24
- }: ThumbnailProps) => {
25
- return (
26
- <div
27
- className={clsx(
28
- "relative cursor-pointer overflow-hidden rounded-lg transition-all duration-200",
29
- aspect,
30
- isSelected
31
- ? "ring-2 ring-primary ring-offset-2 ring-offset-white dark:ring-offset-gray-950"
32
- : "opacity-70 hover:opacity-100 hover:ring-2 hover:ring-gray-200 dark:hover:ring-gray-700",
33
- className
34
- )}
35
- onClick={onClick}
36
- >
37
- {media.mediaType === MediaType.video ? (
38
- <div
39
- className={clsx(
40
- "w-full h-full relative pointer-events-none [&>div]:!aspect-auto [&>div]:!w-full [&>div]:!h-full transition-transform duration-500",
41
- isSelected ? "scale-110" : "scale-100",
42
- imageClass
43
- )}
44
- >
45
- <VideoPlayer media={media} size="small" playButtonClass={playButtonClass} />
46
- </div>
47
- ) : (
48
- <img
49
- src={
50
- media?.file?.thumbnail || media?.file?.resize || media?.file?.url
51
- }
52
- alt={media?.alt}
53
- className={clsx(
54
- "w-full h-full object-cover transition-transform duration-500",
55
- isSelected ? "scale-110" : "scale-100",
56
- imageClass
57
- )}
58
- />
59
- )}
60
- </div>
61
- );
62
- };
@@ -1,114 +0,0 @@
1
- import { Media } from "@rxdrag/rxcms-models";
2
- import React, { useEffect, useRef, useState } from "react";
3
- import Hls from "hls.js";
4
- import clsx from "clsx";
5
-
6
- export function VideoPlayer(props: {
7
- media: Media;
8
- size?: "normal" | "small";
9
- playButtonClass?: string;
10
- }) {
11
- const { media, size = "normal", playButtonClass } = props;
12
- const videoRef = useRef<HTMLVideoElement>(null);
13
- const hlsRef = useRef<Hls | null>(null);
14
- const [isPlaying, setIsPlaying] = useState(false);
15
-
16
- const handlePlayClick = React.useCallback(
17
- (e: React.MouseEvent) => {
18
- e.stopPropagation();
19
- if (videoRef.current) {
20
- if (isPlaying) {
21
- videoRef.current.pause();
22
- } else {
23
- videoRef.current.play();
24
- }
25
- }
26
- },
27
- [isPlaying]
28
- );
29
-
30
- useEffect(() => {
31
- if (!videoRef.current || !media.file?.original) return;
32
-
33
- const video = videoRef.current;
34
- const videoUrl = media.file.original;
35
-
36
- const handlePlay = () => {
37
- setIsPlaying(true);
38
- };
39
-
40
- const handlePause = () => {
41
- setIsPlaying(false);
42
- };
43
-
44
- video.addEventListener("play", handlePlay);
45
- video.addEventListener("pause", handlePause);
46
-
47
- if (media.storageType === "cloudflare_stream") {
48
- if (!Hls.isSupported()) {
49
- if (video.canPlayType("application/vnd.apple.mpegurl")) {
50
- video.src = videoUrl;
51
- }
52
- } else {
53
- const hls = new Hls({
54
- enableWorker: true,
55
- });
56
-
57
- hls.loadSource(videoUrl);
58
- hls.attachMedia(video);
59
- hlsRef.current = hls;
60
- }
61
- } else {
62
- video.src = videoUrl;
63
- }
64
-
65
- return () => {
66
- video.removeEventListener("play", handlePlay);
67
- video.removeEventListener("pause", handlePause);
68
- if (hlsRef.current) {
69
- hlsRef.current.destroy();
70
- hlsRef.current = null;
71
- }
72
- };
73
- }, [media.file?.original, media.storageType]);
74
-
75
- return (
76
- <div className="relative w-full h-full group cursor-pointer" onClick={handlePlayClick}>
77
- <video
78
- ref={videoRef}
79
- preload="metadata"
80
- poster={media.file?.thumbnail}
81
- className="absolute inset-0 w-full h-full object-cover"
82
- controls={false}
83
- playsInline
84
- />
85
- {!isPlaying && (
86
- <div className="absolute inset-0 flex items-center justify-center z-10 bg-black/10 group-hover:bg-black/20 transition-colors">
87
- <div
88
- className={clsx(
89
- "rounded-full bg-white/90 hover:bg-white flex items-center justify-center shadow-lg transition-all hover:scale-110 backdrop-blur-sm",
90
- size === "small" ? "w-8 h-8" : "w-14 h-14",
91
- playButtonClass
92
- )}
93
- >
94
- <svg
95
- xmlns="http://www.w3.org/2000/svg"
96
- viewBox="0 0 24 24"
97
- fill="currentColor"
98
- className={clsx(
99
- "text-gray-900 ml-1",
100
- size === "small" ? "w-4 h-4" : "w-8 h-8"
101
- )}
102
- >
103
- <path
104
- fillRule="evenodd"
105
- d="M4.5 5.653c0-1.426 1.529-2.33 2.779-1.643l11.54 6.348c1.295.712 1.295 2.573 0 3.248L7.28 19.987C6.03 20.673 4.5 19.77 4.5 18.562V5.653z"
106
- clipRule="evenodd"
107
- />
108
- </svg>
109
- </div>
110
- </div>
111
- )}
112
- </div>
113
- );
114
- }
@@ -1,271 +0,0 @@
1
- import clsx from "clsx";
2
- import { forwardRef, useEffect, useState, useCallback } from "react";
3
- import { TMedia } from "../../types/view-model";
4
- import { MainMedia } from "./MainMedia";
5
- import { Thumbnail } from "./Thumbnail";
6
-
7
- export type MediasProps = {
8
- value?: TMedia[];
9
- className?: string;
10
- children?: React.ReactNode;
11
- // Aspect ratio, format is `aspect-[width/height]`
12
- aspect?: string;
13
- thumbnailAspect?: string;
14
- enableZoom?: boolean;
15
- thumbnailPosition?: "bottom" | "left";
16
- visibleCount?: number;
17
- classNames?: {
18
- mainArea?: string;
19
- navigation?: string;
20
- thumbnail?: string;
21
- thumbnailImage?: string;
22
- arrowButton?: string;
23
- arrowIcon?: string;
24
- playButton?: string;
25
- navigationInner?: string;
26
- };
27
- };
28
-
29
- export const Medias = forwardRef<HTMLDivElement, MediasProps>((props, ref) => {
30
- const {
31
- value,
32
- className,
33
- children,
34
- aspect = "aspect-[1/1]",
35
- thumbnailAspect = "aspect-[1/1]",
36
- enableZoom = true,
37
- thumbnailPosition = "bottom",
38
- visibleCount = 6,
39
- classNames,
40
- ...rest
41
- } = props;
42
- const {
43
- mainArea,
44
- navigation,
45
- thumbnail,
46
- thumbnailImage,
47
- arrowButton,
48
- arrowIcon,
49
- playButton,
50
- navigationInner,
51
- } = classNames || {};
52
- const [selectedId, setSelectedId] = useState<string | undefined | null>(
53
- value?.[0]?.id || ""
54
- );
55
-
56
- useEffect(() => {
57
- setSelectedId(value?.[0]?.id || "");
58
- }, [value]);
59
-
60
- const selectedIndex =
61
- value?.findIndex((media) => media.id === selectedId) || 0;
62
-
63
- const totalItems = value?.length || 0;
64
-
65
- const handlePrevious = useCallback(() => {
66
- if (selectedIndex > 0) {
67
- const prevIndex = selectedIndex - 1;
68
- setSelectedId(value?.[prevIndex]?.id || "");
69
- }
70
- }, [selectedIndex, value]);
71
-
72
- const handleNext = useCallback(() => {
73
- if (selectedIndex < totalItems - 1) {
74
- const currentMediaIndex =
75
- value?.findIndex((media) => media.id === selectedId) || 0;
76
- const nextIndex = currentMediaIndex + 1;
77
- setSelectedId(value?.[nextIndex]?.id || "");
78
- }
79
- }, [selectedIndex, totalItems, value, selectedId]);
80
-
81
- const handleKeyDown = useCallback(
82
- (e: KeyboardEvent) => {
83
- if (e.key === "ArrowLeft" || e.key === "ArrowUp") {
84
- handlePrevious();
85
- } else if (e.key === "ArrowRight" || e.key === "ArrowDown") {
86
- handleNext();
87
- }
88
- },
89
- [handleNext, handlePrevious]
90
- );
91
-
92
- useEffect(() => {
93
- window.addEventListener("keydown", handleKeyDown);
94
- return () => window.removeEventListener("keydown", handleKeyDown);
95
- }, [handleKeyDown]);
96
-
97
- const canPrevious = selectedIndex > 0;
98
- const canNext = selectedIndex < totalItems - 1;
99
-
100
- const isVerticalThumbs = thumbnailPosition === "left";
101
-
102
- const actualVisibleCount = visibleCount;
103
- const halfVisible = Math.floor(actualVisibleCount / 2);
104
- const startIndex = Math.max(
105
- 0,
106
- Math.min(selectedIndex - halfVisible, totalItems - actualVisibleCount)
107
- );
108
- const endIndex = Math.min(startIndex + actualVisibleCount, totalItems);
109
-
110
- return (
111
- <div
112
- ref={ref}
113
- className={clsx(
114
- "flex gap-4",
115
- isVerticalThumbs ? "flex-col md:flex-row-reverse" : "flex-col",
116
- className
117
- )}
118
- {...rest}
119
- >
120
- {children}
121
- {/* Main display area */}
122
- <MainMedia
123
- value={value}
124
- selectedId={selectedId}
125
- aspect={aspect}
126
- enableZoom={enableZoom}
127
- className={clsx(
128
- isVerticalThumbs ? "w-full md:flex-1" : "w-full",
129
- mainArea
130
- )}
131
- arrowButtonClass={arrowButton}
132
- arrowIconClass={arrowIcon}
133
- playButtonClass={playButton}
134
- onPrevious={handlePrevious}
135
- onNext={handleNext}
136
- canPrevious={canPrevious}
137
- canNext={canNext}
138
- />
139
-
140
- {/* Thumbnail navigation */}
141
- {totalItems > 1 && (
142
- <div
143
- className={clsx(
144
- "relative group/thumbs",
145
- isVerticalThumbs
146
- ? "w-full mt-4 md:w-24 md:h-full md:px-1"
147
- : "w-full mt-4",
148
- navigation
149
- )}
150
- >
151
- {/* Previous Arrow */}
152
- {totalItems > actualVisibleCount && (
153
- <button
154
- onClick={handlePrevious}
155
- disabled={!canPrevious}
156
- className={clsx(
157
- "absolute z-10 w-8 h-8 flex items-center justify-center rounded-full",
158
- "bg-white/60 dark:bg-black/50 shadow-sm backdrop-blur-sm",
159
- "text-gray-700 dark:text-gray-200 transition-all duration-200",
160
- isVerticalThumbs
161
- ? "left-2 top-1/2 -translate-y-1/2 md:top-2 md:left-1/2 md:-translate-x-1/2"
162
- : "left-2 top-1/2 -translate-y-1/2",
163
- !canPrevious
164
- ? "opacity-0 pointer-events-none"
165
- : "opacity-0 group-hover/thumbs:opacity-100 hover:bg-white/90 dark:hover:bg-black/80",
166
- arrowButton
167
- )}
168
- aria-label="Previous"
169
- >
170
- <svg
171
- xmlns="http://www.w3.org/2000/svg"
172
- className={clsx(
173
- "h-4 w-4",
174
- isVerticalThumbs && "md:rotate-90",
175
- arrowIcon
176
- )}
177
- fill="none"
178
- viewBox="0 0 24 24"
179
- stroke="currentColor"
180
- >
181
- <path
182
- strokeLinecap="round"
183
- strokeLinejoin="round"
184
- strokeWidth={2}
185
- d="M15 19l-7-7 7-7"
186
- />
187
- </svg>
188
- </button>
189
- )}
190
-
191
- <div
192
- className={clsx(
193
- "flex-1",
194
- isVerticalThumbs
195
- ? "w-full px-0.5 md:h-full md:py-0.5"
196
- : "w-full px-0.5"
197
- )}
198
- >
199
- <div
200
- className={clsx(
201
- "gap-2 md:gap-3",
202
- navigationInner,
203
- isVerticalThumbs ? "grid md:flex md:flex-col" : "grid"
204
- )}
205
- style={{
206
- gridTemplateColumns: `repeat(${actualVisibleCount}, minmax(0, 1fr))`,
207
- }}
208
- >
209
- {value?.map((media, index) => {
210
- if (index < startIndex || index >= endIndex) return null;
211
- const isSelected = selectedId === media.id;
212
- return (
213
- <Thumbnail
214
- key={media.id}
215
- media={media}
216
- isSelected={isSelected}
217
- onClick={() => setSelectedId(media.id)}
218
- aspect={thumbnailAspect}
219
- className={thumbnail}
220
- imageClass={thumbnailImage}
221
- playButtonClass={playButton}
222
- />
223
- );
224
- })}
225
- </div>
226
- </div>
227
-
228
- {/* Next Arrow */}
229
- {totalItems > actualVisibleCount && (
230
- <button
231
- onClick={handleNext}
232
- disabled={!canNext}
233
- className={clsx(
234
- "absolute z-10 w-8 h-8 flex items-center justify-center rounded-full",
235
- "bg-white/60 dark:bg-black/50 shadow-sm backdrop-blur-sm",
236
- "text-gray-700 dark:text-gray-200 transition-all duration-200",
237
- isVerticalThumbs
238
- ? "right-2 top-1/2 -translate-y-1/2 md:bottom-2 md:left-1/2 md:-translate-x-1/2"
239
- : "right-2 top-1/2 -translate-y-1/2",
240
- !canNext
241
- ? "opacity-0 pointer-events-none"
242
- : "opacity-0 group-hover/thumbs:opacity-100 hover:bg-white/90 dark:hover:bg-black/80",
243
- arrowButton
244
- )}
245
- aria-label="Next"
246
- >
247
- <svg
248
- xmlns="http://www.w3.org/2000/svg"
249
- className={clsx(
250
- "h-4 w-4",
251
- isVerticalThumbs && "md:rotate-90",
252
- arrowIcon
253
- )}
254
- fill="none"
255
- viewBox="0 0 24 24"
256
- stroke="currentColor"
257
- >
258
- <path
259
- strokeLinecap="round"
260
- strokeLinejoin="round"
261
- strokeWidth={2}
262
- d="M9 5l7 7-7 7"
263
- />
264
- </svg>
265
- </button>
266
- )}
267
- </div>
268
- )}
269
- </div>
270
- );
271
- });