@thewhateverapp/tile-sdk 0.16.3 → 0.16.7

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,11 +1,21 @@
1
1
  /**
2
- * Slideshow Template - Layout
2
+ * Slideshow Templates - Auto-generated from base-image-worker
3
+ *
4
+ * DO NOT EDIT THIS FILE DIRECTLY!
5
+ * Edit the source files in platform/workers/base-image-worker/ and run:
6
+ * pnpm sync-templates
7
+ *
8
+ * Source files:
9
+ * - Tile page: workers/base-image-worker/src/app/tile/page.tsx
10
+ * - Full page: workers/base-image-worker/src/app/page/page.tsx
11
+ */
12
+ /**
13
+ * Slideshow Layout Template
3
14
  *
4
15
  * This layout wraps both /tile and /page routes using a route group.
5
16
  * The Slideshow is rendered ONCE here and persists across route navigation.
6
17
  *
7
18
  * File path: src/app/(slideshow)/layout.tsx
8
- * Routes: src/app/(slideshow)/tile/page.tsx, src/app/(slideshow)/page/page.tsx
9
19
  */
10
20
  export const slideshowLayoutTemplate = `'use client';
11
21
 
@@ -34,47 +44,97 @@ export default function SlideshowLayout({ children }: { children: React.ReactNod
34
44
  /**
35
45
  * Slideshow Tile Overlay Template
36
46
  *
37
- * Overlay-only component for tile view (256×554px).
47
+ * Overlay-only component for tile view (256x554px).
38
48
  * Slideshow is already mounted in the parent layout.
39
49
  *
40
50
  * File path: src/app/(slideshow)/tile/page.tsx
41
51
  */
42
52
  export const slideshowTileOverlayTemplate = `'use client';
43
53
 
44
- import { useSlideshowState, OverlaySlot, GradientOverlay } from '@thewhateverapp/tile-sdk';
45
- import { useTile } from '@thewhateverapp/tile-sdk';
54
+ import { Slideshow, useSlideshowState, useTile } from '@thewhateverapp/tile-sdk';
46
55
 
