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