@thewhateverapp/tile-sdk 0.12.19 → 0.12.21

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.
Files changed (28) hide show
  1. package/dist/index.d.ts +1 -0
  2. package/dist/index.d.ts.map +1 -1
  3. package/dist/index.js +2 -0
  4. package/dist/templates/index.d.ts +9 -0
  5. package/dist/templates/index.d.ts.map +1 -0
  6. package/dist/templates/index.js +10 -0
  7. package/dist/templates/slideshow/PersistentSlideshow.tsx.template.d.ts +8 -0
  8. package/dist/templates/slideshow/PersistentSlideshow.tsx.template.d.ts.map +1 -0
  9. package/dist/templates/slideshow/PersistentSlideshow.tsx.template.js +212 -0
  10. package/dist/templates/slideshow/SlideshowManager.ts.template.d.ts +8 -0
  11. package/dist/templates/slideshow/SlideshowManager.ts.template.d.ts.map +1 -0
  12. package/dist/templates/slideshow/SlideshowManager.ts.template.js +235 -0
  13. package/dist/templates/slideshow/index.d.ts +13 -0
  14. package/dist/templates/slideshow/index.d.ts.map +1 -0
  15. package/dist/templates/slideshow/index.js +13 -0
  16. package/dist/templates/video/PersistentVideo.tsx.template.d.ts +8 -0
  17. package/dist/templates/video/PersistentVideo.tsx.template.d.ts.map +1 -0
  18. package/dist/templates/video/PersistentVideo.tsx.template.js +109 -0
  19. package/dist/templates/video/VideoManager.ts.template.d.ts +8 -0
  20. package/dist/templates/video/VideoManager.ts.template.d.ts.map +1 -0
  21. package/dist/templates/video/VideoManager.ts.template.js +266 -0
  22. package/dist/templates/video/index.d.ts +14 -0
  23. package/dist/templates/video/index.d.ts.map +1 -0
  24. package/dist/templates/video/index.js +14 -0
  25. package/dist/templates/video/layout.tsx.template.d.ts +8 -0
  26. package/dist/templates/video/layout.tsx.template.d.ts.map +1 -0
  27. package/dist/templates/video/layout.tsx.template.js +26 -0
  28. package/package.json +9 -1
package/dist/index.d.ts CHANGED
@@ -12,4 +12,5 @@ export { StateClient } from './state/StateClient';
12
12
  export type { TileStats, ViewResponse } from './state/StateClient';
13
13
  export * from './tools';
14
14
  export * from './types';
15
+ export * from './templates';
15
16
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,YAAY,EAAE,MAAM,sBAAsB,CAAC;AACpD,OAAO,EAAE,OAAO,EAAE,MAAM,iBAAiB,CAAC;AAC1C,OAAO,EAAE,iBAAiB,EAAE,MAAM,2BAA2B,CAAC;AAC9D,OAAO,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAC;AAClD,OAAO,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC;AACtD,OAAO,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAG5C,OAAO,EAEL,WAAW,EACX,aAAa,EAEb,SAAS,EACT,iBAAiB,EAEjB,WAAW,EACX,WAAW,EACX,eAAe,GAChB,MAAM,iBAAiB,CAAC;AACzB,YAAY,EAEV,UAAU,EACV,aAAa,EACb,iBAAiB,EACjB,gBAAgB,EAEhB,UAAU,EACV,cAAc,EACd,iBAAiB,EACjB,qBAAqB,EACrB,cAAc,EAEd,YAAY,EACZ,gBAAgB,EAChB,gBAAgB,EAChB,oBAAoB,GACrB,MAAM,iBAAiB,CAAC;AAGzB,OAAO,EAAE,aAAa,EAAE,UAAU,EAAE,MAAM,qBAAqB,CAAC;AAChE,YAAY,EAAE,WAAW,EAAE,UAAU,EAAE,aAAa,EAAE,aAAa,EAAE,eAAe,EAAE,MAAM,qBAAqB,CAAC;AAGlH,OAAO,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAC;AAClD,YAAY,EAAE,SAAS,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AAGnE,cAAc,SAAS,CAAC;AAGxB,cAAc,SAAS,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,YAAY,EAAE,MAAM,sBAAsB,CAAC;AACpD,OAAO,EAAE,OAAO,EAAE,MAAM,iBAAiB,CAAC;AAC1C,OAAO,EAAE,iBAAiB,EAAE,MAAM,2BAA2B,CAAC;AAC9D,OAAO,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAC;AAClD,OAAO,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC;AACtD,OAAO,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAG5C,OAAO,EAEL,WAAW,EACX,aAAa,EAEb,SAAS,EACT,iBAAiB,EAEjB,WAAW,EACX,WAAW,EACX,eAAe,GAChB,MAAM,iBAAiB,CAAC;AACzB,YAAY,EAEV,UAAU,EACV,aAAa,EACb,iBAAiB,EACjB,gBAAgB,EAEhB,UAAU,EACV,cAAc,EACd,iBAAiB,EACjB,qBAAqB,EACrB,cAAc,EAEd,YAAY,EACZ,gBAAgB,EAChB,gBAAgB,EAChB,oBAAoB,GACrB,MAAM,iBAAiB,CAAC;AAGzB,OAAO,EAAE,aAAa,EAAE,UAAU,EAAE,MAAM,qBAAqB,CAAC;AAChE,YAAY,EAAE,WAAW,EAAE,UAAU,EAAE,aAAa,EAAE,aAAa,EAAE,eAAe,EAAE,MAAM,qBAAqB,CAAC;AAGlH,OAAO,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAC;AAClD,YAAY,EAAE,SAAS,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AAGnE,cAAc,SAAS,CAAC;AAGxB,cAAc,SAAS,CAAC;AAGxB,cAAc,aAAa,CAAC"}
package/dist/index.js CHANGED
@@ -21,3 +21,5 @@ export { StateClient } from './state/StateClient';
21
21
  export * from './tools';
22
22
  // Types
23
23
  export * from './types';
