| Sean McCullough | 618bfb2 | 2025-06-25 20:52:30 +0000 | [diff] [blame] | 1 | <!doctype html> |
| 2 | <html lang="en"> |
| 3 | <head> |
| 4 | <meta charset="UTF-8" /> |
| 5 | <meta name="viewport" content="width=device-width, initial-scale=1.0" /> |
| 6 | <title>Sketch Web Components Demo Runner</title> |
| Sean McCullough | 618bfb2 | 2025-06-25 20:52:30 +0000 | [diff] [blame] | 7 | <link rel="stylesheet" href="/dist/tailwind.css" /> |
| 8 | <style> |
| 9 | :root { |
| 10 | --demo-primary: #0969da; |
| 11 | --demo-secondary: #656d76; |
| 12 | --demo-background: #f6f8fa; |
| 13 | --demo-border: #d1d9e0; |
| banksean | 1ee0bc6 | 2025-07-22 23:24:18 +0000 | [diff] [blame^] | 14 | --demo-text-primary: #24292f; |
| 15 | --demo-hover-bg: #ffffff; |
| 16 | --demo-container-bg: #ffffff; |
| 17 | --demo-error-bg: #ffeaea; |
| 18 | --demo-error-border: #ffcccc; |
| 19 | --demo-error-text: #d73a49; |
| 20 | } |
| 21 | |
| 22 | .dark { |
| 23 | --demo-primary: #4493f8; |
| 24 | --demo-secondary: #8b949e; |
| 25 | --demo-background: #21262d; |
| 26 | --demo-border: #30363d; |
| 27 | --demo-text-primary: #e6edf3; |
| 28 | --demo-hover-bg: #30363d; |
| 29 | --demo-container-bg: #0d1117; |
| 30 | --demo-error-bg: #3c1e1e; |
| 31 | --demo-error-border: #6a2c2c; |
| 32 | --demo-error-text: #f85149; |
| 33 | } |
| 34 | |
| 35 | body { |
| 36 | background: var(--demo-container-bg); |
| 37 | color: var(--demo-text-primary); |
| 38 | transition: |
| 39 | background-color 0.2s, |
| 40 | color 0.2s; |
| 41 | margin: 0; |
| Sean McCullough | 618bfb2 | 2025-06-25 20:52:30 +0000 | [diff] [blame] | 42 | } |
| 43 | |
| 44 | .demo-runner { |
| Sean McCullough | 4337aa7 | 2025-06-27 23:41:33 +0000 | [diff] [blame] | 45 | width: 100%; |
| Sean McCullough | 618bfb2 | 2025-06-25 20:52:30 +0000 | [diff] [blame] | 46 | display: flex; |
| 47 | height: 100vh; |
| 48 | font-family: |
| 49 | -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; |
| 50 | } |
| 51 | |
| 52 | .demo-sidebar { |
| 53 | width: 280px; |
| 54 | background: var(--demo-background); |
| 55 | border-right: 1px solid var(--demo-border); |
| 56 | padding: 20px; |
| 57 | overflow-y: auto; |
| banksean | 1ee0bc6 | 2025-07-22 23:24:18 +0000 | [diff] [blame^] | 58 | transition: |
| 59 | background-color 0.2s, |
| 60 | border-color 0.2s; |
| Sean McCullough | 618bfb2 | 2025-06-25 20:52:30 +0000 | [diff] [blame] | 61 | } |
| 62 | |
| 63 | .demo-content { |
| 64 | flex: 1; |
| 65 | padding: 20px; |
| 66 | overflow-y: auto; |
| banksean | 1ee0bc6 | 2025-07-22 23:24:18 +0000 | [diff] [blame^] | 67 | background: var(--demo-container-bg); |
| 68 | transition: background-color 0.2s; |
| Sean McCullough | 618bfb2 | 2025-06-25 20:52:30 +0000 | [diff] [blame] | 69 | } |
| 70 | |
| 71 | .demo-nav { |
| 72 | list-style: none; |
| 73 | padding: 0; |
| 74 | margin: 0; |
| 75 | } |
| 76 | |
| 77 | .demo-nav li { |
| 78 | margin-bottom: 4px; |
| 79 | } |
| 80 | |
| 81 | .demo-nav button { |
| 82 | width: 100%; |
| 83 | text-align: left; |
| 84 | padding: 8px 12px; |
| 85 | background: transparent; |
| 86 | border: 1px solid transparent; |
| 87 | border-radius: 6px; |
| 88 | cursor: pointer; |
| 89 | font-size: 14px; |
| 90 | color: var(--demo-secondary); |
| 91 | transition: all 0.2s; |
| 92 | } |
| 93 | |
| 94 | .demo-nav button:hover { |
| banksean | 1ee0bc6 | 2025-07-22 23:24:18 +0000 | [diff] [blame^] | 95 | background: var(--demo-hover-bg); |
| Sean McCullough | 618bfb2 | 2025-06-25 20:52:30 +0000 | [diff] [blame] | 96 | border-color: var(--demo-border); |
| 97 | color: var(--demo-primary); |
| 98 | } |
| 99 | |
| 100 | .demo-nav button.active { |
| 101 | background: var(--demo-primary); |
| 102 | color: white; |
| 103 | } |
| 104 | |
| 105 | .demo-header { |
| 106 | margin-bottom: 20px; |
| 107 | padding-bottom: 15px; |
| 108 | border-bottom: 1px solid var(--demo-border); |
| 109 | } |
| 110 | |
| 111 | .demo-title { |
| 112 | font-size: 24px; |
| 113 | font-weight: 600; |
| 114 | margin: 0 0 8px 0; |
| banksean | 1ee0bc6 | 2025-07-22 23:24:18 +0000 | [diff] [blame^] | 115 | color: var(--demo-text-primary); |
| Sean McCullough | 618bfb2 | 2025-06-25 20:52:30 +0000 | [diff] [blame] | 116 | } |
| 117 | |
| 118 | .demo-description { |
| 119 | color: var(--demo-secondary); |
| 120 | margin: 0; |
| 121 | font-size: 14px; |
| 122 | } |
| 123 | |
| 124 | .demo-container { |
| banksean | 1ee0bc6 | 2025-07-22 23:24:18 +0000 | [diff] [blame^] | 125 | background: var(--demo-container-bg); |
| Sean McCullough | 618bfb2 | 2025-06-25 20:52:30 +0000 | [diff] [blame] | 126 | border: 1px solid var(--demo-border); |
| 127 | border-radius: 8px; |
| 128 | min-height: 400px; |
| 129 | padding: 20px; |
| 130 | } |
| 131 | |
| 132 | .demo-loading { |
| 133 | display: flex; |
| 134 | align-items: center; |
| 135 | justify-content: center; |
| 136 | height: 200px; |
| 137 | color: var(--demo-secondary); |
| 138 | } |
| 139 | |
| 140 | .demo-welcome { |
| 141 | text-align: center; |
| 142 | padding: 60px 20px; |
| 143 | color: var(--demo-secondary); |
| 144 | } |
| 145 | |
| 146 | .demo-welcome h2 { |
| 147 | margin-bottom: 10px; |
| banksean | 1ee0bc6 | 2025-07-22 23:24:18 +0000 | [diff] [blame^] | 148 | color: var(--demo-text-primary); |
| Sean McCullough | 618bfb2 | 2025-06-25 20:52:30 +0000 | [diff] [blame] | 149 | } |
| 150 | |
| 151 | .search-box { |
| 152 | width: 100%; |
| 153 | padding: 8px 12px; |
| 154 | margin-bottom: 16px; |
| 155 | border: 1px solid var(--demo-border); |
| 156 | border-radius: 6px; |
| 157 | font-size: 14px; |
| banksean | 1ee0bc6 | 2025-07-22 23:24:18 +0000 | [diff] [blame^] | 158 | background: var(--demo-container-bg); |
| 159 | color: var(--demo-text-primary); |
| 160 | transition: |
| 161 | background-color 0.2s, |
| 162 | border-color 0.2s, |
| 163 | color 0.2s; |
| 164 | } |
| 165 | |
| 166 | .search-box::placeholder { |
| 167 | color: var(--demo-secondary); |
| Sean McCullough | 618bfb2 | 2025-06-25 20:52:30 +0000 | [diff] [blame] | 168 | } |
| 169 | |
| 170 | .search-box:focus { |
| 171 | outline: none; |
| 172 | border-color: var(--demo-primary); |
| 173 | } |
| 174 | |
| 175 | .demo-error { |
| 176 | padding: 20px; |
| banksean | 1ee0bc6 | 2025-07-22 23:24:18 +0000 | [diff] [blame^] | 177 | background: var(--demo-error-bg); |
| 178 | border: 1px solid var(--demo-error-border); |
| Sean McCullough | 618bfb2 | 2025-06-25 20:52:30 +0000 | [diff] [blame] | 179 | border-radius: 6px; |
| banksean | 1ee0bc6 | 2025-07-22 23:24:18 +0000 | [diff] [blame^] | 180 | color: var(--demo-error-text); |
| Sean McCullough | 618bfb2 | 2025-06-25 20:52:30 +0000 | [diff] [blame] | 181 | } |
| 182 | </style> |
| 183 | </head> |
| 184 | <body> |
| 185 | <div class="demo-runner"> |
| 186 | <nav class="demo-sidebar"> |
| banksean | 1ee0bc6 | 2025-07-22 23:24:18 +0000 | [diff] [blame^] | 187 | <h1 |
| 188 | style=" |
| 189 | font-size: 18px; |
| 190 | margin: 0 0 20px 0; |
| 191 | color: var(--demo-text-primary); |
| 192 | " |
| 193 | > |
| Sean McCullough | 618bfb2 | 2025-06-25 20:52:30 +0000 | [diff] [blame] | 194 | Component Demos |
| 195 | </h1> |
| 196 | |
| banksean | 1ee0bc6 | 2025-07-22 23:24:18 +0000 | [diff] [blame^] | 197 | <div |
| 198 | style=" |
| 199 | margin-bottom: 16px; |
| 200 | display: flex; |
| 201 | align-items: center; |
| 202 | justify-content: space-between; |
| 203 | " |
| 204 | > |
| 205 | <span style="font-size: 12px; color: var(--demo-secondary)" |
| 206 | >Theme:</span |
| 207 | > |
| 208 | <sketch-theme-toggle></sketch-theme-toggle> |
| 209 | </div> |
| 210 | |
| Sean McCullough | 618bfb2 | 2025-06-25 20:52:30 +0000 | [diff] [blame] | 211 | <input |
| 212 | type="text" |
| 213 | class="search-box" |
| 214 | placeholder="Search components..." |
| 215 | id="demo-search" |
| 216 | /> |
| 217 | |
| 218 | <ul class="demo-nav" id="demo-nav"> |
| 219 | <!-- Component list will be populated dynamically --> |
| 220 | </ul> |
| 221 | </nav> |
| 222 | |
| 223 | <main class="demo-content"> |
| 224 | <div class="demo-header" id="demo-header" style="display: none"> |
| 225 | <h1 class="demo-title" id="demo-title"></h1> |
| 226 | <p class="demo-description" id="demo-description"></p> |
| 227 | </div> |
| 228 | |
| 229 | <div class="demo-container" id="demo-container"> |
| 230 | <div class="demo-welcome"> |
| 231 | <h2>Welcome to Sketch Component Demos</h2> |
| 232 | <p>Select a component from the sidebar to view its demo.</p> |
| 233 | </div> |
| 234 | </div> |
| 235 | </main> |
| 236 | </div> |
| 237 | |
| 238 | <script type="module"> |
| 239 | import { DemoRunner } from "./demo-framework/demo-runner.ts"; |
| banksean | 1ee0bc6 | 2025-07-22 23:24:18 +0000 | [diff] [blame^] | 240 | import "../sketch-theme-toggle.ts"; |
| 241 | import "../theme-service.ts"; |
| Sean McCullough | 618bfb2 | 2025-06-25 20:52:30 +0000 | [diff] [blame] | 242 | |
| 243 | class DemoRunnerApp { |
| 244 | constructor() { |
| 245 | this.demoRunner = new DemoRunner({ |
| 246 | container: document.getElementById("demo-container"), |
| 247 | onDemoChange: this.onDemoChange.bind(this), |
| 248 | }); |
| 249 | |
| 250 | this.searchBox = document.getElementById("demo-search"); |
| 251 | this.navList = document.getElementById("demo-nav"); |
| 252 | this.demoHeader = document.getElementById("demo-header"); |
| 253 | this.demoTitle = document.getElementById("demo-title"); |
| 254 | this.demoDescription = document.getElementById("demo-description"); |
| 255 | |
| 256 | this.currentComponent = null; |
| 257 | this.availableComponents = []; |
| 258 | |
| banksean | 1ee0bc6 | 2025-07-22 23:24:18 +0000 | [diff] [blame^] | 259 | // Initialize theme service |
| 260 | this.initTheme(); |
| 261 | |
| Sean McCullough | 618bfb2 | 2025-06-25 20:52:30 +0000 | [diff] [blame] | 262 | this.init(); |
| 263 | } |
| 264 | |
| banksean | 1ee0bc6 | 2025-07-22 23:24:18 +0000 | [diff] [blame^] | 265 | initTheme() { |
| 266 | // Import and initialize the theme service |
| 267 | import("../theme-service.ts").then(({ ThemeService }) => { |
| 268 | const themeService = ThemeService.getInstance(); |
| 269 | themeService.initializeTheme(); |
| 270 | }); |
| 271 | } |
| 272 | |
| Sean McCullough | 618bfb2 | 2025-06-25 20:52:30 +0000 | [diff] [blame] | 273 | async init() { |
| 274 | try { |
| 275 | // Load available components |
| 276 | this.availableComponents = |
| 277 | await this.demoRunner.getAvailableComponents(); |
| 278 | this.renderNavigation(); |
| 279 | |
| 280 | // Set up search |
| 281 | this.searchBox.addEventListener( |
| 282 | "input", |
| 283 | this.handleSearch.bind(this), |
| 284 | ); |
| 285 | |
| 286 | // Handle URL hash for direct linking |
| 287 | this.handleHashChange(); |
| 288 | window.addEventListener( |
| 289 | "hashchange", |
| 290 | this.handleHashChange.bind(this), |
| 291 | ); |
| 292 | } catch (error) { |
| 293 | console.error("Failed to initialize demo runner:", error); |
| 294 | this.showError("Failed to load demo components"); |
| 295 | } |
| 296 | } |
| 297 | |
| 298 | renderNavigation(filter = "") { |
| 299 | const filteredComponents = this.availableComponents.filter( |
| 300 | (component) => |
| 301 | component.toLowerCase().includes(filter.toLowerCase()), |
| 302 | ); |
| 303 | |
| 304 | this.navList.innerHTML = ""; |
| 305 | |
| 306 | filteredComponents.forEach((component) => { |
| 307 | const li = document.createElement("li"); |
| 308 | const button = document.createElement("button"); |
| 309 | button.textContent = this.formatComponentName(component); |
| 310 | button.addEventListener("click", () => |
| 311 | this.loadComponent(component), |
| 312 | ); |
| 313 | |
| 314 | if (component === this.currentComponent) { |
| 315 | button.classList.add("active"); |
| 316 | } |
| 317 | |
| 318 | li.appendChild(button); |
| 319 | this.navList.appendChild(li); |
| 320 | }); |
| 321 | } |
| 322 | |
| 323 | formatComponentName(component) { |
| 324 | return component |
| 325 | .replace(/^sketch-/, "") |
| 326 | .replace(/-/g, " ") |
| 327 | .replace(/\b\w/g, (l) => l.toUpperCase()); |
| 328 | } |
| 329 | |
| 330 | async loadComponent(componentName) { |
| 331 | if (this.currentComponent === componentName) { |
| 332 | return; |
| 333 | } |
| 334 | |
| 335 | try { |
| 336 | this.showLoading(); |
| 337 | await this.demoRunner.loadDemo(componentName); |
| 338 | this.currentComponent = componentName; |
| 339 | |
| 340 | // Update URL hash |
| 341 | window.location.hash = componentName; |
| 342 | |
| 343 | // Update navigation |
| 344 | this.renderNavigation(this.searchBox.value); |
| 345 | } catch (error) { |
| 346 | console.error(`Failed to load demo for ${componentName}:`, error); |
| 347 | this.showError(`Failed to load demo for ${componentName}`); |
| 348 | } |
| 349 | } |
| 350 | |
| 351 | onDemoChange(componentName, demo) { |
| 352 | // Update header |
| 353 | this.demoTitle.textContent = demo.title; |
| 354 | this.demoDescription.textContent = demo.description || ""; |
| 355 | |
| 356 | if (demo.description) { |
| 357 | this.demoDescription.style.display = "block"; |
| 358 | } else { |
| 359 | this.demoDescription.style.display = "none"; |
| 360 | } |
| 361 | |
| 362 | this.demoHeader.style.display = "block"; |
| 363 | } |
| 364 | |
| 365 | handleSearch(event) { |
| 366 | this.renderNavigation(event.target.value); |
| 367 | } |
| 368 | |
| 369 | handleHashChange() { |
| 370 | const hash = window.location.hash.slice(1); |
| 371 | if (hash && this.availableComponents.includes(hash)) { |
| 372 | this.loadComponent(hash); |
| 373 | } |
| 374 | } |
| 375 | |
| 376 | showLoading() { |
| 377 | document.getElementById("demo-container").innerHTML = ` |
| 378 | <div class="demo-loading"> |
| 379 | Loading demo... |
| 380 | </div> |
| 381 | `; |
| 382 | } |
| 383 | |
| 384 | showError(message) { |
| 385 | document.getElementById("demo-container").innerHTML = ` |
| 386 | <div class="demo-error"> |
| 387 | <strong>Error:</strong> ${message} |
| 388 | </div> |
| 389 | `; |
| 390 | } |
| 391 | } |
| 392 | |
| 393 | // Initialize the demo runner when DOM is ready |
| 394 | if (document.readyState === "loading") { |
| 395 | document.addEventListener( |
| 396 | "DOMContentLoaded", |
| 397 | () => new DemoRunnerApp(), |
| 398 | ); |
| 399 | } else { |
| 400 | new DemoRunnerApp(); |
| 401 | } |
| 402 | </script> |
| 403 | </body> |
| 404 | </html> |