47
- export default function TileOverlay() {
48
- const tile = useTile();
49
- const { state } = useSlideshowState();
56
+ export default function SlideshowTilePage() {
57
+
58
+ if (!state.images || state.images.length === 0) {
59
+ return (
60
+ <div className="w-full h-full bg-black flex items-center justify-center">
61
+ <p className="text-white/60 text-sm">No images configured</p>
62
+ </div>
63
+ );
64
+ }
50
65
 
66
+ return (
67
+ <Slideshow
68
+ images={state.images}
69
+ autoAdvance={false} // No auto-cycling in tile mode - user swipes manually
70
+ intervalMs={SLIDESHOW_CONFIG.intervalMs}
71
+ transition={SLIDESHOW_CONFIG.transition}
72
+ showDots={false}
73
+ showArrows={false}
74
+ swipeable={true}
75
+ className="w-full h-full"
76
+ >
77
+ <TileOverlay />
78
+ </Slideshow>
79
+ );
80
+ }
81
+
82
+ function TileOverlay() {
83
+ const tile = useTile();
84
+ const { state, controls } = useSlideshowState();
85
+
51
86
  return (
52
87
  <>
53
- {/* Expand button - opens full page view */}
54
- <OverlaySlot position="top-right">
88
+ {/* Expand button - top right */}
89
+ <div className="absolute top-3 right-3 z-20 pointer-events-auto">
55
90
  <button
56
91
  onClick={() => tile.navigateToPage()}
57
92
  className="bg-black/40 backdrop-blur-sm p-2 rounded-full text-white hover:bg-black/60 transition-colors"
58
93
  aria-label="Expand to full view"
59
94
  >
60
- <svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
95
+ <svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
61
96
  <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 8V4m0 0h4M4 4l5 5m11-1V4m0 0h-4m4 0l-5 5M4 16v4m0 0h4m-4 0l5-5m11 5l-5-5m5 5v-4m0 4h-4" />
62
97
  </svg>
63
98
  </button>
64
- </OverlaySlot>
99
+ </div>
100
+
101
+ {/* Slide counter - top left */}
102
+ {state.totalSlides > 1 && (
103
+ <div className="absolute top-3 left-3 z-10 pointer-events-none">
104
+ <div className="bg-black/50 backdrop-blur-sm px-2 py-1 rounded text-xs text-white font-medium">
105
+ {state.currentIndex + 1} / {state.totalSlides}
106
+ </div>
107
+ </div>
108
+ )}
65
109
 
66
- {/* Slide counter */}
67
- <OverlaySlot position="top-left">
68
- <div className="bg-black/50 backdrop-blur-sm px-2 py-1 rounded text-xs text-white">
69
- {state.currentIndex + 1} / {state.totalSlides}
110
+ {/* Navigation dots at bottom */}
111
+ {state.totalSlides > 1 && (
112
+ <div className="absolute bottom-3 left-1/2 -translate-x-1/2 z-20 pointer-events-auto">
113
+ <div className="flex gap-1.5">
114
+ {state.images.map((_, index) => (
115
+ <button
116
+ key={index}
117
+ onClick={() => controls.goTo(index)}
118
+ disabled={state.isTransitioning}
119
+ className={\`w-2 h-2 rounded-full transition-colors \${
120
+ index === state.currentIndex
121
+ ? 'bg-white'
122
+ : 'bg-white/40 hover:bg-white/60'
123
+ }\`}
124
+ aria-label={\`Go to slide \${index + 1}\`}
125
+ />
126
+ ))}
127
+ </div>
70
128
  </div>
71
- </OverlaySlot>
129
+ )}
72
130
 
73
- {/* Caption at bottom */}
131
+ {/* Caption overlay */}
74
132
  {state.images[state.currentIndex]?.caption && (
75
- <GradientOverlay position="bottom" className="p-4">
76
- <p className="text-white text-sm">{state.images[state.currentIndex].caption}</p>
77
- </GradientOverlay>
133
+ <div className="absolute bottom-10 left-3 right-3 z-10 pointer-events-none">
134
+ <div className="bg-black/50 backdrop-blur-sm px-3 py-2 rounded text-white text-sm">
135
+ {state.images[state.currentIndex].caption}
136
+ </div>
137
+ </div>
78
138
  )}
79
139
  </>
80
140
  );
@@ -90,94 +150,181 @@ export default function TileOverlay() {
90
150
  */
91
151
  export const slideshowPageOverlayTemplate = `'use client';
92
152
 
93
- import { useSlideshowState, OverlaySlot, GradientOverlay } from '@thewhateverapp/tile-sdk';
153
+ import { useState, useEffect, useCallback } from 'react';
154
+ import { Slideshow, useSlideshowState, useTile } from '@thewhateverapp/tile-sdk';
94
155
 
95
- export default function PageOverlay() {
96
- const { state, controls } = useSlideshowState();
156
+ export default function SlideshowPageView() {
157
+
158
+ if (!state.images || state.images.length === 0) {
159
+ return (
160
+ <div className="w-full h-full bg-black flex items-center justify-center">
161
+ <p className="text-white/60 text-sm">No images configured</p>
162
+ </div>
163
+ );
164
+ }
97
165
 
98
166
  return (
99
- <>
100
- {/* Slide counter */}
101
- <OverlaySlot position="top-right">
102
- <div className="bg-black/50 backdrop-blur-sm px-3 py-1.5 rounded-full text-sm text-white">
103
- {state.currentIndex + 1} / {state.totalSlides}
104
- </div>
105
- </OverlaySlot>
167
+ <Slideshow
168
+ images={state.images}
169
+ autoAdvance={true} // Always auto-cycle in page/fullscreen mode
170
+ intervalMs={SLIDESHOW_CONFIG.intervalMs}
171
+ transition={SLIDESHOW_CONFIG.transition}
172
+ showDots={true}
173
+ showArrows={true}
174
+ swipeable={true}
175
+ className="w-full h-full object-cover"
176
+ >
177
+ <PageOverlay />
178
+ </Slideshow>
179
+ );
180
+ }
106
181
 
107
- {/* Navigation arrows (larger for full page) */}
108
- <OverlaySlot position="center-left">
109
- <button
110
- onClick={controls.prev}
111
- className="bg-black/40 backdrop-blur-sm p-3 rounded-full text-white hover:bg-black/60 transition-colors"
112
- aria-label="Previous slide"
113
- >
114
- <svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
115
- <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 19l-7-7 7-7" />
116
- </svg>
117
- </button>
118
- </OverlaySlot>
182
+ function PageOverlay() {
183
+ const tile = useTile();
184
+ const { state, controls } = useSlideshowState();
185
+ const [showControls, setShowControls] = useState(true);
186
+ const [hideTimeout, setHideTimeout] = useState<NodeJS.Timeout | null>(null);
187
+
188
+ // Auto-hide controls after 3 seconds when slideshow is playing
189
+ const resetHideTimer = useCallback(() => {
190
+ if (hideTimeout) {
191
+ clearTimeout(hideTimeout);
192
+ }
193
+ setShowControls(true);
194
+
195
+ if (!state.isPaused) {
196
+ const timeout = setTimeout(() => {
197
+ setShowControls(false);
198
+ }, 3000);
199
+ setHideTimeout(timeout);
200
+ }
201
+ }, [state.isPaused, hideTimeout]);
202
+
203
+ // Show controls when slideshow is paused
204
+ useEffect(() => {
205
+ if (state.isPaused) {
206
+ setShowControls(true);
207
+ if (hideTimeout) {
208
+ clearTimeout(hideTimeout);
209
+ setHideTimeout(null);
210
+ }
211
+ } else {
212
+ resetHideTimer();
213
+ }
214
+ }, [state.isPaused]);
215
+
216
+ // Cleanup timeout on unmount
217
+ useEffect(() => {
218
+ return () => {
219
+ if (hideTimeout) {
220
+ clearTimeout(hideTimeout);
221
+ }
222
+ };
223
+ }, [hideTimeout]);
224
+
225
+ // Toggle controls on tap/click
226
+ const handleToggleControls = useCallback((e: React.MouseEvent | React.TouchEvent) => {
227
+ const target = e.target as HTMLElement;
228
+ if (target.closest('button, .controls-area')) {
229
+ return;
230
+ }
231
+ e.preventDefault();
232
+
233
+ if (showControls) {
234
+ setShowControls(false);
235
+ } else {
236
+ resetHideTimer();
237
+ }
238
+ }, [showControls, resetHideTimer]);
239
+
240
+ // Handle mouse movement to show controls
241
+ const handleMouseMove = useCallback(() => {
242
+ if (!state.isPaused) {
243
+ resetHideTimer();
244
+ }
245
+ }, [state.isPaused, resetHideTimer]);
119
246
 
120
- <OverlaySlot position="center-right">
121
- <button
122
- onClick={controls.next}
123
- className="bg-black/40 backdrop-blur-sm p-3 rounded-full text-white hover:bg-black/60 transition-colors"
124
- aria-label="Next slide"
125
- >
126
- <svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
127
- <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
128
- </svg>
129
- </button>
130
- </OverlaySlot>
131
-
132
- {/* Caption and controls at bottom */}
133
- <GradientOverlay position="bottom" className="p-6">
134
- <div className="space-y-3">
135
- {/* Caption */}
136
- {state.images[state.currentIndex]?.caption && (
137
- <p className="text-white text-base">{state.images[state.currentIndex].caption}</p>
138
- )}
139
-
140
- {/* Controls row */}
141
- <div className="flex items-center justify-between">
142
- {/* Play/Pause autoplay */}
247
+ return (
248
+ <div
249
+ className="absolute inset-0 z-20 pointer-events-auto"
250
+ onClick={handleToggleControls}
251
+ onTouchEnd={handleToggleControls}
252
+ onMouseMove={handleMouseMove}
253
+ >
254
+ {/* Controls - toggleable */}
255
+ <div
256
+ className={\`controls-area transition-opacity duration-300 \${
257
+ showControls ? 'opacity-100' : 'opacity-0 pointer-events-none'
258
+ }\`}
259
+ >
260
+ {/* Top bar with slide counter and controls */}
261
+ <div className="absolute top-0 left-0 right-0 h-20 bg-gradient-to-b from-black/60 to-transparent pointer-events-none" />
262
+
263
+ {/* Slide counter - top right */}
264
+ {state.totalSlides > 1 && (
265
+ <div className="absolute top-4 right-4 z-10 pointer-events-auto">
266
+ <div className="bg-black/50 backdrop-blur-sm px-3 py-1.5 rounded text-sm text-white font-medium">
267
+ {state.currentIndex + 1} / {state.totalSlides}
268
+ </div>
269
+ </div>
270
+ )}
271
+
272
+ {/* Play/Pause button - top left */}
273
+ {state.totalSlides > 1 && (
274
+ <div className="absolute top-4 left-4 z-20 pointer-events-auto">
143
275
  <button
144
- onClick={controls.toggle}
145
- className="text-white hover:text-white/80 transition-colors flex items-center gap-2 text-sm"
276
+ onClick={(e) => {
277
+ e.stopPropagation();
278
+ controls.toggle();
279
+ }}
280
+ className="bg-black/40 backdrop-blur-sm p-2.5 rounded-full text-white hover:bg-black/60 transition-colors"
281
+ aria-label={state.isPaused ? 'Resume slideshow' : 'Pause slideshow'}
146
282
  >
147
283
  {state.isPaused ? (
148
- <>
149
- <svg className="w-5 h-5" fill="currentColor" viewBox="0 0 24 24">
150
- <path d="M8 5v14l11-7z" />
151
- </svg>
152
- <span>Play slideshow</span>
153
- </>
284
+ <svg className="w-5 h-5" fill="currentColor" viewBox="0 0 24 24">
285
+ <path d="M8 5v14l11-7z" />
286
+ </svg>
154
287
  ) : (
155
- <>
156
- <svg className="w-5 h-5" fill="currentColor" viewBox="0 0 24 24">
157
- <path d="M6 4h4v16H6V4zm8 0h4v16h-4V4z" />
158
- </svg>
159
- <span>Pause slideshow</span>
160
- </>
288
+ <svg className="w-5 h-5" fill="currentColor" viewBox="0 0 24 24">
289
+ <path d="M6 4h4v16H6V4zm8 0h4v16h-4V4z" />
290
+ </svg>
161
291
  )}
162
292
  </button>
163
-
164
- {/* Dot indicators */}
165
- <div className="flex gap-2">
166
- {state.images.map((_, idx) => (
167
- <button
168
- key={idx}
169
- onClick={() => controls.goTo(idx)}
170
- className={\`w-2 h-2 rounded-full transition-colors \${
171
- idx === state.currentIndex ? 'bg-white' : 'bg-white/40 hover:bg-white/60'
172
- }\`}
173
- aria-label={\`Go to slide \${idx + 1}\`}
174
- />
175
- ))}
293
+ </div>
294
+ )}
295
+
296
+ {/* Bottom bar with caption and minimize */}
297
+ <div className="absolute bottom-0 left-0 right-0 h-32 bg-gradient-to-t from-black/60 to-transparent pointer-events-none" />
298
+
299
+ {/* Caption overlay at bottom */}
300
+ {state.images[state.currentIndex]?.caption && (
301
+ <div className="absolute bottom-16 left-4 right-16 z-10 pointer-events-none">
302
+ <div className="text-white">
303
+ <p className="text-base drop-shadow-lg">{state.images[state.currentIndex].caption}</p>
304
+ {state.images[state.currentIndex]?.alt && (
305
+ <p className="text-sm text-white/70 mt-1 drop-shadow-lg">{state.images[state.currentIndex].alt}</p>
306
+ )}
176
307
  </div>
177
308
  </div>
309
+ )}
310
+
311
+ {/* Minimize button - bottom right */}
312
+ <div className="absolute bottom-4 right-4 z-20 pointer-events-auto">
313
+ <button
314
+ onClick={(e) => {
315
+ e.stopPropagation();
316
+ tile.navigateToTile();
317
+ }}
318
+ className="bg-black/40 backdrop-blur-sm p-2.5 rounded-full text-white hover:bg-black/60 transition-colors"
319
+ aria-label="Minimize to tile"
320
+ >
321
+ <svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
322
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M20 12H4" />
323
+ </svg>
324
+ </button>
178
325
  </div>
179
- </GradientOverlay>
180
- </>
326
+ </div>
327
+ </div>
181
328
  );
182
329
  }
183
330
  `;
@@ -191,42 +338,63 @@ export default function PageOverlay() {
191
338
  */
192
339
  export const slideshowPreviewTileTemplate = `'use client';
193
340
 
194
- import { Slideshow, useSlideshowState, OverlaySlot, GradientOverlay, useTile } from '@thewhateverapp/tile-sdk';
341
+ import { Slideshow, useSlideshowState, useTile } from '@thewhateverapp/tile-sdk';
195
342
 
196
343
  // Slideshow configuration injected at generation time
197
344
  const SLIDESHOW_CONFIG = __SLIDESHOW_CONFIG__;
198
345
 
199
346
  function TileOverlay() {
200
347
  const tile = useTile();
201
- const { state } = useSlideshowState();
348
+ const { state, controls } = useSlideshowState();
202
349
 
203
350
  return (
204
351
  <>
205
- {/* Expand button - opens full page view */}
206
- <OverlaySlot position="top-right">
352
+ <div className="absolute top-3 right-3 z-20 pointer-events-auto">
207
353
  <button
208
354
  onClick={() => tile.navigateToPage()}
209
355
  className="bg-black/40 backdrop-blur-sm p-2 rounded-full text-white hover:bg-black/60 transition-colors"
210
356
  aria-label="Expand to full view"
211
357
  >
212
- <svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
358
+ <svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
213
359
  <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 8V4m0 0h4M4 4l5 5m11-1V4m0 0h-4m4 0l-5 5M4 16v4m0 0h4m-4 0l5-5m11 5l-5-5m5 5v-4m0 4h-4" />
214
360
  </svg>
215
361
  </button>
216
- </OverlaySlot>
362
+ </div>
217
363
 
218
- {/* Slide counter */}
219
- <OverlaySlot position="top-left">
220
- <div className="bg-black/50 backdrop-blur-sm px-2 py-1 rounded text-xs text-white">
221
- {state.currentIndex + 1} / {state.totalSlides}
364
+ {state.totalSlides > 1 && (
365
+ <div className="absolute top-3 left-3 z-10 pointer-events-none">
366
+ <div className="bg-black/50 backdrop-blur-sm px-2 py-1 rounded text-xs text-white font-medium">
367
+ {state.currentIndex + 1} / {state.totalSlides}
368
+ </div>
222
369
  </div>
223
- </OverlaySlot>
370
+ )}
371
+
372
+ {state.totalSlides > 1 && (
373
+ <div className="absolute bottom-3 left-1/2 -translate-x-1/2 z-20 pointer-events-auto">
374
+ <div className="flex gap-1.5">
375
+ {state.images.map((_, index) => (
376
+ <button
377
+ key={index}
378
+ onClick={() => controls.goTo(index)}
379
+ disabled={state.isTransitioning}
380
+ className={\`w-2 h-2 rounded-full transition-colors \${
381
+ index === state.currentIndex
382
+ ? 'bg-white'
383
+ : 'bg-white/40 hover:bg-white/60'
384
+ }\`}
385
+ aria-label={\`Go to slide \${index + 1}\`}
386
+ />
387
+ ))}
388
+ </div>
389
+ </div>
390
+ )}
224
391
 
225
- {/* Caption at bottom */}
226
392
  {state.images[state.currentIndex]?.caption && (
227
- <GradientOverlay position="bottom" className="p-4">
228
- <p className="text-white text-sm">{state.images[state.currentIndex].caption}</p>
229
- </GradientOverlay>
393
+ <div className="absolute bottom-10 left-3 right-3 z-10 pointer-events-none">
394
+ <div className="bg-black/50 backdrop-blur-sm px-3 py-2 rounded text-white text-sm">
395
+ {state.images[state.currentIndex].caption}
396
+ </div>
397
+ </div>
230
398
  )}
231
399
  </>
232
400
  );
@@ -258,97 +426,146 @@ export default function PreviewTile() {
258
426
  */
259
427
  export const slideshowPreviewPageTemplate = `'use client';
260
428
 
261
- import { Slideshow, useSlideshowState, OverlaySlot, GradientOverlay } from '@thewhateverapp/tile-sdk';
429
+ import { Slideshow, useSlideshowState, useTile } from '@thewhateverapp/tile-sdk';
430
+ import { useState, useEffect, useCallback } from 'react';
262
431
 
263
432
  // Slideshow configuration injected at generation time
264
433
  const SLIDESHOW_CONFIG = __SLIDESHOW_CONFIG__;
265
434
 
266
435
  function PageOverlay() {
436
+ const tile = useTile();
267
437
  const { state, controls } = useSlideshowState();
438
+ const [showControls, setShowControls] = useState(true);
439
+ const [hideTimeout, setHideTimeout] = useState<NodeJS.Timeout | null>(null);
440
+
441
+ const resetHideTimer = useCallback(() => {
442
+ if (hideTimeout) {
443
+ clearTimeout(hideTimeout);
444
+ }
445
+ setShowControls(true);
446
+
447
+ if (!state.isPaused) {
448
+ const timeout = setTimeout(() => {
449
+ setShowControls(false);
450
+ }, 3000);
451
+ setHideTimeout(timeout);
452
+ }
453
+ }, [state.isPaused, hideTimeout]);
454
+
455
+ useEffect(() => {
456
+ if (state.isPaused) {
457
+ setShowControls(true);
458
+ if (hideTimeout) {
459
+ clearTimeout(hideTimeout);
460
+ setHideTimeout(null);
461
+ }
462
+ } else {
463
+ resetHideTimer();
464
+ }
465
+ }, [state.isPaused]);
466
+
467
+ useEffect(() => {
468
+ return () => {
469
+ if (hideTimeout) {
470
+ clearTimeout(hideTimeout);
471
+ }
472
+ };
473
+ }, [hideTimeout]);
474
+
475
+ const handleToggleControls = useCallback((e: React.MouseEvent | React.TouchEvent) => {
476
+ const target = e.target as HTMLElement;
477
+ if (target.closest('button, .controls-area')) {
478
+ return;
479
+ }
480
+ e.preventDefault();
481
+
482
+ if (showControls) {
483
+ setShowControls(false);
484
+ } else {
485
+ resetHideTimer();
486
+ }
487
+ }, [showControls, resetHideTimer]);
488
+
489
+ const handleMouseMove = useCallback(() => {
490
+ if (!state.isPaused) {
491
+ resetHideTimer();
492
+ }
493
+ }, [state.isPaused, resetHideTimer]);
268
494
 
269
495
  return (
270
- <>
271
- {/* Slide counter */}
272
- <OverlaySlot position="top-right">
273
- <div className="bg-black/50 backdrop-blur-sm px-3 py-1.5 rounded-full text-sm text-white">
274
- {state.currentIndex + 1} / {state.totalSlides}
275
- </div>
276
- </OverlaySlot>
277
-
278
- {/* Navigation arrows (larger for full page) */}
279
- <OverlaySlot position="center-left">
280
- <button
281
- onClick={controls.prev}
282
- className="bg-black/40 backdrop-blur-sm p-3 rounded-full text-white hover:bg-black/60 transition-colors"
283
- aria-label="Previous slide"
284
- >
285
- <svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
286
- <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 19l-7-7 7-7" />
287
- </svg>
288
- </button>
289
- </OverlaySlot>
496
+ <div
497
+ className="absolute inset-0 z-20 pointer-events-auto"
498
+ onClick={handleToggleControls}
499
+ onTouchEnd={handleToggleControls}
500
+ onMouseMove={handleMouseMove}
501
+ >
502
+ <div
503
+ className={\`controls-area transition-opacity duration-300 \${
504
+ showControls ? 'opacity-100' : 'opacity-0 pointer-events-none'
505
+ }\`}
506
+ >
507
+ <div className="absolute top-0 left-0 right-0 h-20 bg-gradient-to-b from-black/60 to-transparent pointer-events-none" />
508
+
509
+ {state.totalSlides > 1 && (
510
+ <div className="absolute top-4 right-4 z-10 pointer-events-auto">
511
+ <div className="bg-black/50 backdrop-blur-sm px-3 py-1.5 rounded text-sm text-white font-medium">
512
+ {state.currentIndex + 1} / {state.totalSlides}
513
+ </div>
514
+ </div>
515
+ )}
290
516
 
291
- <OverlaySlot position="center-right">
292
- <button
293
- onClick={controls.next}
294
- className="bg-black/40 backdrop-blur-sm p-3 rounded-full text-white hover:bg-black/60 transition-colors"
295
- aria-label="Next slide"
296
- >
297
- <svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
298
- <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
299
- </svg>
300
- </button>
301
- </OverlaySlot>
302
-
303
- {/* Caption and controls at bottom */}
304
- <GradientOverlay position="bottom" className="p-6">
305
- <div className="space-y-3">
306
- {/* Caption */}
307
- {state.images[state.currentIndex]?.caption && (
308
- <p className="text-white text-base">{state.images[state.currentIndex].caption}</p>
309
- )}
310
-
311
- {/* Controls row */}
312
- <div className="flex items-center justify-between">
313
- {/* Play/Pause autoplay */}
517
+ {state.totalSlides > 1 && (
518
+ <div className="absolute top-4 left-4 z-20 pointer-events-auto">
314
519
  <button
315
- onClick={controls.toggle}
316
- className="text-white hover:text-white/80 transition-colors flex items-center gap-2 text-sm"
520
+ onClick={(e) => {
521
+ e.stopPropagation();
522
+ controls.toggle();
523
+ }}
524
+ className="bg-black/40 backdrop-blur-sm p-2.5 rounded-full text-white hover:bg-black/60 transition-colors"
525
+ aria-label={state.isPaused ? 'Resume slideshow' : 'Pause slideshow'}
317
526
  >
318
527
  {state.isPaused ? (
319
- <>
320
- <svg className="w-5 h-5" fill="currentColor" viewBox="0 0 24 24">
321
- <path d="M8 5v14l11-7z" />
322
- </svg>
323
- <span>Play slideshow</span>
324
- </>
528
+ <svg className="w-5 h-5" fill="currentColor" viewBox="0 0 24 24">
529
+ <path d="M8 5v14l11-7z" />
530
+ </svg>
325
531
  ) : (
326
- <>
327
- <svg className="w-5 h-5" fill="currentColor" viewBox="0 0 24 24">
328
- <path d="M6 4h4v16H6V4zm8 0h4v16h-4V4z" />
329
- </svg>
330
- <span>Pause slideshow</span>
331
- </>
532
+ <svg className="w-5 h-5" fill="currentColor" viewBox="0 0 24 24">
533
+ <path d="M6 4h4v16H6V4zm8 0h4v16h-4V4z" />
534
+ </svg>
332
535
  )}
333
536
  </button>
537
+ </div>
538
+ )}
539
+
540
+ <div className="absolute bottom-0 left-0 right-0 h-32 bg-gradient-to-t from-black/60 to-transparent pointer-events-none" />
334
541
 
335
- {/* Dot indicators */}
336
- <div className="flex gap-2">
337
- {state.images.map((_, idx) => (
338
- <button
339
- key={idx}
340
- onClick={() => controls.goTo(idx)}
341
- className={\`w-2 h-2 rounded-full transition-colors \${
342
- idx === state.currentIndex ? 'bg-white' : 'bg-white/40 hover:bg-white/60'
343
- }\`}
344
- aria-label={\`Go to slide \${idx + 1}\`}
345
- />
346
- ))}
542
+ {state.images[state.currentIndex]?.caption && (
543
+ <div className="absolute bottom-16 left-4 right-16 z-10 pointer-events-none">
544
+ <div className="text-white">
545
+ <p className="text-base drop-shadow-lg">{state.images[state.currentIndex].caption}</p>
546
+ {state.images[state.currentIndex]?.alt && (
547
+ <p className="text-sm text-white/70 mt-1 drop-shadow-lg">{state.images[state.currentIndex].alt}</p>
548
+ )}
347
549
  </div>
348
550
  </div>
551
+ )}
552
+
553
+ <div className="absolute bottom-4 right-4 z-20 pointer-events-auto">
554
+ <button
555
+ onClick={(e) => {
556
+ e.stopPropagation();
557
+ tile.navigateToTile();
558
+ }}
559
+ className="bg-black/40 backdrop-blur-sm p-2.5 rounded-full text-white hover:bg-black/60 transition-colors"
560
+ aria-label="Minimize to tile"
561
+ >
562
+ <svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
563
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M20 12H4" />
564
+ </svg>
565
+ </button>
349
566
  </div>
350
- </GradientOverlay>
351
- </>
567
+ </div>
568
+ </div>
352
569
  );
353
570
  }
354
571
 
@@ -359,9 +576,10 @@ export default function PreviewPage() {
359
576
  autoAdvance={SLIDESHOW_CONFIG.autoAdvance}
360
577
  intervalMs={SLIDESHOW_CONFIG.intervalMs}
361
578
  transition={SLIDESHOW_CONFIG.transition}
362
- showDots={false}
363
- showArrows={false}
364
- className="w-full h-full"
579
+ showDots={true}
580
+ showArrows={true}
581
+ swipeable={true}
582
+ className="w-full h-full object-cover"
365
583
  >
366
584
  <PageOverlay />
367
585
  </Slideshow>