@cjboco/cj-image-flip-previewer 1.0.1 → 1.1.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/dist/index.cjs CHANGED
@@ -50,6 +50,7 @@ function FlipPreviewer({
50
50
  const [debugInfo, setDebugInfo] = (0, import_react.useState)("");
51
51
  const [loadedCount, setLoadedCount] = (0, import_react.useState)(0);
52
52
  const [allLoaded, setAllLoaded] = (0, import_react.useState)(false);
53
+ const [isPointerDown, setIsPointerDown] = (0, import_react.useState)(false);
53
54
  const activeIndexRef = (0, import_react.useRef)(0);
54
55
  const isPlayingRef = (0, import_react.useRef)(false);
55
56
  (0, import_react.useEffect)(() => {
@@ -137,7 +138,38 @@ function FlipPreviewer({
137
138
  if (mode === "hover" && !autoPlay) {
138
139
  stopAnimation();
139
140
  }
141
+ setIsPointerDown(false);
140
142
  }, [mode, autoPlay, stopAnimation]);
143
+ const handlePointerDown = (0, import_react.useCallback)(
144
+ (e) => {
145
+ setIsPointerDown(true);
146
+ if (mode === "position") {
147
+ e.target.setPointerCapture(e.pointerId);
148
+ const container = containerRef.current;
149
+ if (container && images.length > 0) {
150
+ const rect = container.getBoundingClientRect();
151
+ const x = e.clientX - rect.left;
152
+ let pos = Math.floor(x / rect.width * images.length);
153
+ pos = Math.max(0, Math.min(pos, images.length - 1));
154
+ if (pos !== activeIndexRef.current) {
155
+ activeIndexRef.current = pos;
156
+ setActiveIndex(pos);
157
+ onIndexChange?.(pos);
158
+ }
159
+ }
160
+ }
161
+ },
162
+ [mode, images.length, onIndexChange]
163
+ );
164
+ const handlePointerUp = (0, import_react.useCallback)(
165
+ (e) => {
166
+ setIsPointerDown(false);
167
+ if (mode === "position") {
168
+ e.target.releasePointerCapture(e.pointerId);
169
+ }
170
+ },
171
+ [mode]
172
+ );
141
173
  const handlePointerMove = (0, import_react.useCallback)(
142
174
  (e) => {
143
175
  if (mode !== "position") return;
@@ -165,6 +197,31 @@ function FlipPreviewer({
165
197
  setActiveIndex(0);
166
198
  activeIndexRef.current = 0;
167
199
  }, [images]);
200
+ const handleKeyDown = (0, import_react.useCallback)(
201
+ (e) => {
202
+ if (images.length <= 1) return;
203
+ let next = null;
204
+ if (e.key === "ArrowRight" || e.key === "ArrowDown") {
205
+ e.preventDefault();
206
+ next = activeIndexRef.current + 1 >= images.length ? 0 : activeIndexRef.current + 1;
207
+ } else if (e.key === "ArrowLeft" || e.key === "ArrowUp") {
208
+ e.preventDefault();
209
+ next = activeIndexRef.current - 1 < 0 ? images.length - 1 : activeIndexRef.current - 1;
210
+ } else if (e.key === "Home") {
211
+ e.preventDefault();
212
+ next = 0;
213
+ } else if (e.key === "End") {
214
+ e.preventDefault();
215
+ next = images.length - 1;
216
+ }
217
+ if (next !== null && next !== activeIndexRef.current) {
218
+ activeIndexRef.current = next;
219
+ setActiveIndex(next);
220
+ onIndexChange?.(next);
221
+ }
222
+ },
223
+ [images.length, onIndexChange]
224
+ );
168
225
  if (images.length === 0) return null;
169
226
  const activeImage = images[activeIndex];
170
227
  const hasLink = !!activeImage.href;
@@ -173,7 +230,8 @@ function FlipPreviewer({
173
230
  width: width ?? "100%",
174
231
  height: height ?? "100%",
175
232
  overflow: "hidden",
176
- cursor: hasLink ? "pointer" : mode === "position" && showCursor ? "ew-resize" : "default",
233
+ cursor: isPointerDown || !(mode === "position" && showCursor) ? hasLink ? "pointer" : "default" : "ew-resize",
234
+ touchAction: mode === "position" ? "none" : void 0,
177
235
  ...style
178
236
  };
179
237
  const imgElement = /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
@@ -198,30 +256,53 @@ function FlipPreviewer({
198
256
  }
199
257
  ) : imgElement;
200
258
  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
- )
259
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
260
+ "section",
261
+ {
262
+ ref: containerRef,
263
+ "aria-roledescription": "image flipper",
264
+ "aria-label": `Image ${activeIndex + 1} of ${images.length}`,
265
+ tabIndex: 0,
266
+ className: `cj-flip-previewer${className ? ` ${className}` : ""}`,
267
+ style: containerStyle,
268
+ onPointerMove: handlePointerMove,
269
+ onPointerDown: handlePointerDown,
270
+ onPointerUp: handlePointerUp,
271
+ onMouseEnter: handleMouseEnter,
272
+ onMouseLeave: handleMouseLeave,
273
+ onKeyDown: handleKeyDown,
274
+ children: [
275
+ content,
276
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
277
+ "div",
278
+ {
279
+ className: "cj-flip-previewer__sr-only",
280
+ "aria-live": "polite",
281
+ "aria-atomic": "true",
282
+ children: activeImage.alt ? `Image ${activeIndex + 1} of ${images.length}: ${activeImage.alt}` : `Image ${activeIndex + 1} of ${images.length}`
283
+ }
284
+ ),
285
+ mode === "hover" && showProgress && !allLoaded && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
286
+ "div",
287
+ {
288
+ className: "cj-flip-previewer__progress",
289
+ role: "progressbar",
290
+ "aria-valuenow": Math.round(progressPct),
291
+ "aria-valuemin": 0,
292
+ "aria-valuemax": 100,
293
+ "aria-label": "Loading images",
294
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
295
+ "div",
296
+ {
297
+ className: "cj-flip-previewer__progress-bar",
298
+ style: { width: `${progressPct}%` }
299
+ }
300
+ )
301
+ }
302
+ ),
303
+ mode === "position" && debug && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "cj-flip-previewer__debug", "aria-hidden": "true", children: debugInfo })
304
+ ]
305
+ }
225
306
  );
