@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 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
+ [![npm version](https://img.shields.io/npm/v/cj-image-flip-previewer)](https://www.npmjs.com/package/cj-image-flip-previewer)
4
+ [![license](https://img.shields.io/npm/l/cj-image-flip-previewer)](https://github.com/cjboco/cj-image-flip-previewer/blob/master/LICENSE)
5
+ [![deploy](https://img.shields.io/github/actions/workflow/status/cjboco/cj-image-flip-previewer/deploy-demo.yml?label=demo)](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":[]}
@@ -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 };
@@ -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":"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","names":[]}
@@ -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 };
@@ -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":[]}
@@ -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
+ }