@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.
- package/dist/bridge/TileBridge.d.ts.map +1 -1
- package/dist/bridge/TileBridge.js +22 -2
- package/dist/excalibur/index.d.ts +17 -3
- package/dist/excalibur/index.d.ts.map +1 -1
- package/dist/excalibur/index.js +32 -4
- package/dist/react/ExcaliburGame.d.ts +15 -1
- package/dist/react/ExcaliburGame.d.ts.map +1 -1
- package/dist/react/ExcaliburGame.js +30 -1
- package/dist/templates/slideshow/layout.tsx.template.d.ts +17 -7
- package/dist/templates/slideshow/layout.tsx.template.d.ts.map +1 -1
- package/dist/templates/slideshow/layout.tsx.template.js +403 -185
- package/dist/templates/video/layout.tsx.template.d.ts +17 -7
- package/dist/templates/video/layout.tsx.template.d.ts.map +1 -1
- package/dist/templates/video/layout.tsx.template.js +575 -122
- package/package.json +4 -1
|
@@ -1,11 +1,21 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Slideshow
|
|
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 (
|
|
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 {
|
|
45
|
-
import { useTile } from '@thewhateverapp/tile-sdk';
|
|
54
|
+
import { Slideshow, useSlideshowState, useTile } from '@thewhateverapp/tile-sdk';
|
|
46
55
|
|
|
47
|
-
export default function
|
|
48
|
-
|
|
49
|
-
|
|
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 -
|
|
54
|
-
<
|
|
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-
|
|
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
|
-
</
|
|
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
|
-
{/*
|
|
67
|
-
|
|
68
|
-
<div className="
|
|
69
|
-
|
|
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
|
-
|
|
129
|
+
)}
|
|
72
130
|
|
|
73
|
-
{/* Caption
|
|
131
|
+
{/* Caption overlay */}
|
|
74
132
|
{state.images[state.currentIndex]?.caption && (
|
|
75
|
-
<
|
|
76
|
-
<
|
|
77
|
-
|
|
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 {
|
|
153
|
+
import { useState, useEffect, useCallback } from 'react';
|
|
154
|
+
import { Slideshow, useSlideshowState, useTile } from '@thewhateverapp/tile-sdk';
|
|
94
155
|
|
|
95
|
-
export default function
|
|
96
|
-
|
|
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
|
-
{
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
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
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
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
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
<div className="
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
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={
|
|
145
|
-
|
|
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
|
-
<
|
|
150
|
-
|
|
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
|
-
<
|
|
157
|
-
|
|
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
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
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
|
-
</
|
|
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,
|
|
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
|
-
|
|
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-
|
|
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
|
-
</
|
|
362
|
+
</div>
|
|
217
363
|
|
|
218
|
-
{
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
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
|
-
|
|
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
|
-
<
|
|
228
|
-
<
|
|
229
|
-
|
|
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,
|
|
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
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
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
|
-
|
|
292
|
-
|
|
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={
|
|
316
|
-
|
|
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
|
-
<
|
|
321
|
-
|
|
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
|
-
<
|
|
328
|
-
|
|
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
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
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
|
-
</
|
|
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={
|
|
363
|
-
showArrows={
|
|
364
|
-
|
|
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>
|