24
+ // Templates (for tile-deploy/agent-service)
25
+ export * from './templates';
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Tile Templates
3
+ *
4
+ * Pre-built templates for common tile types.
5
+ * These get injected as strings by tile-deploy/agent-service.
6
+ */
7
+ export * from './video';
8
+ export * from './slideshow';
9
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/templates/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,cAAc,SAAS,CAAC;AACxB,cAAc,aAAa,CAAC"}
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Tile Templates
3
+ *
4
+ * Pre-built templates for common tile types.
5
+ * These get injected as strings by tile-deploy/agent-service.
6
+ */
7
+ export * from './video';
8
+ export * from './slideshow';
9
+ // Future templates:
10
+ // export * from './image';
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Slideshow Template - PersistentSlideshow Component
3
+ *
4
+ * React component that attaches to the SlideshowManager singleton.
5
+ * The slideshow state persists across route changes.
6
+ */
7
+ export declare const persistentSlideshowTemplate = "'use client';\n\nimport { useEffect, useState, createContext, useContext, ReactNode } from 'react';\nimport { SlideshowManager, SlideshowState, SlideImage } from './SlideshowManager';\n\n// Context to share slideshow state with overlays\ninterface SlideshowContextValue {\n state: SlideshowState;\n controls: {\n next: () => void;\n prev: () => void;\n goTo: (index: number) => void;\n pause: () => void;\n resume: () => void;\n toggle: () => void;\n };\n}\n\nconst SlideshowContext = createContext<SlideshowContextValue | null>(null);\n\nexport function useSlideshow(): SlideshowContextValue {\n const context = useContext(SlideshowContext);\n if (!context) {\n throw new Error('useSlideshow must be used within PersistentSlideshow');\n }\n return context;\n}\n\ninterface PersistentSlideshowProps {\n images: SlideImage[];\n /** Auto-advance interval in milliseconds (default: 5000) */\n intervalMs?: number;\n /** Auto-advance slides (default: true) */\n autoAdvance?: boolean;\n /** Transition type (default: 'fade') */\n transition?: 'fade' | 'slide' | 'none';\n /** Transition duration in ms (default: 500) */\n transitionDuration?: number;\n /** Show navigation dots (default: true) */\n showDots?: boolean;\n /** Show navigation arrows (default: true) */\n showArrows?: boolean;\n /** Additional class names */\n className?: string;\n /** Image container class names */\n imageClassName?: string;\n /** Children rendered as overlay */\n children?: ReactNode;\n}\n\nexport function PersistentSlideshow({\n images,\n intervalMs = 5000,\n autoAdvance = true,\n transition = 'fade',\n transitionDuration = 500,\n showDots = true,\n showArrows = true,\n className = '',\n imageClassName = '',\n children\n}: PersistentSlideshowProps) {\n const [state, setState] = useState<SlideshowState>(SlideshowManager.getState());\n\n useEffect(() => {\n // Initialize slideshow with images\n SlideshowManager.initialize(images, {\n intervalMs,\n transitionDuration,\n autoAdvance,\n });\n\n // Subscribe to state changes\n const unsubscribe = SlideshowManager.onStateChange(setState);\n\n return () => {\n unsubscribe();\n // Don't destroy - let slideshow persist for route changes\n };\n }, [images, intervalMs, transitionDuration, autoAdvance]);\n\n const contextValue: SlideshowContextValue = {\n state,\n controls: {\n next: () => SlideshowManager.next(),\n prev: () => SlideshowManager.prev(),\n goTo: (index) => SlideshowManager.goTo(index),\n pause: () => SlideshowManager.pause(),\n resume: () => SlideshowManager.resume(),\n toggle: () => SlideshowManager.toggle(),\n },\n };\n\n // Get transition styles\n const getTransitionStyles = (index: number): React.CSSProperties => {\n const isActive = index === state.currentIndex;\n\n switch (transition) {\n case 'fade':\n return {\n opacity: isActive ? 1 : 0,\n transition: `opacity ${transitionDuration}ms ease-in-out`,\n position: 'absolute',\n inset: 0,\n };\n case 'slide':\n const offset = (index - state.currentIndex) * 100;\n return {\n transform: `translateX(${offset}%)`,\n transition: `transform ${transitionDuration}ms ease-in-out`,\n position: 'absolute',\n inset: 0,\n };\n case 'none':\n default:\n return {\n opacity: isActive ? 1 : 0,\n position: 'absolute',\n inset: 0,\n };\n }\n };\n\n if (images.length === 0) {\n return (\n <div className={`relative w-full h-full bg-black flex items-center justify-center ${className}`}>\n <p className=\"text-white/60\">No images</p>\n </div>\n );\n }\n\n return (\n <SlideshowContext.Provider value={contextValue}>\n <div className={`relative w-full h-full bg-black overflow-hidden ${className}`}>\n {/* Image container */}\n <div className=\"relative w-full h-full\">\n {state.images.map((image, index) => (\n <div\n key={`${image.url}-${index}`}\n style={getTransitionStyles(index)}\n className={imageClassName}\n >\n <img\n src={image.url}\n alt={image.alt || `Slide ${index + 1}`}\n className=\"w-full h-full object-contain\"\n loading={index === 0 ? 'eager' : 'lazy'}\n />\n </div>\n ))}\n </div>\n\n {/* Navigation arrows */}\n {showArrows && state.totalSlides > 1 && (\n <>\n <button\n onClick={() => SlideshowManager.prev()}\n disabled={state.isTransitioning}\n className=\"absolute left-2 top-1/2 -translate-y-1/2 p-2 bg-black/40 hover:bg-black/60 rounded-full text-white transition-colors disabled:opacity-50\"\n aria-label=\"Previous slide\"\n >\n <svg className=\"w-5 h-5\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <path strokeLinecap=\"round\" strokeLinejoin=\"round\" strokeWidth={2} d=\"M15 19l-7-7 7-7\" />\n </svg>\n </button>\n <button\n onClick={() => SlideshowManager.next()}\n disabled={state.isTransitioning}\n className=\"absolute right-2 top-1/2 -translate-y-1/2 p-2 bg-black/40 hover:bg-black/60 rounded-full text-white transition-colors disabled:opacity-50\"\n aria-label=\"Next slide\"\n >\n <svg className=\"w-5 h-5\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <path strokeLinecap=\"round\" strokeLinejoin=\"round\" strokeWidth={2} d=\"M9 5l7 7-7 7\" />\n </svg>\n </button>\n </>\n )}\n\n {/* Navigation dots */}\n {showDots && state.totalSlides > 1 && (\n <div className=\"absolute bottom-3 left-1/2 -translate-x-1/2 flex gap-1.5\">\n {state.images.map((_, index) => (\n <button\n key={index}\n onClick={() => SlideshowManager.goTo(index)}\n disabled={state.isTransitioning}\n className={`w-2 h-2 rounded-full transition-colors ${\n index === state.currentIndex\n ? 'bg-white'\n : 'bg-white/40 hover:bg-white/60'\n }`}\n aria-label={`Go to slide ${index + 1}`}\n />\n ))}\n </div>\n )}\n\n {/* Overlay children */}\n <div className=\"absolute inset-0 z-10\">\n {children}\n </div>\n </div>\n </SlideshowContext.Provider>\n );\n}\n";
8
+ //# sourceMappingURL=PersistentSlideshow.tsx.template.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"PersistentSlideshow.tsx.template.d.ts","sourceRoot":"","sources":["../../../src/templates/slideshow/PersistentSlideshow.tsx.template.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,eAAO,MAAM,2BAA2B,grNA6MvC,CAAC"}
@@ -0,0 +1,212 @@
1
+ /**
2
+ * Slideshow Template - PersistentSlideshow Component
3
+ *
4
+ * React component that attaches to the SlideshowManager singleton.
5
+ * The slideshow state persists across route changes.
6
+ */
7
+ export const persistentSlideshowTemplate = `'use client';
8
+
9
+ import { useEffect, useState, createContext, useContext, ReactNode } from 'react';
10
+ import { SlideshowManager, SlideshowState, SlideImage } from './SlideshowManager';
11
+
12
+ // Context to share slideshow state with overlays
13
+ interface SlideshowContextValue {
14
+ state: SlideshowState;
15
+ controls: {
16
+ next: () => void;
17
+ prev: () => void;
18
+ goTo: (index: number) => void;
19
+ pause: () => void;
20
+ resume: () => void;
21
+ toggle: () => void;
22
+ };
23
+ }
24
+
25
+ const SlideshowContext = createContext<SlideshowContextValue | null>(null);
26
+
27
+ export function useSlideshow(): SlideshowContextValue {
28
+ const context = useContext(SlideshowContext);
29
+ if (!context) {
30
+ throw new Error('useSlideshow must be used within PersistentSlideshow');
31
+ }
32
+ return context;
33
+ }
34
+
35
+ interface PersistentSlideshowProps {
36
+ images: SlideImage[];
37
+ /** Auto-advance interval in milliseconds (default: 5000) */
38
+ intervalMs?: number;
39
+ /** Auto-advance slides (default: true) */
40
+ autoAdvance?: boolean;
41
+ /** Transition type (default: 'fade') */
42
+ transition?: 'fade' | 'slide' | 'none';
43
+ /** Transition duration in ms (default: 500) */
44
+ transitionDuration?: number;
45
+ /** Show navigation dots (default: true) */
46
+ showDots?: boolean;
47
+ /** Show navigation arrows (default: true) */
48
+ showArrows?: boolean;
49
+ /** Additional class names */
50
+ className?: string;
51
+ /** Image container class names */
52
+ imageClassName?: string;
53
+ /** Children rendered as overlay */
54
+ children?: ReactNode;
55
+ }
56
+
57
+ export function PersistentSlideshow({
58
+ images,
59
+ intervalMs = 5000,
60
+ autoAdvance = true,
61
+ transition = 'fade',
62
+ transitionDuration = 500,
63
+ showDots = true,
64
+ showArrows = true,
65
+ className = '',
66
+ imageClassName = '',
67
+ children
68
+ }: PersistentSlideshowProps) {
69
+ const [state, setState] = useState<SlideshowState>(SlideshowManager.getState());
70
+
71
+ useEffect(() => {
72
+ // Initialize slideshow with images
73
+ SlideshowManager.initialize(images, {
74
+ intervalMs,
75
+ transitionDuration,
76
+ autoAdvance,
77
+ });
78
+
79
+ // Subscribe to state changes
80
+ const unsubscribe = SlideshowManager.onStateChange(setState);
81
+
82
+ return () => {
83
+ unsubscribe();
84
+ // Don't destroy - let slideshow persist for route changes
85
+ };
86
+ }, [images, intervalMs, transitionDuration, autoAdvance]);
87
+
88
+ const contextValue: SlideshowContextValue = {
89
+ state,
90
+ controls: {
91
+ next: () => SlideshowManager.next(),
92
+ prev: () => SlideshowManager.prev(),
93
+ goTo: (index) => SlideshowManager.goTo(index),
94
+ pause: () => SlideshowManager.pause(),
95
+ resume: () => SlideshowManager.resume(),
96
+ toggle: () => SlideshowManager.toggle(),
97
+ },
98
+ };
99
+
100
+ // Get transition styles
101
+ const getTransitionStyles = (index: number): React.CSSProperties => {
102
+ const isActive = index === state.currentIndex;
103
+
104
+ switch (transition) {
105
+ case 'fade':
106
+ return {
107
+ opacity: isActive ? 1 : 0,
108
+ transition: \`opacity \${transitionDuration}ms ease-in-out\`,
109
+ position: 'absolute',
110
+ inset: 0,
111
+ };
112
+ case 'slide':
113
+ const offset = (index - state.currentIndex) * 100;
114
+ return {
115
+ transform: \`translateX(\${offset}%)\`,
116
+ transition: \`transform \${transitionDuration}ms ease-in-out\`,
117
+ position: 'absolute',
118
+ inset: 0,
119
+ };
120
+ case 'none':
121
+ default:
122
+ return {
123
+ opacity: isActive ? 1 : 0,
124
+ position: 'absolute',
125
+ inset: 0,
126
+ };
127
+ }
128
+ };
129
+
130
+ if (images.length === 0) {
131
+ return (
132
+ <div className={\`relative w-full h-full bg-black flex items-center justify-center \${className}\`}>
133
+ <p className="text-white/60">No images</p>
134
+ </div>
135
+ );
136
+ }
137
+
138
+ return (
139
+ <SlideshowContext.Provider value={contextValue}>
140
+ <div className={\`relative w-full h-full bg-black overflow-hidden \${className}\`}>
141
+ {/* Image container */}
142
+ <div className="relative w-full h-full">
143
+ {state.images.map((image, index) => (
144
+ <div
145
+ key={\`\${image.url}-\${index}\`}
146
+ style={getTransitionStyles(index)}
147
+ className={imageClassName}
148
+ >
149
+ <img
150
+ src={image.url}
151
+ alt={image.alt || \`Slide \${index + 1}\`}
152
+ className="w-full h-full object-contain"
153
+ loading={index === 0 ? 'eager' : 'lazy'}
154
+ />
155
+ </div>
156
+ ))}
157
+ </div>
158
+
159
+ {/* Navigation arrows */}
160
+ {showArrows && state.totalSlides > 1 && (
161
+ <>
162
+ <button
163
+ onClick={() => SlideshowManager.prev()}
164
+ disabled={state.isTransitioning}
165
+ className="absolute left-2 top-1/2 -translate-y-1/2 p-2 bg-black/40 hover:bg-black/60 rounded-full text-white transition-colors disabled:opacity-50"
166
+ aria-label="Previous slide"
167
+ >
168
+ <svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
169
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 19l-7-7 7-7" />
170
+ </svg>
171
+ </button>
172
+ <button
173
+ onClick={() => SlideshowManager.next()}
174
+ disabled={state.isTransitioning}
175
+ className="absolute right-2 top-1/2 -translate-y-1/2 p-2 bg-black/40 hover:bg-black/60 rounded-full text-white transition-colors disabled:opacity-50"
176
+ aria-label="Next slide"
177
+ >
178
+ <svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
179
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
180
+ </svg>
181
+ </button>
182
+ </>
183
+ )}
184
+
185
+ {/* Navigation dots */}
186
+ {showDots && state.totalSlides > 1 && (
187
+ <div className="absolute bottom-3 left-1/2 -translate-x-1/2 flex gap-1.5">
188
+ {state.images.map((_, index) => (
189
+ <button
190
+ key={index}
191
+ onClick={() => SlideshowManager.goTo(index)}
192
+ disabled={state.isTransitioning}
193
+ className={\`w-2 h-2 rounded-full transition-colors \${
194
+ index === state.currentIndex
195
+ ? 'bg-white'
196
+ : 'bg-white/40 hover:bg-white/60'
197
+ }\`}
198
+ aria-label={\`Go to slide \${index + 1}\`}
199
+ />
200
+ ))}
201
+ </div>
202
+ )}
203
+
204
+ {/* Overlay children */}
205
+ <div className="absolute inset-0 z-10">
206
+ {children}
207
+ </div>
208
+ </div>
209
+ </SlideshowContext.Provider>
210
+ );
211
+ }
212
+ `;
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Slideshow Template - SlideshowManager
3
+ *
4
+ * Singleton to persist slideshow state across route changes.
5
+ * Similar pattern to VideoManager - holds state globally.
6
+ */
7
+ export declare const slideshowManagerTemplate = "/**\n * SlideshowManager - Singleton to persist slideshow state across route changes\n *\n * Maintains currentIndex, isPaused, and images across Next.js soft navigations.\n * When navigating from /tile to /page, the slideshow stays on the same slide.\n */\n\ndeclare global {\n interface Window {\n __slideshowState?: SlideshowState;\n __slideshowInitialized?: boolean;\n }\n}\n\nexport interface SlideImage {\n url: string;\n alt?: string;\n caption?: string;\n}\n\nexport interface SlideshowState {\n currentIndex: number;\n totalSlides: number;\n isTransitioning: boolean;\n isPaused: boolean;\n images: SlideImage[];\n}\n\ntype StateCallback = (state: SlideshowState) => void;\n\nclass SlideshowManagerClass {\n private stateCallbacks: Set<StateCallback> = new Set();\n private currentState: SlideshowState = {\n currentIndex: 0,\n totalSlides: 0,\n isTransitioning: false,\n isPaused: false,\n images: [],\n };\n private autoAdvanceInterval: NodeJS.Timeout | null = null;\n private transitionDuration = 500;\n private intervalMs = 5000;\n\n constructor() {\n // Restore state from window if available\n if (typeof window !== 'undefined' && window.__slideshowState) {\n this.currentState = window.__slideshowState;\n }\n }\n\n /**\n * Initialize slideshow with images\n */\n initialize(images: SlideImage[], options?: {\n intervalMs?: number;\n transitionDuration?: number;\n autoAdvance?: boolean;\n }): void {\n if (typeof window === 'undefined') return;\n\n // Check if already initialized with same images\n const imagesMatch = this.currentState.images.length === images.length &&\n this.currentState.images.every((img, i) => img.url === images[i]?.url);\n\n if (window.__slideshowInitialized && imagesMatch) {\n // Already initialized with same images, just restore callbacks\n return;\n }\n\n this.intervalMs = options?.intervalMs ?? 5000;\n this.transitionDuration = options?.transitionDuration ?? 500;\n\n this.currentState = {\n ...this.currentState,\n images,\n totalSlides: images.length,\n // Keep current index if valid, otherwise reset\n currentIndex: this.currentState.currentIndex < images.length\n ? this.currentState.currentIndex\n : 0,\n isPaused: options?.autoAdvance === false,\n };\n\n window.__slideshowState = this.currentState;\n window.__slideshowInitialized = true;\n\n this.notifyCallbacks();\n\n // Start auto-advance if enabled\n if (!this.currentState.isPaused && images.length > 1) {\n this.startAutoAdvance();\n }\n }\n\n /**\n * Get current state\n */\n getState(): SlideshowState {\n return { ...this.currentState };\n }\n\n /**\n * Subscribe to state changes\n */\n onStateChange(callback: StateCallback): () => void {\n this.stateCallbacks.add(callback);\n callback(this.currentState);\n return () => this.stateCallbacks.delete(callback);\n }\n\n /**\n * Go to next slide\n */\n next(): void {\n if (this.currentState.isTransitioning || this.currentState.totalSlides <= 1) return;\n\n this.updateState({ isTransitioning: true });\n const nextIndex = (this.currentState.currentIndex + 1) % this.currentState.totalSlides;\n this.updateState({ currentIndex: nextIndex });\n\n setTimeout(() => {\n this.updateState({ isTransitioning: false });\n }, this.transitionDuration);\n }\n\n /**\n * Go to previous slide\n */\n prev(): void {\n if (this.currentState.isTransitioning || this.currentState.totalSlides <= 1) return;\n\n this.updateState({ isTransitioning: true });\n const prevIndex = (this.currentState.currentIndex - 1 + this.currentState.totalSlides) % this.currentState.totalSlides;\n this.updateState({ currentIndex: prevIndex });\n\n setTimeout(() => {\n this.updateState({ isTransitioning: false });\n }, this.transitionDuration);\n }\n\n /**\n * Go to specific slide\n */\n goTo(index: number): void {\n if (\n this.currentState.isTransitioning ||\n index === this.currentState.currentIndex ||\n index < 0 ||\n index >= this.currentState.totalSlides\n ) return;\n\n this.updateState({ isTransitioning: true, currentIndex: index });\n\n setTimeout(() => {\n this.updateState({ isTransitioning: false });\n }, this.transitionDuration);\n }\n\n /**\n * Pause auto-advance\n */\n pause(): void {\n this.updateState({ isPaused: true });\n this.stopAutoAdvance();\n }\n\n /**\n * Resume auto-advance\n */\n resume(): void {\n this.updateState({ isPaused: false });\n if (this.currentState.totalSlides > 1) {\n this.startAutoAdvance();\n }\n }\n\n /**\n * Toggle pause/resume\n */\n toggle(): void {\n if (this.currentState.isPaused) {\n this.resume();\n } else {\n this.pause();\n }\n }\n\n /**\n * Clean up resources\n */\n destroy(): void {\n this.stopAutoAdvance();\n this.stateCallbacks.clear();\n if (typeof window !== 'undefined') {\n window.__slideshowInitialized = false;\n }\n }\n\n private startAutoAdvance(): void {\n this.stopAutoAdvance();\n this.autoAdvanceInterval = setInterval(() => {\n if (!this.currentState.isPaused) {\n this.next();\n }\n }, this.intervalMs);\n }\n\n private stopAutoAdvance(): void {\n if (this.autoAdvanceInterval) {\n clearInterval(this.autoAdvanceInterval);\n this.autoAdvanceInterval = null;\n }\n }\n\n private updateState(partial: Partial<SlideshowState>): void {\n this.currentState = { ...this.currentState, ...partial };\n if (typeof window !== 'undefined') {\n window.__slideshowState = this.currentState;\n }\n this.notifyCallbacks();\n }\n\n private notifyCallbacks(): void {\n this.stateCallbacks.forEach(cb => cb(this.currentState));\n }\n}\n\nexport const SlideshowManager = new SlideshowManagerClass();\n";
8
+ //# sourceMappingURL=SlideshowManager.ts.template.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"SlideshowManager.ts.template.d.ts","sourceRoot":"","sources":["../../../src/templates/slideshow/SlideshowManager.ts.template.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,eAAO,MAAM,wBAAwB,s1LAoOpC,CAAC"}
@@ -0,0 +1,235 @@
1
+ /**
2
+ * Slideshow Template - SlideshowManager
3
+ *
4
+ * Singleton to persist slideshow state across route changes.
5
+ * Similar pattern to VideoManager - holds state globally.
6
+ */
7
+ export const slideshowManagerTemplate = `/**
8
+ * SlideshowManager - Singleton to persist slideshow state across route changes
9
+ *
10
+ * Maintains currentIndex, isPaused, and images across Next.js soft navigations.
11
+ * When navigating from /tile to /page, the slideshow stays on the same slide.
12
+ */
13
+
14
+ declare global {
15
+ interface Window {
16
+ __slideshowState?: SlideshowState;
17
+ __slideshowInitialized?: boolean;
18
+ }
19
+ }
20
+
21
+ export interface SlideImage {
22
+ url: string;
23
+ alt?: string;
24
+ caption?: string;
25
+ }
26
+
27
+ export interface SlideshowState {
28
+ currentIndex: number;
29
+ totalSlides: number;
30
+ isTransitioning: boolean;
31
+ isPaused: boolean;
32
+ images: SlideImage[];
33
+ }
34
+
35
+ type StateCallback = (state: SlideshowState) => void;
36
+
37
+ class SlideshowManagerClass {
38
+ private stateCallbacks: Set<StateCallback> = new Set();
39
+ private currentState: SlideshowState = {
40
+ currentIndex: 0,
41
+ totalSlides: 0,
42
+ isTransitioning: false,
43
+ isPaused: false,
44
+ images: [],
45
+ };
46
+ private autoAdvanceInterval: NodeJS.Timeout | null = null;
47
+ private transitionDuration = 500;
48
+ private intervalMs = 5000;
49
+
50
+ constructor() {
51
+ // Restore state from window if available
52
+ if (typeof window !== 'undefined' && window.__slideshowState) {
53
+ this.currentState = window.__slideshowState;
54
+ }
55
+ }
56
+
57
+ /**
58
+ * Initialize slideshow with images
59
+ */
60
+ initialize(images: SlideImage[], options?: {
61
+ intervalMs?: number;
62
+ transitionDuration?: number;
63
+ autoAdvance?: boolean;
64
+ }): void {
65
+ if (typeof window === 'undefined') return;
66
+
67
+ // Check if already initialized with same images
68
+ const imagesMatch = this.currentState.images.length === images.length &&
69
+ this.currentState.images.every((img, i) => img.url === images[i]?.url);
70
+
71
+ if (window.__slideshowInitialized && imagesMatch) {
72
+ // Already initialized with same images, just restore callbacks
73
+ return;
74
+ }
75
+
76
+ this.intervalMs = options?.intervalMs ?? 5000;
77
+ this.transitionDuration = options?.transitionDuration ?? 500;
78
+
79
+ this.currentState = {
80
+ ...this.currentState,
81
+ images,
82
+ totalSlides: images.length,
83
+ // Keep current index if valid, otherwise reset
84
+ currentIndex: this.currentState.currentIndex < images.length
85
+ ? this.currentState.currentIndex
86
+ : 0,
87
+ isPaused: options?.autoAdvance === false,
88
+ };
89
+
90
+ window.__slideshowState = this.currentState;
91
+ window.__slideshowInitialized = true;
92
+
93
+ this.notifyCallbacks();
94
+
95
+ // Start auto-advance if enabled
96
+ if (!this.currentState.isPaused && images.length > 1) {
97
+ this.startAutoAdvance();
98
+ }
99
+ }
100
+
101
+ /**
102
+ * Get current state
103
+ */
104
+ getState(): SlideshowState {
105
+ return { ...this.currentState };
106
+ }
107
+
108
+ /**
109
+ * Subscribe to state changes
110
+ */
111
+ onStateChange(callback: StateCallback): () => void {
112
+ this.stateCallbacks.add(callback);
113
+ callback(this.currentState);
114
+ return () => this.stateCallbacks.delete(callback);
115
+ }
116
+
117
+ /**
118
+ * Go to next slide
119
+ */
120
+ next(): void {
121
+ if (this.currentState.isTransitioning || this.currentState.totalSlides <= 1) return;
122
+
123
+ this.updateState({ isTransitioning: true });
124
+ const nextIndex = (this.currentState.currentIndex + 1) % this.currentState.totalSlides;
125
+ this.updateState({ currentIndex: nextIndex });
126
+
127
+ setTimeout(() => {
128
+ this.updateState({ isTransitioning: false });
129
+ }, this.transitionDuration);
130
+ }
131
+
132
+ /**
133
+ * Go to previous slide
134
+ */
135
+ prev(): void {
136
+ if (this.currentState.isTransitioning || this.currentState.totalSlides <= 1) return;
137
+
138
+ this.updateState({ isTransitioning: true });
139
+ const prevIndex = (this.currentState.currentIndex - 1 + this.currentState.totalSlides) % this.currentState.totalSlides;
140
+ this.updateState({ currentIndex: prevIndex });
141
+
142
+ setTimeout(() => {
143
+ this.updateState({ isTransitioning: false });
144
+ }, this.transitionDuration);
145
+ }
146
+
147
+ /**
148
+ * Go to specific slide
149
+ */
150
+ goTo(index: number): void {
151
+ if (
152
+ this.currentState.isTransitioning ||
153
+ index === this.currentState.currentIndex ||
154
+ index < 0 ||
155
+ index >= this.currentState.totalSlides
156
+ ) return;
157
+
158
+ this.updateState({ isTransitioning: true, currentIndex: index });
159
+
160
+ setTimeout(() => {
161
+ this.updateState({ isTransitioning: false });
162
+ }, this.transitionDuration);
163
+ }
164
+
165
+ /**
166
+ * Pause auto-advance
167
+ */
168
+ pause(): void {
169
+ this.updateState({ isPaused: true });
170
+ this.stopAutoAdvance();
171
+ }
172
+
173
+ /**
174
+ * Resume auto-advance
175
+ */
176
+ resume(): void {
177
+ this.updateState({ isPaused: false });
178
+ if (this.currentState.totalSlides > 1) {
179
+ this.startAutoAdvance();
180
+ }
181
+ }
182
+
183
+ /**
184
+ * Toggle pause/resume
185
+ */
186
+ toggle(): void {
187
+ if (this.currentState.isPaused) {
188
+ this.resume();
189
+ } else {
190
+ this.pause();
191
+ }
192
+ }
193
+
194
+ /**
195
+ * Clean up resources
196
+ */
197
+ destroy(): void {
198
+ this.stopAutoAdvance();
199
+ this.stateCallbacks.clear();
200
+ if (typeof window !== 'undefined') {
201
+ window.__slideshowInitialized = false;
202
+ }
203
+ }
204
+
205
+ private startAutoAdvance(): void {
206
+ this.stopAutoAdvance();
207
+ this.autoAdvanceInterval = setInterval(() => {
208
+ if (!this.currentState.isPaused) {
209
+ this.next();
210
+ }
211
+ }, this.intervalMs);
212
+ }
213
+
214
+ private stopAutoAdvance(): void {
215
+ if (this.autoAdvanceInterval) {
216
+ clearInterval(this.autoAdvanceInterval);
217
+ this.autoAdvanceInterval = null;
218
+ }
219
+ }
220
+
221
+ private updateState(partial: Partial<SlideshowState>): void {
222
+ this.currentState = { ...this.currentState, ...partial };
223
+ if (typeof window !== 'undefined') {
224
+ window.__slideshowState = this.currentState;
225
+ }
226
+ this.notifyCallbacks();
227
+ }
228
+
229
+ private notifyCallbacks(): void {
230
+ this.stateCallbacks.forEach(cb => cb(this.currentState));
231
+ }
232
+ }
233
+
234
+ export const SlideshowManager = new SlideshowManagerClass();
235
+ `;
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Slideshow Template Exports
3
+ *
4
+ * These templates are used by tile-deploy/agent-service to generate
5
+ * slideshow tiles with persistent state across route navigation.
6
+ */
7
+ export { slideshowManagerTemplate } from './SlideshowManager.ts.template';
8
+ export { persistentSlideshowTemplate } from './PersistentSlideshow.tsx.template';
9
+ export declare const slideshowTemplateFiles: {
10
+ 'src/shared/lib/SlideshowManager.ts': string;
11
+ 'src/shared/components/PersistentSlideshow.tsx': string;
12
+ };
13
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/templates/slideshow/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EAAE,wBAAwB,EAAE,MAAM,gCAAgC,CAAC;AAC1E,OAAO,EAAE,2BAA2B,EAAE,MAAM,oCAAoC,CAAC;AAGjF,eAAO,MAAM,sBAAsB;;;CAGlC,CAAC"}
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Slideshow Template Exports
3
+ *
4
+ * These templates are used by tile-deploy/agent-service to generate
5
+ * slideshow tiles with persistent state across route navigation.
6
+ */
7
+ export { slideshowManagerTemplate } from './SlideshowManager.ts.template';
8
+ export { persistentSlideshowTemplate } from './PersistentSlideshow.tsx.template';
9
+ // Template file structure for slideshow tiles
10
+ export const slideshowTemplateFiles = {
11
+ 'src/shared/lib/SlideshowManager.ts': 'slideshowManagerTemplate',
12
+ 'src/shared/components/PersistentSlideshow.tsx': 'persistentSlideshowTemplate',
13
+ };
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Video Template - PersistentVideo Component
3
+ *
4
+ * React component that attaches to the VideoManager singleton.
5
+ * The video element persists across route changes.
6
+ */
7
+ export declare const persistentVideoTemplate = "'use client';\n\nimport { useEffect, useRef, useState, createContext, useContext, ReactNode } from 'react';\nimport { VideoManager, VideoState } from './VideoManager';\n\n// Context to share video state with overlays\ninterface VideoContextValue {\n state: VideoState;\n controls: {\n play: () => void;\n pause: () => void;\n toggle: () => void;\n seek: (time: number) => void;\n setMuted: (muted: boolean) => void;\n setVolume: (volume: number) => void;\n };\n}\n\nconst VideoContext = createContext<VideoContextValue | null>(null);\n\nexport function useVideo(): VideoContextValue {\n const context = useContext(VideoContext);\n if (!context) {\n throw new Error('useVideo must be used within PersistentVideo');\n }\n return context;\n}\n\ninterface PersistentVideoProps {\n src?: string;\n hlsUrl?: string;\n className?: string;\n children?: ReactNode;\n}\n\nexport function PersistentVideo({\n src,\n hlsUrl,\n className = '',\n children\n}: PersistentVideoProps) {\n const containerRef = useRef<HTMLDivElement>(null);\n const [state, setState] = useState<VideoState>(VideoManager.getState());\n\n useEffect(() => {\n // Subscribe to state changes\n const unsubscribe = VideoManager.onStateChange(setState);\n\n // Attach video to container\n if (containerRef.current) {\n VideoManager.attachTo(containerRef.current);\n }\n\n // Load source if provided\n const videoSrc = hlsUrl || src;\n if (videoSrc) {\n VideoManager.loadSource(videoSrc);\n }\n\n return () => {\n unsubscribe();\n // Don't detach - let video persist for route changes\n };\n }, [src, hlsUrl]);\n\n const contextValue: VideoContextValue = {\n state,\n controls: {\n play: () => VideoManager.play(),\n pause: () => VideoManager.pause(),\n toggle: () => VideoManager.toggle(),\n seek: (time) => VideoManager.seek(time),\n setMuted: (muted) => VideoManager.setMuted(muted),\n setVolume: (volume) => VideoManager.setVolume(volume),\n },\n };\n\n return (\n <VideoContext.Provider value={contextValue}>\n <div className={`relative w-full h-full ${className}`}>\n {/* Container for persistent video element */}\n <div\n ref={containerRef}\n className=\"absolute inset-0\"\n style={{ backgroundColor: 'black' }}\n />\n\n {/* Hide native controls */}\n <style>{`\n #persistent-video::-webkit-media-controls { display: none !important; }\n #persistent-video::-webkit-media-controls-start-playback-button { display: none !important; }\n #persistent-video::-webkit-media-controls-overlay-play-button { display: none !important; }\n `}</style>\n\n {/* Overlay content */}\n <div className=\"absolute inset-0 z-10\">\n {children}\n </div>\n </div>\n </VideoContext.Provider>\n );\n}\n";
8
+ //# sourceMappingURL=PersistentVideo.tsx.template.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"PersistentVideo.tsx.template.d.ts","sourceRoot":"","sources":["../../../src/templates/video/PersistentVideo.tsx.template.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,eAAO,MAAM,uBAAuB,+2FAsGnC,CAAC"}
@@ -0,0 +1,109 @@
1
+ /**
2
+ * Video Template - PersistentVideo Component
3
+ *
4
+ * React component that attaches to the VideoManager singleton.
5
+ * The video element persists across route changes.
6
+ */
7
+ export const persistentVideoTemplate = `'use client';
8
+
9
+ import { useEffect, useRef, useState, createContext, useContext, ReactNode } from 'react';
10
+ import { VideoManager, VideoState } from './VideoManager';
11
+
12
+ // Context to share video state with overlays
13
+ interface VideoContextValue {
14
+ state: VideoState;
15
+ controls: {
16
+ play: () => void;
17
+ pause: () => void;
18
+ toggle: () => void;
19
+ seek: (time: number) => void;
20
+ setMuted: (muted: boolean) => void;
21
+ setVolume: (volume: number) => void;
22
+ };
23
+ }
24
+
25
+ const VideoContext = createContext<VideoContextValue | null>(null);
26
+
27
+ export function useVideo(): VideoContextValue {
28
+ const context = useContext(VideoContext);
29
+ if (!context) {
30
+ throw new Error('useVideo must be used within PersistentVideo');
31
+ }
32
+ return context;
33
+ }
34
+
35
+ interface PersistentVideoProps {
36
+ src?: string;
37
+ hlsUrl?: string;
38
+ className?: string;
39
+ children?: ReactNode;
40
+ }
41
+
42
+ export function PersistentVideo({
43
+ src,
44
+ hlsUrl,
45
+ className = '',
46
+ children
47
+ }: PersistentVideoProps) {
48
+ const containerRef = useRef<HTMLDivElement>(null);
49
+ const [state, setState] = useState<VideoState>(VideoManager.getState());
50
+
51
+ useEffect(() => {
52
+ // Subscribe to state changes
53
+ const unsubscribe = VideoManager.onStateChange(setState);
54
+
55
+ // Attach video to container
56
+ if (containerRef.current) {
57
+ VideoManager.attachTo(containerRef.current);
58
+ }
59
+
60
+ // Load source if provided
61
+ const videoSrc = hlsUrl || src;
62
+ if (videoSrc) {
63
+ VideoManager.loadSource(videoSrc);
64
+ }
65
+
66
+ return () => {
67
+ unsubscribe();
68
+ // Don't detach - let video persist for route changes
69
+ };
70
+ }, [src, hlsUrl]);
71
+
72
+ const contextValue: VideoContextValue = {
73
+ state,
74
+ controls: {
75
+ play: () => VideoManager.play(),
76
+ pause: () => VideoManager.pause(),
77
+ toggle: () => VideoManager.toggle(),
78
+ seek: (time) => VideoManager.seek(time),
79
+ setMuted: (muted) => VideoManager.setMuted(muted),
80
+ setVolume: (volume) => VideoManager.setVolume(volume),
81
+ },
82
+ };
83
+
84
+ return (
85
+ <VideoContext.Provider value={contextValue}>
86
+ <div className={\`relative w-full h-full \${className}\`}>
87
+ {/* Container for persistent video element */}
88
+ <div
89
+ ref={containerRef}
90
+ className="absolute inset-0"
91
+ style={{ backgroundColor: 'black' }}
92
+ />
93
+
94
+ {/* Hide native controls */}
95
+ <style>{\`
96
+ #persistent-video::-webkit-media-controls { display: none !important; }
97
+ #persistent-video::-webkit-media-controls-start-playback-button { display: none !important; }
98
+ #persistent-video::-webkit-media-controls-overlay-play-button { display: none !important; }
99
+ \`}</style>
100
+
101
+ {/* Overlay content */}
102
+ <div className="absolute inset-0 z-10">
103
+ {children}
104
+ </div>
105
+ </div>
106
+ </VideoContext.Provider>
107
+ );
108
+ }
109
+ `;
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Video Template - VideoManager
3
+ *
4
+ * Singleton to persist video playback across route changes.
5
+ * Similar pattern to DoomManager - holds the video element globally.
6
+ */
7
+ export declare const videoManagerTemplate = "/**\n * VideoManager - Singleton to persist video across route changes\n *\n * Uses a global video element that persists across Next.js soft navigations.\n * When navigating from /tile to /page, the video keeps playing seamlessly.\n */\n\nimport { getTileBridge } from '@thewhateverapp/tile-sdk';\n\ndeclare global {\n interface Window {\n __videoElement?: HTMLVideoElement;\n __videoHls?: any; // HLS.js instance\n __videoInitialized?: boolean;\n __videoVisible?: boolean;\n }\n}\n\ntype StateCallback = (state: VideoState) => void;\n\nexport interface VideoState {\n isPlaying: boolean;\n currentTime: number;\n duration: number;\n isLoading: boolean;\n isMuted: boolean;\n volume: number;\n error: string | null;\n}\n\nclass VideoManagerClass {\n private stateCallbacks: Set<StateCallback> = new Set();\n private currentState: VideoState = {\n isPlaying: false,\n currentTime: 0,\n duration: 0,\n isLoading: true,\n isMuted: true,\n volume: 1,\n error: null,\n };\n private hlsScriptLoaded = false;\n\n /**\n * Get or create the persistent video element\n */\n getVideo(): HTMLVideoElement {\n if (typeof window === 'undefined') {\n throw new Error('VideoManager requires browser environment');\n }\n\n if (!window.__videoElement) {\n const video = document.createElement('video');\n video.id = 'persistent-video';\n video.playsInline = true;\n video.muted = true; // Start muted\n video.loop = true;\n video.preload = 'auto';\n video.style.width = '100%';\n video.style.height = '100%';\n video.style.objectFit = 'contain';\n video.style.backgroundColor = 'black';\n\n // Set up event listeners\n video.addEventListener('play', () => this.updateState({ isPlaying: true }));\n video.addEventListener('pause', () => this.updateState({ isPlaying: false }));\n video.addEventListener('timeupdate', () => {\n this.updateState({ currentTime: video.currentTime });\n });\n video.addEventListener('durationchange', () => {\n this.updateState({ duration: video.duration });\n });\n video.addEventListener('loadeddata', () => {\n this.updateState({ isLoading: false });\n });\n video.addEventListener('waiting', () => {\n this.updateState({ isLoading: true });\n });\n video.addEventListener('playing', () => {\n this.updateState({ isLoading: false, isPlaying: true });\n });\n video.addEventListener('volumechange', () => {\n this.updateState({ isMuted: video.muted, volume: video.volume });\n });\n video.addEventListener('error', () => {\n this.updateState({ error: 'Video playback error' });\n });\n\n window.__videoElement = video;\n this.setupVisibilityHandling();\n }\n\n return window.__videoElement;\n }\n\n /**\n * Load a video source (supports HLS and regular video)\n */\n async loadSource(src: string): Promise<void> {\n const video = this.getVideo();\n\n // Check if this is an HLS stream\n if (src.includes('.m3u8')) {\n await this.loadHls(src);\n } else {\n // Regular video file\n video.src = src;\n video.load();\n }\n }\n\n /**\n * Load HLS stream using hls.js\n */\n private async loadHls(src: string): Promise<void> {\n const video = this.getVideo();\n\n // Load HLS.js from CDN if not already loaded\n if (!this.hlsScriptLoaded && !(window as any).Hls) {\n await new Promise<void>((resolve, reject) => {\n const script = document.createElement('script');\n script.src = 'https://cdn.jsdelivr.net/npm/hls.js@1';\n script.onload = () => {\n this.hlsScriptLoaded = true;\n resolve();\n };\n script.onerror = reject;\n document.head.appendChild(script);\n });\n }\n\n const Hls = (window as any).Hls;\n\n // Clean up existing HLS instance\n if (window.__videoHls) {\n window.__videoHls.destroy();\n }\n\n if (Hls.isSupported()) {\n const hls = new Hls({\n enableWorker: true,\n lowLatencyMode: true,\n });\n hls.loadSource(src);\n hls.attachMedia(video);\n hls.on(Hls.Events.MANIFEST_PARSED, () => {\n this.updateState({ isLoading: false });\n });\n hls.on(Hls.Events.ERROR, (_: any, data: any) => {\n if (data.fatal) {\n this.updateState({ error: 'HLS playback error' });\n }\n });\n window.__videoHls = hls;\n } else if (video.canPlayType('application/vnd.apple.mpegurl')) {\n // Native HLS support (Safari)\n video.src = src;\n }\n }\n\n /**\n * Set up visibility handling via TileBridge\n */\n private setupVisibilityHandling(): void {\n if (typeof window === 'undefined') return;\n\n try {\n const bridge = getTileBridge();\n\n bridge.onVisibilityChange((state) => {\n const video = this.getVideo();\n window.__videoVisible = state.visible;\n\n if (state.visible && !state.muted) {\n video.muted = false;\n video.play().catch(() => {\n video.muted = true;\n video.play().catch(() => {});\n });\n } else {\n video.muted = true;\n if (!state.visible) {\n video.pause();\n }\n }\n });\n } catch (err) {\n console.log('[VideoManager] TileBridge not available');\n }\n }\n\n /**\n * Attach video element to a container\n */\n attachTo(container: HTMLElement): void {\n const video = this.getVideo();\n if (!container.contains(video)) {\n container.appendChild(video);\n }\n }\n\n /**\n * Detach video element (but keep it in memory)\n */\n detach(): void {\n const video = this.getVideo();\n if (video.parentElement) {\n video.parentElement.removeChild(video);\n }\n }\n\n // Playback controls\n play(): void {\n this.getVideo().play().catch(() => {});\n }\n\n pause(): void {\n this.getVideo().pause();\n }\n\n toggle(): void {\n const video = this.getVideo();\n if (video.paused) {\n video.play().catch(() => {});\n } else {\n video.pause();\n }\n }\n\n seek(time: number): void {\n this.getVideo().currentTime = time;\n }\n\n setMuted(muted: boolean): void {\n this.getVideo().muted = muted;\n }\n\n setVolume(volume: number): void {\n this.getVideo().volume = Math.max(0, Math.min(1, volume));\n }\n\n // State management\n getState(): VideoState {\n return { ...this.currentState };\n }\n\n onStateChange(callback: StateCallback): () => void {\n this.stateCallbacks.add(callback);\n callback(this.currentState);\n return () => this.stateCallbacks.delete(callback);\n }\n\n private updateState(partial: Partial<VideoState>): void {\n this.currentState = { ...this.currentState, ...partial };\n this.stateCallbacks.forEach(cb => cb(this.currentState));\n }\n}\n\nexport const VideoManager = new VideoManagerClass();\n";
8
+ //# sourceMappingURL=VideoManager.ts.template.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"VideoManager.ts.template.d.ts","sourceRoot":"","sources":["../../../src/templates/video/VideoManager.ts.template.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,eAAO,MAAM,oBAAoB,83NAmQhC,CAAC"}
@@ -0,0 +1,266 @@
1
+ /**
2
+ * Video Template - VideoManager
3
+ *
4
+ * Singleton to persist video playback across route changes.
5
+ * Similar pattern to DoomManager - holds the video element globally.
6
+ */
7
+ export const videoManagerTemplate = `/**
8
+ * VideoManager - Singleton to persist video across route changes
9
+ *
10
+ * Uses a global video element that persists across Next.js soft navigations.
11
+ * When navigating from /tile to /page, the video keeps playing seamlessly.
12
+ */
13
+
14
+ import { getTileBridge } from '@thewhateverapp/tile-sdk';
15
+
16
+ declare global {
17
+ interface Window {
18
+ __videoElement?: HTMLVideoElement;
19
+ __videoHls?: any; // HLS.js instance
20
+ __videoInitialized?: boolean;
21
+ __videoVisible?: boolean;
22
+ }
23
+ }
24
+
25
+ type StateCallback = (state: VideoState) => void;
26
+
27
+ export interface VideoState {
28
+ isPlaying: boolean;
29
+ currentTime: number;
30
+ duration: number;
31
+ isLoading: boolean;
32
+ isMuted: boolean;
33
+ volume: number;
34
+ error: string | null;
35
+ }
36
+
37
+ class VideoManagerClass {
38
+ private stateCallbacks: Set<StateCallback> = new Set();
39
+ private currentState: VideoState = {
40
+ isPlaying: false,
41
+ currentTime: 0,
42
+ duration: 0,
43
+ isLoading: true,
44
+ isMuted: true,
45
+ volume: 1,
46
+ error: null,
47
+ };
48
+ private hlsScriptLoaded = false;
49
+
50
+ /**
51
+ * Get or create the persistent video element
52
+ */
53
+ getVideo(): HTMLVideoElement {
54
+ if (typeof window === 'undefined') {
55
+ throw new Error('VideoManager requires browser environment');
56
+ }
57
+
58
+ if (!window.__videoElement) {
59
+ const video = document.createElement('video');
60
+ video.id = 'persistent-video';
61
+ video.playsInline = true;
62
+ video.muted = true; // Start muted
63
+ video.loop = true;
64
+ video.preload = 'auto';
65
+ video.style.width = '100%';
66
+ video.style.height = '100%';
67
+ video.style.objectFit = 'contain';
68
+ video.style.backgroundColor = 'black';
69
+
70
+ // Set up event listeners
71
+ video.addEventListener('play', () => this.updateState({ isPlaying: true }));
72
+ video.addEventListener('pause', () => this.updateState({ isPlaying: false }));
73
+ video.addEventListener('timeupdate', () => {
74
+ this.updateState({ currentTime: video.currentTime });
75
+ });
76
+ video.addEventListener('durationchange', () => {
77
+ this.updateState({ duration: video.duration });
78
+ });
79
+ video.addEventListener('loadeddata', () => {
80
+ this.updateState({ isLoading: false });
81
+ });
82
+ video.addEventListener('waiting', () => {
83
+ this.updateState({ isLoading: true });
84
+ });
85
+ video.addEventListener('playing', () => {
86
+ this.updateState({ isLoading: false, isPlaying: true });
87
+ });
88
+ video.addEventListener('volumechange', () => {
89
+ this.updateState({ isMuted: video.muted, volume: video.volume });
90
+ });
91
+ video.addEventListener('error', () => {
92
+ this.updateState({ error: 'Video playback error' });
93
+ });
94
+
95
+ window.__videoElement = video;
96
+ this.setupVisibilityHandling();
97
+ }
98
+
99
+ return window.__videoElement;
100
+ }
101
+
102
+ /**
103
+ * Load a video source (supports HLS and regular video)
104
+ */
105
+ async loadSource(src: string): Promise<void> {
106
+ const video = this.getVideo();
107
+
108
+ // Check if this is an HLS stream
109
+ if (src.includes('.m3u8')) {
110
+ await this.loadHls(src);
111
+ } else {
112
+ // Regular video file
113
+ video.src = src;
114
+ video.load();
115
+ }
116
+ }
117
+
118
+ /**
119
+ * Load HLS stream using hls.js
120
+ */
121
+ private async loadHls(src: string): Promise<void> {
122
+ const video = this.getVideo();
123
+
124
+ // Load HLS.js from CDN if not already loaded
125
+ if (!this.hlsScriptLoaded && !(window as any).Hls) {
126
+ await new Promise<void>((resolve, reject) => {
127
+ const script = document.createElement('script');
128
+ script.src = 'https://cdn.jsdelivr.net/npm/hls.js@1';
129
+ script.onload = () => {
130
+ this.hlsScriptLoaded = true;
131
+ resolve();
132
+ };
133
+ script.onerror = reject;
134
+ document.head.appendChild(script);
135
+ });
136
+ }
137
+
138
+ const Hls = (window as any).Hls;
139
+
140
+ // Clean up existing HLS instance
141
+ if (window.__videoHls) {
142
+ window.__videoHls.destroy();
143
+ }
144
+
145
+ if (Hls.isSupported()) {
146
+ const hls = new Hls({
147
+ enableWorker: true,
148
+ lowLatencyMode: true,
149
+ });
150
+ hls.loadSource(src);
151
+ hls.attachMedia(video);
152
+ hls.on(Hls.Events.MANIFEST_PARSED, () => {
153
+ this.updateState({ isLoading: false });
154
+ });
155
+ hls.on(Hls.Events.ERROR, (_: any, data: any) => {
156
+ if (data.fatal) {
157
+ this.updateState({ error: 'HLS playback error' });
158
+ }
159
+ });
160
+ window.__videoHls = hls;
161
+ } else if (video.canPlayType('application/vnd.apple.mpegurl')) {
162
+ // Native HLS support (Safari)
163
+ video.src = src;
164
+ }
165
+ }
166
+
167
+ /**
168
+ * Set up visibility handling via TileBridge
169
+ */
170
+ private setupVisibilityHandling(): void {
171
+ if (typeof window === 'undefined') return;
172
+
173
+ try {
174
+ const bridge = getTileBridge();
175
+
176
+ bridge.onVisibilityChange((state) => {
177
+ const video = this.getVideo();
178
+ window.__videoVisible = state.visible;
179
+
180
+ if (state.visible && !state.muted) {
181
+ video.muted = false;
182
+ video.play().catch(() => {
183
+ video.muted = true;
184
+ video.play().catch(() => {});
185
+ });
186
+ } else {
187
+ video.muted = true;
188
+ if (!state.visible) {
189
+ video.pause();
190
+ }
191
+ }
192
+ });
193
+ } catch (err) {
194
+ console.log('[VideoManager] TileBridge not available');
195
+ }
196
+ }
197
+
198
+ /**
199
+ * Attach video element to a container
200
+ */
201
+ attachTo(container: HTMLElement): void {
202
+ const video = this.getVideo();
203
+ if (!container.contains(video)) {
204
+ container.appendChild(video);
205
+ }
206
+ }
207
+
208
+ /**
209
+ * Detach video element (but keep it in memory)
210
+ */
211
+ detach(): void {
212
+ const video = this.getVideo();
213
+ if (video.parentElement) {
214
+ video.parentElement.removeChild(video);
215
+ }
216
+ }
217
+
218
+ // Playback controls
219
+ play(): void {
220
+ this.getVideo().play().catch(() => {});
221
+ }
222
+
223
+ pause(): void {
224
+ this.getVideo().pause();
225
+ }
226
+
227
+ toggle(): void {
228
+ const video = this.getVideo();
229
+ if (video.paused) {
230
+ video.play().catch(() => {});
231
+ } else {
232
+ video.pause();
233
+ }
234
+ }
235
+
236
+ seek(time: number): void {
237
+ this.getVideo().currentTime = time;
238
+ }
239
+
240
+ setMuted(muted: boolean): void {
241
+ this.getVideo().muted = muted;
242
+ }
243
+
244
+ setVolume(volume: number): void {
245
+ this.getVideo().volume = Math.max(0, Math.min(1, volume));
246
+ }
247
+
248
+ // State management
249
+ getState(): VideoState {
250
+ return { ...this.currentState };
251
+ }
252
+
253
+ onStateChange(callback: StateCallback): () => void {
254
+ this.stateCallbacks.add(callback);
255
+ callback(this.currentState);
256
+ return () => this.stateCallbacks.delete(callback);
257
+ }
258
+
259
+ private updateState(partial: Partial<VideoState>): void {
260
+ this.currentState = { ...this.currentState, ...partial };
261
+ this.stateCallbacks.forEach(cb => cb(this.currentState));
262
+ }
263
+ }
264
+
265
+ export const VideoManager = new VideoManagerClass();
266
+ `;
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Video Template Exports
3
+ *
4
+ * These templates are used by tile-deploy/agent-service to generate
5
+ * video tiles with persistent playback across route navigation.
6
+ */
7
+ export { videoLayoutTemplate } from './layout.tsx.template';
8
+ export { videoManagerTemplate } from './VideoManager.ts.template';
9
+ export { persistentVideoTemplate } from './PersistentVideo.tsx.template';
10
+ export declare const videoTemplateFiles: {
11
+ 'src/shared/lib/VideoManager.ts': string;
12
+ 'src/shared/components/PersistentVideo.tsx': string;
13
+ };
14
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/templates/video/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EAAE,mBAAmB,EAAE,MAAM,uBAAuB,CAAC;AAC5D,OAAO,EAAE,oBAAoB,EAAE,MAAM,4BAA4B,CAAC;AAClE,OAAO,EAAE,uBAAuB,EAAE,MAAM,gCAAgC,CAAC;AAGzE,eAAO,MAAM,kBAAkB;;;CAG9B,CAAC"}
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Video Template Exports
3
+ *
4
+ * These templates are used by tile-deploy/agent-service to generate
5
+ * video tiles with persistent playback across route navigation.
6
+ */
7
+ export { videoLayoutTemplate } from './layout.tsx.template';
8
+ export { videoManagerTemplate } from './VideoManager.ts.template';
9
+ export { persistentVideoTemplate } from './PersistentVideo.tsx.template';
10
+ // Template file structure for video tiles
11
+ export const videoTemplateFiles = {
12
+ 'src/shared/lib/VideoManager.ts': 'videoManagerTemplate',
13
+ 'src/shared/components/PersistentVideo.tsx': 'persistentVideoTemplate',
14
+ };
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Video Template - Layout
3
+ *
4
+ * This layout wraps both /tile and /page routes.
5
+ * The VideoPlayer is rendered here so it persists across route navigation.
6
+ */
7
+ export declare const videoLayoutTemplate = "'use client';\n\nimport { VideoPlayer } from '@thewhateverapp/tile-sdk/react';\nimport { usePathname } from 'next/navigation';\n\nexport default function VideoLayout({ children }: { children: React.ReactNode }) {\n const pathname = usePathname();\n const isPageView = pathname === '/page';\n\n return (\n <div className=\"relative w-full h-full bg-black\">\n {/* VideoPlayer persists across route changes */}\n <VideoPlayer className=\"absolute inset-0\">\n {/* Children are the route-specific overlays */}\n {children}\n </VideoPlayer>\n </div>\n );\n}\n";
8
+ //# sourceMappingURL=layout.tsx.template.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"layout.tsx.template.d.ts","sourceRoot":"","sources":["../../../src/templates/video/layout.tsx.template.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,eAAO,MAAM,mBAAmB,ulBAmB/B,CAAC"}
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Video Template - Layout
3
+ *
4
+ * This layout wraps both /tile and /page routes.
5
+ * The VideoPlayer is rendered here so it persists across route navigation.
6
+ */
7
+ export const videoLayoutTemplate = `'use client';
8
+
9
+ import { VideoPlayer } from '@thewhateverapp/tile-sdk/react';
10
+ import { usePathname } from 'next/navigation';
11
+
12
+ export default function VideoLayout({ children }: { children: React.ReactNode }) {
13
+ const pathname = usePathname();
14
+ const isPageView = pathname === '/page';
15
+
16
+ return (
17
+ <div className="relative w-full h-full bg-black">
18
+ {/* VideoPlayer persists across route changes */}
19
+ <VideoPlayer className="absolute inset-0">
20
+ {/* Children are the route-specific overlays */}
21
+ {children}
22
+ </VideoPlayer>
23
+ </div>
24
+ );
25
+ }
26
+ `;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@thewhateverapp/tile-sdk",
3
- "version": "0.12.19",
3
+ "version": "0.12.21",
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",
@@ -16,6 +16,14 @@
16
16
  "./tools": {
17
17
  "types": "./dist/tools/index.d.ts",
18
18
  "import": "./dist/tools/index.js"
19
+ },
20
+ "./templates": {
21
+ "types": "./dist/templates/index.d.ts",
22
+ "import": "./dist/templates/index.js"
23
+ },
24
+ "./templates/video": {
25
+ "types": "./dist/templates/video/index.d.ts",
26
+ "import": "./dist/templates/video/index.js"
19
27
  }
20
28
  },
21
29
  "files": [