@thewhateverapp/tile-sdk 0.17.11 → 0.17.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.
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/react/confetti.d.ts +85 -0
- package/dist/react/confetti.d.ts.map +1 -0
- package/dist/react/confetti.js +134 -0
- package/dist/react/index.d.ts +2 -0
- package/dist/react/index.d.ts.map +1 -1
- package/dist/react/index.js +2 -0
- package/dist/templates/video/layout.tsx.template.d.ts +2 -2
- package/dist/templates/video/layout.tsx.template.d.ts.map +1 -1
- package/dist/templates/video/layout.tsx.template.js +6 -0
- package/package.json +3 -1
package/dist/index.d.ts
CHANGED
|
@@ -9,6 +9,8 @@ export { VideoPlayer, useVideoState, useVideo, // Alias for useVideoState
|
|
|
9
9
|
useCuePoint, useCuePoints, useVideoProgress, Slideshow, useSlideshowState, useSlideshow, // Alias for useSlideshowState
|
|
10
10
|
OverlaySlot, FullOverlay, GradientOverlay, } from './react/overlay/index.js';
|
|
11
11
|
export type { VideoState, VideoControls, VideoContextValue, VideoPlayerProps, CuePoint, SlideImage, SlideshowState, SlideshowControls, SlideshowContextValue, SlideshowProps, SlotPosition, OverlaySlotProps, FullOverlayProps, GradientOverlayProps, } from './react/overlay/index.js';
|
|
12
|
+
export { confetti } from './react/confetti.js';
|
|
13
|
+
export type { ConfettiOptions } from './react/confetti.js';
|
|
12
14
|
export { getTileBridge, TileBridge } from './bridge/TileBridge.js';
|
|
13
15
|
export type { TileMessage, TileConfig, TileTokenData, KeyboardState, VisibilityState } from './bridge/TileBridge.js';
|
|
14
16
|
export { StateClient } from './state/StateClient.js';
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,YAAY,EAAE,MAAM,yBAAyB,CAAC;AACvD,OAAO,EAAE,OAAO,EAAE,MAAM,oBAAoB,CAAC;AAC7C,OAAO,EAAE,iBAAiB,EAAE,MAAM,8BAA8B,CAAC;AACjE,OAAO,EAAE,WAAW,EAAE,MAAM,wBAAwB,CAAC;AACrD,OAAO,EAAE,WAAW,EAAE,MAAM,wBAAwB,CAAC;AACrD,OAAO,EAAE,aAAa,EAAE,MAAM,0BAA0B,CAAC;AACzD,OAAO,EAAE,QAAQ,EAAE,MAAM,qBAAqB,CAAC;AAG/C,OAAO,EAEL,WAAW,EACX,aAAa,EACb,QAAQ,EAAE,0BAA0B;AACpC,WAAW,EACX,YAAY,EACZ,gBAAgB,EAEhB,SAAS,EACT,iBAAiB,EACjB,YAAY,EAAE,8BAA8B;AAE5C,WAAW,EACX,WAAW,EACX,eAAe,GAChB,MAAM,0BAA0B,CAAC;AAClC,YAAY,EAEV,UAAU,EACV,aAAa,EACb,iBAAiB,EACjB,gBAAgB,EAChB,QAAQ,EAER,UAAU,EACV,cAAc,EACd,iBAAiB,EACjB,qBAAqB,EACrB,cAAc,EAEd,YAAY,EACZ,gBAAgB,EAChB,gBAAgB,EAChB,oBAAoB,GACrB,MAAM,0BAA0B,CAAC;AAGlC,OAAO,EAAE,aAAa,EAAE,UAAU,EAAE,MAAM,wBAAwB,CAAC;AACnE,YAAY,EAAE,WAAW,EAAE,UAAU,EAAE,aAAa,EAAE,aAAa,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAC;AAGrH,OAAO,EAAE,WAAW,EAAE,MAAM,wBAAwB,CAAC;AACrD,YAAY,EAAE,SAAS,EAAE,YAAY,EAAE,MAAM,wBAAwB,CAAC;AAGtE,cAAc,kBAAkB,CAAC;AAGjC,cAAc,YAAY,CAAC;AAG3B,cAAc,sBAAsB,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,YAAY,EAAE,MAAM,yBAAyB,CAAC;AACvD,OAAO,EAAE,OAAO,EAAE,MAAM,oBAAoB,CAAC;AAC7C,OAAO,EAAE,iBAAiB,EAAE,MAAM,8BAA8B,CAAC;AACjE,OAAO,EAAE,WAAW,EAAE,MAAM,wBAAwB,CAAC;AACrD,OAAO,EAAE,WAAW,EAAE,MAAM,wBAAwB,CAAC;AACrD,OAAO,EAAE,aAAa,EAAE,MAAM,0BAA0B,CAAC;AACzD,OAAO,EAAE,QAAQ,EAAE,MAAM,qBAAqB,CAAC;AAG/C,OAAO,EAEL,WAAW,EACX,aAAa,EACb,QAAQ,EAAE,0BAA0B;AACpC,WAAW,EACX,YAAY,EACZ,gBAAgB,EAEhB,SAAS,EACT,iBAAiB,EACjB,YAAY,EAAE,8BAA8B;AAE5C,WAAW,EACX,WAAW,EACX,eAAe,GAChB,MAAM,0BAA0B,CAAC;AAClC,YAAY,EAEV,UAAU,EACV,aAAa,EACb,iBAAiB,EACjB,gBAAgB,EAChB,QAAQ,EAER,UAAU,EACV,cAAc,EACd,iBAAiB,EACjB,qBAAqB,EACrB,cAAc,EAEd,YAAY,EACZ,gBAAgB,EAChB,gBAAgB,EAChB,oBAAoB,GACrB,MAAM,0BAA0B,CAAC;AAGlC,OAAO,EAAE,QAAQ,EAAE,MAAM,qBAAqB,CAAC;AAC/C,YAAY,EAAE,eAAe,EAAE,MAAM,qBAAqB,CAAC;AAG3D,OAAO,EAAE,aAAa,EAAE,UAAU,EAAE,MAAM,wBAAwB,CAAC;AACnE,YAAY,EAAE,WAAW,EAAE,UAAU,EAAE,aAAa,EAAE,aAAa,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAC;AAGrH,OAAO,EAAE,WAAW,EAAE,MAAM,wBAAwB,CAAC;AACrD,YAAY,EAAE,SAAS,EAAE,YAAY,EAAE,MAAM,wBAAwB,CAAC;AAGtE,cAAc,kBAAkB,CAAC;AAGjC,cAAc,YAAY,CAAC;AAG3B,cAAc,sBAAsB,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -15,6 +15,8 @@ useCuePoint, useCuePoints, useVideoProgress,
|
|
|
15
15
|
Slideshow, useSlideshowState, useSlideshow, // Alias for useSlideshowState
|
|
16
16
|
// Positioning components
|
|
17
17
|
OverlaySlot, FullOverlay, GradientOverlay, } from './react/overlay/index.js';
|
|
18
|
+
// Confetti - CSS-based, mobile WebView safe
|
|
19
|
+
export { confetti } from './react/confetti.js';
|
|
18
20
|
// Bridge for secure communication
|
|
19
21
|
export { getTileBridge, TileBridge } from './bridge/TileBridge.js';
|
|
20
22
|
// State API client
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Canvas-confetti compatible options
|
|
3
|
+
* Maps to react-confetti-explosion props under the hood
|
|
4
|
+
*/
|
|
5
|
+
export interface ConfettiOptions {
|
|
6
|
+
/**
|
|
7
|
+
* Number of confetti particles (default: 100)
|
|
8
|
+
* Keep under 400 for optimal performance
|
|
9
|
+
*/
|
|
10
|
+
particleCount?: number;
|
|
11
|
+
/**
|
|
12
|
+
* Spread angle in degrees (default: 70)
|
|
13
|
+
* Mapped to width: spread * 15 pixels
|
|
14
|
+
*/
|
|
15
|
+
spread?: number;
|
|
16
|
+
/**
|
|
17
|
+
* Origin point for the confetti burst
|
|
18
|
+
* x: 0-1 (left to right), y: 0-1 (top to bottom)
|
|
19
|
+
* Default: { x: 0.5, y: 0.5 } (center)
|
|
20
|
+
*/
|
|
21
|
+
origin?: {
|
|
22
|
+
x?: number;
|
|
23
|
+
y?: number;
|
|
24
|
+
};
|
|
25
|
+
/**
|
|
26
|
+
* Initial velocity/force of particles (default: 0.5)
|
|
27
|
+
* Range: 0-1 (higher = more explosive)
|
|
28
|
+
*/
|
|
29
|
+
startVelocity?: number;
|
|
30
|
+
/**
|
|
31
|
+
* Animation duration in milliseconds (default: 2200)
|
|
32
|
+
*/
|
|
33
|
+
ticks?: number;
|
|
34
|
+
/**
|
|
35
|
+
* Size of particles in pixels (default: 12)
|
|
36
|
+
*/
|
|
37
|
+
scalar?: number;
|
|
38
|
+
/**
|
|
39
|
+
* Array of CSS color strings
|
|
40
|
+
* Default: festive colors
|
|
41
|
+
*/
|
|
42
|
+
colors?: string[];
|
|
43
|
+
/**
|
|
44
|
+
* Z-index for the confetti layer
|
|
45
|
+
*/
|
|
46
|
+
zIndex?: number;
|
|
47
|
+
/**
|
|
48
|
+
* Callback when animation completes
|
|
49
|
+
*/
|
|
50
|
+
onComplete?: () => void;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Launch confetti with canvas-confetti compatible API
|
|
54
|
+
*
|
|
55
|
+
* Uses CSS animations (react-confetti-explosion) under the hood,
|
|
56
|
+
* which is safe for mobile WebViews that may have issues with
|
|
57
|
+
* canvas-confetti's OffscreenCanvas/WebWorker approach.
|
|
58
|
+
*
|
|
59
|
+
* @example
|
|
60
|
+
* // Basic usage
|
|
61
|
+
* confetti();
|
|
62
|
+
*
|
|
63
|
+
* @example
|
|
64
|
+
* // Custom configuration
|
|
65
|
+
* confetti({
|
|
66
|
+
* particleCount: 150,
|
|
67
|
+
* spread: 90,
|
|
68
|
+
* origin: { x: 0.5, y: 0.3 }
|
|
69
|
+
* });
|
|
70
|
+
*
|
|
71
|
+
* @example
|
|
72
|
+
* // Celebration burst from bottom
|
|
73
|
+
* confetti({
|
|
74
|
+
* particleCount: 200,
|
|
75
|
+
* spread: 120,
|
|
76
|
+
* startVelocity: 0.8,
|
|
77
|
+
* origin: { x: 0.5, y: 1 }
|
|
78
|
+
* });
|
|
79
|
+
*/
|
|
80
|
+
export declare function confetti(options?: ConfettiOptions): Promise<void>;
|
|
81
|
+
export declare namespace confetti {
|
|
82
|
+
var reset: () => void;
|
|
83
|
+
}
|
|
84
|
+
export default confetti;
|
|
85
|
+
//# sourceMappingURL=confetti.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"confetti.d.ts","sourceRoot":"","sources":["../../src/react/confetti.tsx"],"names":[],"mappings":"AAMA;;;GAGG;AACH,MAAM,WAAW,eAAe;IAC9B;;;OAGG;IACH,aAAa,CAAC,EAAE,MAAM,CAAC;IAEvB;;;OAGG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC;IAEhB;;;;OAIG;IACH,MAAM,CAAC,EAAE;QAAE,CAAC,CAAC,EAAE,MAAM,CAAC;QAAC,CAAC,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IAEpC;;;OAGG;IACH,aAAa,CAAC,EAAE,MAAM,CAAC;IAEvB;;OAEG;IACH,KAAK,CAAC,EAAE,MAAM,CAAC;IAEf;;OAEG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC;IAEhB;;;OAGG;IACH,MAAM,CAAC,EAAE,MAAM,EAAE,CAAC;IAElB;;OAEG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC;IAEhB;;OAEG;IACH,UAAU,CAAC,EAAE,MAAM,IAAI,CAAC;CACzB;AA2ED;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,wBAAgB,QAAQ,CAAC,OAAO,GAAE,eAAoB,GAAG,OAAO,CAAC,IAAI,CAAC,CAuDrE;yBAvDe,QAAQ;qBA4DK,IAAI;;AAWjC,eAAe,QAAQ,CAAC"}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import React from 'react';
|
|
3
|
+
import { createRoot } from 'react-dom/client';
|
|
4
|
+
import ConfettiExplosion from 'react-confetti-explosion';
|
|
5
|
+
// Container for mounting confetti explosions
|
|
6
|
+
let confettiContainer = null;
|
|
7
|
+
let confettiRoot = null;
|
|
8
|
+
let activeExplosions = new Set();
|
|
9
|
+
let explosionCounter = 0;
|
|
10
|
+
function getConfettiContainer() {
|
|
11
|
+
if (!confettiContainer) {
|
|
12
|
+
confettiContainer = document.createElement('div');
|
|
13
|
+
confettiContainer.id = 'tile-sdk-confetti-container';
|
|
14
|
+
confettiContainer.style.cssText = `
|
|
15
|
+
position: fixed;
|
|
16
|
+
top: 0;
|
|
17
|
+
left: 0;
|
|
18
|
+
width: 100%;
|
|
19
|
+
height: 100%;
|
|
20
|
+
pointer-events: none;
|
|
21
|
+
z-index: 9999;
|
|
22
|
+
overflow: hidden;
|
|
23
|
+
`;
|
|
24
|
+
document.body.appendChild(confettiContainer);
|
|
25
|
+
confettiRoot = createRoot(confettiContainer);
|
|
26
|
+
}
|
|
27
|
+
return confettiContainer;
|
|
28
|
+
}
|
|
29
|
+
function renderExplosions() {
|
|
30
|
+
if (!confettiRoot)
|
|
31
|
+
return;
|
|
32
|
+
// Get all active explosion data
|
|
33
|
+
const explosionData = Array.from(activeExplosions).map(id => {
|
|
34
|
+
return window.__confettiExplosions?.[id];
|
|
35
|
+
}).filter(Boolean);
|
|
36
|
+
if (explosionData.length === 0) {
|
|
37
|
+
confettiRoot.render(null);
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
confettiRoot.render(React.createElement(React.Fragment, null, explosionData.map((data) => (React.createElement("div", { key: data.id, style: {
|
|
41
|
+
position: 'absolute',
|
|
42
|
+
left: `${data.origin.x * 100}%`,
|
|
43
|
+
top: `${data.origin.y * 100}%`,
|
|
44
|
+
transform: 'translate(-50%, -50%)',
|
|
45
|
+
} },
|
|
46
|
+
React.createElement(ConfettiExplosion, { particleCount: data.particleCount, force: data.force, duration: data.duration, particleSize: data.particleSize, width: data.width, colors: data.colors, zIndex: data.zIndex, onComplete: () => {
|
|
47
|
+
// Cleanup this explosion
|
|
48
|
+
activeExplosions.delete(data.id);
|
|
49
|
+
delete window.__confettiExplosions?.[data.id];
|
|
50
|
+
data.onComplete?.();
|
|
51
|
+
renderExplosions();
|
|
52
|
+
} }))))));
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Launch confetti with canvas-confetti compatible API
|
|
56
|
+
*
|
|
57
|
+
* Uses CSS animations (react-confetti-explosion) under the hood,
|
|
58
|
+
* which is safe for mobile WebViews that may have issues with
|
|
59
|
+
* canvas-confetti's OffscreenCanvas/WebWorker approach.
|
|
60
|
+
*
|
|
61
|
+
* @example
|
|
62
|
+
* // Basic usage
|
|
63
|
+
* confetti();
|
|
64
|
+
*
|
|
65
|
+
* @example
|
|
66
|
+
* // Custom configuration
|
|
67
|
+
* confetti({
|
|
68
|
+
* particleCount: 150,
|
|
69
|
+
* spread: 90,
|
|
70
|
+
* origin: { x: 0.5, y: 0.3 }
|
|
71
|
+
* });
|
|
72
|
+
*
|
|
73
|
+
* @example
|
|
74
|
+
* // Celebration burst from bottom
|
|
75
|
+
* confetti({
|
|
76
|
+
* particleCount: 200,
|
|
77
|
+
* spread: 120,
|
|
78
|
+
* startVelocity: 0.8,
|
|
79
|
+
* origin: { x: 0.5, y: 1 }
|
|
80
|
+
* });
|
|
81
|
+
*/
|
|
82
|
+
export function confetti(options = {}) {
|
|
83
|
+
const { particleCount = 100, spread = 70, origin = {}, startVelocity = 0.5, ticks = 2200, scalar = 12, colors, zIndex = 9999, onComplete, } = options;
|
|
84
|
+
// Ensure container exists
|
|
85
|
+
getConfettiContainer();
|
|
86
|
+
// Initialize explosion storage
|
|
87
|
+
if (!window.__confettiExplosions) {
|
|
88
|
+
window.__confettiExplosions = {};
|
|
89
|
+
}
|
|
90
|
+
// Create unique ID for this explosion
|
|
91
|
+
const id = `explosion_${++explosionCounter}`;
|
|
92
|
+
// Map canvas-confetti options to react-confetti-explosion props
|
|
93
|
+
const explosionData = {
|
|
94
|
+
id,
|
|
95
|
+
particleCount: Math.min(particleCount, 400), // Cap for performance
|
|
96
|
+
force: Math.max(0, Math.min(1, startVelocity)), // Clamp 0-1
|
|
97
|
+
duration: ticks,
|
|
98
|
+
particleSize: scalar,
|
|
99
|
+
width: spread * 15, // Approximate spread-to-width conversion
|
|
100
|
+
origin: {
|
|
101
|
+
x: origin.x ?? 0.5,
|
|
102
|
+
y: origin.y ?? 0.5,
|
|
103
|
+
},
|
|
104
|
+
colors: colors || ['#FFC700', '#FF0000', '#2E3191', '#41BBC7', '#FF6B6B', '#4ECDC4', '#45B7D1', '#96CEB4'],
|
|
105
|
+
zIndex,
|
|
106
|
+
onComplete,
|
|
107
|
+
};
|
|
108
|
+
// Store explosion data
|
|
109
|
+
window.__confettiExplosions[id] = explosionData;
|
|
110
|
+
activeExplosions.add(id);
|
|
111
|
+
// Render all active explosions
|
|
112
|
+
renderExplosions();
|
|
113
|
+
// Return promise that resolves when animation completes
|
|
114
|
+
return new Promise((resolve) => {
|
|
115
|
+
explosionData.onComplete = () => {
|
|
116
|
+
onComplete?.();
|
|
117
|
+
resolve();
|
|
118
|
+
};
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Reset/clear all active confetti
|
|
123
|
+
*/
|
|
124
|
+
confetti.reset = function () {
|
|
125
|
+
activeExplosions.clear();
|
|
126
|
+
if (window.__confettiExplosions) {
|
|
127
|
+
window.__confettiExplosions = {};
|
|
128
|
+
}
|
|
129
|
+
if (confettiRoot) {
|
|
130
|
+
confettiRoot.render(null);
|
|
131
|
+
}
|
|
132
|
+
};
|
|
133
|
+
// Default export for drop-in replacement
|
|
134
|
+
export default confetti;
|
package/dist/react/index.d.ts
CHANGED
|
@@ -6,4 +6,6 @@ export { useViewport } from './useViewport.js';
|
|
|
6
6
|
export { TileContainer } from './TileContainer.js';
|
|
7
7
|
export { withTile } from './withTile.js';
|
|
8
8
|
export * from './overlay/index.js';
|
|
9
|
+
export { confetti } from './confetti.js';
|
|
10
|
+
export type { ConfettiOptions } from './confetti.js';
|
|
9
11
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/react/index.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,YAAY,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAC9D,YAAY,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAC;AAC1D,OAAO,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AACvC,OAAO,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AAC/C,OAAO,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AAC/C,OAAO,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AACnD,OAAO,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AAQzC,cAAc,oBAAoB,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/react/index.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,YAAY,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAC9D,YAAY,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAC;AAC1D,OAAO,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AACvC,OAAO,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AAC/C,OAAO,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AAC/C,OAAO,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AACnD,OAAO,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AAQzC,cAAc,oBAAoB,CAAC;AAGnC,OAAO,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AACzC,YAAY,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC"}
|
package/dist/react/index.js
CHANGED
|
@@ -11,3 +11,5 @@ export { withTile } from './withTile.js';
|
|
|
11
11
|
// import { ExcaliburGame, useEngine, useGameLoop, ... } from '@thewhateverapp/tile-sdk/excalibur'
|
|
12
12
|
// Overlay components for hybrid tiles (video/image with interactive overlays)
|
|
13
13
|
export * from './overlay/index.js';
|
|
14
|
+
// Confetti - CSS-based, mobile WebView safe
|
|
15
|
+
export { confetti } from './confetti.js';
|
|
@@ -26,7 +26,7 @@ export declare const videoLayoutTemplate = "'use client';\n\nimport { VideoPlaye
|
|
|
26
26
|
*
|
|
27
27
|
* File path: src/app/(video)/tile/page.tsx
|
|
28
28
|
*/
|
|
29
|
-
export declare const videoTileOverlayTemplate = "'use client';\n\nimport { useState, useEffect, useRef, useCallback } from 'react';\nimport { useVideoState, useTile } from '@thewhateverapp/tile-sdk';\n\nfunction formatTime(seconds: number): string {\n if (!seconds || !isFinite(seconds)) return '0:00';\n const mins = Math.floor(seconds / 60);\n const secs = Math.floor(seconds % 60);\n return `${mins}:${secs.toString().padStart(2, '0')}`;\n}\n\nconst CONTROLS_HIDE_DELAY = 3000;\n\nexport function TileOverlay() {\n const tile = useTile();\n const { state, controls } = useVideoState();\n \n const [hasInteracted, setHasInteracted] = useState(false);\n const [controlsVisible, setControlsVisible] = useState(false);\n const hideTimeoutRef = useRef<NodeJS.Timeout | null>(null);\n\n const showControls = useCallback(() => {\n setHasInteracted(true);\n setControlsVisible(true);\n\n if (hideTimeoutRef.current) {\n clearTimeout(hideTimeoutRef.current);\n }\n\n if (state.isPlaying) {\n hideTimeoutRef.current = setTimeout(() => {\n setControlsVisible(false);\n }, CONTROLS_HIDE_DELAY);\n }\n }, [state.isPlaying]);\n\n useEffect(() => {\n if (!state.isPlaying && hasInteracted) {\n setControlsVisible(true);\n if (hideTimeoutRef.current) {\n clearTimeout(hideTimeoutRef.current);\n }\n }\n }, [state.isPlaying, hasInteracted]);\n\n useEffect(() => {\n return () => {\n if (hideTimeoutRef.current) {\n clearTimeout(hideTimeoutRef.current);\n }\n };\n }, []);\n\n const handleInteraction = useCallback(() => {\n if (!hasInteracted) {\n showControls();\n controls.toggle();\n } else if (controlsVisible) {\n controls.toggle();\n showControls();\n } else {\n showControls();\n }\n }, [hasInteracted, controlsVisible, showControls, controls]);\n\n const showCenterPlayButton = hasInteracted && !state.isPlaying && !state.isLoading && controlsVisible;\n const showBottomControls = hasInteracted && controlsVisible;\n\n return (\n <>\n <div\n className=\"absolute inset-0 z-5 pointer-events-auto cursor-pointer\"\n onClick={handleInteraction}\n onMouseMove={() => hasInteracted && showControls()}\n />\n\n <div className=\"absolute top-3 right-3 z-20 pointer-events-auto\">\n <button\n onClick={(e) => {\n e.stopPropagation();\n tile.navigateToPage();\n }}\n className=\"bg-black/40 backdrop-blur-sm p-2 rounded-full text-white hover:bg-black/60 transition-colors\"\n aria-label=\"Expand to full view\"\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=\"M4 8V4m0 0h4M4 4l5 5m11-1V4m0 0h-4m4 0l-5 5M4 16v4m0 0h4m-4 0l5-5m11 5l-5-5m5 5v-4m0 4h-4\" />\n </svg>\n </button>\n </div>\n\n <div\n className={`absolute inset-0 flex items-center justify-center z-10 pointer-events-none transition-opacity duration-300 ${\n showCenterPlayButton ? 'opacity-100' : 'opacity-0'\n }`}\n >\n <button\n onClick={(e) => {\n e.stopPropagation();\n controls.play();\n showControls();\n }}\n className=\"bg-black/50 backdrop-blur-sm p-4 rounded-full text-white hover:bg-black/70 transition-colors pointer-events-auto\"\n aria-label=\"Play video\"\n style={{ pointerEvents: showCenterPlayButton ? 'auto' : 'none' }}\n >\n <svg className=\"w-12 h-12\" fill=\"currentColor\" viewBox=\"0 0 24 24\">\n <path d=\"M8 5v14l11-7z\" />\n </svg>\n </button>\n </div>\n\n <div\n className={`absolute bottom-0 left-0 right-0 h-24 bg-gradient-to-t from-black/80 to-transparent pointer-events-none transition-opacity duration-300 ${\n showBottomControls ? 'opacity-100' : 'opacity-0'\n }`}\n />\n\n <div\n className={`absolute bottom-3 left-3 right-3 z-20 transition-opacity duration-300 ${\n showBottomControls ? 'opacity-100 pointer-events-auto' : 'opacity-0 pointer-events-none'\n }`}\n >\n <div\n className=\"h-1 bg-white/30 rounded-full overflow-hidden mb-2 cursor-pointer\"\n onClick={(e) => {\n e.stopPropagation();\n const rect = e.currentTarget.getBoundingClientRect();\n const percent = (e.clientX - rect.left) / rect.width;\n controls.seek(percent * state.duration);\n showControls();\n }}\n >\n <div\n className=\"h-full bg-white rounded-full transition-all duration-100\"\n style={{ width: `${(state.currentTime / state.duration) * 100 || 0}%` }}\n />\n </div>\n\n <div className=\"flex items-center justify-between text-white text-xs\">\n <div className=\"flex items-center gap-2\">\n <button\n onClick={(e) => {\n e.stopPropagation();\n controls.toggle();\n showControls();\n }}\n className=\"p-1 hover:bg-white/20 rounded transition-colors\"\n aria-label={state.isPlaying ? 'Pause' : 'Play'}\n >\n {state.isPlaying ? (\n <svg className=\"w-4 h-4\" fill=\"currentColor\" viewBox=\"0 0 24 24\">\n <path d=\"M6 4h4v16H6V4zm8 0h4v16h-4V4z\" />\n </svg>\n ) : (\n <svg className=\"w-4 h-4\" fill=\"currentColor\" viewBox=\"0 0 24 24\">\n <path d=\"M8 5v14l11-7z\" />\n </svg>\n )}\n </button>\n <span className=\"font-mono\">\n {formatTime(state.currentTime)} / {formatTime(state.duration)}\n </span>\n </div>\n\n <button\n onClick={(e) => {\n e.stopPropagation();\n controls.setMuted(!state.muted);\n showControls();\n }}\n className=\"p-1 hover:bg-white/20 rounded transition-colors\"\n aria-label={state.muted ? 'Unmute' : 'Mute'}\n >\n {state.muted ? (\n <svg className=\"w-4 h-4\" fill=\"currentColor\" viewBox=\"0 0 24 24\">\n <path d=\"M16.5 12c0-1.77-1.02-3.29-2.5-4.03v2.21l2.45 2.45c.03-.2.05-.41.05-.63zm2.5 0c0 .94-.2 1.82-.54 2.64l1.51 1.51C20.63 14.91 21 13.5 21 12c0-4.28-2.99-7.86-7-8.77v2.06c2.89.86 5 3.54 5 6.71zM4.27 3L3 4.27 7.73 9H3v6h4l5 5v-6.73l4.25 4.25c-.67.52-1.42.93-2.25 1.18v2.06c1.38-.31 2.63-.95 3.69-1.81L19.73 21 21 19.73l-9-9L4.27 3zM12 4L9.91 6.09 12 8.18V4z\" />\n </svg>\n ) : (\n <svg className=\"w-4 h-4\" fill=\"currentColor\" viewBox=\"0 0 24 24\">\n <path d=\"M3 9v6h4l5 5V4L7 9H3zm13.5 3c0-1.77-1.02-3.29-2.5-4.03v8.05c1.48-.73 2.5-2.25 2.5-4.02zM14 3.23v2.06c2.89.86 5 3.54 5 6.71s-2.11 5.85-5 6.71v2.06c4.01-.91 7-4.49 7-8.77s-2.99-7.86-7-8.77z\" />\n </svg>\n )}\n </button>\n </div>\n </div>\n\n </>\n );\n}\n";
|
|
29
|
+
export declare const videoTileOverlayTemplate = "'use client';\n\nimport { useState, useEffect, useRef, useCallback } from 'react';\nimport { useVideoState, useTile } from '@thewhateverapp/tile-sdk';\n\nfunction formatTime(seconds: number): string {\n if (!seconds || !isFinite(seconds)) return '0:00';\n const mins = Math.floor(seconds / 60);\n const secs = Math.floor(seconds % 60);\n return `${mins}:${secs.toString().padStart(2, '0')}`;\n}\n\nconst CONTROLS_HIDE_DELAY = 3000;\n\nexport function TileOverlay() {\n const tile = useTile();\n const { state, controls } = useVideoState();\n \n const [hasInteracted, setHasInteracted] = useState(false);\n const [controlsVisible, setControlsVisible] = useState(false);\n const hideTimeoutRef = useRef<NodeJS.Timeout | null>(null);\n\n const showControls = useCallback(() => {\n setHasInteracted(true);\n setControlsVisible(true);\n\n if (hideTimeoutRef.current) {\n clearTimeout(hideTimeoutRef.current);\n }\n\n if (state.isPlaying) {\n hideTimeoutRef.current = setTimeout(() => {\n setControlsVisible(false);\n }, CONTROLS_HIDE_DELAY);\n }\n }, [state.isPlaying]);\n\n useEffect(() => {\n if (!state.isPlaying && hasInteracted) {\n setControlsVisible(true);\n if (hideTimeoutRef.current) {\n clearTimeout(hideTimeoutRef.current);\n }\n }\n }, [state.isPlaying, hasInteracted]);\n\n useEffect(() => {\n return () => {\n if (hideTimeoutRef.current) {\n clearTimeout(hideTimeoutRef.current);\n }\n };\n }, []);\n\n const handleInteraction = useCallback(() => {\n if (!hasInteracted) {\n showControls();\n controls.toggle();\n } else if (controlsVisible) {\n controls.toggle();\n showControls();\n } else {\n showControls();\n }\n }, [hasInteracted, controlsVisible, showControls, controls]);\n\n const showCenterPlayButton = hasInteracted && !state.isPlaying && !state.isLoading && controlsVisible;\n const showBottomControls = hasInteracted && controlsVisible;\n\n return (\n <>\n <div\n className=\"absolute inset-0 z-5 pointer-events-auto cursor-pointer\"\n onClick={handleInteraction}\n onMouseMove={() => hasInteracted && showControls()}\n />\n\n <div className=\"absolute top-3 right-3 z-20 pointer-events-auto\">\n <button\n onClick={(e) => {\n e.stopPropagation();\n tile.navigateToPage();\n }}\n className=\"bg-black/40 backdrop-blur-sm p-2 rounded-full text-white hover:bg-black/60 transition-colors\"\n aria-label=\"Expand to full view\"\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=\"M4 8V4m0 0h4M4 4l5 5m11-1V4m0 0h-4m4 0l-5 5M4 16v4m0 0h4m-4 0l5-5m11 5l-5-5m5 5v-4m0 4h-4\" />\n </svg>\n </button>\n </div>\n\n <div\n className={`absolute inset-0 flex items-center justify-center z-10 pointer-events-none transition-opacity duration-300 ${\n showCenterPlayButton ? 'opacity-100' : 'opacity-0'\n }`}\n >\n <button\n onClick={(e) => {\n e.stopPropagation();\n controls.play();\n showControls();\n }}\n className=\"bg-black/50 backdrop-blur-sm p-4 rounded-full text-white hover:bg-black/70 transition-colors pointer-events-auto\"\n aria-label=\"Play video\"\n style={{ pointerEvents: showCenterPlayButton ? 'auto' : 'none' }}\n >\n <svg className=\"w-12 h-12\" fill=\"currentColor\" viewBox=\"0 0 24 24\">\n <path d=\"M8 5v14l11-7z\" />\n </svg>\n </button>\n </div>\n\n <div\n className={`absolute bottom-0 left-0 right-0 h-24 bg-gradient-to-t from-black/80 to-transparent pointer-events-none transition-opacity duration-300 ${\n showBottomControls ? 'opacity-100' : 'opacity-0'\n }`}\n />\n\n <div\n className={`absolute bottom-3 left-3 right-3 z-20 transition-opacity duration-300 ${\n showBottomControls ? 'opacity-100 pointer-events-auto' : 'opacity-0 pointer-events-none'\n }`}\n >\n <div\n className=\"h-1 bg-white/30 rounded-full overflow-hidden mb-2 cursor-pointer\"\n onClick={(e) => {\n e.stopPropagation();\n const rect = e.currentTarget.getBoundingClientRect();\n const percent = (e.clientX - rect.left) / rect.width;\n controls.seek(percent * state.duration);\n showControls();\n }}\n >\n <div\n className=\"h-full bg-white rounded-full transition-all duration-100\"\n style={{ width: `${(state.currentTime / state.duration) * 100 || 0}%` }}\n />\n </div>\n\n <div className=\"flex items-center justify-between text-white text-xs\">\n <div className=\"flex items-center gap-2\">\n <button\n onClick={(e) => {\n e.stopPropagation();\n controls.toggle();\n showControls();\n }}\n className=\"p-1 hover:bg-white/20 rounded transition-colors\"\n aria-label={state.isPlaying ? 'Pause' : 'Play'}\n >\n {state.isPlaying ? (\n <svg className=\"w-4 h-4\" fill=\"currentColor\" viewBox=\"0 0 24 24\">\n <path d=\"M6 4h4v16H6V4zm8 0h4v16h-4V4z\" />\n </svg>\n ) : (\n <svg className=\"w-4 h-4\" fill=\"currentColor\" viewBox=\"0 0 24 24\">\n <path d=\"M8 5v14l11-7z\" />\n </svg>\n )}\n </button>\n <span className=\"font-mono\">\n {formatTime(state.currentTime)} / {formatTime(state.duration)}\n </span>\n </div>\n\n <button\n onClick={(e) => {\n e.stopPropagation();\n controls.setMuted(!state.muted);\n showControls();\n }}\n className=\"p-1 hover:bg-white/20 rounded transition-colors\"\n aria-label={state.muted ? 'Unmute' : 'Mute'}\n >\n {state.muted ? (\n <svg className=\"w-4 h-4\" fill=\"currentColor\" viewBox=\"0 0 24 24\">\n <path d=\"M16.5 12c0-1.77-1.02-3.29-2.5-4.03v2.21l2.45 2.45c.03-.2.05-.41.05-.63zm2.5 0c0 .94-.2 1.82-.54 2.64l1.51 1.51C20.63 14.91 21 13.5 21 12c0-4.28-2.99-7.86-7-8.77v2.06c2.89.86 5 3.54 5 6.71zM4.27 3L3 4.27 7.73 9H3v6h4l5 5v-6.73l4.25 4.25c-.67.52-1.42.93-2.25 1.18v2.06c1.38-.31 2.63-.95 3.69-1.81L19.73 21 21 19.73l-9-9L4.27 3zM12 4L9.91 6.09 12 8.18V4z\" />\n </svg>\n ) : (\n <svg className=\"w-4 h-4\" fill=\"currentColor\" viewBox=\"0 0 24 24\">\n <path d=\"M3 9v6h4l5 5V4L7 9H3zm13.5 3c0-1.77-1.02-3.29-2.5-4.03v8.05c1.48-.73 2.5-2.25 2.5-4.02zM14 3.23v2.06c2.89.86 5 3.54 5 6.71s-2.11 5.85-5 6.71v2.06c4.01-.91 7-4.49 7-8.77s-2.99-7.86-7-8.77z\" />\n </svg>\n )}\n </button>\n </div>\n </div>\n\n </>\n );\n}\n\n\nexport default TileOverlay;\n";
|
|
30
30
|
/**
|
|
31
31
|
* Video Page Overlay Template
|
|
32
32
|
*
|
|
@@ -35,7 +35,7 @@ export declare const videoTileOverlayTemplate = "'use client';\n\nimport { useSt
|
|
|
35
35
|
*
|
|
36
36
|
* File path: src/app/(video)/page/page.tsx
|
|
37
37
|
*/
|
|
38
|
-
export declare const videoPageOverlayTemplate = "'use client';\n\nimport { useState, useEffect, useCallback } from 'react';\nimport { useVideoState, useTile } from '@thewhateverapp/tile-sdk';\n\nfunction formatTime(seconds: number): string {\n if (!seconds || !isFinite(seconds)) return '0:00';\n const mins = Math.floor(seconds / 60);\n const secs = Math.floor(seconds % 60);\n return `${mins}:${secs.toString().padStart(2, '0')}`;\n}\n\nexport function PageOverlay() {\n const tile = useTile();\n const { state, controls } = useVideoState();\n const [showControls, setShowControls] = useState(true);\n const [hideTimeout, setHideTimeout] = useState<NodeJS.Timeout | null>(null);\n\n // Auto-hide controls after 3 seconds of inactivity when playing\n const resetHideTimer = useCallback(() => {\n if (hideTimeout) {\n clearTimeout(hideTimeout);\n }\n setShowControls(true);\n\n if (state.isPlaying) {\n const timeout = setTimeout(() => {\n setShowControls(false);\n }, 3000);\n setHideTimeout(timeout);\n }\n }, [state.isPlaying, hideTimeout]);\n\n // Show controls when video is paused\n useEffect(() => {\n if (!state.isPlaying) {\n setShowControls(true);\n if (hideTimeout) {\n clearTimeout(hideTimeout);\n setHideTimeout(null);\n }\n } else {\n resetHideTimer();\n }\n }, [state.isPlaying]);\n\n // Cleanup timeout on unmount\n useEffect(() => {\n return () => {\n if (hideTimeout) {\n clearTimeout(hideTimeout);\n }\n };\n }, [hideTimeout]);\n\n // Toggle controls on tap/click\n const handleToggleControls = useCallback((e: React.MouseEvent | React.TouchEvent) => {\n // Don't toggle if clicking on a button or control\n const target = e.target as HTMLElement;\n if (target.closest('button, .controls-area')) {\n return;\n }\n\n // Prevent default to avoid double-firing on touch devices\n e.preventDefault();\n\n if (showControls) {\n setShowControls(false);\n } else {\n resetHideTimer();\n }\n }, [showControls, resetHideTimer]);\n\n // Handle mouse movement to show controls\n const handleMouseMove = useCallback(() => {\n if (state.isPlaying) {\n resetHideTimer();\n }\n }, [state.isPlaying, resetHideTimer]);\n\n return (\n <div\n className=\"absolute inset-0 z-20 pointer-events-auto\"\n onClick={handleToggleControls}\n onTouchEnd={handleToggleControls}\n onMouseMove={handleMouseMove}\n >\n {/* Center play button when paused */}\n {!state.isPlaying && !state.isLoading && (\n <div className=\"absolute inset-0 flex items-center justify-center pointer-events-auto\">\n <button\n onClick={controls.play}\n className=\"bg-black/50 backdrop-blur-sm p-6 rounded-full text-white hover:bg-black/70 transition-colors\"\n aria-label=\"Play video\"\n >\n <svg className=\"w-16 h-16\" fill=\"currentColor\" viewBox=\"0 0 24 24\">\n <path d=\"M8 5v14l11-7z\" />\n </svg>\n </button>\n </div>\n )}\n\n {/* Controls - toggleable */}\n <div\n className={`controls-area absolute bottom-0 left-0 right-0 transition-opacity duration-300 ${\n showControls ? 'opacity-100' : 'opacity-0 pointer-events-none'\n }`}\n >\n {/* Gradient background */}\n <div className=\"absolute bottom-0 left-0 right-0 h-32 bg-gradient-to-t from-black/80 to-transparent pointer-events-none\" />\n\n <div className=\"absolute bottom-4 left-4 right-4 z-20 pointer-events-auto\">\n {/* Progress bar / Timeline */}\n <div\n className=\"h-1.5 bg-white/30 rounded-full overflow-hidden mb-3 cursor-pointer group hover:h-2 transition-all\"\n onClick={(e) => {\n e.stopPropagation();\n const rect = e.currentTarget.getBoundingClientRect();\n const percent = (e.clientX - rect.left) / rect.width;\n controls.seek(percent * state.duration);\n resetHideTimer();\n }}\n >\n <div\n className=\"h-full bg-white rounded-full transition-all duration-100 group-hover:bg-blue-400\"\n style={{ width: `${(state.currentTime / state.duration) * 100 || 0}%` }}\n />\n </div>\n\n {/* Controls row */}\n <div className=\"flex items-center justify-between text-white\">\n <div className=\"flex items-center gap-3\">\n {/* Play/Pause */}\n <button\n onClick={(e) => {\n e.stopPropagation();\n controls.toggle();\n }}\n className=\"p-2 hover:bg-white/20 rounded-full transition-colors\"\n aria-label={state.isPlaying ? 'Pause' : 'Play'}\n >\n {state.isPlaying ? (\n <svg className=\"w-6 h-6\" fill=\"currentColor\" viewBox=\"0 0 24 24\">\n <path d=\"M6 4h4v16H6V4zm8 0h4v16h-4V4z\" />\n </svg>\n ) : (\n <svg className=\"w-6 h-6\" fill=\"currentColor\" viewBox=\"0 0 24 24\">\n <path d=\"M8 5v14l11-7z\" />\n </svg>\n )}\n </button>\n\n {/* Volume */}\n <button\n onClick={(e) => {\n e.stopPropagation();\n controls.setMuted(!state.muted);\n resetHideTimer();\n }}\n className=\"p-2 hover:bg-white/20 rounded-full transition-colors\"\n aria-label={state.muted ? 'Unmute' : 'Mute'}\n >\n {state.muted ? (\n <svg className=\"w-5 h-5\" fill=\"currentColor\" viewBox=\"0 0 24 24\">\n <path d=\"M16.5 12c0-1.77-1.02-3.29-2.5-4.03v2.21l2.45 2.45c.03-.2.05-.41.05-.63zm2.5 0c0 .94-.2 1.82-.54 2.64l1.51 1.51C20.63 14.91 21 13.5 21 12c0-4.28-2.99-7.86-7-8.77v2.06c2.89.86 5 3.54 5 6.71zM4.27 3L3 4.27 7.73 9H3v6h4l5 5v-6.73l4.25 4.25c-.67.52-1.42.93-2.25 1.18v2.06c1.38-.31 2.63-.95 3.69-1.81L19.73 21 21 19.73l-9-9L4.27 3zM12 4L9.91 6.09 12 8.18V4z\" />\n </svg>\n ) : (\n <svg className=\"w-5 h-5\" fill=\"currentColor\" viewBox=\"0 0 24 24\">\n <path d=\"M3 9v6h4l5 5V4L7 9H3zm13.5 3c0-1.77-1.02-3.29-2.5-4.03v8.05c1.48-.73 2.5-2.25 2.5-4.02zM14 3.23v2.06c2.89.86 5 3.54 5 6.71s-2.11 5.85-5 6.71v2.06c4.01-.91 7-4.49 7-8.77s-2.99-7.86-7-8.77z\" />\n </svg>\n )}\n </button>\n\n {/* Time */}\n <span className=\"text-sm font-mono\">\n {formatTime(state.currentTime)} / {formatTime(state.duration)}\n </span>\n </div>\n\n {/* Right side - minimize button */}\n <button\n onClick={(e) => {\n e.stopPropagation();\n tile.navigateToTile();\n }}\n className=\"p-2 hover:bg-white/20 rounded-full transition-colors\"\n aria-label=\"Minimize to tile\"\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=\"M20 12H4\" />\n </svg>\n </button>\n </div>\n </div>\n </div>\n </div>\n );\n}\n";
|
|
38
|
+
export declare const videoPageOverlayTemplate = "'use client';\n\nimport { useState, useEffect, useCallback } from 'react';\nimport { useVideoState, useTile } from '@thewhateverapp/tile-sdk';\n\nfunction formatTime(seconds: number): string {\n if (!seconds || !isFinite(seconds)) return '0:00';\n const mins = Math.floor(seconds / 60);\n const secs = Math.floor(seconds % 60);\n return `${mins}:${secs.toString().padStart(2, '0')}`;\n}\n\nexport function PageOverlay() {\n const tile = useTile();\n const { state, controls } = useVideoState();\n const [showControls, setShowControls] = useState(true);\n const [hideTimeout, setHideTimeout] = useState<NodeJS.Timeout | null>(null);\n\n // Auto-hide controls after 3 seconds of inactivity when playing\n const resetHideTimer = useCallback(() => {\n if (hideTimeout) {\n clearTimeout(hideTimeout);\n }\n setShowControls(true);\n\n if (state.isPlaying) {\n const timeout = setTimeout(() => {\n setShowControls(false);\n }, 3000);\n setHideTimeout(timeout);\n }\n }, [state.isPlaying, hideTimeout]);\n\n // Show controls when video is paused\n useEffect(() => {\n if (!state.isPlaying) {\n setShowControls(true);\n if (hideTimeout) {\n clearTimeout(hideTimeout);\n setHideTimeout(null);\n }\n } else {\n resetHideTimer();\n }\n }, [state.isPlaying]);\n\n // Cleanup timeout on unmount\n useEffect(() => {\n return () => {\n if (hideTimeout) {\n clearTimeout(hideTimeout);\n }\n };\n }, [hideTimeout]);\n\n // Toggle controls on tap/click\n const handleToggleControls = useCallback((e: React.MouseEvent | React.TouchEvent) => {\n // Don't toggle if clicking on a button or control\n const target = e.target as HTMLElement;\n if (target.closest('button, .controls-area')) {\n return;\n }\n\n // Prevent default to avoid double-firing on touch devices\n e.preventDefault();\n\n if (showControls) {\n setShowControls(false);\n } else {\n resetHideTimer();\n }\n }, [showControls, resetHideTimer]);\n\n // Handle mouse movement to show controls\n const handleMouseMove = useCallback(() => {\n if (state.isPlaying) {\n resetHideTimer();\n }\n }, [state.isPlaying, resetHideTimer]);\n\n return (\n <div\n className=\"absolute inset-0 z-20 pointer-events-auto\"\n onClick={handleToggleControls}\n onTouchEnd={handleToggleControls}\n onMouseMove={handleMouseMove}\n >\n {/* Center play button when paused */}\n {!state.isPlaying && !state.isLoading && (\n <div className=\"absolute inset-0 flex items-center justify-center pointer-events-auto\">\n <button\n onClick={controls.play}\n className=\"bg-black/50 backdrop-blur-sm p-6 rounded-full text-white hover:bg-black/70 transition-colors\"\n aria-label=\"Play video\"\n >\n <svg className=\"w-16 h-16\" fill=\"currentColor\" viewBox=\"0 0 24 24\">\n <path d=\"M8 5v14l11-7z\" />\n </svg>\n </button>\n </div>\n )}\n\n {/* Controls - toggleable */}\n <div\n className={`controls-area absolute bottom-0 left-0 right-0 transition-opacity duration-300 ${\n showControls ? 'opacity-100' : 'opacity-0 pointer-events-none'\n }`}\n >\n {/* Gradient background */}\n <div className=\"absolute bottom-0 left-0 right-0 h-32 bg-gradient-to-t from-black/80 to-transparent pointer-events-none\" />\n\n <div className=\"absolute bottom-4 left-4 right-4 z-20 pointer-events-auto\">\n {/* Progress bar / Timeline */}\n <div\n className=\"h-1.5 bg-white/30 rounded-full overflow-hidden mb-3 cursor-pointer group hover:h-2 transition-all\"\n onClick={(e) => {\n e.stopPropagation();\n const rect = e.currentTarget.getBoundingClientRect();\n const percent = (e.clientX - rect.left) / rect.width;\n controls.seek(percent * state.duration);\n resetHideTimer();\n }}\n >\n <div\n className=\"h-full bg-white rounded-full transition-all duration-100 group-hover:bg-blue-400\"\n style={{ width: `${(state.currentTime / state.duration) * 100 || 0}%` }}\n />\n </div>\n\n {/* Controls row */}\n <div className=\"flex items-center justify-between text-white\">\n <div className=\"flex items-center gap-3\">\n {/* Play/Pause */}\n <button\n onClick={(e) => {\n e.stopPropagation();\n controls.toggle();\n }}\n className=\"p-2 hover:bg-white/20 rounded-full transition-colors\"\n aria-label={state.isPlaying ? 'Pause' : 'Play'}\n >\n {state.isPlaying ? (\n <svg className=\"w-6 h-6\" fill=\"currentColor\" viewBox=\"0 0 24 24\">\n <path d=\"M6 4h4v16H6V4zm8 0h4v16h-4V4z\" />\n </svg>\n ) : (\n <svg className=\"w-6 h-6\" fill=\"currentColor\" viewBox=\"0 0 24 24\">\n <path d=\"M8 5v14l11-7z\" />\n </svg>\n )}\n </button>\n\n {/* Volume */}\n <button\n onClick={(e) => {\n e.stopPropagation();\n controls.setMuted(!state.muted);\n resetHideTimer();\n }}\n className=\"p-2 hover:bg-white/20 rounded-full transition-colors\"\n aria-label={state.muted ? 'Unmute' : 'Mute'}\n >\n {state.muted ? (\n <svg className=\"w-5 h-5\" fill=\"currentColor\" viewBox=\"0 0 24 24\">\n <path d=\"M16.5 12c0-1.77-1.02-3.29-2.5-4.03v2.21l2.45 2.45c.03-.2.05-.41.05-.63zm2.5 0c0 .94-.2 1.82-.54 2.64l1.51 1.51C20.63 14.91 21 13.5 21 12c0-4.28-2.99-7.86-7-8.77v2.06c2.89.86 5 3.54 5 6.71zM4.27 3L3 4.27 7.73 9H3v6h4l5 5v-6.73l4.25 4.25c-.67.52-1.42.93-2.25 1.18v2.06c1.38-.31 2.63-.95 3.69-1.81L19.73 21 21 19.73l-9-9L4.27 3zM12 4L9.91 6.09 12 8.18V4z\" />\n </svg>\n ) : (\n <svg className=\"w-5 h-5\" fill=\"currentColor\" viewBox=\"0 0 24 24\">\n <path d=\"M3 9v6h4l5 5V4L7 9H3zm13.5 3c0-1.77-1.02-3.29-2.5-4.03v8.05c1.48-.73 2.5-2.25 2.5-4.02zM14 3.23v2.06c2.89.86 5 3.54 5 6.71s-2.11 5.85-5 6.71v2.06c4.01-.91 7-4.49 7-8.77s-2.99-7.86-7-8.77z\" />\n </svg>\n )}\n </button>\n\n {/* Time */}\n <span className=\"text-sm font-mono\">\n {formatTime(state.currentTime)} / {formatTime(state.duration)}\n </span>\n </div>\n\n {/* Right side - minimize button */}\n <button\n onClick={(e) => {\n e.stopPropagation();\n tile.navigateToTile();\n }}\n className=\"p-2 hover:bg-white/20 rounded-full transition-colors\"\n aria-label=\"Minimize to tile\"\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=\"M20 12H4\" />\n </svg>\n </button>\n </div>\n </div>\n </div>\n </div>\n );\n}\n\n\nexport default PageOverlay;\n";
|
|
39
39
|
/**
|
|
40
40
|
* Video Preview Entry - Tile
|
|
41
41
|
*
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"layout.tsx.template.d.ts","sourceRoot":"","sources":["../../../src/templates/video/layout.tsx.template.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH;;;;;;;GAOG;AACH,eAAO,MAAM,mBAAmB,uUAY/B,CAAC;AAEF;;;;;;;GAOG;AACH,eAAO,MAAM,wBAAwB,
|
|
1
|
+
{"version":3,"file":"layout.tsx.template.d.ts","sourceRoot":"","sources":["../../../src/templates/video/layout.tsx.template.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH;;;;;;;GAOG;AACH,eAAO,MAAM,mBAAmB,uUAY/B,CAAC;AAEF;;;;;;;GAOG;AACH,eAAO,MAAM,wBAAwB,2mOAkMpC,CAAC;AAEF;;;;;;;GAOG;AACH,eAAO,MAAM,wBAAwB,+6OAyMpC,CAAC;AAEF;;;;;;;GAOG;AACH,eAAO,MAAM,wBAAwB,suOAsMpC,CAAC;AAEF;;;;;;;GAOG;AACH,eAAO,MAAM,wBAAwB,q4NA6LpC,CAAC"}
|
|
@@ -229,6 +229,9 @@ export function TileOverlay() {
|
|
|
229
229
|
</>
|
|
230
230
|
);
|
|
231
231
|
}
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
export default TileOverlay;
|
|
232
235
|
`;
|
|
233
236
|
/**
|
|
234
237
|
* Video Page Overlay Template
|
|
@@ -436,6 +439,9 @@ export function PageOverlay() {
|
|
|
436
439
|
</div>
|
|
437
440
|
);
|
|
438
441
|
}
|
|
442
|
+
|
|
443
|
+
|
|
444
|
+
export default PageOverlay;
|
|
439
445
|
`;
|
|
440
446
|
/**
|
|
441
447
|
* Video Preview Entry - Tile
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@thewhateverapp/tile-sdk",
|
|
3
|
-
"version": "0.17.
|
|
3
|
+
"version": "0.17.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",
|
|
@@ -69,6 +69,7 @@
|
|
|
69
69
|
"@thewhateverapp/scene-sdk": "^0.1.1",
|
|
70
70
|
"embla-carousel-react": "^8.5.1",
|
|
71
71
|
"matter-js": "^0.19.0",
|
|
72
|
+
"react-confetti-explosion": "^2.1.2",
|
|
72
73
|
"zod": "^3.22.0"
|
|
73
74
|
},
|
|
74
75
|
"peerDependencies": {
|
|
@@ -85,6 +86,7 @@
|
|
|
85
86
|
"@types/matter-js": "^0.19.0",
|
|
86
87
|
"@types/node": "^20.0.0",
|
|
87
88
|
"@types/react": "^18.2.48",
|
|
89
|
+
"@types/react-dom": "^18.2.18",
|
|
88
90
|
"eslint": "^9.39.1",
|
|
89
91
|
"excalibur": "^0.29.3",
|
|
90
92
|
"next": "^14.2.0",
|