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