blob: a547c0a4e8d1c2ab3866f1a8cd38901b6467d3a4 [file] [log] [blame]
Sean McCullough618bfb22025-06-25 20:52:30 +00001/**
2 * Demo runner that dynamically loads and executes demo modules
3 */
4
5import {
6 DemoModule,
7 DemoRegistry,
8 DemoRunnerOptions,
9 DemoNavigationEvent,
10} from "./types";
11
12export class DemoRunner {
13 private container: HTMLElement;
14 private basePath: string;
15 private currentDemo: DemoModule | null = null;
16 private currentComponentName: string | null = null;
17 private onDemoChange?: (componentName: string, demo: DemoModule) => void;
18
19 constructor(options: DemoRunnerOptions) {
20 this.container = options.container;
21 this.basePath = options.basePath || "../";
22 this.onDemoChange = options.onDemoChange;
23 }
24
25 /**
26 * Load and display a demo for the specified component
27 */
28 async loadDemo(componentName: string): Promise<void> {
29 try {
30 // Cleanup current demo if any
31 await this.cleanup();
32
33 // Dynamically import the demo module
34 const demoModule = await import(
35 /* @vite-ignore */ `../${componentName}.demo.ts`
36 );
37 const demo: DemoModule = demoModule.default;
38
39 if (!demo) {
40 throw new Error(
41 `Demo module for ${componentName} does not export a default DemoModule`,
42 );
43 }
44
45 // Clear container
46 this.container.innerHTML = "";
47
48 // Load additional styles if specified
49 if (demo.styles) {
50 for (const styleUrl of demo.styles) {
51 await this.loadStylesheet(styleUrl);
52 }
53 }
54
55 // Add custom styles if specified
56 if (demo.customStyles) {
57 this.addCustomStyles(demo.customStyles, componentName);
58 }
59
60 // Import required component modules
61 if (demo.imports) {
62 for (const importPath of demo.imports) {
63 await import(/* @vite-ignore */ this.basePath + importPath);
64 }
65 }
66
67 // Set up the demo
68 await demo.setup(this.container);
69
70 // Update current state
71 this.currentDemo = demo;
72 this.currentComponentName = componentName;
73
74 // Notify listeners
75 if (this.onDemoChange) {
76 this.onDemoChange(componentName, demo);
77 }
78
79 // Dispatch navigation event
80 const event: DemoNavigationEvent = new CustomEvent("demo-navigation", {
81 detail: { componentName, demo },
82 });
83 document.dispatchEvent(event);
84 } catch (error) {
85 console.error(`Failed to load demo for ${componentName}:`, error);
86 this.showError(`Failed to load demo for ${componentName}`, error);
87 }
88 }
89
90 /**
91 * Get list of available demo components by scanning for .demo.ts files
92 */
93 async getAvailableComponents(): Promise<string[]> {
94 // For now, we'll maintain a registry of known demo components
95 // This could be improved with build-time generation
96 const knownComponents = [
Sean McCulloughb3795922025-06-27 01:59:41 +000097 "chat-input",
Sean McCullough4337aa72025-06-27 23:41:33 +000098 "sketch-app-shell",
banksean659b9832025-06-27 00:50:41 +000099 "sketch-call-status",
Sean McCullough618bfb22025-06-25 20:52:30 +0000100 "sketch-chat-input",
101 "sketch-container-status",
bankseane59a2e12025-06-28 01:38:19 +0000102 "sketch-timeline",
Sean McCullough618bfb22025-06-25 20:52:30 +0000103 "sketch-tool-calls",
bankseand5c849d2025-06-26 15:48:31 +0000104 "sketch-view-mode-select",
Sean McCullough618bfb22025-06-25 20:52:30 +0000105 ];
106
107 // Filter to only components that actually have demo files
108 const availableComponents: string[] = [];
109 for (const component of knownComponents) {
110 try {
111 // Test if the demo module exists by attempting to import it
112 const demoModule = await import(
113 /* @vite-ignore */ `../${component}.demo.ts`
114 );
115 if (demoModule.default) {
116 availableComponents.push(component);
117 }
118 } catch (error) {
119 console.warn(`Demo not available for ${component}:`, error);
120 // Component demo doesn't exist, skip it
121 }
122 }
123
124 return availableComponents;
125 }
126
127 /**
128 * Cleanup current demo
129 */
130 private async cleanup(): Promise<void> {
131 if (this.currentDemo?.cleanup) {
132 await this.currentDemo.cleanup();
133 }
134
135 // Remove custom styles
136 if (this.currentComponentName) {
137 this.removeCustomStyles(this.currentComponentName);
138 }
139
140 this.currentDemo = null;
141 this.currentComponentName = null;
142 }
143
144 /**
145 * Load a CSS stylesheet dynamically
146 */
147 private async loadStylesheet(url: string): Promise<void> {
148 return new Promise((resolve, reject) => {
149 const link = document.createElement("link");
150 link.rel = "stylesheet";
151 link.href = url;
152 link.onload = () => resolve();
153 link.onerror = () =>
154 reject(new Error(`Failed to load stylesheet: ${url}`));
155 document.head.appendChild(link);
156 });
157 }
158
159 /**
160 * Add custom CSS styles for a demo
161 */
162 private addCustomStyles(css: string, componentName: string): void {
163 const styleId = `demo-custom-styles-${componentName}`;
164
165 // Remove existing styles for this component
166 const existing = document.getElementById(styleId);
167 if (existing) {
168 existing.remove();
169 }
170
171 // Add new styles
172 const style = document.createElement("style");
173 style.id = styleId;
174 style.textContent = css;
175 document.head.appendChild(style);
176 }
177
178 /**
179 * Remove custom styles for a component
180 */
181 private removeCustomStyles(componentName: string): void {
182 const styleId = `demo-custom-styles-${componentName}`;
183 const existing = document.getElementById(styleId);
184 if (existing) {
185 existing.remove();
186 }
187 }
188
189 /**
190 * Show error message in the demo container
191 */
192 private showError(message: string, error: any): void {
193 this.container.innerHTML = `
194 <div style="
195 padding: 20px;
196 background: #fee;
197 border: 1px solid #fcc;
198 border-radius: 4px;
199 color: #800;
200 font-family: monospace;
201 ">
202 <h3>Demo Error</h3>
203 <p><strong>${message}</strong></p>
204 <details>
205 <summary>Error Details</summary>
206 <pre>${error.stack || error.message || error}</pre>
207 </details>
208 </div>
209 `;
210 }
211
212 /**
213 * Get current demo info
214 */
215 getCurrentDemo(): { componentName: string; demo: DemoModule } | null {
216 if (this.currentComponentName && this.currentDemo) {
217 return {
218 componentName: this.currentComponentName,
219 demo: this.currentDemo,
220 };
221 }
222 return null;
223 }
224}