blob: 0ac84e8e803a1a544a5253e6165d3655614dddda [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",
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 = `
199 <div style="
200 padding: 20px;
201 background: #fee;
202 border: 1px solid #fcc;
203 border-radius: 4px;
204 color: #800;
205 font-family: monospace;
206 ">
207 <h3>Demo Error</h3>
208 <p><strong>${message}</strong></p>
209 <details>
210 <summary>Error Details</summary>
211 <pre>${error.stack || error.message || error}</pre>
212 </details>
213 </div>
214 `;
215 }
216
217 /**
218 * Get current demo info
219 */
220 getCurrentDemo(): { componentName: string; demo: DemoModule } | null {
221 if (this.currentComponentName && this.currentDemo) {
222 return {
223 componentName: this.currentComponentName,
224 demo: this.currentDemo,
225 };
226 }
227 return null;
228 }
229}