react-carousel-photo-gallery 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,207 @@
1
+ # react-carousel-photo-gallery
2
+
3
+ A lightweight, customizable React carousel component specifically designed for photo galleries, featuring thumbnail support, fullscreen mode, and flexible navigation overrides
4
+
5
+ ---
6
+
7
+ ## Installation
8
+
9
+ ```bash
10
+ npm install react-carousel-photo-gallery
11
+ ```
12
+
13
+ or
14
+
15
+ ```bash
16
+ yarn add react-carousel-photo-gallery
17
+ ```
18
+
19
+ ---
20
+
21
+ ## Quick Start
22
+
23
+ ```jsx
24
+ import { CoderCarousel } from 'react-carousel-photo-gallery';
25
+
26
+
27
+ const App = () => {
28
+ const images = [
29
+ <img src="img1.jpg" alt="1" />,
30
+ <img src="img2.jpg" alt="2" />,
31
+ <img src="img3.jpg" alt="3" />,
32
+ ];
33
+
34
+ const thumbnails = [
35
+ <img src="thumb1.jpg" alt="t1" />,
36
+ <img src="thumb2.jpg" alt="t2" />,
37
+ <img src="thumb3.jpg" alt="t3" />,
38
+ ];
39
+
40
+ return (
41
+ <CoderCarousel
42
+ displayThumbs={true}
43
+ thumbs={thumbnails}
44
+ slideDelay={3000}
45
+ >
46
+ {images}
47
+ </CoderCarousel>
48
+ );
49
+ };
50
+ ```
51
+
52
+ ## Using Captions & Titles
53
+
54
+ You can wrap your images in a container to display titles or overlays. Here is how to implement the imageWithCaptionContainer structure within the carousel:
55
+
56
+ ```jsx
57
+ import { CoderCarousel } from 'react-carousel-photo-gallery';
58
+
59
+ const App = () => {
60
+ const slides = [
61
+ <div className="imageWithCaptionContainer" key="1">
62
+ <img src="img1.jpg" alt="Mountain View" />
63
+ <div className="info">Sunset in the Mountains</div>
64
+ </div>,
65
+ <div className="imageWithCaptionContainer" key="2">
66
+ <img src="img2.jpg" alt="Ocean Breeze" />
67
+ <div className="info">Summer Beach Days</div>
68
+ </div>
69
+ ];
70
+
71
+ return (
72
+ <div style={{ width: '800px', margin: '0 auto' }}>
73
+ <CoderCarousel
74
+ displayThumbs={false}
75
+ width="100%"
76
+ >
77
+ {slides}
78
+ </CoderCarousel>
79
+ </div>
80
+ );
81
+ };
82
+
83
+ export default App;
84
+ ```
85
+
86
+ ---
87
+
88
+
89
+ ---
90
+
91
+ ## Props
92
+
93
+ | Prop | Type | Default | Description |
94
+ | ----------------- | ---------------------------------------- | ----------- | -------------------------------------------------------------------------- |
95
+ | `title` | `React.ReactNode` | — | Content displayed inside the tooltip. |
96
+ | `children` | `React.ReactElement` | — | The element that triggers the tooltip. Must be a single React element. |
97
+ | `container` | `HTMLDivElement \| null` | `undefined` | Optional container element for rendering the tooltip (useful for portals). |
98
+ | `placement` | `'top' \| 'bottom' \| 'left' \| 'right'` | `'top'` | Position of the tooltip relative to the trigger element. |
99
+ | `arrow` | `boolean` | `false` | Displays an arrow pointing to the trigger element. |
100
+ | `delayDuration` | `number` | `0` | Delay in milliseconds before showing the tooltip. |
101
+ | `enterTouchDelay` | `number` | `0` | Delay in milliseconds before showing the tooltip on touch devices. |
102
+ | `leaveTouchDelay` | `number` | `1500` | Delay in milliseconds before hiding the tooltip on touch devices. |
103
+ | `style` | `React.CSSProperties` | `undefined` | Inline styles applied to the tooltip container. |
104
+ | `slotProps` | `{ tooltip?, content?, arrow? }` | `undefined` | Advanced customization for tooltip internal slots. |
105
+
106
+ ---
107
+
108
+ ## `slotProps` Structure
109
+
110
+ | Key | Type | Description |
111
+ | --------- | ------------------------- | ---------------------------------------------------- |
112
+ | `tooltip` | `Record<string, unknown>` | Custom props applied to the tooltip wrapper element. |
113
+ | `content` | `Record<string, unknown>` | Custom props applied to the tooltip content element. |
114
+ | `arrow` | `Record<string, unknown>` | Custom props applied to the arrow element. |
115
+
116
+
117
+ ---
118
+
119
+ ## Styling (CSS Variables)
120
+
121
+ This component supports theming via standard CSS custom properties.
122
+ You can override these variables globally or within a scoped container.
123
+
124
+ ---
125
+
126
+ ### Available CSS Variables
127
+
128
+ | Variable | Fallback | Description |
129
+ | ---------------------------------- | ----------- | --------------------------------------- |
130
+ | `--border-color` | `#EDEDED99` | Border color used across the component. |
131
+ | `--accent-color` | `#36C2` | Primary accent color. |
132
+ | `--accent-secondary-color` | `#36C2` | Secondary accent color. |
133
+ | `--background-color` | `#36C2` | Default background color. |
134
+ | `--background-color-maximized` | `#000` | Background color when maximized. |
135
+ | `--button-background-color` | `#36C2` | Default button background color. |
136
+ | `--button-active-background-color` | `#36C2` | Active button background color. |
137
+ | `--navigation-background-color` | `#fff` | Navigation area background color. |
138
+
139
+ ---
140
+
141
+ ## 🛠️ How to Override
142
+
143
+ ### Global Theme
144
+
145
+ ```css
146
+ :root {
147
+ --accent-color: #6366f1;
148
+ --background-color: #0f172a;
149
+ --border-color: #334155;
150
+ }
151
+ ```
152
+
153
+ ---
154
+
155
+ ### Scoped Theme
156
+
157
+ ```css
158
+ .my-custom-theme {
159
+ --accent-color: #22c55e;
160
+ --background-color: #111827;
161
+ }
162
+ ```
163
+
164
+ ```tsx
165
+ <div className="my-custom-theme">
166
+ <YourComponent />
167
+ </div>
168
+ ```
169
+
170
+ ---
171
+
172
+ ## 🌗 Dark Mode Example
173
+
174
+ ```css
175
+ body.dark {
176
+ --background-color: #000;
177
+ --navigation-background-color: #111;
178
+ --border-color: #222;
179
+ }
180
+ ```
181
+ ---
182
+
183
+ ## Compatibility
184
+
185
+ * ✅ React 18+
186
+ * ✅ Next.js (App & Pages Router)
187
+ * ✅ Vite / CRA
188
+
189
+ ---
190
+
191
+ ## License
192
+
193
+ MIT © Abdullah Ibne Alam
194
+
195
+ ---
196
+
197
+ ## Contributing
198
+
199
+ Pull requests are welcome! <br />
200
+ If you have ideas for enhancements or performance improvements, feel free to open an issue.<br />
201
+ [https://github.com/RepulsiveCoder/react-carousel-photo-gallery](https://github.com/RepulsiveCoder/react-carousel-photo-gallery)
202
+
203
+ ---
204
+
205
+ ## If you like it…
206
+
207
+ Drop a ⭐ on the repo and use it to make your UI feel alive!
package/package.json ADDED
@@ -0,0 +1,38 @@
1
+ {
2
+ "name": "react-carousel-photo-gallery",
3
+ "version": "0.0.1",
4
+ "description": "A lightweight, high-performance React input library featuring Material-style floating labels with minimal DOM footprint. Supports Input, TextArea, and Select with a unique dual-text feature (separate placeholder and label). Smooth CSS animations, border-integrated labels, and zero fieldset dependencies.",
5
+ "main": "dist/index.js",
6
+ "module": "dist/index.mjs",
7
+ "types": "dist/index.d.ts",
8
+ "scripts": {
9
+ "test": "echo \"Error: no test specified\" && exit 1",
10
+ "rollup": "rollup -c --bundleConfigAsCjs"
11
+ },
12
+ "keywords": [],
13
+ "author": {
14
+ "name": "Abdullah Ibne Alam",
15
+ "url": "https://abdullahibnealam.com"
16
+ },
17
+ "license": "ISC",
18
+ "peerDependencies": {
19
+ "react": ">=16.8.0",
20
+ "react-dom": ">=16.8.0",
21
+ "@radix-ui/react-tooltip": "^1.2.8"
22
+ },
23
+ "devDependencies": {
24
+ "@rollup/plugin-commonjs": "^29.0.0",
25
+ "@rollup/plugin-node-resolve": "^16.0.3",
26
+ "@rollup/plugin-terser": "^0.4.4",
27
+ "@rollup/plugin-typescript": "^12.3.0",
28
+ "@types/node": "^25.2.3",
29
+ "@types/react": "^19.2.13",
30
+ "@types/react-dom": "^19.2.3",
31
+ "react": "^19.2.4",
32
+ "rollup-plugin-dts": "^6.3.0",
33
+ "rollup-plugin-peer-deps-external": "^2.2.4",
34
+ "rollup-plugin-postcss": "^4.0.2",
35
+ "tslib": "^2.8.1",
36
+ "typescript": "^5.9.3"
37
+ }
38
+ }
@@ -0,0 +1,46 @@
1
+ import peerDepsExternal from 'rollup-plugin-peer-deps-external';
2
+ import resolve from '@rollup/plugin-node-resolve';
3
+ import commonjs from '@rollup/plugin-commonjs';
4
+ import typescript from '@rollup/plugin-typescript';
5
+ import terser from '@rollup/plugin-terser';
6
+ import dts from 'rollup-plugin-dts';
7
+ import postcss from 'rollup-plugin-postcss';
8
+
9
+ const packageJson = require('./package.json');
10
+
11
+ export default [
12
+ {
13
+ input: 'src/index.ts',
14
+ output: [
15
+ {
16
+ file: packageJson.main,
17
+ format: 'cjs',
18
+ sourcemap: true,
19
+ },
20
+ {
21
+ file: packageJson.module,
22
+ format: 'esm',
23
+ sourcemap: true,
24
+ },
25
+ ],
26
+ plugins: [
27
+ peerDepsExternal(),
28
+ resolve(),
29
+ commonjs(),
30
+ typescript({ tsconfig: './tsconfig.json' }),
31
+ postcss(),
32
+ terser(),
33
+ ],
34
+ external: ['react', 'react-dom'],
35
+ },
36
+ {
37
+ input: 'src/index.ts',
38
+ output: [{ file: packageJson.types, }],
39
+ plugins: [
40
+ dts.default(),
41
+ postcss({
42
+ extensions: ['.css'],
43
+ })
44
+ ],
45
+ }
46
+ ];
@@ -0,0 +1,259 @@
1
+ .rcpg-content-wrapper {
2
+
3
+ --rcpg-color-border: var(--border-color, #EDEDED99);
4
+ --rcpg-accent-color: var(--accent-color, #36C2);
5
+ --rcpg-accent-secondary-color: var(--accent-secondary-color, #36C2);
6
+ --rcpg-color-background: var(--background-color, #36C2);
7
+ --rcpg-color-background-maximized: var(--background-color-maximized, #000);
8
+ --rcpg-color-button-background: var(--button-background-color, #36C2);
9
+ --rcpg-color-button-active-background: var(--button-active-background-color, #36C2);
10
+ --rcpg-color-navigation-background: var(--navigation-background-color, #fff);
11
+
12
+
13
+ &.carouselMaximizedCover {
14
+ width: 100vw;
15
+ height: 100vh;
16
+ position: fixed;
17
+ background: var(--rcpg-color-background-maximized);
18
+ top: 0px;
19
+ z-index: 150;
20
+
21
+ .closeButton {
22
+ position: absolute;
23
+ right: 18px;
24
+ top: 6px;
25
+ cursor: pointer;
26
+ padding: 1px 8px 0px;
27
+ border: 1px solid var(--rcpg-color-border);
28
+ border-radius: 8px;
29
+ transition: all 0.5s linear;
30
+ z-index: 250;
31
+
32
+ &.hidden {
33
+ display: none;
34
+ }
35
+
36
+ &:hover {
37
+ color: var(--rcpg-accent-color);
38
+ box-shadow: 0 0 5px 3px var(--rcpg-accent-secondary-color);
39
+ }
40
+ }
41
+
42
+ .fullscreenModeButton {
43
+ position: absolute;
44
+ right: 18px;
45
+ bottom: 6px;
46
+ padding: 1px 4px 0px;
47
+ border: 1px solid var(--rcpg-color-border);
48
+ border-radius: 8px;
49
+ transition: all 0.5s linear;
50
+ z-index: 250;
51
+
52
+ &:hover {
53
+ color: var(--rcpg-accent-color);
54
+ box-shadow: 0 0 5px 3px var(--rcpg-accent-secondary-color);
55
+ }
56
+ }
57
+ }
58
+
59
+ .carouselContainerMaster {
60
+ display: flex;
61
+ justify-content: center;
62
+ align-items: center;
63
+ width: 100%;
64
+
65
+ &.maximized {
66
+ width: 100vw;
67
+ height: 100vh;
68
+ position: absolute;
69
+ aspect-ratio: auto;
70
+
71
+ .carouselLinksContainer.displayThumbs {
72
+ bottom: 2%;
73
+ }
74
+
75
+ .carouselBottomLinks.displayThumbs {
76
+ width: 64px;
77
+ height: 36px
78
+ }
79
+
80
+
81
+ .carouselBottomLinks.displayThumbs {
82
+ border-radius: 3px;
83
+ overflow: hidden;
84
+ }
85
+
86
+ .carouselBottomLinks.displayThumbs img {
87
+ border-radius: 3px;
88
+ }
89
+
90
+ .carouselItem img {
91
+ width: 100vw;
92
+ height: 100vh;
93
+ object-fit: contain;
94
+ }
95
+
96
+ .imageWithCaptionContainer .info {
97
+ padding: 10px 0 8vh;
98
+ }
99
+
100
+ &.displayThumbs .imageWithCaptionContainer .info {
101
+ padding: 10px 0 max(7vh, 45px);
102
+ }
103
+
104
+ }
105
+
106
+ &.fullscreen .carouselItem img {
107
+ max-width: 100vw;
108
+ max-height: 100vh;
109
+ object-fit: contain;
110
+ }
111
+
112
+ .carouselContainer {
113
+ position: relative;
114
+ width: 100%;
115
+ /* width: 900px; */
116
+ max-width: 90vw;
117
+ max-height: 60vw;
118
+ aspect-ratio: 16 / 9;
119
+ padding: 0;
120
+ overflow: hidden;
121
+ display: flex;
122
+ justify-content: flex-start;
123
+ align-items: center;
124
+ flex-direction: row;
125
+ flex-wrap: nowrap;
126
+ background-color: var(--rcpg-color-background);
127
+
128
+ &.maximized {
129
+ width: 100vw;
130
+ height: 100vh;
131
+ position: absolute;
132
+ aspect-ratio: auto;
133
+ z-index: 200;
134
+ }
135
+
136
+ &.fullscreen {
137
+ max-width: 100vw;
138
+ max-height: 100vh;
139
+ }
140
+
141
+ &>button {
142
+ background-color: var(--surface-color);
143
+ background-color: transparent;
144
+ border-radius: 50%;
145
+ outline: none;
146
+ border: 0;
147
+ /* padding: 4px 15px 8px; */
148
+ padding: 4px 5px 8px;
149
+ color: var(--rcpg-accent-secondary-color);
150
+ font-size: 30px;
151
+ line-height: 30px;
152
+ cursor: pointer;
153
+ }
154
+ &>button:hover {
155
+ color: var(--rcpg-accent-color);
156
+ }
157
+
158
+ &>button:active {
159
+ color: var(--rcpg-color-button-active-background);
160
+ }
161
+
162
+ @media(min--moz-device-pixel-ratio:0) {
163
+ &>button {
164
+ padding: 2px 4px 6px 6px;
165
+ }
166
+ }
167
+
168
+ .carouselButtonNext {
169
+ position: absolute;
170
+ font-weight: bolder;
171
+ right: 2px;
172
+ }
173
+
174
+ .carouselButtonPrev {
175
+ position: absolute;
176
+ font-weight: bolder;
177
+ left: 2px;
178
+ }
179
+
180
+ .carouselLinksContainer {
181
+ position: absolute;
182
+ bottom: 5%;
183
+ left: 50%;
184
+ transform: translateX(-50%);
185
+ display: flex;
186
+ align-items: center;
187
+ }
188
+
189
+ .carouselBottomLinks {
190
+ width: 24px;
191
+ height: 8px;
192
+ padding: 0;
193
+ border-radius: 50%;
194
+ border-radius: 0;
195
+ outline: none;
196
+ border: 0;
197
+ background-color: var(--rcpg-color-navigation-background);
198
+ border: 1px solid var(--rcpg-color-border);
199
+ transition: background-color 1s ease;
200
+ margin: 0 2px;
201
+
202
+ &.displayThumbs {
203
+ position: relative;
204
+ height: 12px;
205
+
206
+ img {
207
+ position: absolute;
208
+ width: 100%;
209
+ height: 100%;
210
+ top: 50%;
211
+ left: 50%;
212
+ transform: translate(-50%, -50%);
213
+ object-fit: cover;
214
+ filter: blur(.6px);
215
+ border-radius: 0;
216
+ transition: all 0.5s ease;
217
+ }
218
+
219
+ &.active img {
220
+ filter: grayscale(0%) blur(0);
221
+ }
222
+
223
+ &:hover img {
224
+ /* transform: translate(-50%, -50%) scale(3); */
225
+ z-index: 5;
226
+ filter: grayscale(0%);
227
+ }
228
+ }
229
+
230
+ &.active {
231
+ background-color: var(--rcpg-color-button-active-background);
232
+ border: 1px solid var(--rcpg-accent-color);
233
+ box-shadow: 0 0 5px 2px var(--rcpg-accent-color);
234
+ }
235
+ }
236
+
237
+ .carouselItem {
238
+ box-sizing: border-box;
239
+ min-width: 100%;
240
+ height: auto;
241
+ overflow: hidden;
242
+ object-fit: contain;
243
+ height: auto;
244
+ cursor: pointer;
245
+ transform: translateX(calc(var(--slider-active-index) * -100%));
246
+ transition: transform 1s ease;
247
+
248
+ img {
249
+ width: 100%;
250
+ height: 100%;
251
+ aspect-ratio: 16 / 9;
252
+ max-width: 90vw;
253
+ max-height: 60vw;
254
+ object-fit: scale-down;
255
+ }
256
+ }
257
+ }
258
+ }
259
+ }
@@ -0,0 +1,242 @@
1
+ 'use client'
2
+
3
+ import React from 'react';
4
+ import { CustomTooltip } from './CustomTooltip';
5
+ import './CoderCarousel.css';
6
+
7
+ export type SvgContainerProps = {
8
+ height?: string | number;
9
+ width?: string | number;
10
+ className?: string;
11
+ color?: string;
12
+ style?: React.CSSProperties;
13
+ }
14
+
15
+ const FullScreenIcon = ({height = '60px', width = '60px', className='', color = "#FFF", style={}} : SvgContainerProps) => {
16
+ return (
17
+ <div className={className} style={{height, width, display: 'inline-block', maxHeight: height, ...style }}>
18
+ <svg fill={color} viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
19
+ <path d="M7.69233 18.2781L9.70711 20.2929C9.9931 20.5789 10.0787 21.009 9.92388 21.3827C9.7691 21.7564 9.40446 22 9 22H3C2.44772 22 2 21.5523 2 21V15C2 14.5955 2.24364 14.2309 2.61732 14.0761C2.99099 13.9213 3.42111 14.0069 3.70711 14.2929L5.571 16.1568L9.25289 12.4749C9.64342 12.0844 10.2766 12.0844 10.6671 12.4749L11.3742 13.182C11.7647 13.5725 11.7647 14.2057 11.3742 14.5962L7.69233 18.2781Z" />
20
+ <path d="M16.3077 5.72187L14.2929 3.70711C14.0069 3.42111 13.9213 2.99099 14.0761 2.61732C14.2309 2.24364 14.5955 2 15 2H21C21.5523 2 22 2.44772 22 3V9C22 9.40446 21.7564 9.7691 21.3827 9.92388C21.009 10.0787 20.5789 9.9931 20.2929 9.70711L18.429 7.84319L14.7471 11.5251C14.3566 11.9156 13.7234 11.9156 13.3329 11.5251L12.6258 10.818C12.2352 10.4275 12.2352 9.7943 12.6258 9.40378L16.3077 5.72187Z" />
21
+ </svg>
22
+ </div>
23
+ );
24
+ };
25
+
26
+ const FullScreenExitIcon = ({height = '60px', width = '60px', className='', color = "#FFF", style={}} : SvgContainerProps) => {
27
+ return (
28
+ <div className={className} style={{height, width, display: 'inline-block', maxHeight: height, ...style }}>
29
+ <svg fill={color} viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
30
+ <path d="M20.04 10.1109L18.0252 8.09612L21.7071 4.41421C22.0976 4.02369 22.0976 3.39052 21.7071 3L21 2.29289C20.6095 1.90237 19.9763 1.90237 19.5858 2.29289L15.9039 5.9748L14.04 4.11089C13.754 3.82489 13.3239 3.73933 12.9502 3.89411C12.5765 4.04889 12.3329 4.41353 12.3329 4.81799V10.818C12.3329 11.3703 12.7806 11.818 13.3329 11.818H19.3329C19.7373 11.818 20.102 11.5744 20.2568 11.2007C20.4115 10.827 20.326 10.3969 20.04 10.1109Z" />
31
+ <path d="M3.96 13.8891L5.97478 15.9039L2.29289 19.5858C1.90237 19.9763 1.90237 20.6095 2.29289 21L3 21.7071C3.39052 22.0976 4.02369 22.0976 4.41421 21.7071L8.0961 18.0252L9.96 19.8891C10.246 20.1751 10.6761 20.2607 11.0498 20.1059C11.4235 19.9511 11.6671 19.5865 11.6671 19.182V13.182C11.6671 12.6297 11.2194 12.182 10.6671 12.182H4.66711C4.26265 12.182 3.89801 12.4256 3.74323 12.7993C3.58845 13.173 3.674 13.6031 3.96 13.8891Z" />
32
+ </svg>
33
+ </div>
34
+ );
35
+ };
36
+
37
+ type CoderCarouselProps = {
38
+ children: React.ReactNode[];
39
+ slideDelay?: number;
40
+ startIndex?: number;
41
+ width?: string;
42
+ displayThumbs?: boolean;
43
+ thumbs?: React.ReactNode[];
44
+ nextButton?: React.ReactElement<any, string | React.JSXElementConstructor<any>>;
45
+ prevButton?: React.ReactElement<any, string | React.JSXElementConstructor<any>>;
46
+ fullScreenButtonIcon?: React.ReactElement<any, string | React.JSXElementConstructor<any>>;
47
+ fullScreenExitButtonIcon?: React.ReactElement<any, string | React.JSXElementConstructor<any>>;
48
+ }
49
+
50
+ const CoderCarousel = ({
51
+ children,
52
+ slideDelay = 5000,
53
+ startIndex = 0,
54
+ width = "900px",
55
+ displayThumbs = false,
56
+ thumbs = [],
57
+ nextButton = <span>⟩</span>,
58
+ prevButton = <span>⟨</span>,
59
+ fullScreenButtonIcon = <FullScreenIcon width='20px' height='16px' color='var(--accent-color)' />,
60
+ fullScreenExitButtonIcon = <FullScreenExitIcon width='20px' height='16px' color='var(--accent-color)' />
61
+ }: CoderCarouselProps) => {
62
+ const [activeIndex, setActiveIndex] = React.useState(startIndex | 0);
63
+ const [nextIndex, setNextIndex] = React.useState(((startIndex | 0) + 1) % children.length);
64
+ const [prevIndex, setPrevIndex] = React.useState(((startIndex | 0) - 1 + children.length) % children.length);
65
+ const [maximized, setMaximized] = React.useState<boolean>(false);
66
+ const [fullscreenMode, setFullscreenMode] = React.useState<boolean>(false);
67
+ const coderCarouselWindowContainer = React.useRef<HTMLDivElement | null>(null);
68
+ const intervalHandler = React.useRef<NodeJS.Timeout | null>(null);
69
+ const pauseSlideShow = React.useRef<boolean>(false);
70
+
71
+ React.useEffect(() => {
72
+ const handleFullscreenChange = () => {
73
+ if (document.fullscreenElement === null) {
74
+ setFullscreenMode(false);
75
+ }
76
+ };
77
+
78
+ intervalHandler.current = setInterval(() => { slideNextAuto(); }, slideDelay);
79
+ window.addEventListener('keydown', handleKeyDown);
80
+ document.addEventListener('fullscreenchange', handleFullscreenChange);
81
+
82
+ return () => {
83
+ window.removeEventListener('keydown', handleKeyDown);
84
+ document.removeEventListener('fullscreenchange', handleFullscreenChange);
85
+ if (intervalHandler.current) {
86
+ clearInterval(intervalHandler.current);
87
+ }
88
+ }
89
+ }, []);
90
+
91
+ React.useEffect(() => {
92
+ if (fullscreenMode) {
93
+ coderCarouselWindowContainer.current?.requestFullscreen();
94
+ } else if (document.fullscreenElement !== null && document.fullscreenElement === coderCarouselWindowContainer.current) {
95
+ document.exitFullscreen();
96
+ }
97
+ }, [fullscreenMode]);
98
+
99
+ const stopSliderTimer = () => {
100
+ if (intervalHandler.current) {
101
+ clearInterval(intervalHandler.current);
102
+ }
103
+ };
104
+
105
+ const startSliderTimer = () => {
106
+ intervalHandler.current = setInterval(() => { slideNextAuto(); }, slideDelay);
107
+ };
108
+
109
+ const resetSliderTimer = () => {
110
+ stopSliderTimer();
111
+ startSliderTimer();
112
+ };
113
+
114
+ const slideNextAuto = () => {
115
+ if (!pauseSlideShow.current) {
116
+ slideNext();
117
+ }
118
+ };
119
+
120
+ const slideNext = (reset = false) => {
121
+ if (reset) {
122
+ resetSliderTimer();
123
+ }
124
+ setActiveIndex((val) => {
125
+ setPrevIndex((val) % children.length);
126
+ setNextIndex((val + 2) % children.length);
127
+ return (val + 1) % children.length;
128
+ });
129
+ };
130
+
131
+ const slidePrev = (reset = false) => {
132
+ if (reset) {
133
+ resetSliderTimer();
134
+ }
135
+ setActiveIndex((val) => {
136
+ setPrevIndex((val - 2 + children.length) % children.length);
137
+ setNextIndex((val + 1) % children.length);
138
+ return (val - 1 + children.length) % children.length;
139
+ });
140
+ };
141
+
142
+ const handleKeyDown = (event: { key: string; }) => {
143
+ if (event.key === 'ArrowLeft') {
144
+ slidePrev(true);
145
+ } else if (event.key === 'ArrowRight') {
146
+ slideNext(true);
147
+ } else if (event.key === 'Escape') {
148
+ setMaximized(false);
149
+ }
150
+ };
151
+
152
+ const toggleMaximized = () => {
153
+ setMaximized((maximized) => {
154
+ if (!maximized) {
155
+ pauseSlideShow.current = false;
156
+ }
157
+ return !maximized;
158
+ });
159
+ }
160
+
161
+
162
+ return (
163
+ <div className={`rcfg-content-wrapper ${maximized ? ' carouselMaximizedCover' : ''}`} style={{ width: !maximized && width ? `${width}px` : '100%' }} ref={coderCarouselWindowContainer}>
164
+ <span className={`closeButton ${maximized ? '' : 'hidden'}`} onClick={() => { if (fullscreenMode) setFullscreenMode(false); else setMaximized(false); }}>X</span>
165
+ {maximized &&
166
+ <button className='fullscreenModeButton' onClick={() => setFullscreenMode((fullscreenMode) => !fullscreenMode)}
167
+ title={fullscreenMode ? 'Exit Fullscreen' : 'Enter Fullscreen'}>
168
+ {!fullscreenMode && fullScreenButtonIcon}
169
+ {fullscreenMode && fullScreenExitButtonIcon}
170
+ </button>
171
+ }
172
+ <div className={`carouselContainerMaster ${maximized ? ' maximized' : ''} ${displayThumbs ? 'displayThumbs' : ''} ${fullscreenMode ? 'fullscreen' : ''}`} >
173
+ <div
174
+ className={`carouselContainer ${maximized ? 'maximized' : ''} ${fullscreenMode ? 'fullscreen' : ''}`}
175
+ onMouseEnter={() => { if (!maximized) pauseSlideShow.current = true; }}
176
+ onMouseLeave={() => { if (!maximized) pauseSlideShow.current = false; }}
177
+ style={{ '--slider-active-index': activeIndex } as React.CSSProperties}
178
+ >
179
+ {children.map((item, index) => {
180
+ return (
181
+ <div className={`carouselItem`} key={index} onClick={toggleMaximized}>
182
+ {item}
183
+ </div>
184
+ );
185
+ })}
186
+
187
+ <div className={`carouselLinksContainer ${displayThumbs ? 'displayThumbs' : ''}`}>
188
+ {children.map((item, index) => {
189
+ return (
190
+ <button
191
+ key={index}
192
+ className={`carouselBottomLinks ${activeIndex === index ? 'active' : ''} ${displayThumbs ? 'displayThumbs' : ''}`}
193
+ onClick={(e) => {
194
+ e.preventDefault();
195
+ resetSliderTimer();
196
+ setPrevIndex((index - 1 + children.length) % children.length);
197
+ setNextIndex((index + 1) % children.length);
198
+ setActiveIndex(index);
199
+ }}
200
+ >
201
+ {displayThumbs && thumbs.length > 0 && thumbs[index] && (
202
+ <CustomTooltip title={<>{thumbs[index]}</>}
203
+ >
204
+ <span>{thumbs[index]}</span>
205
+ </CustomTooltip>
206
+ )}
207
+ </button>
208
+ );
209
+ })}
210
+ </div>
211
+
212
+ <button className='carouselButtonPrev'
213
+ onClick={(e) => { e.preventDefault(); slidePrev(true); }}
214
+ >
215
+ {displayThumbs && thumbs.length > 0 && thumbs[prevIndex] && (
216
+ <CustomTooltip title={<>{thumbs[prevIndex]}</>}
217
+ >
218
+ {prevButton ? prevButton : <span>⟨</span>}
219
+ </CustomTooltip>
220
+ )}
221
+ {!(displayThumbs) && <>{prevButton ? prevButton : <span>⟨</span>}</>}
222
+ </button>
223
+
224
+ <button className='carouselButtonNext'
225
+ onClick={(e) => { e.preventDefault(); slideNext(true); }}
226
+ >
227
+ {displayThumbs && thumbs.length > 0 && thumbs[nextIndex] && (
228
+ <CustomTooltip title={<>{thumbs[nextIndex]}</>}
229
+ >
230
+ {nextButton ? nextButton : <span>⟩</span>}
231
+ </CustomTooltip>
232
+ )}
233
+ {!(displayThumbs && thumbs.length > 0 && thumbs[nextIndex]) && <>{nextButton ? nextButton : <span>⟩</span>}</>}
234
+ </button>
235
+ </div>
236
+ </div>
237
+ </div>
238
+ );
239
+ }
240
+
241
+
242
+ export default CoderCarousel;
@@ -0,0 +1,61 @@
1
+ import * as Tooltip from '@radix-ui/react-tooltip';
2
+ import React from 'react';
3
+
4
+ type CustomTooltipProps = {
5
+ title: React.ReactNode;
6
+ children: React.ReactElement<any>;
7
+ container?: HTMLDivElement | null | undefined,
8
+ placement?: 'top' | 'bottom' | 'left' | 'right';
9
+ arrow?: boolean;
10
+ delayDuration?: number;
11
+ enterTouchDelay?: number;
12
+ leaveTouchDelay?: number;
13
+ style?: React.CSSProperties;
14
+ slotProps?: {
15
+ tooltip?: { [slot: string]: unknown };
16
+ content?: { [slot: string]: unknown };
17
+ arrow?: { [slot: string]: unknown };
18
+ };
19
+ }
20
+
21
+ export function CustomTooltip({ title = <></>, children, placement = 'top', container, style = {} }: CustomTooltipProps) {
22
+ const [open, setOpen] = React.useState(false);
23
+ const [isTouch, setIsTouch] = React.useState(false);
24
+
25
+ React.useEffect(() => {
26
+ const detectTouch = () => setIsTouch(true);
27
+ window.addEventListener('touchstart', detectTouch, { once: true });
28
+ return () => window.removeEventListener('touchstart', detectTouch);
29
+ }, []);
30
+
31
+ return (
32
+ <Tooltip.Provider>
33
+ <Tooltip.Root open={isTouch ? open : undefined} onOpenChange={setOpen}>
34
+ <Tooltip.Trigger asChild>
35
+ {React.cloneElement(children, {
36
+ onClick: (e: React.MouseEvent) => {
37
+ setOpen(true);
38
+ if (children.props.onClick) {
39
+ children.props.onClick(e);
40
+ }
41
+ }
42
+ })}
43
+ </Tooltip.Trigger>
44
+ <Tooltip.Portal container={container ?? undefined}>
45
+ <Tooltip.Content
46
+ className="bg-gray-800 text-white px-2 py-2 rounded shadow-md text-sm z-100"
47
+ side={placement}
48
+ sideOffset={5}
49
+ >
50
+ <div style={style}>
51
+ {title}
52
+ </div>
53
+ <Tooltip.Arrow className="fill-gray-800" />
54
+ </Tooltip.Content>
55
+ </Tooltip.Portal>
56
+ </Tooltip.Root>
57
+ </Tooltip.Provider>
58
+ );
59
+ }
60
+
61
+ export default CustomTooltip;
package/src/index.ts ADDED
@@ -0,0 +1,3 @@
1
+ import CoderCarousel from "./components/CoderCarousel";
2
+
3
+ export default CoderCarousel;
package/tsconfig.json ADDED
@@ -0,0 +1,23 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES5",
4
+ "lib": ["DOM", "DOM.Iterable", "ESNext", "ES2020"],
5
+ "allowJs": true,
6
+ "module": "ESNext",
7
+ "moduleResolution": "node",
8
+ "resolveJsonModule": true,
9
+ "isolatedModules": true,
10
+ "noEmit": true,
11
+ "jsx": "react-jsx",
12
+ "declaration": true,
13
+ "rootDir": "./src",
14
+ "outDir": "./dist",
15
+ "strict": true,
16
+ "noFallthroughCasesInSwitch": true,
17
+ "allowSyntheticDefaultImports": true,
18
+ "esModuleInterop": true,
19
+ "skipLibCheck": true,
20
+ "forceConsistentCasingInFileNames": true
21
+ },
22
+ "include": ["src"]
23
+ }