@thewhateverapp/tile-sdk 0.13.12 → 0.13.14

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.
@@ -1 +1 @@
1
- {"version":3,"file":"Slideshow.d.ts","sourceRoot":"","sources":["../../../src/react/overlay/Slideshow.tsx"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"Slideshow.d.ts","sourceRoot":"","sources":["../../../src/react/overlay/Slideshow.tsx"],"names":[],"mappings":"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"}
@@ -213,56 +213,81 @@ export function Slideshow({ images, intervalMs = 5000, autoAdvance = true, trans
213
213
  resume: useCallback(() => singleton.resume(), []),
214
214
  toggle: useCallback(() => singleton.toggle(), []),
215
215
  };
216
- // Touch/swipe handlers with tracking for more reliable detection
216
+ // Touch/swipe handlers using native events (React touch events are passive)
217
217
  const touchCurrentRef = useRef(null);
218
- const handleTouchStart = useCallback((e) => {
219
- if (!swipeable || state.totalSlides <= 1)
218
+ useEffect(() => {
219
+ const container = containerRef.current;
220
+ if (!container || !swipeable || state.totalSlides <= 1)
220
221
  return;
221
- const touch = e.touches[0];
222
- touchStartRef.current = {
223
- x: touch.clientX,
224
- y: touch.clientY,
225
- time: Date.now(),
222
+ const handleTouchStart = (e) => {
223
+ const touch = e.touches[0];
224
+ if (!touch)
225
+ return;
226
+ touchStartRef.current = {
227
+ x: touch.clientX,
228
+ y: touch.clientY,
229
+ time: Date.now(),
230
+ };
231
+ touchCurrentRef.current = { x: touch.clientX, y: touch.clientY };
226
232
  };
227
- touchCurrentRef.current = { x: touch.clientX, y: touch.clientY };
228
- }, [swipeable, state.totalSlides]);
229
- const handleTouchMove = useCallback((e) => {
230
- if (!swipeable || !touchStartRef.current)
231
- return;
232
- const touch = e.touches[0];
233
- touchCurrentRef.current = { x: touch.clientX, y: touch.clientY };
234
- // Prevent vertical scroll if horizontal swipe is detected
235
- const deltaX = touch.clientX - touchStartRef.current.x;
236
- const deltaY = touch.clientY - touchStartRef.current.y;
237
- if (Math.abs(deltaX) > Math.abs(deltaY) && Math.abs(deltaX) > 10) {
238
- e.preventDefault();
239
- }
240
- }, [swipeable]);
241
- const handleTouchEnd = useCallback(() => {
242
- if (!swipeable || !touchStartRef.current || !touchCurrentRef.current || state.isTransitioning) {
243
- touchStartRef.current = null;
244
- touchCurrentRef.current = null;
245
- return;
246
- }
247
- const deltaX = touchCurrentRef.current.x - touchStartRef.current.x;
248
- const deltaY = touchCurrentRef.current.y - touchStartRef.current.y;
249
- const deltaTime = Date.now() - touchStartRef.current.time;
250
- // Minimum swipe distance (50px) and max time (300ms for quick swipe, or slower with more distance)
251
- const minSwipeDistance = 50;
252
- const isHorizontalSwipe = Math.abs(deltaX) > Math.abs(deltaY);
253
- const isValidSwipe = Math.abs(deltaX) > minSwipeDistance && isHorizontalSwipe;
254
- const isQuickSwipe = deltaTime < 300 || Math.abs(deltaX) > 100;
255
- if (isValidSwipe && isQuickSwipe) {
256
- if (deltaX > 0) {
257
- controls.prev();
233
+ const handleTouchMove = (e) => {
234
+ if (!touchStartRef.current)
235
+ return;
236
+ const touch = e.touches[0];
237
+ if (!touch)
238
+ return;
239
+ touchCurrentRef.current = { x: touch.clientX, y: touch.clientY };
240
+ // Prevent default and stop propagation for horizontal swipes
241
+ const deltaX = touch.clientX - touchStartRef.current.x;
242
+ const deltaY = touch.clientY - touchStartRef.current.y;
243
+ if (Math.abs(deltaX) > Math.abs(deltaY) && Math.abs(deltaX) > 10) {
244
+ e.preventDefault();
245
+ e.stopPropagation();
258
246
  }
259
- else {
260
- controls.next();
247
+ };
248
+ const handleTouchEnd = (e) => {
249
+ if (!touchStartRef.current || !touchCurrentRef.current) {
250
+ touchStartRef.current = null;
251
+ touchCurrentRef.current = null;
252
+ return;
261
253
  }
262
- }
263
- touchStartRef.current = null;
264
- touchCurrentRef.current = null;
265
- }, [swipeable, state.isTransitioning, controls]);
254
+ // Don't process if transitioning
255
+ if (state.isTransitioning) {
256
+ touchStartRef.current = null;
257
+ touchCurrentRef.current = null;
258
+ return;
259
+ }
260
+ const deltaX = touchCurrentRef.current.x - touchStartRef.current.x;
261
+ const deltaY = touchCurrentRef.current.y - touchStartRef.current.y;
262
+ const deltaTime = Date.now() - touchStartRef.current.time;
263
+ // Lower threshold for smaller tiles - 30px minimum swipe distance
264
+ const minSwipeDistance = 30;
265
+ const isHorizontalSwipe = Math.abs(deltaX) > Math.abs(deltaY);
266
+ const isValidSwipe = Math.abs(deltaX) > minSwipeDistance && isHorizontalSwipe;
267
+ const isQuickSwipe = deltaTime < 500 || Math.abs(deltaX) > 60;
268
+ if (isValidSwipe && isQuickSwipe) {
269
+ e.preventDefault();
270
+ e.stopPropagation();
271
+ if (deltaX > 0) {
272
+ singleton.prev();
273
+ }
274
+ else {
275
+ singleton.next();
276
+ }
277
+ }
278
+ touchStartRef.current = null;
279
+ touchCurrentRef.current = null;
280
+ };
281
+ // Use passive: false on all to allow preventDefault()
282
+ container.addEventListener('touchstart', handleTouchStart, { passive: false });
283
+ container.addEventListener('touchmove', handleTouchMove, { passive: false });
284
+ container.addEventListener('touchend', handleTouchEnd, { passive: false });
285
+ return () => {
286
+ container.removeEventListener('touchstart', handleTouchStart);
287
+ container.removeEventListener('touchmove', handleTouchMove);
288
+ container.removeEventListener('touchend', handleTouchEnd);
289
+ };
290
+ }, [swipeable, state.totalSlides, state.isTransitioning, singleton]);
266
291
  // Get transition styles
267
292
  const getTransitionStyles = (index) => {
268
293
  const isActive = index === state.currentIndex;
@@ -300,7 +325,7 @@ export function Slideshow({ images, intervalMs = 5000, autoAdvance = true, trans
300
325
  React.createElement("p", { className: "text-white/60" }, "No images")));
301
326
  }
302
327
  return (React.createElement(SlideshowContext.Provider, { value: contextValue },
303
- React.createElement("div", { ref: containerRef, className: `relative w-full h-full bg-black overflow-hidden ${className}`, style: { touchAction: swipeable && state.totalSlides > 1 ? 'pan-y pinch-zoom' : 'auto' }, onTouchStart: handleTouchStart, onTouchMove: handleTouchMove, onTouchEnd: handleTouchEnd },
328
+ React.createElement("div", { ref: containerRef, className: `relative w-full h-full bg-black overflow-hidden ${className}`, style: { touchAction: swipeable && state.totalSlides > 1 ? 'none' : 'auto' } },
304
329
  React.createElement("div", { className: "relative w-full h-full" }, images.map((image, index) => (React.createElement("div", { key: `${image.url}-${index}`, style: getTransitionStyles(index), className: `flex items-center justify-center ${imageClassName}` },
305
330
  React.createElement("img", { src: image.url, alt: image.alt || `Slide ${index + 1}`, className: `w-full h-full ${objectFit === 'cover' ? 'object-cover' : 'object-contain'}`, style: { objectPosition: 'center' }, loading: index === 0 ? 'eager' : 'lazy', draggable: false }))))),
306
331
  showArrows && state.totalSlides > 1 && (React.createElement(React.Fragment, null,
@@ -4,5 +4,5 @@
4
4
  * React component that attaches to the SlideshowManager singleton.
5
5
  * The slideshow state persists across route changes.
6
6
  */
7
- export declare const persistentSlideshowTemplate = "'use client';\n\nimport { useEffect, useState, useRef, useCallback, createContext, useContext, ReactNode } from 'react';\nimport { SlideshowManager, SlideshowState, SlideImage } from '../lib/SlideshowManager';\n\n// Context to share slideshow state with overlays\ninterface SlideshowContextValue {\n state: SlideshowState;\n controls: {\n next: () => void;\n prev: () => void;\n goTo: (index: number) => void;\n pause: () => void;\n resume: () => void;\n toggle: () => void;\n };\n}\n\nconst SlideshowContext = createContext<SlideshowContextValue | null>(null);\n\nexport function useSlideshow(): SlideshowContextValue {\n const context = useContext(SlideshowContext);\n if (!context) {\n throw new Error('useSlideshow must be used within PersistentSlideshow');\n }\n return context;\n}\n\ninterface PersistentSlideshowProps {\n images: SlideImage[];\n /** Auto-advance interval in milliseconds (default: 5000) */\n intervalMs?: number;\n /** Auto-advance slides (default: true) */\n autoAdvance?: boolean;\n /** Transition type (default: 'fade') */\n transition?: 'fade' | 'slide' | 'none';\n /** Transition duration in ms (default: 500) */\n transitionDuration?: number;\n /** Show navigation dots (default: true) */\n showDots?: boolean;\n /** Show navigation arrows (default: true) */\n showArrows?: boolean;\n /** Enable swipe gestures on touch devices (default: true) */\n swipeable?: boolean;\n /** Additional class names */\n className?: string;\n /** Image container class names */\n imageClassName?: string;\n /** Children rendered as overlay */\n children?: ReactNode;\n}\n\nexport function PersistentSlideshow({\n images,\n intervalMs = 5000,\n autoAdvance = true,\n transition = 'fade',\n transitionDuration = 500,\n showDots = true,\n showArrows = true,\n swipeable = true,\n className = '',\n imageClassName = '',\n children\n}: PersistentSlideshowProps) {\n const [state, setState] = useState<SlideshowState>(SlideshowManager.getState());\n const touchStartRef = useRef<{ x: number; y: number; time: number } | null>(null);\n const touchCurrentRef = useRef<{ x: number; y: number } | null>(null);\n\n // Touch/swipe handlers with tracking for more reliable detection\n const handleTouchStart = useCallback((e: React.TouchEvent) => {\n if (!swipeable || state.totalSlides <= 1) return;\n const touch = e.touches[0];\n touchStartRef.current = {\n x: touch.clientX,\n y: touch.clientY,\n time: Date.now(),\n };\n touchCurrentRef.current = { x: touch.clientX, y: touch.clientY };\n }, [swipeable, state.totalSlides]);\n\n const handleTouchMove = useCallback((e: React.TouchEvent) => {\n if (!swipeable || !touchStartRef.current) return;\n const touch = e.touches[0];\n touchCurrentRef.current = { x: touch.clientX, y: touch.clientY };\n\n // Prevent vertical scroll if horizontal swipe is detected\n const deltaX = touch.clientX - touchStartRef.current.x;\n const deltaY = touch.clientY - touchStartRef.current.y;\n if (Math.abs(deltaX) > Math.abs(deltaY) && Math.abs(deltaX) > 10) {\n e.preventDefault();\n }\n }, [swipeable]);\n\n const handleTouchEnd = useCallback(() => {\n if (!swipeable || !touchStartRef.current || !touchCurrentRef.current || state.isTransitioning) {\n touchStartRef.current = null;\n touchCurrentRef.current = null;\n return;\n }\n\n const deltaX = touchCurrentRef.current.x - touchStartRef.current.x;\n const deltaY = touchCurrentRef.current.y - touchStartRef.current.y;\n const deltaTime = Date.now() - touchStartRef.current.time;\n\n // Minimum swipe distance (50px) and max time (300ms for quick swipe, or slower with more distance)\n const minSwipeDistance = 50;\n const isHorizontalSwipe = Math.abs(deltaX) > Math.abs(deltaY);\n const isValidSwipe = Math.abs(deltaX) > minSwipeDistance && isHorizontalSwipe;\n const isQuickSwipe = deltaTime < 300 || Math.abs(deltaX) > 100;\n\n if (isValidSwipe && isQuickSwipe) {\n if (deltaX > 0) {\n SlideshowManager.prev();\n } else {\n SlideshowManager.next();\n }\n }\n\n touchStartRef.current = null;\n touchCurrentRef.current = null;\n }, [swipeable, state.isTransitioning]);\n\n useEffect(() => {\n // Initialize slideshow with images\n SlideshowManager.initialize(images, {\n intervalMs,\n transitionDuration,\n autoAdvance,\n });\n\n // Subscribe to state changes\n const unsubscribe = SlideshowManager.onStateChange(setState);\n\n return () => {\n unsubscribe();\n // Don't destroy - let slideshow persist for route changes\n };\n }, [images, intervalMs, transitionDuration, autoAdvance]);\n\n const contextValue: SlideshowContextValue = {\n state,\n controls: {\n next: () => SlideshowManager.next(),\n prev: () => SlideshowManager.prev(),\n goTo: (index) => SlideshowManager.goTo(index),\n pause: () => SlideshowManager.pause(),\n resume: () => SlideshowManager.resume(),\n toggle: () => SlideshowManager.toggle(),\n },\n };\n\n // Get transition styles\n const getTransitionStyles = (index: number): React.CSSProperties => {\n const isActive = index === state.currentIndex;\n\n switch (transition) {\n case 'fade':\n return {\n opacity: isActive ? 1 : 0,\n transition: `opacity ${transitionDuration}ms ease-in-out`,\n position: 'absolute',\n inset: 0,\n };\n case 'slide':\n const offset = (index - state.currentIndex) * 100;\n return {\n transform: `translateX(${offset}%)`,\n transition: `transform ${transitionDuration}ms ease-in-out`,\n position: 'absolute',\n inset: 0,\n };\n case 'none':\n default:\n return {\n opacity: isActive ? 1 : 0,\n position: 'absolute',\n inset: 0,\n };\n }\n };\n\n if (images.length === 0) {\n return (\n <div className={`relative w-full h-full bg-black flex items-center justify-center ${className}`}>\n <p className=\"text-white/60\">No images</p>\n </div>\n );\n }\n\n return (\n <SlideshowContext.Provider value={contextValue}>\n <div\n className={`relative w-full h-full bg-black overflow-hidden ${className}`}\n style={{ touchAction: swipeable && state.totalSlides > 1 ? 'pan-y pinch-zoom' : 'auto' }}\n onTouchStart={handleTouchStart}\n onTouchMove={handleTouchMove}\n onTouchEnd={handleTouchEnd}\n >\n {/* Image container */}\n <div className=\"relative w-full h-full\">\n {state.images.map((image, index) => (\n <div\n key={`${image.url}-${index}`}\n style={getTransitionStyles(index)}\n className={imageClassName}\n >\n <img\n src={image.url}\n alt={image.alt || `Slide ${index + 1}`}\n className=\"w-full h-full object-contain\"\n loading={index === 0 ? 'eager' : 'lazy'}\n />\n </div>\n ))}\n </div>\n\n {/* Navigation arrows */}\n {showArrows && state.totalSlides > 1 && (\n <>\n <button\n onClick={() => SlideshowManager.prev()}\n disabled={state.isTransitioning}\n className=\"absolute left-2 top-1/2 -translate-y-1/2 p-2 bg-black/40 hover:bg-black/60 rounded-full text-white transition-colors disabled:opacity-50\"\n aria-label=\"Previous slide\"\n >\n <svg className=\"w-5 h-5\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <path strokeLinecap=\"round\" strokeLinejoin=\"round\" strokeWidth={2} d=\"M15 19l-7-7 7-7\" />\n </svg>\n </button>\n <button\n onClick={() => SlideshowManager.next()}\n disabled={state.isTransitioning}\n className=\"absolute right-2 top-1/2 -translate-y-1/2 p-2 bg-black/40 hover:bg-black/60 rounded-full text-white transition-colors disabled:opacity-50\"\n aria-label=\"Next slide\"\n >\n <svg className=\"w-5 h-5\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <path strokeLinecap=\"round\" strokeLinejoin=\"round\" strokeWidth={2} d=\"M9 5l7 7-7 7\" />\n </svg>\n </button>\n </>\n )}\n\n {/* Navigation dots */}\n {showDots && state.totalSlides > 1 && (\n <div className=\"absolute bottom-3 left-1/2 -translate-x-1/2 flex gap-1.5\">\n {state.images.map((_, index) => (\n <button\n key={index}\n onClick={() => SlideshowManager.goTo(index)}\n disabled={state.isTransitioning}\n className={`w-2 h-2 rounded-full transition-colors ${\n index === state.currentIndex\n ? 'bg-white'\n : 'bg-white/40 hover:bg-white/60'\n }`}\n aria-label={`Go to slide ${index + 1}`}\n />\n ))}\n </div>\n )}\n\n {/* Overlay children - pointer-events managed by individual OverlaySlot components */}\n <div className=\"absolute inset-0 z-10 pointer-events-none\">\n {children}\n </div>\n </div>\n </SlideshowContext.Provider>\n );\n}\n";
7
+ export declare const persistentSlideshowTemplate = "'use client';\n\nimport { useEffect, useState, useRef, useCallback, createContext, useContext, ReactNode } from 'react';\nimport { SlideshowManager, SlideshowState, SlideImage } from '../lib/SlideshowManager';\n\n// Context to share slideshow state with overlays\ninterface SlideshowContextValue {\n state: SlideshowState;\n controls: {\n next: () => void;\n prev: () => void;\n goTo: (index: number) => void;\n pause: () => void;\n resume: () => void;\n toggle: () => void;\n };\n}\n\nconst SlideshowContext = createContext<SlideshowContextValue | null>(null);\n\nexport function useSlideshow(): SlideshowContextValue {\n const context = useContext(SlideshowContext);\n if (!context) {\n throw new Error('useSlideshow must be used within PersistentSlideshow');\n }\n return context;\n}\n\ninterface PersistentSlideshowProps {\n images: SlideImage[];\n /** Auto-advance interval in milliseconds (default: 5000) */\n intervalMs?: number;\n /** Auto-advance slides (default: true) */\n autoAdvance?: boolean;\n /** Transition type (default: 'fade') */\n transition?: 'fade' | 'slide' | 'none';\n /** Transition duration in ms (default: 500) */\n transitionDuration?: number;\n /** Show navigation dots (default: true) */\n showDots?: boolean;\n /** Show navigation arrows (default: true) */\n showArrows?: boolean;\n /** Enable swipe gestures on touch devices (default: true) */\n swipeable?: boolean;\n /** Additional class names */\n className?: string;\n /** Image container class names */\n imageClassName?: string;\n /** Children rendered as overlay */\n children?: ReactNode;\n}\n\nexport function PersistentSlideshow({\n images,\n intervalMs = 5000,\n autoAdvance = true,\n transition = 'fade',\n transitionDuration = 500,\n showDots = true,\n showArrows = true,\n swipeable = true,\n className = '',\n imageClassName = '',\n children\n}: PersistentSlideshowProps) {\n const [state, setState] = useState<SlideshowState>(SlideshowManager.getState());\n const containerRef = useRef<HTMLDivElement>(null);\n const touchStartRef = useRef<{ x: number; y: number; time: number } | null>(null);\n const touchCurrentRef = useRef<{ x: number; y: number } | null>(null);\n\n // Touch/swipe handlers using native events (React touch events are passive)\n useEffect(() => {\n const container = containerRef.current;\n if (!container || !swipeable || state.totalSlides <= 1) return;\n\n const handleTouchStart = (e: TouchEvent) => {\n const touch = e.touches[0];\n if (!touch) return;\n\n touchStartRef.current = {\n x: touch.clientX,\n y: touch.clientY,\n time: Date.now(),\n };\n touchCurrentRef.current = { x: touch.clientX, y: touch.clientY };\n };\n\n const handleTouchMove = (e: TouchEvent) => {\n if (!touchStartRef.current) return;\n const touch = e.touches[0];\n if (!touch) return;\n\n touchCurrentRef.current = { x: touch.clientX, y: touch.clientY };\n\n // Prevent default and stop propagation for horizontal swipes\n const deltaX = touch.clientX - touchStartRef.current.x;\n const deltaY = touch.clientY - touchStartRef.current.y;\n if (Math.abs(deltaX) > Math.abs(deltaY) && Math.abs(deltaX) > 10) {\n e.preventDefault();\n e.stopPropagation();\n }\n };\n\n const handleTouchEnd = (e: TouchEvent) => {\n if (!touchStartRef.current || !touchCurrentRef.current) {\n touchStartRef.current = null;\n touchCurrentRef.current = null;\n return;\n }\n\n // Don't process if transitioning\n if (state.isTransitioning) {\n touchStartRef.current = null;\n touchCurrentRef.current = null;\n return;\n }\n\n const deltaX = touchCurrentRef.current.x - touchStartRef.current.x;\n const deltaY = touchCurrentRef.current.y - touchStartRef.current.y;\n const deltaTime = Date.now() - touchStartRef.current.time;\n\n // Lower threshold for smaller tiles - 30px minimum swipe distance\n const minSwipeDistance = 30;\n const isHorizontalSwipe = Math.abs(deltaX) > Math.abs(deltaY);\n const isValidSwipe = Math.abs(deltaX) > minSwipeDistance && isHorizontalSwipe;\n const isQuickSwipe = deltaTime < 500 || Math.abs(deltaX) > 60;\n\n if (isValidSwipe && isQuickSwipe) {\n e.preventDefault();\n e.stopPropagation();\n\n if (deltaX > 0) {\n SlideshowManager.prev();\n } else {\n SlideshowManager.next();\n }\n }\n\n touchStartRef.current = null;\n touchCurrentRef.current = null;\n };\n\n // Use passive: false on all to allow preventDefault()\n container.addEventListener('touchstart', handleTouchStart, { passive: false });\n container.addEventListener('touchmove', handleTouchMove, { passive: false });\n container.addEventListener('touchend', handleTouchEnd, { passive: false });\n\n return () => {\n container.removeEventListener('touchstart', handleTouchStart);\n container.removeEventListener('touchmove', handleTouchMove);\n container.removeEventListener('touchend', handleTouchEnd);\n };\n }, [swipeable, state.totalSlides, state.isTransitioning]);\n\n useEffect(() => {\n // Initialize slideshow with images\n SlideshowManager.initialize(images, {\n intervalMs,\n transitionDuration,\n autoAdvance,\n });\n\n // Subscribe to state changes\n const unsubscribe = SlideshowManager.onStateChange(setState);\n\n return () => {\n unsubscribe();\n // Don't destroy - let slideshow persist for route changes\n };\n }, [images, intervalMs, transitionDuration, autoAdvance]);\n\n const contextValue: SlideshowContextValue = {\n state,\n controls: {\n next: () => SlideshowManager.next(),\n prev: () => SlideshowManager.prev(),\n goTo: (index) => SlideshowManager.goTo(index),\n pause: () => SlideshowManager.pause(),\n resume: () => SlideshowManager.resume(),\n toggle: () => SlideshowManager.toggle(),\n },\n };\n\n // Get transition styles\n const getTransitionStyles = (index: number): React.CSSProperties => {\n const isActive = index === state.currentIndex;\n\n switch (transition) {\n case 'fade':\n return {\n opacity: isActive ? 1 : 0,\n transition: `opacity ${transitionDuration}ms ease-in-out`,\n position: 'absolute',\n inset: 0,\n };\n case 'slide':\n const offset = (index - state.currentIndex) * 100;\n return {\n transform: `translateX(${offset}%)`,\n transition: `transform ${transitionDuration}ms ease-in-out`,\n position: 'absolute',\n inset: 0,\n };\n case 'none':\n default:\n return {\n opacity: isActive ? 1 : 0,\n position: 'absolute',\n inset: 0,\n };\n }\n };\n\n if (images.length === 0) {\n return (\n <div className={`relative w-full h-full bg-black flex items-center justify-center ${className}`}>\n <p className=\"text-white/60\">No images</p>\n </div>\n );\n }\n\n return (\n <SlideshowContext.Provider value={contextValue}>\n <div\n ref={containerRef}\n className={`relative w-full h-full bg-black overflow-hidden ${className}`}\n style={{ touchAction: swipeable && state.totalSlides > 1 ? 'none' : 'auto' }}\n >\n {/* Image container */}\n <div className=\"relative w-full h-full\">\n {state.images.map((image, index) => (\n <div\n key={`${image.url}-${index}`}\n style={getTransitionStyles(index)}\n className={imageClassName}\n >\n <img\n src={image.url}\n alt={image.alt || `Slide ${index + 1}`}\n className=\"w-full h-full object-contain\"\n loading={index === 0 ? 'eager' : 'lazy'}\n />\n </div>\n ))}\n </div>\n\n {/* Navigation arrows */}\n {showArrows && state.totalSlides > 1 && (\n <>\n <button\n onClick={() => SlideshowManager.prev()}\n disabled={state.isTransitioning}\n className=\"absolute left-2 top-1/2 -translate-y-1/2 p-2 bg-black/40 hover:bg-black/60 rounded-full text-white transition-colors disabled:opacity-50\"\n aria-label=\"Previous slide\"\n >\n <svg className=\"w-5 h-5\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <path strokeLinecap=\"round\" strokeLinejoin=\"round\" strokeWidth={2} d=\"M15 19l-7-7 7-7\" />\n </svg>\n </button>\n <button\n onClick={() => SlideshowManager.next()}\n disabled={state.isTransitioning}\n className=\"absolute right-2 top-1/2 -translate-y-1/2 p-2 bg-black/40 hover:bg-black/60 rounded-full text-white transition-colors disabled:opacity-50\"\n aria-label=\"Next slide\"\n >\n <svg className=\"w-5 h-5\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <path strokeLinecap=\"round\" strokeLinejoin=\"round\" strokeWidth={2} d=\"M9 5l7 7-7 7\" />\n </svg>\n </button>\n </>\n )}\n\n {/* Navigation dots */}\n {showDots && state.totalSlides > 1 && (\n <div className=\"absolute bottom-3 left-1/2 -translate-x-1/2 flex gap-1.5\">\n {state.images.map((_, index) => (\n <button\n key={index}\n onClick={() => SlideshowManager.goTo(index)}\n disabled={state.isTransitioning}\n className={`w-2 h-2 rounded-full transition-colors ${\n index === state.currentIndex\n ? 'bg-white'\n : 'bg-white/40 hover:bg-white/60'\n }`}\n aria-label={`Go to slide ${index + 1}`}\n />\n ))}\n </div>\n )}\n\n {/* Overlay children - pointer-events managed by individual OverlaySlot components */}\n <div className=\"absolute inset-0 z-10 pointer-events-none\">\n {children}\n </div>\n </div>\n </SlideshowContext.Provider>\n );\n}\n";
8
8
  //# sourceMappingURL=PersistentSlideshow.tsx.template.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"PersistentSlideshow.tsx.template.d.ts","sourceRoot":"","sources":["../../../src/templates/slideshow/PersistentSlideshow.tsx.template.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,eAAO,MAAM,2BAA2B,s0SA8QvC,CAAC"}
1
+ {"version":3,"file":"PersistentSlideshow.tsx.template.d.ts","sourceRoot":"","sources":["../../../src/templates/slideshow/PersistentSlideshow.tsx.template.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,eAAO,MAAM,2BAA2B,8qUA2SvC,CAAC"}
@@ -70,62 +70,93 @@ export function PersistentSlideshow({
70
70
  children
71
71
  }: PersistentSlideshowProps) {
72
72
  const [state, setState] = useState<SlideshowState>(SlideshowManager.getState());
73
+ const containerRef = useRef<HTMLDivElement>(null);
73
74
  const touchStartRef = useRef<{ x: number; y: number; time: number } | null>(null);
74
75
  const touchCurrentRef = useRef<{ x: number; y: number } | null>(null);
75
76
 
76
- // Touch/swipe handlers with tracking for more reliable detection
77
- const handleTouchStart = useCallback((e: React.TouchEvent) => {
78
- if (!swipeable || state.totalSlides <= 1) return;
79
- const touch = e.touches[0];
80
- touchStartRef.current = {
81
- x: touch.clientX,
82
- y: touch.clientY,
83
- time: Date.now(),
77
+ // Touch/swipe handlers using native events (React touch events are passive)
78
+ useEffect(() => {
79
+ const container = containerRef.current;
80
+ if (!container || !swipeable || state.totalSlides <= 1) return;
81
+
82
+ const handleTouchStart = (e: TouchEvent) => {
83
+ const touch = e.touches[0];
84
+ if (!touch) return;
85
+
86
+ touchStartRef.current = {
87
+ x: touch.clientX,
88
+ y: touch.clientY,
89
+ time: Date.now(),
90
+ };
91
+ touchCurrentRef.current = { x: touch.clientX, y: touch.clientY };
84
92
  };
85
- touchCurrentRef.current = { x: touch.clientX, y: touch.clientY };
86
- }, [swipeable, state.totalSlides]);
87
-
88
- const handleTouchMove = useCallback((e: React.TouchEvent) => {
89
- if (!swipeable || !touchStartRef.current) return;
90
- const touch = e.touches[0];
91
- touchCurrentRef.current = { x: touch.clientX, y: touch.clientY };
92
-
93
- // Prevent vertical scroll if horizontal swipe is detected
94
- const deltaX = touch.clientX - touchStartRef.current.x;
95
- const deltaY = touch.clientY - touchStartRef.current.y;
96
- if (Math.abs(deltaX) > Math.abs(deltaY) && Math.abs(deltaX) > 10) {
97
- e.preventDefault();
98
- }
99
- }, [swipeable]);
100
93
 
101
- const handleTouchEnd = useCallback(() => {
102
- if (!swipeable || !touchStartRef.current || !touchCurrentRef.current || state.isTransitioning) {
94
+ const handleTouchMove = (e: TouchEvent) => {
95
+ if (!touchStartRef.current) return;
96
+ const touch = e.touches[0];
97
+ if (!touch) return;
98
+
99
+ touchCurrentRef.current = { x: touch.clientX, y: touch.clientY };
100
+
101
+ // Prevent default and stop propagation for horizontal swipes
102
+ const deltaX = touch.clientX - touchStartRef.current.x;
103
+ const deltaY = touch.clientY - touchStartRef.current.y;
104
+ if (Math.abs(deltaX) > Math.abs(deltaY) && Math.abs(deltaX) > 10) {
105
+ e.preventDefault();
106
+ e.stopPropagation();
107
+ }
108
+ };
109
+
110
+ const handleTouchEnd = (e: TouchEvent) => {
111
+ if (!touchStartRef.current || !touchCurrentRef.current) {
112
+ touchStartRef.current = null;
113
+ touchCurrentRef.current = null;
114
+ return;
115
+ }
116
+
117
+ // Don't process if transitioning
118
+ if (state.isTransitioning) {
119
+ touchStartRef.current = null;
120
+ touchCurrentRef.current = null;
121
+ return;
122
+ }
123
+
124
+ const deltaX = touchCurrentRef.current.x - touchStartRef.current.x;
125
+ const deltaY = touchCurrentRef.current.y - touchStartRef.current.y;
126
+ const deltaTime = Date.now() - touchStartRef.current.time;
127
+
128
+ // Lower threshold for smaller tiles - 30px minimum swipe distance
129
+ const minSwipeDistance = 30;
130
+ const isHorizontalSwipe = Math.abs(deltaX) > Math.abs(deltaY);
131
+ const isValidSwipe = Math.abs(deltaX) > minSwipeDistance && isHorizontalSwipe;
132
+ const isQuickSwipe = deltaTime < 500 || Math.abs(deltaX) > 60;
133
+
134
+ if (isValidSwipe && isQuickSwipe) {
135
+ e.preventDefault();
136
+ e.stopPropagation();
137
+
138
+ if (deltaX > 0) {
139
+ SlideshowManager.prev();
140
+ } else {
141
+ SlideshowManager.next();
142
+ }
143
+ }
144
+
103
145
  touchStartRef.current = null;
104
146
  touchCurrentRef.current = null;
105
- return;
106
- }
147
+ };
107
148
 
108
- const deltaX = touchCurrentRef.current.x - touchStartRef.current.x;
109
- const deltaY = touchCurrentRef.current.y - touchStartRef.current.y;
110
- const deltaTime = Date.now() - touchStartRef.current.time;
111
-
112
- // Minimum swipe distance (50px) and max time (300ms for quick swipe, or slower with more distance)
113
- const minSwipeDistance = 50;
114
- const isHorizontalSwipe = Math.abs(deltaX) > Math.abs(deltaY);
115
- const isValidSwipe = Math.abs(deltaX) > minSwipeDistance && isHorizontalSwipe;
116
- const isQuickSwipe = deltaTime < 300 || Math.abs(deltaX) > 100;
117
-
118
- if (isValidSwipe && isQuickSwipe) {
119
- if (deltaX > 0) {
120
- SlideshowManager.prev();
121
- } else {
122
- SlideshowManager.next();
123
- }
124
- }
149
+ // Use passive: false on all to allow preventDefault()
150
+ container.addEventListener('touchstart', handleTouchStart, { passive: false });
151
+ container.addEventListener('touchmove', handleTouchMove, { passive: false });
152
+ container.addEventListener('touchend', handleTouchEnd, { passive: false });
125
153
 
126
- touchStartRef.current = null;
127
- touchCurrentRef.current = null;
128
- }, [swipeable, state.isTransitioning]);
154
+ return () => {
155
+ container.removeEventListener('touchstart', handleTouchStart);
156
+ container.removeEventListener('touchmove', handleTouchMove);
157
+ container.removeEventListener('touchend', handleTouchEnd);
158
+ };
159
+ }, [swipeable, state.totalSlides, state.isTransitioning]);
129
160
 
130
161
  useEffect(() => {
131
162
  // Initialize slideshow with images
@@ -197,11 +228,9 @@ export function PersistentSlideshow({
197
228
  return (
198
229
  <SlideshowContext.Provider value={contextValue}>
199
230
  <div
231
+ ref={containerRef}
200
232
  className={\`relative w-full h-full bg-black overflow-hidden \${className}\`}
201
- style={{ touchAction: swipeable && state.totalSlides > 1 ? 'pan-y pinch-zoom' : 'auto' }}
202
- onTouchStart={handleTouchStart}
203
- onTouchMove={handleTouchMove}
204
- onTouchEnd={handleTouchEnd}
233
+ style={{ touchAction: swipeable && state.totalSlides > 1 ? 'none' : 'auto' }}
205
234
  >
206
235
  {/* Image container */}
207
236
  <div className="relative w-full h-full">
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@thewhateverapp/tile-sdk",
3
- "version": "0.13.12",
3
+ "version": "0.13.14",
4
4
  "description": "SDK for building interactive tiles on The Whatever App platform",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",