@cjboco/cj-image-flip-previewer 1.0.0
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/LICENSE +28 -0
- package/README.md +260 -0
- package/dist/index.cjs +231 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +66 -0
- package/dist/index.d.ts +66 -0
- package/dist/index.js +210 -0
- package/dist/index.js.map +1 -0
- package/dist/server.cjs +59 -0
- package/dist/server.cjs.map +1 -0
- package/dist/server.d.cts +14 -0
- package/dist/server.d.ts +14 -0
- package/dist/server.js +34 -0
- package/dist/server.js.map +1 -0
- package/dist/styles.css +49 -0
- package/dist/styles.css.map +1 -0
- package/package.json +71 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
BSD 3-Clause License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026, Doug Jones - Creative Juices, Bo. Co.
|
|
4
|
+
|
|
5
|
+
Redistribution and use in source and binary forms, with or without
|
|
6
|
+
modification, are permitted provided that the following conditions are met:
|
|
7
|
+
|
|
8
|
+
1. Redistributions of source code must retain the above copyright notice, this
|
|
9
|
+
list of conditions and the following disclaimer.
|
|
10
|
+
|
|
11
|
+
2. Redistributions in binary form must reproduce the above copyright notice,
|
|
12
|
+
this list of conditions and the following disclaimer in the documentation
|
|
13
|
+
and/or other materials provided with the distribution.
|
|
14
|
+
|
|
15
|
+
3. Neither the name of the copyright holder nor the names of its
|
|
16
|
+
contributors may be used to endorse or promote products derived from
|
|
17
|
+
this software without specific prior written permission.
|
|
18
|
+
|
|
19
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
|
20
|
+
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
21
|
+
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
22
|
+
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
|
23
|
+
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
|
24
|
+
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
|
25
|
+
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
|
26
|
+
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
|
27
|
+
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
28
|
+
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
package/README.md
ADDED
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
# cj-image-flip-previewer
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/cj-image-flip-previewer)
|
|
4
|
+
[](https://github.com/cjboco/cj-image-flip-previewer/blob/master/LICENSE)
|
|
5
|
+
[](https://cjboco.github.io/cj-image-flip-previewer/)
|
|
6
|
+
|
|
7
|
+
A React component for interactive image previews. Zero dependencies beyond React 19+.
|
|
8
|
+
|
|
9
|
+
**[Live Demo](https://cjboco.github.io/cj-image-flip-previewer/)**
|
|
10
|
+
|
|
11
|
+
One component, two modes:
|
|
12
|
+
- **`mode="position"`** — Image changes based on horizontal mouse/touch position (360-degree product views)
|
|
13
|
+
- **`mode="hover"`** — Images auto-cycle on a timer when hovered (video thumbnail previews)
|
|
14
|
+
|
|
15
|
+
## Install
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
npm install cj-image-flip-previewer
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Setup
|
|
22
|
+
|
|
23
|
+
Import the stylesheet once in your app (e.g., in your root layout or entry file):
|
|
24
|
+
|
|
25
|
+
```tsx
|
|
26
|
+
import "cj-image-flip-previewer/styles.css";
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
---
|
|
30
|
+
|
|
31
|
+
## Position Mode (default)
|
|
32
|
+
|
|
33
|
+
Image changes based on where the mouse is horizontally within the container. Move left-to-right to cycle through all images.
|
|
34
|
+
|
|
35
|
+
```tsx
|
|
36
|
+
import { FlipPreviewer } from "cj-image-flip-previewer";
|
|
37
|
+
|
|
38
|
+
<FlipPreviewer
|
|
39
|
+
width={320}
|
|
40
|
+
height={240}
|
|
41
|
+
images={[
|
|
42
|
+
{ src: "/images/angle-1.jpg", alt: "Front" },
|
|
43
|
+
{ src: "/images/angle-2.jpg", alt: "Side" },
|
|
44
|
+
{ src: "/images/angle-3.jpg", alt: "Back" },
|
|
45
|
+
{ src: "/images/angle-4.jpg", alt: "Other side" },
|
|
46
|
+
]}
|
|
47
|
+
/>
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
### With Links
|
|
51
|
+
|
|
52
|
+
Each image can have its own link:
|
|
53
|
+
|
|
54
|
+
```tsx
|
|
55
|
+
<FlipPreviewer
|
|
56
|
+
width={320}
|
|
57
|
+
height={240}
|
|
58
|
+
images={[
|
|
59
|
+
{ src: "/img1.jpg", href: "/products/1", title: "View product" },
|
|
60
|
+
{ src: "/img2.jpg", href: "/products/2", target: "_blank", rel: "noopener noreferrer" },
|
|
61
|
+
]}
|
|
62
|
+
/>
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
---
|
|
66
|
+
|
|
67
|
+
## Hover Mode
|
|
68
|
+
|
|
69
|
+
Images auto-cycle on a timer when the mouse enters the container, like Netflix/YouTube thumbnail previews.
|
|
70
|
+
|
|
71
|
+
```tsx
|
|
72
|
+
<FlipPreviewer
|
|
73
|
+
mode="hover"
|
|
74
|
+
width={160}
|
|
75
|
+
height={110}
|
|
76
|
+
images={[
|
|
77
|
+
{ src: "/images/frame-01.jpg" },
|
|
78
|
+
{ src: "/images/frame-02.jpg" },
|
|
79
|
+
{ src: "/images/frame-03.jpg" },
|
|
80
|
+
{ src: "/images/frame-04.jpg" },
|
|
81
|
+
]}
|
|
82
|
+
/>
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
### Auto-Play
|
|
86
|
+
|
|
87
|
+
Start cycling immediately without waiting for hover:
|
|
88
|
+
|
|
89
|
+
```tsx
|
|
90
|
+
<FlipPreviewer
|
|
91
|
+
mode="hover"
|
|
92
|
+
autoPlay
|
|
93
|
+
delay={300}
|
|
94
|
+
showProgress={false}
|
|
95
|
+
width={320}
|
|
96
|
+
height={240}
|
|
97
|
+
images={[
|
|
98
|
+
{ src: "/frame1.jpg", href: "/watch/123" },
|
|
99
|
+
{ src: "/frame2.jpg", href: "/watch/123" },
|
|
100
|
+
{ src: "/frame3.jpg", href: "/watch/123" },
|
|
101
|
+
]}
|
|
102
|
+
/>
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
### Imperative Control
|
|
106
|
+
|
|
107
|
+
Use a ref to programmatically start and pause the animation:
|
|
108
|
+
|
|
109
|
+
```tsx
|
|
110
|
+
import { useRef } from "react";
|
|
111
|
+
import { FlipPreviewer, type FlipPreviewerRef } from "cj-image-flip-previewer";
|
|
112
|
+
|
|
113
|
+
function ControlledPreviewer() {
|
|
114
|
+
const ref = useRef<FlipPreviewerRef>(null);
|
|
115
|
+
|
|
116
|
+
return (
|
|
117
|
+
<>
|
|
118
|
+
<FlipPreviewer
|
|
119
|
+
ref={ref}
|
|
120
|
+
mode="hover"
|
|
121
|
+
width={320}
|
|
122
|
+
height={240}
|
|
123
|
+
images={[
|
|
124
|
+
{ src: "/frame1.jpg" },
|
|
125
|
+
{ src: "/frame2.jpg" },
|
|
126
|
+
{ src: "/frame3.jpg" },
|
|
127
|
+
]}
|
|
128
|
+
/>
|
|
129
|
+
<button onClick={() => ref.current?.start()}>Play</button>
|
|
130
|
+
<button onClick={() => ref.current?.pause()}>Pause</button>
|
|
131
|
+
</>
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
---
|
|
137
|
+
|
|
138
|
+
## Props
|
|
139
|
+
|
|
140
|
+
| Prop | Type | Default | Description |
|
|
141
|
+
|------|------|---------|-------------|
|
|
142
|
+
| `mode` | `"position" \| "hover"` | `"position"` | How images cycle |
|
|
143
|
+
| `images` | `FlipPreviewerImage[]` | *required* | Array of images |
|
|
144
|
+
| `width` | `number \| string` | `"100%"` | Container width |
|
|
145
|
+
| `height` | `number \| string` | `"100%"` | Container height |
|
|
146
|
+
| `delay` | `number` | — | Ms between frames (hover mode only). Omit for max speed (requestAnimationFrame). |
|
|
147
|
+
| `autoPlay` | `boolean` | `false` | Auto-start animation (hover mode only) |
|
|
148
|
+
| `showProgress` | `boolean` | `true` | Show preload progress bar (hover mode only) |
|
|
149
|
+
| `showCursor` | `boolean` | `true` | Show horizontal resize cursor (position mode only) |
|
|
150
|
+
| `debug` | `boolean` | `false` | Show debug overlay (position mode only) |
|
|
151
|
+
| `className` | `string` | — | Additional CSS class(es) |
|
|
152
|
+
| `style` | `CSSProperties` | — | Additional inline styles |
|
|
153
|
+
| `onIndexChange` | `(index: number) => void` | — | Called when the active image changes |
|
|
154
|
+
| `onImagesLoaded` | `() => void` | — | Called when all images finish preloading (hover mode only) |
|
|
155
|
+
| `ref` | `Ref<FlipPreviewerRef>` | — | Imperative handle for start/pause (hover mode only) |
|
|
156
|
+
|
|
157
|
+
### FlipPreviewerImage
|
|
158
|
+
|
|
159
|
+
| Property | Type | Description |
|
|
160
|
+
|----------|------|-------------|
|
|
161
|
+
| `src` | `string` | Image source URL (required) |
|
|
162
|
+
| `alt` | `string` | Alt text |
|
|
163
|
+
| `href` | `string` | Link URL |
|
|
164
|
+
| `title` | `string` | Link title |
|
|
165
|
+
| `target` | `string` | Link target (e.g., `"_blank"`) |
|
|
166
|
+
| `rel` | `string` | Link rel (e.g., `"noopener noreferrer"`) |
|
|
167
|
+
|
|
168
|
+
### FlipPreviewerRef
|
|
169
|
+
|
|
170
|
+
| Method | Description |
|
|
171
|
+
|--------|-------------|
|
|
172
|
+
| `start()` | Start the frame animation |
|
|
173
|
+
| `pause()` | Pause and reset to the first frame |
|
|
174
|
+
|
|
175
|
+
---
|
|
176
|
+
|
|
177
|
+
## Styling
|
|
178
|
+
|
|
179
|
+
### Default CSS
|
|
180
|
+
|
|
181
|
+
The included stylesheet uses `@layer components`, which works both as standalone CSS and with Tailwind CSS v4+. Import it once in your app:
|
|
182
|
+
|
|
183
|
+
```tsx
|
|
184
|
+
import "cj-image-flip-previewer/styles.css";
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
### Using with Tailwind CSS
|
|
188
|
+
|
|
189
|
+
Since the component styles are in the `components` layer, Tailwind utility classes automatically take priority — no `!important` needed. Just add classes via the `className` prop:
|
|
190
|
+
|
|
191
|
+
```tsx
|
|
192
|
+
<FlipPreviewer
|
|
193
|
+
className="rounded-lg shadow-md cursor-grab"
|
|
194
|
+
width={320}
|
|
195
|
+
height={240}
|
|
196
|
+
images={images}
|
|
197
|
+
/>
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
If you prefer full Tailwind control and don't want the default stylesheet at all, simply skip the CSS import. The component applies its essential layout styles inline, so it will still function correctly. You can then style everything with utility classes.
|
|
201
|
+
|
|
202
|
+
### CSS Custom Properties
|
|
203
|
+
|
|
204
|
+
The stylesheet exposes CSS variables for common customizations. Override them in your own CSS or Tailwind's `globals.css`:
|
|
205
|
+
|
|
206
|
+
| Variable | Default | Description |
|
|
207
|
+
|----------|---------|-------------|
|
|
208
|
+
| `--cj-flip-progress-height` | `4px` | Height of the preload progress bar |
|
|
209
|
+
| `--cj-flip-progress-bg` | `rgba(0,0,0,0.3)` | Progress bar track background |
|
|
210
|
+
| `--cj-flip-progress-color` | `#6bc4f7` | Progress bar fill color |
|
|
211
|
+
| `--cj-flip-progress-speed` | `0.2s` | Progress bar transition speed |
|
|
212
|
+
| `--cj-flip-debug-font-size` | `11px` | Debug overlay font size |
|
|
213
|
+
| `--cj-flip-debug-bg` | `rgba(0,0,0,0.6)` | Debug overlay background |
|
|
214
|
+
| `--cj-flip-debug-color` | `#fff` | Debug overlay text color |
|
|
215
|
+
|
|
216
|
+
**Override globally:**
|
|
217
|
+
|
|
218
|
+
```css
|
|
219
|
+
:root {
|
|
220
|
+
--cj-flip-progress-color: hotpink;
|
|
221
|
+
--cj-flip-progress-height: 6px;
|
|
222
|
+
}
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
**Override per instance:**
|
|
226
|
+
|
|
227
|
+
```tsx
|
|
228
|
+
<FlipPreviewer
|
|
229
|
+
style={{ '--cj-flip-progress-color': '#facc15' } as CSSProperties}
|
|
230
|
+
images={images}
|
|
231
|
+
/>
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
### BEM Class Names
|
|
235
|
+
|
|
236
|
+
For more targeted CSS overrides, all elements use BEM-style class names:
|
|
237
|
+
|
|
238
|
+
```css
|
|
239
|
+
.cj-flip-previewer { }
|
|
240
|
+
.cj-flip-previewer__img { }
|
|
241
|
+
.cj-flip-previewer__link { }
|
|
242
|
+
.cj-flip-previewer__debug { }
|
|
243
|
+
.cj-flip-previewer__progress { }
|
|
244
|
+
.cj-flip-previewer__progress-bar { }
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
---
|
|
248
|
+
|
|
249
|
+
## Requirements
|
|
250
|
+
|
|
251
|
+
- React 19+
|
|
252
|
+
- React DOM 19+
|
|
253
|
+
|
|
254
|
+
## License
|
|
255
|
+
|
|
256
|
+
BSD-3-Clause
|
|
257
|
+
|
|
258
|
+
## Author
|
|
259
|
+
|
|
260
|
+
Doug Jones — [Creative Juices, Bo. Co.](https://www.cjboco.com)
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/index.ts
|
|
21
|
+
var index_exports = {};
|
|
22
|
+
__export(index_exports, {
|
|
23
|
+
FlipPreviewer: () => FlipPreviewer
|
|
24
|
+
});
|
|
25
|
+
module.exports = __toCommonJS(index_exports);
|
|
26
|
+
|
|
27
|
+
// src/FlipPreviewer.tsx
|
|
28
|
+
var import_react = require("react");
|
|
29
|
+
var import_jsx_runtime = require("react/jsx-runtime");
|
|
30
|
+
function FlipPreviewer({
|
|
31
|
+
mode = "position",
|
|
32
|
+
images,
|
|
33
|
+
width,
|
|
34
|
+
height,
|
|
35
|
+
fit = "cover",
|
|
36
|
+
delay,
|
|
37
|
+
autoPlay = false,
|
|
38
|
+
showProgress = true,
|
|
39
|
+
showCursor = true,
|
|
40
|
+
debug = false,
|
|
41
|
+
className,
|
|
42
|
+
style,
|
|
43
|
+
onIndexChange,
|
|
44
|
+
onImagesLoaded,
|
|
45
|
+
ref
|
|
46
|
+
}) {
|
|
47
|
+
const containerRef = (0, import_react.useRef)(null);
|
|
48
|
+
const timerRef = (0, import_react.useRef)(null);
|
|
49
|
+
const [activeIndex, setActiveIndex] = (0, import_react.useState)(0);
|
|
50
|
+
const [debugInfo, setDebugInfo] = (0, import_react.useState)("");
|
|
51
|
+
const [loadedCount, setLoadedCount] = (0, import_react.useState)(0);
|
|
52
|
+
const [allLoaded, setAllLoaded] = (0, import_react.useState)(false);
|
|
53
|
+
const activeIndexRef = (0, import_react.useRef)(0);
|
|
54
|
+
const isPlayingRef = (0, import_react.useRef)(false);
|
|
55
|
+
(0, import_react.useEffect)(() => {
|
|
56
|
+
if (mode !== "hover" || images.length === 0) {
|
|
57
|
+
setAllLoaded(true);
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
let loaded = 0;
|
|
61
|
+
setLoadedCount(0);
|
|
62
|
+
setAllLoaded(false);
|
|
63
|
+
const total = images.length;
|
|
64
|
+
images.forEach(({ src }) => {
|
|
65
|
+
const img = new Image();
|
|
66
|
+
img.onload = img.onerror = () => {
|
|
67
|
+
loaded++;
|
|
68
|
+
setLoadedCount(loaded);
|
|
69
|
+
if (loaded >= total) {
|
|
70
|
+
setAllLoaded(true);
|
|
71
|
+
onImagesLoaded?.();
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
img.src = src;
|
|
75
|
+
});
|
|
76
|
+
}, [mode, images, onImagesLoaded]);
|
|
77
|
+
const clearTimer = (0, import_react.useCallback)(() => {
|
|
78
|
+
if (timerRef.current !== null) {
|
|
79
|
+
if (delay != null) {
|
|
80
|
+
clearTimeout(timerRef.current);
|
|
81
|
+
} else {
|
|
82
|
+
cancelAnimationFrame(timerRef.current);
|
|
83
|
+
}
|
|
84
|
+
timerRef.current = null;
|
|
85
|
+
}
|
|
86
|
+
}, [delay]);
|
|
87
|
+
const advanceFrame = (0, import_react.useCallback)(() => {
|
|
88
|
+
if (!isPlayingRef.current) return;
|
|
89
|
+
if (images.length <= 1) return;
|
|
90
|
+
const next = activeIndexRef.current + 1 >= images.length ? 0 : activeIndexRef.current + 1;
|
|
91
|
+
activeIndexRef.current = next;
|
|
92
|
+
setActiveIndex(next);
|
|
93
|
+
onIndexChange?.(next);
|
|
94
|
+
if (delay != null) {
|
|
95
|
+
timerRef.current = setTimeout(advanceFrame, delay);
|
|
96
|
+
} else {
|
|
97
|
+
timerRef.current = requestAnimationFrame(advanceFrame);
|
|
98
|
+
}
|
|
99
|
+
}, [images.length, delay, onIndexChange]);
|
|
100
|
+
const startAnimation = (0, import_react.useCallback)(() => {
|
|
101
|
+
if (!allLoaded || images.length <= 1) return;
|
|
102
|
+
isPlayingRef.current = true;
|
|
103
|
+
clearTimer();
|
|
104
|
+
if (delay != null) {
|
|
105
|
+
timerRef.current = setTimeout(advanceFrame, delay);
|
|
106
|
+
} else {
|
|
107
|
+
timerRef.current = requestAnimationFrame(advanceFrame);
|
|
108
|
+
}
|
|
109
|
+
}, [allLoaded, images.length, delay, advanceFrame, clearTimer]);
|
|
110
|
+
const stopAnimation = (0, import_react.useCallback)(() => {
|
|
111
|
+
isPlayingRef.current = false;
|
|
112
|
+
clearTimer();
|
|
113
|
+
activeIndexRef.current = 0;
|
|
114
|
+
setActiveIndex(0);
|
|
115
|
+
onIndexChange?.(0);
|
|
116
|
+
}, [clearTimer, onIndexChange]);
|
|
117
|
+
(0, import_react.useEffect)(() => {
|
|
118
|
+
if (mode === "hover" && autoPlay && allLoaded) {
|
|
119
|
+
startAnimation();
|
|
120
|
+
}
|
|
121
|
+
return () => clearTimer();
|
|
122
|
+
}, [mode, autoPlay, allLoaded, startAnimation, clearTimer]);
|
|
123
|
+
(0, import_react.useImperativeHandle)(
|
|
124
|
+
ref,
|
|
125
|
+
() => ({
|
|
126
|
+
start: startAnimation,
|
|
127
|
+
pause: stopAnimation
|
|
128
|
+
}),
|
|
129
|
+
[startAnimation, stopAnimation]
|
|
130
|
+
);
|
|
131
|
+
const handleMouseEnter = (0, import_react.useCallback)(() => {
|
|
132
|
+
if (mode === "hover" && !autoPlay) {
|
|
133
|
+
startAnimation();
|
|
134
|
+
}
|
|
135
|
+
}, [mode, autoPlay, startAnimation]);
|
|
136
|
+
const handleMouseLeave = (0, import_react.useCallback)(() => {
|
|
137
|
+
if (mode === "hover" && !autoPlay) {
|
|
138
|
+
stopAnimation();
|
|
139
|
+
}
|
|
140
|
+
}, [mode, autoPlay, stopAnimation]);
|
|
141
|
+
const handlePointerMove = (0, import_react.useCallback)(
|
|
142
|
+
(e) => {
|
|
143
|
+
if (mode !== "position") return;
|
|
144
|
+
const container = containerRef.current;
|
|
145
|
+
if (!container || images.length === 0) return;
|
|
146
|
+
const rect = container.getBoundingClientRect();
|
|
147
|
+
const x = e.clientX - rect.left;
|
|
148
|
+
let pos = Math.floor(x / rect.width * images.length);
|
|
149
|
+
pos = Math.max(0, Math.min(pos, images.length - 1));
|
|
150
|
+
if (pos !== activeIndexRef.current) {
|
|
151
|
+
activeIndexRef.current = pos;
|
|
152
|
+
setActiveIndex(pos);
|
|
153
|
+
onIndexChange?.(pos);
|
|
154
|
+
}
|
|
155
|
+
if (debug) {
|
|
156
|
+
const y = e.clientY - rect.top;
|
|
157
|
+
setDebugInfo(
|
|
158
|
+
`x:${Math.round(x)}, y:${Math.round(y)}, img:${pos + 1}/${images.length}, w:${Math.round(rect.width)}, h:${Math.round(rect.height)}`
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
},
|
|
162
|
+
[mode, images.length, debug, onIndexChange]
|
|
163
|
+
);
|
|
164
|
+
(0, import_react.useEffect)(() => {
|
|
165
|
+
setActiveIndex(0);
|
|
166
|
+
activeIndexRef.current = 0;
|
|
167
|
+
}, [images]);
|
|
168
|
+
if (images.length === 0) return null;
|
|
169
|
+
const activeImage = images[activeIndex];
|
|
170
|
+
const hasLink = !!activeImage.href;
|
|
171
|
+
const containerStyle = {
|
|
172
|
+
position: "relative",
|
|
173
|
+
width: width ?? "100%",
|
|
174
|
+
height: height ?? "100%",
|
|
175
|
+
overflow: "hidden",
|
|
176
|
+
cursor: hasLink ? "pointer" : mode === "position" && showCursor ? "ew-resize" : "default",
|
|
177
|
+
...style
|
|
178
|
+
};
|
|
179
|
+
const imgElement = /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
180
|
+
"img",
|
|
181
|
+
{
|
|
182
|
+
src: activeImage.src,
|
|
183
|
+
alt: activeImage.alt ?? "",
|
|
184
|
+
className: "cj-flip-previewer__img",
|
|
185
|
+
style: { objectFit: fit },
|
|
186
|
+
draggable: false
|
|
187
|
+
}
|
|
188
|
+
);
|
|
189
|
+
const content = hasLink ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
190
|
+
"a",
|
|
191
|
+
{
|
|
192
|
+
href: activeImage.href,
|
|
193
|
+
title: activeImage.title,
|
|
194
|
+
target: activeImage.target,
|
|
195
|
+
rel: activeImage.rel,
|
|
196
|
+
className: "cj-flip-previewer__link",
|
|
197
|
+
children: imgElement
|
|
198
|
+
}
|
|
199
|
+
) : imgElement;
|
|
200
|
+
const progressPct = images.length > 0 ? loadedCount / images.length * 100 : 0;
|
|
201
|
+
return (
|
|
202
|
+
// biome-ignore lint/a11y/noStaticElementInteractions: container tracks pointer position for image flipping
|
|
203
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
|
|
204
|
+
"div",
|
|
205
|
+
{
|
|
206
|
+
ref: containerRef,
|
|
207
|
+
className: `cj-flip-previewer${className ? ` ${className}` : ""}`,
|
|
208
|
+
style: containerStyle,
|
|
209
|
+
onPointerMove: handlePointerMove,
|
|
210
|
+
onMouseEnter: handleMouseEnter,
|
|
211
|
+
onMouseLeave: handleMouseLeave,
|
|
212
|
+
children: [
|
|
213
|
+
content,
|
|
214
|
+
mode === "hover" && showProgress && !allLoaded && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "cj-flip-previewer__progress", children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
215
|
+
"div",
|
|
216
|
+
{
|
|
217
|
+
className: "cj-flip-previewer__progress-bar",
|
|
218
|
+
style: { width: `${progressPct}%` }
|
|
219
|
+
}
|
|
220
|
+
) }),
|
|
221
|
+
mode === "position" && debug && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "cj-flip-previewer__debug", children: debugInfo })
|
|
222
|
+
]
|
|
223
|
+
}
|
|
224
|
+
)
|
|
225
|
+
);
|
|
226
|
+
}
|
|
227
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
228
|
+
0 && (module.exports = {
|
|
229
|
+
FlipPreviewer
|
|
230
|
+
});
|
|
231
|
+
//# sourceMappingURL=index.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/FlipPreviewer.tsx"],"sourcesContent":["export { FlipPreviewer } from \"./FlipPreviewer\";\nexport type {\n FlipPreviewerProps,\n FlipPreviewerImage,\n FlipPreviewerRef,\n} from \"./FlipPreviewer\";\n","import {\n\ttype CSSProperties,\n\ttype Ref,\n\tuseCallback,\n\tuseEffect,\n\tuseImperativeHandle,\n\tuseRef,\n\tuseState,\n} from \"react\";\n\nexport interface FlipPreviewerImage {\n\t/** Image source URL */\n\tsrc: string;\n\t/** Alt text for the image */\n\talt?: string;\n\t/** Optional link URL — clicking the image navigates here */\n\thref?: string;\n\t/** Link title attribute */\n\ttitle?: string;\n\t/** Link target attribute (e.g., \"_blank\") */\n\ttarget?: string;\n\t/** Link rel attribute (e.g., \"noopener noreferrer\") */\n\trel?: string;\n}\n\nexport interface FlipPreviewerRef {\n\t/** Start the hover animation (only applies in \"hover\" mode) */\n\tstart: () => void;\n\t/** Pause the animation and reset to the first frame (only applies in \"hover\" mode) */\n\tpause: () => void;\n}\n\nexport interface FlipPreviewerProps {\n\t/**\n\t * How images cycle:\n\t * - `\"position\"` — image changes based on horizontal mouse/touch position (default)\n\t * - `\"hover\"` — images auto-cycle on a timer when hovered\n\t */\n\tmode?: \"position\" | \"hover\";\n\t/** Array of images to flip through */\n\timages: FlipPreviewerImage[];\n\t/** Width of the component (CSS value). Omit to fill parent at 100%. */\n\twidth?: number | string;\n\t/** Height of the component (CSS value). Omit to fill parent at 100%. */\n\theight?: number | string;\n\t/**\n\t * How images fit within the container:\n\t * - `\"cover\"` — image covers the entire container, may crop (default)\n\t * - `\"contain\"` — image fits entirely within the container, may letterbox\n\t */\n\tfit?: \"cover\" | \"contain\";\n\t/** Delay in ms between frame transitions — only used in \"hover\" mode. Omit for max speed (requestAnimationFrame). */\n\tdelay?: number;\n\t/** Start animating automatically without hover — only used in \"hover\" mode (default: false) */\n\tautoPlay?: boolean;\n\t/** Show a progress bar while images preload — only used in \"hover\" mode (default: true) */\n\tshowProgress?: boolean;\n\t/** Show a horizontal resize cursor when in \"position\" mode (default: true) */\n\tshowCursor?: boolean;\n\t/** Show debug overlay with mouse coordinates and position info — only used in \"position\" mode */\n\tdebug?: boolean;\n\t/** Additional CSS class name(s) for the container */\n\tclassName?: string;\n\t/** Additional inline styles for the container */\n\tstyle?: CSSProperties;\n\t/** Callback fired when the active image index changes */\n\tonIndexChange?: (index: number) => void;\n\t/** Callback fired when all images have finished preloading — only used in \"hover\" mode */\n\tonImagesLoaded?: () => void;\n\t/** Imperative handle ref for start/pause control in \"hover\" mode */\n\tref?: Ref<FlipPreviewerRef>;\n}\n\nexport function FlipPreviewer({\n\tmode = \"position\",\n\timages,\n\twidth,\n\theight,\n\tfit = \"cover\",\n\tdelay,\n\tautoPlay = false,\n\tshowProgress = true,\n\tshowCursor = true,\n\tdebug = false,\n\tclassName,\n\tstyle,\n\tonIndexChange,\n\tonImagesLoaded,\n\tref,\n}: FlipPreviewerProps) {\n\tconst containerRef = useRef<HTMLDivElement>(null);\n\tconst timerRef = useRef<ReturnType<typeof setTimeout> | number | null>(null);\n\n\tconst [activeIndex, setActiveIndex] = useState(0);\n\tconst [debugInfo, setDebugInfo] = useState(\"\");\n\tconst [loadedCount, setLoadedCount] = useState(0);\n\tconst [allLoaded, setAllLoaded] = useState(false);\n\n\tconst activeIndexRef = useRef(0);\n\tconst isPlayingRef = useRef(false);\n\n\t// ── Preload images (hover mode) ──────────────────────────────\n\tuseEffect(() => {\n\t\tif (mode !== \"hover\" || images.length === 0) {\n\t\t\tsetAllLoaded(true);\n\t\t\treturn;\n\t\t}\n\n\t\tlet loaded = 0;\n\t\tsetLoadedCount(0);\n\t\tsetAllLoaded(false);\n\n\t\tconst total = images.length;\n\t\timages.forEach(({ src }) => {\n\t\t\tconst img = new Image();\n\t\t\timg.onload = img.onerror = () => {\n\t\t\t\tloaded++;\n\t\t\t\tsetLoadedCount(loaded);\n\t\t\t\tif (loaded >= total) {\n\t\t\t\t\tsetAllLoaded(true);\n\t\t\t\t\tonImagesLoaded?.();\n\t\t\t\t}\n\t\t\t};\n\t\t\timg.src = src;\n\t\t});\n\t}, [mode, images, onImagesLoaded]);\n\n\t// ── Hover mode: animation ────────────────────────────────────\n\tconst clearTimer = useCallback(() => {\n\t\tif (timerRef.current !== null) {\n\t\t\tif (delay != null) {\n\t\t\t\tclearTimeout(timerRef.current as ReturnType<typeof setTimeout>);\n\t\t\t} else {\n\t\t\t\tcancelAnimationFrame(timerRef.current as number);\n\t\t\t}\n\t\t\ttimerRef.current = null;\n\t\t}\n\t}, [delay]);\n\n\tconst advanceFrame = useCallback(() => {\n\t\tif (!isPlayingRef.current) return;\n\t\tif (images.length <= 1) return;\n\n\t\tconst next =\n\t\t\tactiveIndexRef.current + 1 >= images.length\n\t\t\t\t? 0\n\t\t\t\t: activeIndexRef.current + 1;\n\n\t\tactiveIndexRef.current = next;\n\t\tsetActiveIndex(next);\n\t\tonIndexChange?.(next);\n\n\t\tif (delay != null) {\n\t\t\ttimerRef.current = setTimeout(advanceFrame, delay);\n\t\t} else {\n\t\t\ttimerRef.current = requestAnimationFrame(advanceFrame);\n\t\t}\n\t}, [images.length, delay, onIndexChange]);\n\n\tconst startAnimation = useCallback(() => {\n\t\tif (!allLoaded || images.length <= 1) return;\n\t\tisPlayingRef.current = true;\n\t\tclearTimer();\n\t\tif (delay != null) {\n\t\t\ttimerRef.current = setTimeout(advanceFrame, delay);\n\t\t} else {\n\t\t\ttimerRef.current = requestAnimationFrame(advanceFrame);\n\t\t}\n\t}, [allLoaded, images.length, delay, advanceFrame, clearTimer]);\n\n\tconst stopAnimation = useCallback(() => {\n\t\tisPlayingRef.current = false;\n\t\tclearTimer();\n\t\tactiveIndexRef.current = 0;\n\t\tsetActiveIndex(0);\n\t\tonIndexChange?.(0);\n\t}, [clearTimer, onIndexChange]);\n\n\t// Auto-play on mount if enabled (hover mode)\n\tuseEffect(() => {\n\t\tif (mode === \"hover\" && autoPlay && allLoaded) {\n\t\t\tstartAnimation();\n\t\t}\n\t\treturn () => clearTimer();\n\t}, [mode, autoPlay, allLoaded, startAnimation, clearTimer]);\n\n\t// Expose start/pause via ref (hover mode)\n\tuseImperativeHandle(\n\t\tref,\n\t\t() => ({\n\t\t\tstart: startAnimation,\n\t\t\tpause: stopAnimation,\n\t\t}),\n\t\t[startAnimation, stopAnimation],\n\t);\n\n\tconst handleMouseEnter = useCallback(() => {\n\t\tif (mode === \"hover\" && !autoPlay) {\n\t\t\tstartAnimation();\n\t\t}\n\t}, [mode, autoPlay, startAnimation]);\n\n\tconst handleMouseLeave = useCallback(() => {\n\t\tif (mode === \"hover\" && !autoPlay) {\n\t\t\tstopAnimation();\n\t\t}\n\t}, [mode, autoPlay, stopAnimation]);\n\n\t// ── Position mode: mouse-position-based ──────────────────────\n\tconst handlePointerMove = useCallback(\n\t\t(e: React.PointerEvent<HTMLDivElement>) => {\n\t\t\tif (mode !== \"position\") return;\n\n\t\t\tconst container = containerRef.current;\n\t\t\tif (!container || images.length === 0) return;\n\n\t\t\tconst rect = container.getBoundingClientRect();\n\t\t\tconst x = e.clientX - rect.left;\n\t\t\tlet pos = Math.floor((x / rect.width) * images.length);\n\t\t\tpos = Math.max(0, Math.min(pos, images.length - 1));\n\n\t\t\tif (pos !== activeIndexRef.current) {\n\t\t\t\tactiveIndexRef.current = pos;\n\t\t\t\tsetActiveIndex(pos);\n\t\t\t\tonIndexChange?.(pos);\n\t\t\t}\n\n\t\t\tif (debug) {\n\t\t\t\tconst y = e.clientY - rect.top;\n\t\t\t\tsetDebugInfo(\n\t\t\t\t\t`x:${Math.round(x)}, y:${Math.round(y)}, img:${pos + 1}/${images.length}, w:${Math.round(rect.width)}, h:${Math.round(rect.height)}`,\n\t\t\t\t);\n\t\t\t}\n\t\t},\n\t\t[mode, images.length, debug, onIndexChange],\n\t);\n\n\t// Reset index if images change\n\t// biome-ignore lint/correctness/useExhaustiveDependencies: intentional trigger when images change\n\tuseEffect(() => {\n\t\tsetActiveIndex(0);\n\t\tactiveIndexRef.current = 0;\n\t}, [images]);\n\n\t// ── Render ───────────────────────────────────────────────────\n\tif (images.length === 0) return null;\n\n\tconst activeImage = images[activeIndex];\n\tconst hasLink = !!activeImage.href;\n\n\tconst containerStyle: CSSProperties = {\n\t\tposition: \"relative\",\n\t\twidth: width ?? \"100%\",\n\t\theight: height ?? \"100%\",\n\t\toverflow: \"hidden\",\n\t\tcursor: hasLink\n\t\t\t? \"pointer\"\n\t\t\t: mode === \"position\" && showCursor\n\t\t\t\t? \"ew-resize\"\n\t\t\t\t: \"default\",\n\t\t...style,\n\t};\n\n\tconst imgElement = (\n\t\t<img\n\t\t\tsrc={activeImage.src}\n\t\t\talt={activeImage.alt ?? \"\"}\n\t\t\tclassName=\"cj-flip-previewer__img\"\n\t\t\tstyle={{ objectFit: fit }}\n\t\t\tdraggable={false}\n\t\t/>\n\t);\n\n\tconst content = hasLink ? (\n\t\t<a\n\t\t\thref={activeImage.href}\n\t\t\ttitle={activeImage.title}\n\t\t\ttarget={activeImage.target}\n\t\t\trel={activeImage.rel}\n\t\t\tclassName=\"cj-flip-previewer__link\"\n\t\t>\n\t\t\t{imgElement}\n\t\t</a>\n\t) : (\n\t\timgElement\n\t);\n\n\tconst progressPct =\n\t\timages.length > 0 ? (loadedCount / images.length) * 100 : 0;\n\n\treturn (\n\t\t// biome-ignore lint/a11y/noStaticElementInteractions: container tracks pointer position for image flipping\n\t\t<div\n\t\t\tref={containerRef}\n\t\t\tclassName={`cj-flip-previewer${className ? ` ${className}` : \"\"}`}\n\t\t\tstyle={containerStyle}\n\t\t\tonPointerMove={handlePointerMove}\n\t\t\tonMouseEnter={handleMouseEnter}\n\t\t\tonMouseLeave={handleMouseLeave}\n\t\t>\n\t\t\t{content}\n\n\t\t\t{mode === \"hover\" && showProgress && !allLoaded && (\n\t\t\t\t<div className=\"cj-flip-previewer__progress\">\n\t\t\t\t\t<div\n\t\t\t\t\t\tclassName=\"cj-flip-previewer__progress-bar\"\n\t\t\t\t\t\tstyle={{ width: `${progressPct}%` }}\n\t\t\t\t\t/>\n\t\t\t\t</div>\n\t\t\t)}\n\n\t\t\t{mode === \"position\" && debug && (\n\t\t\t\t<div className=\"cj-flip-previewer__debug\">{debugInfo}</div>\n\t\t\t)}\n\t\t</div>\n\t);\n}\n"],"mappings":"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","names":[]}
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
2
|
+
import { CSSProperties, Ref } from 'react';
|
|
3
|
+
|
|
4
|
+
interface FlipPreviewerImage {
|
|
5
|
+
/** Image source URL */
|
|
6
|
+
src: string;
|
|
7
|
+
/** Alt text for the image */
|
|
8
|
+
alt?: string;
|
|
9
|
+
/** Optional link URL — clicking the image navigates here */
|
|
10
|
+
href?: string;
|
|
11
|
+
/** Link title attribute */
|
|
12
|
+
title?: string;
|
|
13
|
+
/** Link target attribute (e.g., "_blank") */
|
|
14
|
+
target?: string;
|
|
15
|
+
/** Link rel attribute (e.g., "noopener noreferrer") */
|
|
16
|
+
rel?: string;
|
|
17
|
+
}
|
|
18
|
+
interface FlipPreviewerRef {
|
|
19
|
+
/** Start the hover animation (only applies in "hover" mode) */
|
|
20
|
+
start: () => void;
|
|
21
|
+
/** Pause the animation and reset to the first frame (only applies in "hover" mode) */
|
|
22
|
+
pause: () => void;
|
|
23
|
+
}
|
|
24
|
+
interface FlipPreviewerProps {
|
|
25
|
+
/**
|
|
26
|
+
* How images cycle:
|
|
27
|
+
* - `"position"` — image changes based on horizontal mouse/touch position (default)
|
|
28
|
+
* - `"hover"` — images auto-cycle on a timer when hovered
|
|
29
|
+
*/
|
|
30
|
+
mode?: "position" | "hover";
|
|
31
|
+
/** Array of images to flip through */
|
|
32
|
+
images: FlipPreviewerImage[];
|
|
33
|
+
/** Width of the component (CSS value). Omit to fill parent at 100%. */
|
|
34
|
+
width?: number | string;
|
|
35
|
+
/** Height of the component (CSS value). Omit to fill parent at 100%. */
|
|
36
|
+
height?: number | string;
|
|
37
|
+
/**
|
|
38
|
+
* How images fit within the container:
|
|
39
|
+
* - `"cover"` — image covers the entire container, may crop (default)
|
|
40
|
+
* - `"contain"` — image fits entirely within the container, may letterbox
|
|
41
|
+
*/
|
|
42
|
+
fit?: "cover" | "contain";
|
|
43
|
+
/** Delay in ms between frame transitions — only used in "hover" mode. Omit for max speed (requestAnimationFrame). */
|
|
44
|
+
delay?: number;
|
|
45
|
+
/** Start animating automatically without hover — only used in "hover" mode (default: false) */
|
|
46
|
+
autoPlay?: boolean;
|
|
47
|
+
/** Show a progress bar while images preload — only used in "hover" mode (default: true) */
|
|
48
|
+
showProgress?: boolean;
|
|
49
|
+
/** Show a horizontal resize cursor when in "position" mode (default: true) */
|
|
50
|
+
showCursor?: boolean;
|
|
51
|
+
/** Show debug overlay with mouse coordinates and position info — only used in "position" mode */
|
|
52
|
+
debug?: boolean;
|
|
53
|
+
/** Additional CSS class name(s) for the container */
|
|
54
|
+
className?: string;
|
|
55
|
+
/** Additional inline styles for the container */
|
|
56
|
+
style?: CSSProperties;
|
|
57
|
+
/** Callback fired when the active image index changes */
|
|
58
|
+
onIndexChange?: (index: number) => void;
|
|
59
|
+
/** Callback fired when all images have finished preloading — only used in "hover" mode */
|
|
60
|
+
onImagesLoaded?: () => void;
|
|
61
|
+
/** Imperative handle ref for start/pause control in "hover" mode */
|
|
62
|
+
ref?: Ref<FlipPreviewerRef>;
|
|
63
|
+
}
|
|
64
|
+
declare function FlipPreviewer({ mode, images, width, height, fit, delay, autoPlay, showProgress, showCursor, debug, className, style, onIndexChange, onImagesLoaded, ref, }: FlipPreviewerProps): react_jsx_runtime.JSX.Element | null;
|
|
65
|
+
|
|
66
|
+
export { FlipPreviewer, type FlipPreviewerImage, type FlipPreviewerProps, type FlipPreviewerRef };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
2
|
+
import { CSSProperties, Ref } from 'react';
|
|
3
|
+
|
|
4
|
+
interface FlipPreviewerImage {
|
|
5
|
+
/** Image source URL */
|
|
6
|
+
src: string;
|
|
7
|
+
/** Alt text for the image */
|
|
8
|
+
alt?: string;
|
|
9
|
+
/** Optional link URL — clicking the image navigates here */
|
|
10
|
+
href?: string;
|
|
11
|
+
/** Link title attribute */
|
|
12
|
+
title?: string;
|
|
13
|
+
/** Link target attribute (e.g., "_blank") */
|
|
14
|
+
target?: string;
|
|
15
|
+
/** Link rel attribute (e.g., "noopener noreferrer") */
|
|
16
|
+
rel?: string;
|
|
17
|
+
}
|
|
18
|
+
interface FlipPreviewerRef {
|
|
19
|
+
/** Start the hover animation (only applies in "hover" mode) */
|
|
20
|
+
start: () => void;
|
|
21
|
+
/** Pause the animation and reset to the first frame (only applies in "hover" mode) */
|
|
22
|
+
pause: () => void;
|
|
23
|
+
}
|
|
24
|
+
interface FlipPreviewerProps {
|
|
25
|
+
/**
|
|
26
|
+
* How images cycle:
|
|
27
|
+
* - `"position"` — image changes based on horizontal mouse/touch position (default)
|
|
28
|
+
* - `"hover"` — images auto-cycle on a timer when hovered
|
|
29
|
+
*/
|
|
30
|
+
mode?: "position" | "hover";
|
|
31
|
+
/** Array of images to flip through */
|
|
32
|
+
images: FlipPreviewerImage[];
|
|
33
|
+
/** Width of the component (CSS value). Omit to fill parent at 100%. */
|
|
34
|
+
width?: number | string;
|
|
35
|
+
/** Height of the component (CSS value). Omit to fill parent at 100%. */
|
|
36
|
+
height?: number | string;
|
|
37
|
+
/**
|
|
38
|
+
* How images fit within the container:
|
|
39
|
+
* - `"cover"` — image covers the entire container, may crop (default)
|
|
40
|
+
* - `"contain"` — image fits entirely within the container, may letterbox
|
|
41
|
+
*/
|
|
42
|
+
fit?: "cover" | "contain";
|
|
43
|
+
/** Delay in ms between frame transitions — only used in "hover" mode. Omit for max speed (requestAnimationFrame). */
|
|
44
|
+
delay?: number;
|
|
45
|
+
/** Start animating automatically without hover — only used in "hover" mode (default: false) */
|
|
46
|
+
autoPlay?: boolean;
|
|
47
|
+
/** Show a progress bar while images preload — only used in "hover" mode (default: true) */
|
|
48
|
+
showProgress?: boolean;
|
|
49
|
+
/** Show a horizontal resize cursor when in "position" mode (default: true) */
|
|
50
|
+
showCursor?: boolean;
|
|
51
|
+
/** Show debug overlay with mouse coordinates and position info — only used in "position" mode */
|
|
52
|
+
debug?: boolean;
|
|
53
|
+
/** Additional CSS class name(s) for the container */
|
|
54
|
+
className?: string;
|
|
55
|
+
/** Additional inline styles for the container */
|
|
56
|
+
style?: CSSProperties;
|
|
57
|
+
/** Callback fired when the active image index changes */
|
|
58
|
+
onIndexChange?: (index: number) => void;
|
|
59
|
+
/** Callback fired when all images have finished preloading — only used in "hover" mode */
|
|
60
|
+
onImagesLoaded?: () => void;
|
|
61
|
+
/** Imperative handle ref for start/pause control in "hover" mode */
|
|
62
|
+
ref?: Ref<FlipPreviewerRef>;
|
|
63
|
+
}
|
|
64
|
+
declare function FlipPreviewer({ mode, images, width, height, fit, delay, autoPlay, showProgress, showCursor, debug, className, style, onIndexChange, onImagesLoaded, ref, }: FlipPreviewerProps): react_jsx_runtime.JSX.Element | null;
|
|
65
|
+
|
|
66
|
+
export { FlipPreviewer, type FlipPreviewerImage, type FlipPreviewerProps, type FlipPreviewerRef };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
// src/FlipPreviewer.tsx
|
|
2
|
+
import {
|
|
3
|
+
useCallback,
|
|
4
|
+
useEffect,
|
|
5
|
+
useImperativeHandle,
|
|
6
|
+
useRef,
|
|
7
|
+
useState
|
|
8
|
+
} from "react";
|
|
9
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
10
|
+
function FlipPreviewer({
|
|
11
|
+
mode = "position",
|
|
12
|
+
images,
|
|
13
|
+
width,
|
|
14
|
+
height,
|
|
15
|
+
fit = "cover",
|
|
16
|
+
delay,
|
|
17
|
+
autoPlay = false,
|
|
18
|
+
showProgress = true,
|
|
19
|
+
showCursor = true,
|
|
20
|
+
debug = false,
|
|
21
|
+
className,
|
|
22
|
+
style,
|
|
23
|
+
onIndexChange,
|
|
24
|
+
onImagesLoaded,
|
|
25
|
+
ref
|
|
26
|
+
}) {
|
|
27
|
+
const containerRef = useRef(null);
|
|
28
|
+
const timerRef = useRef(null);
|
|
29
|
+
const [activeIndex, setActiveIndex] = useState(0);
|
|
30
|
+
const [debugInfo, setDebugInfo] = useState("");
|
|
31
|
+
const [loadedCount, setLoadedCount] = useState(0);
|
|
32
|
+
const [allLoaded, setAllLoaded] = useState(false);
|
|
33
|
+
const activeIndexRef = useRef(0);
|
|
34
|
+
const isPlayingRef = useRef(false);
|
|
35
|
+
useEffect(() => {
|
|
36
|
+
if (mode !== "hover" || images.length === 0) {
|
|
37
|
+
setAllLoaded(true);
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
let loaded = 0;
|
|
41
|
+
setLoadedCount(0);
|
|
42
|
+
setAllLoaded(false);
|
|
43
|
+
const total = images.length;
|
|
44
|
+
images.forEach(({ src }) => {
|
|
45
|
+
const img = new Image();
|
|
46
|
+
img.onload = img.onerror = () => {
|
|
47
|
+
loaded++;
|
|
48
|
+
setLoadedCount(loaded);
|
|
49
|
+
if (loaded >= total) {
|
|
50
|
+
setAllLoaded(true);
|
|
51
|
+
onImagesLoaded?.();
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
img.src = src;
|
|
55
|
+
});
|
|
56
|
+
}, [mode, images, onImagesLoaded]);
|
|
57
|
+
const clearTimer = useCallback(() => {
|
|
58
|
+
if (timerRef.current !== null) {
|
|
59
|
+
if (delay != null) {
|
|
60
|
+
clearTimeout(timerRef.current);
|
|
61
|
+
} else {
|
|
62
|
+
cancelAnimationFrame(timerRef.current);
|
|
63
|
+
}
|
|
64
|
+
timerRef.current = null;
|
|
65
|
+
}
|
|
66
|
+
}, [delay]);
|
|
67
|
+
const advanceFrame = useCallback(() => {
|
|
68
|
+
if (!isPlayingRef.current) return;
|
|
69
|
+
if (images.length <= 1) return;
|
|
70
|
+
const next = activeIndexRef.current + 1 >= images.length ? 0 : activeIndexRef.current + 1;
|
|
71
|
+
activeIndexRef.current = next;
|
|
72
|
+
setActiveIndex(next);
|
|
73
|
+
onIndexChange?.(next);
|
|
74
|
+
if (delay != null) {
|
|
75
|
+
timerRef.current = setTimeout(advanceFrame, delay);
|
|
76
|
+
} else {
|
|
77
|
+
timerRef.current = requestAnimationFrame(advanceFrame);
|
|
78
|
+
}
|
|
79
|
+
}, [images.length, delay, onIndexChange]);
|
|
80
|
+
const startAnimation = useCallback(() => {
|
|
81
|
+
if (!allLoaded || images.length <= 1) return;
|
|
82
|
+
isPlayingRef.current = true;
|
|
83
|
+
clearTimer();
|
|
84
|
+
if (delay != null) {
|
|
85
|
+
timerRef.current = setTimeout(advanceFrame, delay);
|
|
86
|
+
} else {
|
|
87
|
+
timerRef.current = requestAnimationFrame(advanceFrame);
|
|
88
|
+
}
|
|
89
|
+
}, [allLoaded, images.length, delay, advanceFrame, clearTimer]);
|
|
90
|
+
const stopAnimation = useCallback(() => {
|
|
91
|
+
isPlayingRef.current = false;
|
|
92
|
+
clearTimer();
|
|
93
|
+
activeIndexRef.current = 0;
|
|
94
|
+
setActiveIndex(0);
|
|
95
|
+
onIndexChange?.(0);
|
|
96
|
+
}, [clearTimer, onIndexChange]);
|
|
97
|
+
useEffect(() => {
|
|
98
|
+
if (mode === "hover" && autoPlay && allLoaded) {
|
|
99
|
+
startAnimation();
|
|
100
|
+
}
|
|
101
|
+
return () => clearTimer();
|
|
102
|
+
}, [mode, autoPlay, allLoaded, startAnimation, clearTimer]);
|
|
103
|
+
useImperativeHandle(
|
|
104
|
+
ref,
|
|
105
|
+
() => ({
|
|
106
|
+
start: startAnimation,
|
|
107
|
+
pause: stopAnimation
|
|
108
|
+
}),
|
|
109
|
+
[startAnimation, stopAnimation]
|
|
110
|
+
);
|
|
111
|
+
const handleMouseEnter = useCallback(() => {
|
|
112
|
+
if (mode === "hover" && !autoPlay) {
|
|
113
|
+
startAnimation();
|
|
114
|
+
}
|
|
115
|
+
}, [mode, autoPlay, startAnimation]);
|
|
116
|
+
const handleMouseLeave = useCallback(() => {
|
|
117
|
+
if (mode === "hover" && !autoPlay) {
|
|
118
|
+
stopAnimation();
|
|
119
|
+
}
|
|
120
|
+
}, [mode, autoPlay, stopAnimation]);
|
|
121
|
+
const handlePointerMove = useCallback(
|
|
122
|
+
(e) => {
|
|
123
|
+
if (mode !== "position") return;
|
|
124
|
+
const container = containerRef.current;
|
|
125
|
+
if (!container || images.length === 0) return;
|
|
126
|
+
const rect = container.getBoundingClientRect();
|
|
127
|
+
const x = e.clientX - rect.left;
|
|
128
|
+
let pos = Math.floor(x / rect.width * images.length);
|
|
129
|
+
pos = Math.max(0, Math.min(pos, images.length - 1));
|
|
130
|
+
if (pos !== activeIndexRef.current) {
|
|
131
|
+
activeIndexRef.current = pos;
|
|
132
|
+
setActiveIndex(pos);
|
|
133
|
+
onIndexChange?.(pos);
|
|
134
|
+
}
|
|
135
|
+
if (debug) {
|
|
136
|
+
const y = e.clientY - rect.top;
|
|
137
|
+
setDebugInfo(
|
|
138
|
+
`x:${Math.round(x)}, y:${Math.round(y)}, img:${pos + 1}/${images.length}, w:${Math.round(rect.width)}, h:${Math.round(rect.height)}`
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
},
|
|
142
|
+
[mode, images.length, debug, onIndexChange]
|
|
143
|
+
);
|
|
144
|
+
useEffect(() => {
|
|
145
|
+
setActiveIndex(0);
|
|
146
|
+
activeIndexRef.current = 0;
|
|
147
|
+
}, [images]);
|
|
148
|
+
if (images.length === 0) return null;
|
|
149
|
+
const activeImage = images[activeIndex];
|
|
150
|
+
const hasLink = !!activeImage.href;
|
|
151
|
+
const containerStyle = {
|
|
152
|
+
position: "relative",
|
|
153
|
+
width: width ?? "100%",
|
|
154
|
+
height: height ?? "100%",
|
|
155
|
+
overflow: "hidden",
|
|
156
|
+
cursor: hasLink ? "pointer" : mode === "position" && showCursor ? "ew-resize" : "default",
|
|
157
|
+
...style
|
|
158
|
+
};
|
|
159
|
+
const imgElement = /* @__PURE__ */ jsx(
|
|
160
|
+
"img",
|
|
161
|
+
{
|
|
162
|
+
src: activeImage.src,
|
|
163
|
+
alt: activeImage.alt ?? "",
|
|
164
|
+
className: "cj-flip-previewer__img",
|
|
165
|
+
style: { objectFit: fit },
|
|
166
|
+
draggable: false
|
|
167
|
+
}
|
|
168
|
+
);
|
|
169
|
+
const content = hasLink ? /* @__PURE__ */ jsx(
|
|
170
|
+
"a",
|
|
171
|
+
{
|
|
172
|
+
href: activeImage.href,
|
|
173
|
+
title: activeImage.title,
|
|
174
|
+
target: activeImage.target,
|
|
175
|
+
rel: activeImage.rel,
|
|
176
|
+
className: "cj-flip-previewer__link",
|
|
177
|
+
children: imgElement
|
|
178
|
+
}
|
|
179
|
+
) : imgElement;
|
|
180
|
+
const progressPct = images.length > 0 ? loadedCount / images.length * 100 : 0;
|
|
181
|
+
return (
|
|
182
|
+
// biome-ignore lint/a11y/noStaticElementInteractions: container tracks pointer position for image flipping
|
|
183
|
+
/* @__PURE__ */ jsxs(
|
|
184
|
+
"div",
|
|
185
|
+
{
|
|
186
|
+
ref: containerRef,
|
|
187
|
+
className: `cj-flip-previewer${className ? ` ${className}` : ""}`,
|
|
188
|
+
style: containerStyle,
|
|
189
|
+
onPointerMove: handlePointerMove,
|
|
190
|
+
onMouseEnter: handleMouseEnter,
|
|
191
|
+
onMouseLeave: handleMouseLeave,
|
|
192
|
+
children: [
|
|
193
|
+
content,
|
|
194
|
+
mode === "hover" && showProgress && !allLoaded && /* @__PURE__ */ jsx("div", { className: "cj-flip-previewer__progress", children: /* @__PURE__ */ jsx(
|
|
195
|
+
"div",
|
|
196
|
+
{
|
|
197
|
+
className: "cj-flip-previewer__progress-bar",
|
|
198
|
+
style: { width: `${progressPct}%` }
|
|
199
|
+
}
|
|
200
|
+
) }),
|
|
201
|
+
mode === "position" && debug && /* @__PURE__ */ jsx("div", { className: "cj-flip-previewer__debug", children: debugInfo })
|
|
202
|
+
]
|
|
203
|
+
}
|
|
204
|
+
)
|
|
205
|
+
);
|
|
206
|
+
}
|
|
207
|
+
export {
|
|
208
|
+
FlipPreviewer
|
|
209
|
+
};
|
|
210
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/FlipPreviewer.tsx"],"sourcesContent":["import {\n\ttype CSSProperties,\n\ttype Ref,\n\tuseCallback,\n\tuseEffect,\n\tuseImperativeHandle,\n\tuseRef,\n\tuseState,\n} from \"react\";\n\nexport interface FlipPreviewerImage {\n\t/** Image source URL */\n\tsrc: string;\n\t/** Alt text for the image */\n\talt?: string;\n\t/** Optional link URL — clicking the image navigates here */\n\thref?: string;\n\t/** Link title attribute */\n\ttitle?: string;\n\t/** Link target attribute (e.g., \"_blank\") */\n\ttarget?: string;\n\t/** Link rel attribute (e.g., \"noopener noreferrer\") */\n\trel?: string;\n}\n\nexport interface FlipPreviewerRef {\n\t/** Start the hover animation (only applies in \"hover\" mode) */\n\tstart: () => void;\n\t/** Pause the animation and reset to the first frame (only applies in \"hover\" mode) */\n\tpause: () => void;\n}\n\nexport interface FlipPreviewerProps {\n\t/**\n\t * How images cycle:\n\t * - `\"position\"` — image changes based on horizontal mouse/touch position (default)\n\t * - `\"hover\"` — images auto-cycle on a timer when hovered\n\t */\n\tmode?: \"position\" | \"hover\";\n\t/** Array of images to flip through */\n\timages: FlipPreviewerImage[];\n\t/** Width of the component (CSS value). Omit to fill parent at 100%. */\n\twidth?: number | string;\n\t/** Height of the component (CSS value). Omit to fill parent at 100%. */\n\theight?: number | string;\n\t/**\n\t * How images fit within the container:\n\t * - `\"cover\"` — image covers the entire container, may crop (default)\n\t * - `\"contain\"` — image fits entirely within the container, may letterbox\n\t */\n\tfit?: \"cover\" | \"contain\";\n\t/** Delay in ms between frame transitions — only used in \"hover\" mode. Omit for max speed (requestAnimationFrame). */\n\tdelay?: number;\n\t/** Start animating automatically without hover — only used in \"hover\" mode (default: false) */\n\tautoPlay?: boolean;\n\t/** Show a progress bar while images preload — only used in \"hover\" mode (default: true) */\n\tshowProgress?: boolean;\n\t/** Show a horizontal resize cursor when in \"position\" mode (default: true) */\n\tshowCursor?: boolean;\n\t/** Show debug overlay with mouse coordinates and position info — only used in \"position\" mode */\n\tdebug?: boolean;\n\t/** Additional CSS class name(s) for the container */\n\tclassName?: string;\n\t/** Additional inline styles for the container */\n\tstyle?: CSSProperties;\n\t/** Callback fired when the active image index changes */\n\tonIndexChange?: (index: number) => void;\n\t/** Callback fired when all images have finished preloading — only used in \"hover\" mode */\n\tonImagesLoaded?: () => void;\n\t/** Imperative handle ref for start/pause control in \"hover\" mode */\n\tref?: Ref<FlipPreviewerRef>;\n}\n\nexport function FlipPreviewer({\n\tmode = \"position\",\n\timages,\n\twidth,\n\theight,\n\tfit = \"cover\",\n\tdelay,\n\tautoPlay = false,\n\tshowProgress = true,\n\tshowCursor = true,\n\tdebug = false,\n\tclassName,\n\tstyle,\n\tonIndexChange,\n\tonImagesLoaded,\n\tref,\n}: FlipPreviewerProps) {\n\tconst containerRef = useRef<HTMLDivElement>(null);\n\tconst timerRef = useRef<ReturnType<typeof setTimeout> | number | null>(null);\n\n\tconst [activeIndex, setActiveIndex] = useState(0);\n\tconst [debugInfo, setDebugInfo] = useState(\"\");\n\tconst [loadedCount, setLoadedCount] = useState(0);\n\tconst [allLoaded, setAllLoaded] = useState(false);\n\n\tconst activeIndexRef = useRef(0);\n\tconst isPlayingRef = useRef(false);\n\n\t// ── Preload images (hover mode) ──────────────────────────────\n\tuseEffect(() => {\n\t\tif (mode !== \"hover\" || images.length === 0) {\n\t\t\tsetAllLoaded(true);\n\t\t\treturn;\n\t\t}\n\n\t\tlet loaded = 0;\n\t\tsetLoadedCount(0);\n\t\tsetAllLoaded(false);\n\n\t\tconst total = images.length;\n\t\timages.forEach(({ src }) => {\n\t\t\tconst img = new Image();\n\t\t\timg.onload = img.onerror = () => {\n\t\t\t\tloaded++;\n\t\t\t\tsetLoadedCount(loaded);\n\t\t\t\tif (loaded >= total) {\n\t\t\t\t\tsetAllLoaded(true);\n\t\t\t\t\tonImagesLoaded?.();\n\t\t\t\t}\n\t\t\t};\n\t\t\timg.src = src;\n\t\t});\n\t}, [mode, images, onImagesLoaded]);\n\n\t// ── Hover mode: animation ────────────────────────────────────\n\tconst clearTimer = useCallback(() => {\n\t\tif (timerRef.current !== null) {\n\t\t\tif (delay != null) {\n\t\t\t\tclearTimeout(timerRef.current as ReturnType<typeof setTimeout>);\n\t\t\t} else {\n\t\t\t\tcancelAnimationFrame(timerRef.current as number);\n\t\t\t}\n\t\t\ttimerRef.current = null;\n\t\t}\n\t}, [delay]);\n\n\tconst advanceFrame = useCallback(() => {\n\t\tif (!isPlayingRef.current) return;\n\t\tif (images.length <= 1) return;\n\n\t\tconst next =\n\t\t\tactiveIndexRef.current + 1 >= images.length\n\t\t\t\t? 0\n\t\t\t\t: activeIndexRef.current + 1;\n\n\t\tactiveIndexRef.current = next;\n\t\tsetActiveIndex(next);\n\t\tonIndexChange?.(next);\n\n\t\tif (delay != null) {\n\t\t\ttimerRef.current = setTimeout(advanceFrame, delay);\n\t\t} else {\n\t\t\ttimerRef.current = requestAnimationFrame(advanceFrame);\n\t\t}\n\t}, [images.length, delay, onIndexChange]);\n\n\tconst startAnimation = useCallback(() => {\n\t\tif (!allLoaded || images.length <= 1) return;\n\t\tisPlayingRef.current = true;\n\t\tclearTimer();\n\t\tif (delay != null) {\n\t\t\ttimerRef.current = setTimeout(advanceFrame, delay);\n\t\t} else {\n\t\t\ttimerRef.current = requestAnimationFrame(advanceFrame);\n\t\t}\n\t}, [allLoaded, images.length, delay, advanceFrame, clearTimer]);\n\n\tconst stopAnimation = useCallback(() => {\n\t\tisPlayingRef.current = false;\n\t\tclearTimer();\n\t\tactiveIndexRef.current = 0;\n\t\tsetActiveIndex(0);\n\t\tonIndexChange?.(0);\n\t}, [clearTimer, onIndexChange]);\n\n\t// Auto-play on mount if enabled (hover mode)\n\tuseEffect(() => {\n\t\tif (mode === \"hover\" && autoPlay && allLoaded) {\n\t\t\tstartAnimation();\n\t\t}\n\t\treturn () => clearTimer();\n\t}, [mode, autoPlay, allLoaded, startAnimation, clearTimer]);\n\n\t// Expose start/pause via ref (hover mode)\n\tuseImperativeHandle(\n\t\tref,\n\t\t() => ({\n\t\t\tstart: startAnimation,\n\t\t\tpause: stopAnimation,\n\t\t}),\n\t\t[startAnimation, stopAnimation],\n\t);\n\n\tconst handleMouseEnter = useCallback(() => {\n\t\tif (mode === \"hover\" && !autoPlay) {\n\t\t\tstartAnimation();\n\t\t}\n\t}, [mode, autoPlay, startAnimation]);\n\n\tconst handleMouseLeave = useCallback(() => {\n\t\tif (mode === \"hover\" && !autoPlay) {\n\t\t\tstopAnimation();\n\t\t}\n\t}, [mode, autoPlay, stopAnimation]);\n\n\t// ── Position mode: mouse-position-based ──────────────────────\n\tconst handlePointerMove = useCallback(\n\t\t(e: React.PointerEvent<HTMLDivElement>) => {\n\t\t\tif (mode !== \"position\") return;\n\n\t\t\tconst container = containerRef.current;\n\t\t\tif (!container || images.length === 0) return;\n\n\t\t\tconst rect = container.getBoundingClientRect();\n\t\t\tconst x = e.clientX - rect.left;\n\t\t\tlet pos = Math.floor((x / rect.width) * images.length);\n\t\t\tpos = Math.max(0, Math.min(pos, images.length - 1));\n\n\t\t\tif (pos !== activeIndexRef.current) {\n\t\t\t\tactiveIndexRef.current = pos;\n\t\t\t\tsetActiveIndex(pos);\n\t\t\t\tonIndexChange?.(pos);\n\t\t\t}\n\n\t\t\tif (debug) {\n\t\t\t\tconst y = e.clientY - rect.top;\n\t\t\t\tsetDebugInfo(\n\t\t\t\t\t`x:${Math.round(x)}, y:${Math.round(y)}, img:${pos + 1}/${images.length}, w:${Math.round(rect.width)}, h:${Math.round(rect.height)}`,\n\t\t\t\t);\n\t\t\t}\n\t\t},\n\t\t[mode, images.length, debug, onIndexChange],\n\t);\n\n\t// Reset index if images change\n\t// biome-ignore lint/correctness/useExhaustiveDependencies: intentional trigger when images change\n\tuseEffect(() => {\n\t\tsetActiveIndex(0);\n\t\tactiveIndexRef.current = 0;\n\t}, [images]);\n\n\t// ── Render ───────────────────────────────────────────────────\n\tif (images.length === 0) return null;\n\n\tconst activeImage = images[activeIndex];\n\tconst hasLink = !!activeImage.href;\n\n\tconst containerStyle: CSSProperties = {\n\t\tposition: \"relative\",\n\t\twidth: width ?? \"100%\",\n\t\theight: height ?? \"100%\",\n\t\toverflow: \"hidden\",\n\t\tcursor: hasLink\n\t\t\t? \"pointer\"\n\t\t\t: mode === \"position\" && showCursor\n\t\t\t\t? \"ew-resize\"\n\t\t\t\t: \"default\",\n\t\t...style,\n\t};\n\n\tconst imgElement = (\n\t\t<img\n\t\t\tsrc={activeImage.src}\n\t\t\talt={activeImage.alt ?? \"\"}\n\t\t\tclassName=\"cj-flip-previewer__img\"\n\t\t\tstyle={{ objectFit: fit }}\n\t\t\tdraggable={false}\n\t\t/>\n\t);\n\n\tconst content = hasLink ? (\n\t\t<a\n\t\t\thref={activeImage.href}\n\t\t\ttitle={activeImage.title}\n\t\t\ttarget={activeImage.target}\n\t\t\trel={activeImage.rel}\n\t\t\tclassName=\"cj-flip-previewer__link\"\n\t\t>\n\t\t\t{imgElement}\n\t\t</a>\n\t) : (\n\t\timgElement\n\t);\n\n\tconst progressPct =\n\t\timages.length > 0 ? (loadedCount / images.length) * 100 : 0;\n\n\treturn (\n\t\t// biome-ignore lint/a11y/noStaticElementInteractions: container tracks pointer position for image flipping\n\t\t<div\n\t\t\tref={containerRef}\n\t\t\tclassName={`cj-flip-previewer${className ? ` ${className}` : \"\"}`}\n\t\t\tstyle={containerStyle}\n\t\t\tonPointerMove={handlePointerMove}\n\t\t\tonMouseEnter={handleMouseEnter}\n\t\t\tonMouseLeave={handleMouseLeave}\n\t\t>\n\t\t\t{content}\n\n\t\t\t{mode === \"hover\" && showProgress && !allLoaded && (\n\t\t\t\t<div className=\"cj-flip-previewer__progress\">\n\t\t\t\t\t<div\n\t\t\t\t\t\tclassName=\"cj-flip-previewer__progress-bar\"\n\t\t\t\t\t\tstyle={{ width: `${progressPct}%` }}\n\t\t\t\t\t/>\n\t\t\t\t</div>\n\t\t\t)}\n\n\t\t\t{mode === \"position\" && debug && (\n\t\t\t\t<div className=\"cj-flip-previewer__debug\">{debugInfo}</div>\n\t\t\t)}\n\t\t</div>\n\t);\n}\n"],"mappings":";AAAA;AAAA,EAGC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACM;AAgQL,cA4BA,YA5BA;AA/LK,SAAS,cAAc;AAAA,EAC7B,OAAO;AAAA,EACP;AAAA,EACA;AAAA,EACA;AAAA,EACA,MAAM;AAAA,EACN;AAAA,EACA,WAAW;AAAA,EACX,eAAe;AAAA,EACf,aAAa;AAAA,EACb,QAAQ;AAAA,EACR;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACD,GAAuB;AACtB,QAAM,eAAe,OAAuB,IAAI;AAChD,QAAM,WAAW,OAAsD,IAAI;AAE3E,QAAM,CAAC,aAAa,cAAc,IAAI,SAAS,CAAC;AAChD,QAAM,CAAC,WAAW,YAAY,IAAI,SAAS,EAAE;AAC7C,QAAM,CAAC,aAAa,cAAc,IAAI,SAAS,CAAC;AAChD,QAAM,CAAC,WAAW,YAAY,IAAI,SAAS,KAAK;AAEhD,QAAM,iBAAiB,OAAO,CAAC;AAC/B,QAAM,eAAe,OAAO,KAAK;AAGjC,YAAU,MAAM;AACf,QAAI,SAAS,WAAW,OAAO,WAAW,GAAG;AAC5C,mBAAa,IAAI;AACjB;AAAA,IACD;AAEA,QAAI,SAAS;AACb,mBAAe,CAAC;AAChB,iBAAa,KAAK;AAElB,UAAM,QAAQ,OAAO;AACrB,WAAO,QAAQ,CAAC,EAAE,IAAI,MAAM;AAC3B,YAAM,MAAM,IAAI,MAAM;AACtB,UAAI,SAAS,IAAI,UAAU,MAAM;AAChC;AACA,uBAAe,MAAM;AACrB,YAAI,UAAU,OAAO;AACpB,uBAAa,IAAI;AACjB,2BAAiB;AAAA,QAClB;AAAA,MACD;AACA,UAAI,MAAM;AAAA,IACX,CAAC;AAAA,EACF,GAAG,CAAC,MAAM,QAAQ,cAAc,CAAC;AAGjC,QAAM,aAAa,YAAY,MAAM;AACpC,QAAI,SAAS,YAAY,MAAM;AAC9B,UAAI,SAAS,MAAM;AAClB,qBAAa,SAAS,OAAwC;AAAA,MAC/D,OAAO;AACN,6BAAqB,SAAS,OAAiB;AAAA,MAChD;AACA,eAAS,UAAU;AAAA,IACpB;AAAA,EACD,GAAG,CAAC,KAAK,CAAC;AAEV,QAAM,eAAe,YAAY,MAAM;AACtC,QAAI,CAAC,aAAa,QAAS;AAC3B,QAAI,OAAO,UAAU,EAAG;AAExB,UAAM,OACL,eAAe,UAAU,KAAK,OAAO,SAClC,IACA,eAAe,UAAU;AAE7B,mBAAe,UAAU;AACzB,mBAAe,IAAI;AACnB,oBAAgB,IAAI;AAEpB,QAAI,SAAS,MAAM;AAClB,eAAS,UAAU,WAAW,cAAc,KAAK;AAAA,IAClD,OAAO;AACN,eAAS,UAAU,sBAAsB,YAAY;AAAA,IACtD;AAAA,EACD,GAAG,CAAC,OAAO,QAAQ,OAAO,aAAa,CAAC;AAExC,QAAM,iBAAiB,YAAY,MAAM;AACxC,QAAI,CAAC,aAAa,OAAO,UAAU,EAAG;AACtC,iBAAa,UAAU;AACvB,eAAW;AACX,QAAI,SAAS,MAAM;AAClB,eAAS,UAAU,WAAW,cAAc,KAAK;AAAA,IAClD,OAAO;AACN,eAAS,UAAU,sBAAsB,YAAY;AAAA,IACtD;AAAA,EACD,GAAG,CAAC,WAAW,OAAO,QAAQ,OAAO,cAAc,UAAU,CAAC;AAE9D,QAAM,gBAAgB,YAAY,MAAM;AACvC,iBAAa,UAAU;AACvB,eAAW;AACX,mBAAe,UAAU;AACzB,mBAAe,CAAC;AAChB,oBAAgB,CAAC;AAAA,EAClB,GAAG,CAAC,YAAY,aAAa,CAAC;AAG9B,YAAU,MAAM;AACf,QAAI,SAAS,WAAW,YAAY,WAAW;AAC9C,qBAAe;AAAA,IAChB;AACA,WAAO,MAAM,WAAW;AAAA,EACzB,GAAG,CAAC,MAAM,UAAU,WAAW,gBAAgB,UAAU,CAAC;AAG1D;AAAA,IACC;AAAA,IACA,OAAO;AAAA,MACN,OAAO;AAAA,MACP,OAAO;AAAA,IACR;AAAA,IACA,CAAC,gBAAgB,aAAa;AAAA,EAC/B;AAEA,QAAM,mBAAmB,YAAY,MAAM;AAC1C,QAAI,SAAS,WAAW,CAAC,UAAU;AAClC,qBAAe;AAAA,IAChB;AAAA,EACD,GAAG,CAAC,MAAM,UAAU,cAAc,CAAC;AAEnC,QAAM,mBAAmB,YAAY,MAAM;AAC1C,QAAI,SAAS,WAAW,CAAC,UAAU;AAClC,oBAAc;AAAA,IACf;AAAA,EACD,GAAG,CAAC,MAAM,UAAU,aAAa,CAAC;AAGlC,QAAM,oBAAoB;AAAA,IACzB,CAAC,MAA0C;AAC1C,UAAI,SAAS,WAAY;AAEzB,YAAM,YAAY,aAAa;AAC/B,UAAI,CAAC,aAAa,OAAO,WAAW,EAAG;AAEvC,YAAM,OAAO,UAAU,sBAAsB;AAC7C,YAAM,IAAI,EAAE,UAAU,KAAK;AAC3B,UAAI,MAAM,KAAK,MAAO,IAAI,KAAK,QAAS,OAAO,MAAM;AACrD,YAAM,KAAK,IAAI,GAAG,KAAK,IAAI,KAAK,OAAO,SAAS,CAAC,CAAC;AAElD,UAAI,QAAQ,eAAe,SAAS;AACnC,uBAAe,UAAU;AACzB,uBAAe,GAAG;AAClB,wBAAgB,GAAG;AAAA,MACpB;AAEA,UAAI,OAAO;AACV,cAAM,IAAI,EAAE,UAAU,KAAK;AAC3B;AAAA,UACC,KAAK,KAAK,MAAM,CAAC,CAAC,OAAO,KAAK,MAAM,CAAC,CAAC,SAAS,MAAM,CAAC,IAAI,OAAO,MAAM,OAAO,KAAK,MAAM,KAAK,KAAK,CAAC,OAAO,KAAK,MAAM,KAAK,MAAM,CAAC;AAAA,QACnI;AAAA,MACD;AAAA,IACD;AAAA,IACA,CAAC,MAAM,OAAO,QAAQ,OAAO,aAAa;AAAA,EAC3C;AAIA,YAAU,MAAM;AACf,mBAAe,CAAC;AAChB,mBAAe,UAAU;AAAA,EAC1B,GAAG,CAAC,MAAM,CAAC;AAGX,MAAI,OAAO,WAAW,EAAG,QAAO;AAEhC,QAAM,cAAc,OAAO,WAAW;AACtC,QAAM,UAAU,CAAC,CAAC,YAAY;AAE9B,QAAM,iBAAgC;AAAA,IACrC,UAAU;AAAA,IACV,OAAO,SAAS;AAAA,IAChB,QAAQ,UAAU;AAAA,IAClB,UAAU;AAAA,IACV,QAAQ,UACL,YACA,SAAS,cAAc,aACtB,cACA;AAAA,IACJ,GAAG;AAAA,EACJ;AAEA,QAAM,aACL;AAAA,IAAC;AAAA;AAAA,MACA,KAAK,YAAY;AAAA,MACjB,KAAK,YAAY,OAAO;AAAA,MACxB,WAAU;AAAA,MACV,OAAO,EAAE,WAAW,IAAI;AAAA,MACxB,WAAW;AAAA;AAAA,EACZ;AAGD,QAAM,UAAU,UACf;AAAA,IAAC;AAAA;AAAA,MACA,MAAM,YAAY;AAAA,MAClB,OAAO,YAAY;AAAA,MACnB,QAAQ,YAAY;AAAA,MACpB,KAAK,YAAY;AAAA,MACjB,WAAU;AAAA,MAET;AAAA;AAAA,EACF,IAEA;AAGD,QAAM,cACL,OAAO,SAAS,IAAK,cAAc,OAAO,SAAU,MAAM;AAE3D;AAAA;AAAA,IAEC;AAAA,MAAC;AAAA;AAAA,QACA,KAAK;AAAA,QACL,WAAW,oBAAoB,YAAY,IAAI,SAAS,KAAK,EAAE;AAAA,QAC/D,OAAO;AAAA,QACP,eAAe;AAAA,QACf,cAAc;AAAA,QACd,cAAc;AAAA,QAEb;AAAA;AAAA,UAEA,SAAS,WAAW,gBAAgB,CAAC,aACrC,oBAAC,SAAI,WAAU,+BACd;AAAA,YAAC;AAAA;AAAA,cACA,WAAU;AAAA,cACV,OAAO,EAAE,OAAO,GAAG,WAAW,IAAI;AAAA;AAAA,UACnC,GACD;AAAA,UAGA,SAAS,cAAc,SACvB,oBAAC,SAAI,WAAU,4BAA4B,qBAAU;AAAA;AAAA;AAAA,IAEvD;AAAA;AAEF;","names":[]}
|
package/dist/server.cjs
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/server.ts
|
|
21
|
+
var server_exports = {};
|
|
22
|
+
__export(server_exports, {
|
|
23
|
+
getImagesFromDirectory: () => getImagesFromDirectory
|
|
24
|
+
});
|
|
25
|
+
module.exports = __toCommonJS(server_exports);
|
|
26
|
+
var import_promises = require("fs/promises");
|
|
27
|
+
var import_node_path = require("path");
|
|
28
|
+
var IMAGE_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
29
|
+
".jpg",
|
|
30
|
+
".jpeg",
|
|
31
|
+
".png",
|
|
32
|
+
".gif",
|
|
33
|
+
".webp",
|
|
34
|
+
".avif",
|
|
35
|
+
".svg",
|
|
36
|
+
".bmp",
|
|
37
|
+
".ico"
|
|
38
|
+
]);
|
|
39
|
+
async function getImagesFromDirectory(fsPath, urlPrefix) {
|
|
40
|
+
const entries = await (0, import_promises.readdir)(fsPath, { withFileTypes: true });
|
|
41
|
+
const images = [];
|
|
42
|
+
for (const entry of entries) {
|
|
43
|
+
if (!entry.isFile()) continue;
|
|
44
|
+
const ext = (0, import_node_path.extname)(entry.name).toLowerCase();
|
|
45
|
+
if (!IMAGE_EXTENSIONS.has(ext)) continue;
|
|
46
|
+
const src = import_node_path.posix.join(
|
|
47
|
+
urlPrefix.endsWith("/") ? urlPrefix : urlPrefix + "/",
|
|
48
|
+
entry.name
|
|
49
|
+
);
|
|
50
|
+
images.push({ src, alt: entry.name });
|
|
51
|
+
}
|
|
52
|
+
images.sort((a, b) => a.src.localeCompare(b.src));
|
|
53
|
+
return images;
|
|
54
|
+
}
|
|
55
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
56
|
+
0 && (module.exports = {
|
|
57
|
+
getImagesFromDirectory
|
|
58
|
+
});
|
|
59
|
+
//# sourceMappingURL=server.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/server.ts"],"sourcesContent":["import { readdir } from \"node:fs/promises\";\nimport { extname, join, posix } from \"node:path\";\nimport type { FlipPreviewerImage } from \"./FlipPreviewer\";\n\nconst IMAGE_EXTENSIONS = new Set([\n \".jpg\",\n \".jpeg\",\n \".png\",\n \".gif\",\n \".webp\",\n \".avif\",\n \".svg\",\n \".bmp\",\n \".ico\",\n]);\n\n/**\n * Reads a directory on the server and returns a sorted array of\n * `FlipPreviewerImage` objects suitable for passing to `<FlipPreviewer>`.\n *\n * @param fsPath - Absolute or relative filesystem path to the image directory\n * @param urlPrefix - URL path prefix for the images (e.g., \"/photos\")\n */\nexport async function getImagesFromDirectory(\n fsPath: string,\n urlPrefix: string\n): Promise<FlipPreviewerImage[]> {\n const entries = await readdir(fsPath, { withFileTypes: true });\n\n const images: FlipPreviewerImage[] = [];\n\n for (const entry of entries) {\n if (!entry.isFile()) continue;\n const ext = extname(entry.name).toLowerCase();\n if (!IMAGE_EXTENSIONS.has(ext)) continue;\n\n const src = posix.join(\n urlPrefix.endsWith(\"/\") ? urlPrefix : urlPrefix + \"/\",\n entry.name\n );\n\n images.push({ src, alt: entry.name });\n }\n\n images.sort((a, b) => a.src.localeCompare(b.src));\n\n return images;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,sBAAwB;AACxB,uBAAqC;AAGrC,IAAM,mBAAmB,oBAAI,IAAI;AAAA,EAC/B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,CAAC;AASD,eAAsB,uBACpB,QACA,WAC+B;AAC/B,QAAM,UAAU,UAAM,yBAAQ,QAAQ,EAAE,eAAe,KAAK,CAAC;AAE7D,QAAM,SAA+B,CAAC;AAEtC,aAAW,SAAS,SAAS;AAC3B,QAAI,CAAC,MAAM,OAAO,EAAG;AACrB,UAAM,UAAM,0BAAQ,MAAM,IAAI,EAAE,YAAY;AAC5C,QAAI,CAAC,iBAAiB,IAAI,GAAG,EAAG;AAEhC,UAAM,MAAM,uBAAM;AAAA,MAChB,UAAU,SAAS,GAAG,IAAI,YAAY,YAAY;AAAA,MAClD,MAAM;AAAA,IACR;AAEA,WAAO,KAAK,EAAE,KAAK,KAAK,MAAM,KAAK,CAAC;AAAA,EACtC;AAEA,SAAO,KAAK,CAAC,GAAG,MAAM,EAAE,IAAI,cAAc,EAAE,GAAG,CAAC;AAEhD,SAAO;AACT;","names":[]}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { FlipPreviewerImage } from './index.cjs';
|
|
2
|
+
import 'react/jsx-runtime';
|
|
3
|
+
import 'react';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Reads a directory on the server and returns a sorted array of
|
|
7
|
+
* `FlipPreviewerImage` objects suitable for passing to `<FlipPreviewer>`.
|
|
8
|
+
*
|
|
9
|
+
* @param fsPath - Absolute or relative filesystem path to the image directory
|
|
10
|
+
* @param urlPrefix - URL path prefix for the images (e.g., "/photos")
|
|
11
|
+
*/
|
|
12
|
+
declare function getImagesFromDirectory(fsPath: string, urlPrefix: string): Promise<FlipPreviewerImage[]>;
|
|
13
|
+
|
|
14
|
+
export { getImagesFromDirectory };
|
package/dist/server.d.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { FlipPreviewerImage } from './index.js';
|
|
2
|
+
import 'react/jsx-runtime';
|
|
3
|
+
import 'react';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Reads a directory on the server and returns a sorted array of
|
|
7
|
+
* `FlipPreviewerImage` objects suitable for passing to `<FlipPreviewer>`.
|
|
8
|
+
*
|
|
9
|
+
* @param fsPath - Absolute or relative filesystem path to the image directory
|
|
10
|
+
* @param urlPrefix - URL path prefix for the images (e.g., "/photos")
|
|
11
|
+
*/
|
|
12
|
+
declare function getImagesFromDirectory(fsPath: string, urlPrefix: string): Promise<FlipPreviewerImage[]>;
|
|
13
|
+
|
|
14
|
+
export { getImagesFromDirectory };
|
package/dist/server.js
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
// src/server.ts
|
|
2
|
+
import { readdir } from "fs/promises";
|
|
3
|
+
import { extname, posix } from "path";
|
|
4
|
+
var IMAGE_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
5
|
+
".jpg",
|
|
6
|
+
".jpeg",
|
|
7
|
+
".png",
|
|
8
|
+
".gif",
|
|
9
|
+
".webp",
|
|
10
|
+
".avif",
|
|
11
|
+
".svg",
|
|
12
|
+
".bmp",
|
|
13
|
+
".ico"
|
|
14
|
+
]);
|
|
15
|
+
async function getImagesFromDirectory(fsPath, urlPrefix) {
|
|
16
|
+
const entries = await readdir(fsPath, { withFileTypes: true });
|
|
17
|
+
const images = [];
|
|
18
|
+
for (const entry of entries) {
|
|
19
|
+
if (!entry.isFile()) continue;
|
|
20
|
+
const ext = extname(entry.name).toLowerCase();
|
|
21
|
+
if (!IMAGE_EXTENSIONS.has(ext)) continue;
|
|
22
|
+
const src = posix.join(
|
|
23
|
+
urlPrefix.endsWith("/") ? urlPrefix : urlPrefix + "/",
|
|
24
|
+
entry.name
|
|
25
|
+
);
|
|
26
|
+
images.push({ src, alt: entry.name });
|
|
27
|
+
}
|
|
28
|
+
images.sort((a, b) => a.src.localeCompare(b.src));
|
|
29
|
+
return images;
|
|
30
|
+
}
|
|
31
|
+
export {
|
|
32
|
+
getImagesFromDirectory
|
|
33
|
+
};
|
|
34
|
+
//# sourceMappingURL=server.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/server.ts"],"sourcesContent":["import { readdir } from \"node:fs/promises\";\nimport { extname, join, posix } from \"node:path\";\nimport type { FlipPreviewerImage } from \"./FlipPreviewer\";\n\nconst IMAGE_EXTENSIONS = new Set([\n \".jpg\",\n \".jpeg\",\n \".png\",\n \".gif\",\n \".webp\",\n \".avif\",\n \".svg\",\n \".bmp\",\n \".ico\",\n]);\n\n/**\n * Reads a directory on the server and returns a sorted array of\n * `FlipPreviewerImage` objects suitable for passing to `<FlipPreviewer>`.\n *\n * @param fsPath - Absolute or relative filesystem path to the image directory\n * @param urlPrefix - URL path prefix for the images (e.g., \"/photos\")\n */\nexport async function getImagesFromDirectory(\n fsPath: string,\n urlPrefix: string\n): Promise<FlipPreviewerImage[]> {\n const entries = await readdir(fsPath, { withFileTypes: true });\n\n const images: FlipPreviewerImage[] = [];\n\n for (const entry of entries) {\n if (!entry.isFile()) continue;\n const ext = extname(entry.name).toLowerCase();\n if (!IMAGE_EXTENSIONS.has(ext)) continue;\n\n const src = posix.join(\n urlPrefix.endsWith(\"/\") ? urlPrefix : urlPrefix + \"/\",\n entry.name\n );\n\n images.push({ src, alt: entry.name });\n }\n\n images.sort((a, b) => a.src.localeCompare(b.src));\n\n return images;\n}\n"],"mappings":";AAAA,SAAS,eAAe;AACxB,SAAS,SAAe,aAAa;AAGrC,IAAM,mBAAmB,oBAAI,IAAI;AAAA,EAC/B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,CAAC;AASD,eAAsB,uBACpB,QACA,WAC+B;AAC/B,QAAM,UAAU,MAAM,QAAQ,QAAQ,EAAE,eAAe,KAAK,CAAC;AAE7D,QAAM,SAA+B,CAAC;AAEtC,aAAW,SAAS,SAAS;AAC3B,QAAI,CAAC,MAAM,OAAO,EAAG;AACrB,UAAM,MAAM,QAAQ,MAAM,IAAI,EAAE,YAAY;AAC5C,QAAI,CAAC,iBAAiB,IAAI,GAAG,EAAG;AAEhC,UAAM,MAAM,MAAM;AAAA,MAChB,UAAU,SAAS,GAAG,IAAI,YAAY,YAAY;AAAA,MAClD,MAAM;AAAA,IACR;AAEA,WAAO,KAAK,EAAE,KAAK,KAAK,MAAM,KAAK,CAAC;AAAA,EACtC;AAEA,SAAO,KAAK,CAAC,GAAG,MAAM,EAAE,IAAI,cAAc,EAAE,GAAG,CAAC;AAEhD,SAAO;AACT;","names":[]}
|
package/dist/styles.css
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/* src/styles.css */
|
|
2
|
+
@layer components {
|
|
3
|
+
.cj-flip-previewer {
|
|
4
|
+
display: inline-block;
|
|
5
|
+
user-select: none;
|
|
6
|
+
touch-action: none;
|
|
7
|
+
}
|
|
8
|
+
.cj-flip-previewer__img {
|
|
9
|
+
display: block;
|
|
10
|
+
width: 100%;
|
|
11
|
+
height: 100%;
|
|
12
|
+
object-fit: cover;
|
|
13
|
+
pointer-events: none;
|
|
14
|
+
}
|
|
15
|
+
.cj-flip-previewer__link {
|
|
16
|
+
display: block;
|
|
17
|
+
width: 100%;
|
|
18
|
+
height: 100%;
|
|
19
|
+
}
|
|
20
|
+
.cj-flip-previewer__debug {
|
|
21
|
+
position: absolute;
|
|
22
|
+
top: 2px;
|
|
23
|
+
left: 2px;
|
|
24
|
+
font-family: monospace;
|
|
25
|
+
font-size: var(--cj-flip-debug-font-size, 11px);
|
|
26
|
+
background: var(--cj-flip-debug-bg, rgba(0, 0, 0, 0.6));
|
|
27
|
+
color: var(--cj-flip-debug-color, #fff);
|
|
28
|
+
padding: 2px 6px;
|
|
29
|
+
border-radius: 3px;
|
|
30
|
+
pointer-events: none;
|
|
31
|
+
z-index: 10;
|
|
32
|
+
}
|
|
33
|
+
.cj-flip-previewer__progress {
|
|
34
|
+
position: absolute;
|
|
35
|
+
top: 0;
|
|
36
|
+
left: 0;
|
|
37
|
+
width: 100%;
|
|
38
|
+
height: var(--cj-flip-progress-height, 4px);
|
|
39
|
+
background: var(--cj-flip-progress-bg, rgba(0, 0, 0, 0.3));
|
|
40
|
+
z-index: 20;
|
|
41
|
+
overflow: hidden;
|
|
42
|
+
}
|
|
43
|
+
.cj-flip-previewer__progress-bar {
|
|
44
|
+
height: 100%;
|
|
45
|
+
background: var(--cj-flip-progress-color, #6bc4f7);
|
|
46
|
+
transition: width var(--cj-flip-progress-speed, 0.2s) ease;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
/*# sourceMappingURL=styles.css.map */
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/styles.css"],"sourcesContent":["@layer components {\n .cj-flip-previewer {\n display: inline-block;\n user-select: none;\n touch-action: none;\n }\n\n .cj-flip-previewer__img {\n display: block;\n width: 100%;\n height: 100%;\n object-fit: cover;\n pointer-events: none;\n }\n\n .cj-flip-previewer__link {\n display: block;\n width: 100%;\n height: 100%;\n }\n\n .cj-flip-previewer__debug {\n position: absolute;\n top: 2px;\n left: 2px;\n font-family: monospace;\n font-size: var(--cj-flip-debug-font-size, 11px);\n background: var(--cj-flip-debug-bg, rgba(0, 0, 0, 0.6));\n color: var(--cj-flip-debug-color, #fff);\n padding: 2px 6px;\n border-radius: 3px;\n pointer-events: none;\n z-index: 10;\n }\n\n .cj-flip-previewer__progress {\n position: absolute;\n top: 0;\n left: 0;\n width: 100%;\n height: var(--cj-flip-progress-height, 4px);\n background: var(--cj-flip-progress-bg, rgba(0, 0, 0, 0.3));\n z-index: 20;\n overflow: hidden;\n }\n\n .cj-flip-previewer__progress-bar {\n height: 100%;\n background: var(--cj-flip-progress-color, #6bc4f7);\n transition: width var(--cj-flip-progress-speed, 0.2s) ease;\n }\n}\n"],"mappings":";AAAA;AACE,GAAC;AACC,aAAS;AACT,iBAAa;AACb,kBAAc;AAChB;AAEA,GAAC;AACC,aAAS;AACT,WAAO;AACP,YAAQ;AACR,gBAAY;AACZ,oBAAgB;AAClB;AAEA,GAAC;AACC,aAAS;AACT,WAAO;AACP,YAAQ;AACV;AAEA,GAAC;AACC,cAAU;AACV,SAAK;AACL,UAAM;AACN,iBAAa;AACb,eAAW,IAAI,yBAAyB,EAAE;AAC1C,gBAAY,IAAI,kBAAkB,EAAE,KAAK,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE;AAClD,WAAO,IAAI,qBAAqB,EAAE;AAClC,aAAS,IAAI;AACb,mBAAe;AACf,oBAAgB;AAChB,aAAS;AACX;AAEA,GAAC;AACC,cAAU;AACV,SAAK;AACL,UAAM;AACN,WAAO;AACP,YAAQ,IAAI,yBAAyB,EAAE;AACvC,gBAAY,IAAI,qBAAqB,EAAE,KAAK,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE;AACrD,aAAS;AACT,cAAU;AACZ;AAEA,GAAC;AACC,YAAQ;AACR,gBAAY,IAAI,wBAAwB,EAAE;AAC1C,gBAAY,MAAM,IAAI,wBAAwB,EAAE,MAAM;AACxD;AACF;","names":[]}
|
package/package.json
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@cjboco/cj-image-flip-previewer",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "React components for interactive image previews — flip through images by mouse position (FlipPreviewer) or animate frames on hover like a video previewer (VideoPreviewer).",
|
|
5
|
+
"author": "Doug Jones",
|
|
6
|
+
"license": "BSD-3-Clause",
|
|
7
|
+
"keywords": [
|
|
8
|
+
"react",
|
|
9
|
+
"image",
|
|
10
|
+
"flipbox",
|
|
11
|
+
"video-previewer",
|
|
12
|
+
"360",
|
|
13
|
+
"product-view",
|
|
14
|
+
"interactive",
|
|
15
|
+
"mouseover",
|
|
16
|
+
"hover",
|
|
17
|
+
"thumbnail",
|
|
18
|
+
"animation"
|
|
19
|
+
],
|
|
20
|
+
"type": "module",
|
|
21
|
+
"main": "./dist/index.cjs",
|
|
22
|
+
"module": "./dist/index.js",
|
|
23
|
+
"types": "./dist/index.d.ts",
|
|
24
|
+
"exports": {
|
|
25
|
+
".": {
|
|
26
|
+
"import": {
|
|
27
|
+
"types": "./dist/index.d.ts",
|
|
28
|
+
"default": "./dist/index.js"
|
|
29
|
+
},
|
|
30
|
+
"require": {
|
|
31
|
+
"types": "./dist/index.d.cts",
|
|
32
|
+
"default": "./dist/index.cjs"
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
"./server": {
|
|
36
|
+
"import": {
|
|
37
|
+
"types": "./dist/server.d.ts",
|
|
38
|
+
"default": "./dist/server.js"
|
|
39
|
+
},
|
|
40
|
+
"require": {
|
|
41
|
+
"types": "./dist/server.d.cts",
|
|
42
|
+
"default": "./dist/server.cjs"
|
|
43
|
+
}
|
|
44
|
+
},
|
|
45
|
+
"./styles.css": "./dist/styles.css"
|
|
46
|
+
},
|
|
47
|
+
"publishConfig": {
|
|
48
|
+
"access": "public"
|
|
49
|
+
},
|
|
50
|
+
"files": [
|
|
51
|
+
"dist"
|
|
52
|
+
],
|
|
53
|
+
"scripts": {
|
|
54
|
+
"build": "tsup",
|
|
55
|
+
"dev": "tsup --watch",
|
|
56
|
+
"prepublishOnly": "npm run build"
|
|
57
|
+
},
|
|
58
|
+
"peerDependencies": {
|
|
59
|
+
"react": ">=19.0.0",
|
|
60
|
+
"react-dom": ">=19.0.0"
|
|
61
|
+
},
|
|
62
|
+
"devDependencies": {
|
|
63
|
+
"@types/node": "^25.9.1",
|
|
64
|
+
"@types/react": "^19.0.0",
|
|
65
|
+
"@types/react-dom": "^19.0.0",
|
|
66
|
+
"react": "^19.0.0",
|
|
67
|
+
"react-dom": "^19.0.0",
|
|
68
|
+
"tsup": "^8.0.0",
|
|
69
|
+
"typescript": "^5.7.0"
|
|
70
|
+
}
|
|
71
|
+
}
|