226
307
  }
227
308
  // Annotate the CommonJS export names for ESM import in node:
@@ -1 +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":[]}
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<HTMLElement>(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\tconst [isPointerDown, setIsPointerDown] = 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\tsetIsPointerDown(false);\n\t}, [mode, autoPlay, stopAnimation]);\n\n\tconst handlePointerDown = useCallback(\n\t\t(e: React.PointerEvent<HTMLElement>) => {\n\t\t\tsetIsPointerDown(true);\n\n\t\t\tif (mode === \"position\") {\n\t\t\t\t// Capture pointer so move events keep firing on touch devices\n\t\t\t\t(e.target as HTMLElement).setPointerCapture(e.pointerId);\n\n\t\t\t\t// Update index immediately on press\n\t\t\t\tconst container = containerRef.current;\n\t\t\t\tif (container && images.length > 0) {\n\t\t\t\t\tconst rect = container.getBoundingClientRect();\n\t\t\t\t\tconst x = e.clientX - rect.left;\n\t\t\t\t\tlet pos = Math.floor((x / rect.width) * images.length);\n\t\t\t\t\tpos = Math.max(0, Math.min(pos, images.length - 1));\n\n\t\t\t\t\tif (pos !== activeIndexRef.current) {\n\t\t\t\t\t\tactiveIndexRef.current = pos;\n\t\t\t\t\t\tsetActiveIndex(pos);\n\t\t\t\t\t\tonIndexChange?.(pos);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t},\n\t\t[mode, images.length, onIndexChange],\n\t);\n\n\tconst handlePointerUp = useCallback(\n\t\t(e: React.PointerEvent<HTMLElement>) => {\n\t\t\tsetIsPointerDown(false);\n\n\t\t\tif (mode === \"position\") {\n\t\t\t\t(e.target as HTMLElement).releasePointerCapture(e.pointerId);\n\t\t\t}\n\t\t},\n\t\t[mode],\n\t);\n\n\t// ── Position mode: mouse-position-based ──────────────────────\n\tconst handlePointerMove = useCallback(\n\t\t(e: React.PointerEvent<HTMLElement>) => {\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// ── Keyboard navigation ─────────────────────────────────────\n\tconst handleKeyDown = useCallback(\n\t\t(e: React.KeyboardEvent<HTMLElement>) => {\n\t\t\tif (images.length <= 1) return;\n\n\t\t\tlet next: number | null = null;\n\n\t\t\tif (e.key === \"ArrowRight\" || e.key === \"ArrowDown\") {\n\t\t\t\te.preventDefault();\n\t\t\t\tnext =\n\t\t\t\t\tactiveIndexRef.current + 1 >= images.length\n\t\t\t\t\t\t? 0\n\t\t\t\t\t\t: activeIndexRef.current + 1;\n\t\t\t} else if (e.key === \"ArrowLeft\" || e.key === \"ArrowUp\") {\n\t\t\t\te.preventDefault();\n\t\t\t\tnext =\n\t\t\t\t\tactiveIndexRef.current - 1 < 0\n\t\t\t\t\t\t? images.length - 1\n\t\t\t\t\t\t: activeIndexRef.current - 1;\n\t\t\t} else if (e.key === \"Home\") {\n\t\t\t\te.preventDefault();\n\t\t\t\tnext = 0;\n\t\t\t} else if (e.key === \"End\") {\n\t\t\t\te.preventDefault();\n\t\t\t\tnext = images.length - 1;\n\t\t\t}\n\n\t\t\tif (next !== null && next !== activeIndexRef.current) {\n\t\t\t\tactiveIndexRef.current = next;\n\t\t\t\tsetActiveIndex(next);\n\t\t\t\tonIndexChange?.(next);\n\t\t\t}\n\t\t},\n\t\t[images.length, onIndexChange],\n\t);\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:\n\t\t\tisPointerDown || !(mode === \"position\" && showCursor)\n\t\t\t\t? hasLink\n\t\t\t\t\t? \"pointer\"\n\t\t\t\t\t: \"default\"\n\t\t\t\t: \"ew-resize\",\n\t\ttouchAction: mode === \"position\" ? \"none\" : undefined,\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<section\n\t\t\tref={containerRef}\n\t\t\taria-roledescription=\"image flipper\"\n\t\t\taria-label={`Image ${activeIndex + 1} of ${images.length}`}\n\t\t\ttabIndex={0}\n\t\t\tclassName={`cj-flip-previewer${className ? ` ${className}` : \"\"}`}\n\t\t\tstyle={containerStyle}\n\t\t\tonPointerMove={handlePointerMove}\n\t\t\tonPointerDown={handlePointerDown}\n\t\t\tonPointerUp={handlePointerUp}\n\t\t\tonMouseEnter={handleMouseEnter}\n\t\t\tonMouseLeave={handleMouseLeave}\n\t\t\tonKeyDown={handleKeyDown}\n\t\t>\n\t\t\t{content}\n\n\t\t\t<div\n\t\t\t\tclassName=\"cj-flip-previewer__sr-only\"\n\t\t\t\taria-live=\"polite\"\n\t\t\t\taria-atomic=\"true\"\n\t\t\t>\n\t\t\t\t{activeImage.alt\n\t\t\t\t\t? `Image ${activeIndex + 1} of ${images.length}: ${activeImage.alt}`\n\t\t\t\t\t: `Image ${activeIndex + 1} of ${images.length}`}\n\t\t\t</div>\n\n\t\t\t{mode === \"hover\" && showProgress && !allLoaded && (\n\t\t\t\t<div\n\t\t\t\t\tclassName=\"cj-flip-previewer__progress\"\n\t\t\t\t\trole=\"progressbar\"\n\t\t\t\t\taria-valuenow={Math.round(progressPct)}\n\t\t\t\t\taria-valuemin={0}\n\t\t\t\t\taria-valuemax={100}\n\t\t\t\t\taria-label=\"Loading images\"\n\t\t\t\t>\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\" aria-hidden=\"true\">\n\t\t\t\t\t{debugInfo}\n\t\t\t\t</div>\n\t\t\t)}\n\t\t</section>\n\t);\n}\n"],"mappings":"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","names":[]}
package/dist/index.js CHANGED
@@ -30,6 +30,7 @@ function FlipPreviewer({
30
30
  const [debugInfo, setDebugInfo] = useState("");
31
31
  const [loadedCount, setLoadedCount] = useState(0);
32
32
  const [allLoaded, setAllLoaded] = useState(false);
33
+ const [isPointerDown, setIsPointerDown] = useState(false);
33
34
  const activeIndexRef = useRef(0);
34
35
  const isPlayingRef = useRef(false);
35
36
  useEffect(() => {
@@ -117,7 +118,38 @@ function FlipPreviewer({
117
118
  if (mode === "hover" && !autoPlay) {
118
119
  stopAnimation();
119
120
  }
121
+ setIsPointerDown(false);
120
122
  }, [mode, autoPlay, stopAnimation]);
123
+ const handlePointerDown = useCallback(
124
+ (e) => {
125
+ setIsPointerDown(true);
126
+ if (mode === "position") {
127
+ e.target.setPointerCapture(e.pointerId);
128
+ const container = containerRef.current;
129
+ if (container && images.length > 0) {
130
+ const rect = container.getBoundingClientRect();
131
+ const x = e.clientX - rect.left;
132
+ let pos = Math.floor(x / rect.width * images.length);
133
+ pos = Math.max(0, Math.min(pos, images.length - 1));
134
+ if (pos !== activeIndexRef.current) {
135
+ activeIndexRef.current = pos;
136
+ setActiveIndex(pos);
137
+ onIndexChange?.(pos);
138
+ }
139
+ }
140
+ }
141
+ },
142
+ [mode, images.length, onIndexChange]
143
+ );
144
+ const handlePointerUp = useCallback(
145
+ (e) => {
146
+ setIsPointerDown(false);
147
+ if (mode === "position") {
148
+ e.target.releasePointerCapture(e.pointerId);
149
+ }
150
+ },
151
+ [mode]
152
+ );
121
153
  const handlePointerMove = useCallback(
122
154
  (e) => {
123
155
  if (mode !== "position") return;
@@ -145,6 +177,31 @@ function FlipPreviewer({
145
177
  setActiveIndex(0);
146
178
  activeIndexRef.current = 0;
147
179
  }, [images]);
180
+ const handleKeyDown = useCallback(
181
+ (e) => {
182
+ if (images.length <= 1) return;
183
+ let next = null;
184
+ if (e.key === "ArrowRight" || e.key === "ArrowDown") {
185
+ e.preventDefault();
186
+ next = activeIndexRef.current + 1 >= images.length ? 0 : activeIndexRef.current + 1;
187
+ } else if (e.key === "ArrowLeft" || e.key === "ArrowUp") {
188
+ e.preventDefault();
189
+ next = activeIndexRef.current - 1 < 0 ? images.length - 1 : activeIndexRef.current - 1;
190
+ } else if (e.key === "Home") {
191
+ e.preventDefault();
192
+ next = 0;
193
+ } else if (e.key === "End") {
194
+ e.preventDefault();
195
+ next = images.length - 1;
196
+ }
197
+ if (next !== null && next !== activeIndexRef.current) {
198
+ activeIndexRef.current = next;
199
+ setActiveIndex(next);
200
+ onIndexChange?.(next);
201
+ }
202
+ },
203
+ [images.length, onIndexChange]
204
+ );
148
205
  if (images.length === 0) return null;
149
206
  const activeImage = images[activeIndex];
150
207
  const hasLink = !!activeImage.href;
@@ -153,7 +210,8 @@ function FlipPreviewer({
153
210
  width: width ?? "100%",
154
211
  height: height ?? "100%",
155
212
  overflow: "hidden",
156
- cursor: hasLink ? "pointer" : mode === "position" && showCursor ? "ew-resize" : "default",
213
+ cursor: isPointerDown || !(mode === "position" && showCursor) ? hasLink ? "pointer" : "default" : "ew-resize",
214
+ touchAction: mode === "position" ? "none" : void 0,
157
215
  ...style
158
216
  };
159
217
  const imgElement = /* @__PURE__ */ jsx(
@@ -178,30 +236,53 @@ function FlipPreviewer({
178
236
  }
179
237
  ) : imgElement;
180
238
  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
- )
239
+ return /* @__PURE__ */ jsxs(
240
+ "section",
241
+ {
242
+ ref: containerRef,
243
+ "aria-roledescription": "image flipper",
244
+ "aria-label": `Image ${activeIndex + 1} of ${images.length}`,
245
+ tabIndex: 0,
246
+ className: `cj-flip-previewer${className ? ` ${className}` : ""}`,
247
+ style: containerStyle,
248
+ onPointerMove: handlePointerMove,
249
+ onPointerDown: handlePointerDown,
250
+ onPointerUp: handlePointerUp,
251
+ onMouseEnter: handleMouseEnter,
252
+ onMouseLeave: handleMouseLeave,
253
+ onKeyDown: handleKeyDown,
254
+ children: [
255
+ content,
256
+ /* @__PURE__ */ jsx(
257
+ "div",
258
+ {
259
+ className: "cj-flip-previewer__sr-only",
260
+ "aria-live": "polite",
261
+ "aria-atomic": "true",
262
+ children: activeImage.alt ? `Image ${activeIndex + 1} of ${images.length}: ${activeImage.alt}` : `Image ${activeIndex + 1} of ${images.length}`
263
+ }
264
+ ),
265
+ mode === "hover" && showProgress && !allLoaded && /* @__PURE__ */ jsx(
266
+ "div",
267
+ {
268
+ className: "cj-flip-previewer__progress",
269
+ role: "progressbar",
270
+ "aria-valuenow": Math.round(progressPct),
271
+ "aria-valuemin": 0,
272
+ "aria-valuemax": 100,
273
+ "aria-label": "Loading images",
274
+ children: /* @__PURE__ */ jsx(
275
+ "div",
276
+ {
277
+ className: "cj-flip-previewer__progress-bar",
278
+ style: { width: `${progressPct}%` }
279
+ }
280
+ )
281
+ }
282
+ ),
283
+ mode === "position" && debug && /* @__PURE__ */ jsx("div", { className: "cj-flip-previewer__debug", "aria-hidden": "true", children: debugInfo })
284
+ ]
285
+ }
205
286
  );
206
287
  }
207
288
  export {
package/dist/index.js.map CHANGED
@@ -1 +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":[]}
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<HTMLElement>(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\tconst [isPointerDown, setIsPointerDown] = 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\tsetIsPointerDown(false);\n\t}, [mode, autoPlay, stopAnimation]);\n\n\tconst handlePointerDown = useCallback(\n\t\t(e: React.PointerEvent<HTMLElement>) => {\n\t\t\tsetIsPointerDown(true);\n\n\t\t\tif (mode === \"position\") {\n\t\t\t\t// Capture pointer so move events keep firing on touch devices\n\t\t\t\t(e.target as HTMLElement).setPointerCapture(e.pointerId);\n\n\t\t\t\t// Update index immediately on press\n\t\t\t\tconst container = containerRef.current;\n\t\t\t\tif (container && images.length > 0) {\n\t\t\t\t\tconst rect = container.getBoundingClientRect();\n\t\t\t\t\tconst x = e.clientX - rect.left;\n\t\t\t\t\tlet pos = Math.floor((x / rect.width) * images.length);\n\t\t\t\t\tpos = Math.max(0, Math.min(pos, images.length - 1));\n\n\t\t\t\t\tif (pos !== activeIndexRef.current) {\n\t\t\t\t\t\tactiveIndexRef.current = pos;\n\t\t\t\t\t\tsetActiveIndex(pos);\n\t\t\t\t\t\tonIndexChange?.(pos);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t},\n\t\t[mode, images.length, onIndexChange],\n\t);\n\n\tconst handlePointerUp = useCallback(\n\t\t(e: React.PointerEvent<HTMLElement>) => {\n\t\t\tsetIsPointerDown(false);\n\n\t\t\tif (mode === \"position\") {\n\t\t\t\t(e.target as HTMLElement).releasePointerCapture(e.pointerId);\n\t\t\t}\n\t\t},\n\t\t[mode],\n\t);\n\n\t// ── Position mode: mouse-position-based ──────────────────────\n\tconst handlePointerMove = useCallback(\n\t\t(e: React.PointerEvent<HTMLElement>) => {\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// ── Keyboard navigation ─────────────────────────────────────\n\tconst handleKeyDown = useCallback(\n\t\t(e: React.KeyboardEvent<HTMLElement>) => {\n\t\t\tif (images.length <= 1) return;\n\n\t\t\tlet next: number | null = null;\n\n\t\t\tif (e.key === \"ArrowRight\" || e.key === \"ArrowDown\") {\n\t\t\t\te.preventDefault();\n\t\t\t\tnext =\n\t\t\t\t\tactiveIndexRef.current + 1 >= images.length\n\t\t\t\t\t\t? 0\n\t\t\t\t\t\t: activeIndexRef.current + 1;\n\t\t\t} else if (e.key === \"ArrowLeft\" || e.key === \"ArrowUp\") {\n\t\t\t\te.preventDefault();\n\t\t\t\tnext =\n\t\t\t\t\tactiveIndexRef.current - 1 < 0\n\t\t\t\t\t\t? images.length - 1\n\t\t\t\t\t\t: activeIndexRef.current - 1;\n\t\t\t} else if (e.key === \"Home\") {\n\t\t\t\te.preventDefault();\n\t\t\t\tnext = 0;\n\t\t\t} else if (e.key === \"End\") {\n\t\t\t\te.preventDefault();\n\t\t\t\tnext = images.length - 1;\n\t\t\t}\n\n\t\t\tif (next !== null && next !== activeIndexRef.current) {\n\t\t\t\tactiveIndexRef.current = next;\n\t\t\t\tsetActiveIndex(next);\n\t\t\t\tonIndexChange?.(next);\n\t\t\t}\n\t\t},\n\t\t[images.length, onIndexChange],\n\t);\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:\n\t\t\tisPointerDown || !(mode === \"position\" && showCursor)\n\t\t\t\t? hasLink\n\t\t\t\t\t? \"pointer\"\n\t\t\t\t\t: \"default\"\n\t\t\t\t: \"ew-resize\",\n\t\ttouchAction: mode === \"position\" ? \"none\" : undefined,\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<section\n\t\t\tref={containerRef}\n\t\t\taria-roledescription=\"image flipper\"\n\t\t\taria-label={`Image ${activeIndex + 1} of ${images.length}`}\n\t\t\ttabIndex={0}\n\t\t\tclassName={`cj-flip-previewer${className ? ` ${className}` : \"\"}`}\n\t\t\tstyle={containerStyle}\n\t\t\tonPointerMove={handlePointerMove}\n\t\t\tonPointerDown={handlePointerDown}\n\t\t\tonPointerUp={handlePointerUp}\n\t\t\tonMouseEnter={handleMouseEnter}\n\t\t\tonMouseLeave={handleMouseLeave}\n\t\t\tonKeyDown={handleKeyDown}\n\t\t>\n\t\t\t{content}\n\n\t\t\t<div\n\t\t\t\tclassName=\"cj-flip-previewer__sr-only\"\n\t\t\t\taria-live=\"polite\"\n\t\t\t\taria-atomic=\"true\"\n\t\t\t>\n\t\t\t\t{activeImage.alt\n\t\t\t\t\t? `Image ${activeIndex + 1} of ${images.length}: ${activeImage.alt}`\n\t\t\t\t\t: `Image ${activeIndex + 1} of ${images.length}`}\n\t\t\t</div>\n\n\t\t\t{mode === \"hover\" && showProgress && !allLoaded && (\n\t\t\t\t<div\n\t\t\t\t\tclassName=\"cj-flip-previewer__progress\"\n\t\t\t\t\trole=\"progressbar\"\n\t\t\t\t\taria-valuenow={Math.round(progressPct)}\n\t\t\t\t\taria-valuemin={0}\n\t\t\t\t\taria-valuemax={100}\n\t\t\t\t\taria-label=\"Loading images\"\n\t\t\t\t>\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\" aria-hidden=\"true\">\n\t\t\t\t\t{debugInfo}\n\t\t\t\t</div>\n\t\t\t)}\n\t\t</section>\n\t);\n}\n"],"mappings":"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","names":[]}
package/dist/styles.css CHANGED
@@ -5,6 +5,21 @@
5
5
  user-select: none;
6
6
  touch-action: none;
7
7
  }
8
+ .cj-flip-previewer:focus-visible {
9
+ outline: var(--cj-flip-focus-outline, 2px solid #005fcc);
10
+ outline-offset: var(--cj-flip-focus-outline-offset, 2px);
11
+ }
12
+ .cj-flip-previewer__sr-only {
13
+ position: absolute;
14
+ width: 1px;
15
+ height: 1px;
16
+ padding: 0;
17
+ margin: -1px;
18
+ overflow: hidden;
19
+ clip: rect(0, 0, 0, 0);
20
+ white-space: nowrap;
21
+ border: 0;
22
+ }
8
23
  .cj-flip-previewer__img {
9
24
  display: block;
10
25
  width: 100%;
@@ -1 +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":[]}
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:focus-visible {\n outline: var(--cj-flip-focus-outline, 2px solid #005fcc);\n outline-offset: var(--cj-flip-focus-outline-offset, 2px);\n }\n\n .cj-flip-previewer__sr-only {\n position: absolute;\n width: 1px;\n height: 1px;\n padding: 0;\n margin: -1px;\n overflow: hidden;\n clip: rect(0, 0, 0, 0);\n white-space: nowrap;\n border: 0;\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,GANC,iBAMiB;AAChB,aAAS,IAAI,uBAAuB,EAAE,IAAI,MAAM;AAChD,oBAAgB,IAAI,8BAA8B,EAAE;AACtD;AAEA,GAAC;AACC,cAAU;AACV,WAAO;AACP,YAAQ;AACR,aAAS;AACT,YAAQ;AACR,cAAU;AACV,UAAM,KAAK,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE;AACpB,iBAAa;AACb,YAAQ;AACV;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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cjboco/cj-image-flip-previewer",
3
- "version": "1.0.1",
3
+ "version": "1.1.0",
4
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
5
  "author": "Doug Jones",
6
6
  "license": "BSD-3-Clause",