blob: 287192982272afde0cafd03cfe1848717a03b6bf [file] [log] [blame]
/**
* Demo runner that dynamically loads and executes demo modules
*/
import { DemoModule, DemoRunnerOptions, DemoNavigationEvent } from "./types";
export class DemoRunner {
private container: HTMLElement;
private basePath: string;
private currentDemo: DemoModule | null = null;
private currentComponentName: string | null = null;
private onDemoChange?: (componentName: string, demo: DemoModule) => void;
constructor(options: DemoRunnerOptions) {
this.container = options.container;
this.basePath = options.basePath || "../";
this.onDemoChange = options.onDemoChange;
}
/**
* Load and display a demo for the specified component
*/
async loadDemo(componentName: string): Promise<void> {
try {
// Cleanup current demo if any
await this.cleanup();
// Dynamically import the demo module
const demoModule = await import(
/* @vite-ignore */ `../${componentName}.demo.ts`
);
const demo: DemoModule = demoModule.default;
if (!demo) {
throw new Error(
`Demo module for ${componentName} does not export a default DemoModule`,
);
}
// Clear container
this.container.innerHTML = "";
// Load additional styles if specified
if (demo.styles) {
for (const styleUrl of demo.styles) {
await this.loadStylesheet(styleUrl);
}
}
// Add custom styles if specified
if (demo.customStyles) {
this.addCustomStyles(demo.customStyles, componentName);
}
// Import required component modules
if (demo.imports) {
for (const importPath of demo.imports) {
await import(/* @vite-ignore */ this.basePath + importPath);
}
}
// Set up the demo
await demo.setup(this.container);
// Update current state
this.currentDemo = demo;
this.currentComponentName = componentName;
// Notify listeners
if (this.onDemoChange) {
this.onDemoChange(componentName, demo);
}
// Dispatch navigation event
const event: DemoNavigationEvent = new CustomEvent("demo-navigation", {
detail: { componentName, demo },
});
document.dispatchEvent(event);
} catch (error) {
console.error(`Failed to load demo for ${componentName}:`, error);
this.showError(`Failed to load demo for ${componentName}`, error);
}
}
/**
* Get list of available demo components by scanning for .demo.ts files
*/
async getAvailableComponents(): Promise<string[]> {
// For now, we'll maintain a registry of known demo components
// This could be improved with build-time generation
const knownComponents = [
"sketch-app-shell",
"sketch-call-status",
"sketch-chat-input",
"sketch-container-status",
"sketch-diff-range-picker",
"sketch-timeline",
"sketch-timeline-message",
"sketch-external-message",
"sketch-todo-panel",
"sketch-tool-calls",
"sketch-view-mode-select",
"sketch-theme-toggle",
"mobile-chat",
"sketch-diff2-view",
"sketch-monaco-view",
"sketch-timeline-viewport",
"sketch-tool-card",
"status-indicators",
];
// Filter to only components that actually have demo files
const availableComponents: string[] = [];
for (const component of knownComponents) {
try {
// Test if the demo module exists by attempting to import it
const demoModule = await import(
/* @vite-ignore */ `../${component}.demo.ts`
);
if (demoModule.default) {
availableComponents.push(component);
}
} catch (error) {
console.warn(`Demo not available for ${component}:`, error);
// Component demo doesn't exist, skip it
}
}
return availableComponents;
}
/**
* Cleanup current demo
*/
private async cleanup(): Promise<void> {
if (this.currentDemo?.cleanup) {
await this.currentDemo.cleanup();
}
// Remove custom styles
if (this.currentComponentName) {
this.removeCustomStyles(this.currentComponentName);
}
this.currentDemo = null;
this.currentComponentName = null;
}
/**
* Load a CSS stylesheet dynamically
*/
private async loadStylesheet(url: string): Promise<void> {
return new Promise((resolve, reject) => {
const link = document.createElement("link");
link.rel = "stylesheet";
link.href = url;
link.onload = () => resolve();
link.onerror = () =>
reject(new Error(`Failed to load stylesheet: ${url}`));
document.head.appendChild(link);
});
}
/**
* Add custom CSS styles for a demo
*/
private addCustomStyles(css: string, componentName: string): void {
const styleId = `demo-custom-styles-${componentName}`;
// Remove existing styles for this component
const existing = document.getElementById(styleId);
if (existing) {
existing.remove();
}
// Add new styles
const style = document.createElement("style");
style.id = styleId;
style.textContent = css;
document.head.appendChild(style);
}
/**
* Remove custom styles for a component
*/
private removeCustomStyles(componentName: string): void {
const styleId = `demo-custom-styles-${componentName}`;
const existing = document.getElementById(styleId);
if (existing) {
existing.remove();
}
}
/**
* Show error message in the demo container
*/
private showError(message: string, error: any): void {
this.container.innerHTML = `
<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">
<h3 class="text-lg font-semibold mb-2">Demo Error</h3>
<p class="mb-4"><strong>${message}</strong></p>
<details class="text-sm">
<summary class="cursor-pointer hover:text-red-600 dark:hover:text-red-300">Error Details</summary>
<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>
</details>
</div>
`;
}
/**
* Get current demo info
*/
getCurrentDemo(): { componentName: string; demo: DemoModule } | null {
if (this.currentComponentName && this.currentDemo) {
return {
componentName: this.currentComponentName,
demo: this.currentDemo,
};
}
return null;
}
}