| 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; |
| 14 | } |
| 15 | |
| 16 | .demo-runner { |
| Sean McCullough | 4337aa7 | 2025-06-27 23:41:33 +0000 | [diff] [blame] | 17 | width: 100%; |
| Sean McCullough | 618bfb2 | 2025-06-25 20:52:30 +0000 | [diff] [blame] | 18 | display: flex; |
| 19 | height: 100vh; |
| 20 | font-family: |
| 21 | -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; |
| 22 | } |
| 23 | |
| 24 | .demo-sidebar { |
| 25 | width: 280px; |
| 26 | background: var(--demo-background); |
| 27 | border-right: 1px solid var(--demo-border); |
| 28 | padding: 20px; |
| 29 | overflow-y: auto; |
| 30 | } |
| 31 | |
| 32 | .demo-content { |
| 33 | flex: 1; |
| 34 | padding: 20px; |
| 35 | overflow-y: auto; |
| 36 | } |
| 37 | |
| 38 | .demo-nav { |
| 39 | list-style: none; |
| 40 | padding: 0; |
| 41 | margin: 0; |
| 42 | } |
| 43 | |
| 44 | .demo-nav li { |
| 45 | margin-bottom: 4px; |
| 46 | } |
| 47 | |
| 48 | .demo-nav button { |
| 49 | width: 100%; |
| 50 | text-align: left; |
| 51 | padding: 8px 12px; |
| 52 | background: transparent; |
| 53 | border: 1px solid transparent; |
| 54 | border-radius: 6px; |
| 55 | cursor: pointer; |
| 56 | font-size: 14px; |
| 57 | color: var(--demo-secondary); |
| 58 | transition: all 0.2s; |
| 59 | } |
| 60 | |
| 61 | .demo-nav button:hover { |
| 62 | background: #ffffff; |
| 63 | border-color: var(--demo-border); |
| 64 | color: var(--demo-primary); |
| 65 | } |
| 66 | |
| 67 | .demo-nav button.active { |
| 68 | background: var(--demo-primary); |
| 69 | color: white; |
| 70 | } |
| 71 | |
| 72 | .demo-header { |
| 73 | margin-bottom: 20px; |
| 74 | padding-bottom: 15px; |
| 75 | border-bottom: 1px solid var(--demo-border); |
| 76 | } |
| 77 | |
| 78 | .demo-title { |
| 79 | font-size: 24px; |
| 80 | font-weight: 600; |
| 81 | margin: 0 0 8px 0; |
| 82 | color: #24292f; |
| 83 | } |
| 84 | |
| 85 | .demo-description { |
| 86 | color: var(--demo-secondary); |
| 87 | margin: 0; |
| 88 | font-size: 14px; |
| 89 | } |
| 90 | |
| 91 | .demo-container { |
| 92 | background: white; |
| 93 | border: 1px solid var(--demo-border); |
| 94 | border-radius: 8px; |
| 95 | min-height: 400px; |
| 96 | padding: 20px; |
| 97 | } |
| 98 | |
| 99 | .demo-loading { |
| 100 | display: flex; |
| 101 | align-items: center; |
| 102 | justify-content: center; |
| 103 | height: 200px; |
| 104 | color: var(--demo-secondary); |
| 105 | } |
| 106 | |
| 107 | .demo-welcome { |
| 108 | text-align: center; |
| 109 | padding: 60px 20px; |
| 110 | color: var(--demo-secondary); |
| 111 | } |
| 112 | |
| 113 | .demo-welcome h2 { |
| 114 | margin-bottom: 10px; |
| 115 | color: #24292f; |
| 116 | } |
| 117 | |
| 118 | .search-box { |
| 119 | width: 100%; |
| 120 | padding: 8px 12px; |
| 121 | margin-bottom: 16px; |
| 122 | border: 1px solid var(--demo-border); |
| 123 | border-radius: 6px; |
| 124 | font-size: 14px; |
| 125 | } |
| 126 | |
| 127 | .search-box:focus { |
| 128 | outline: none; |
| 129 | border-color: var(--demo-primary); |
| 130 | } |
| 131 | |
| 132 | .demo-error { |
| 133 | padding: 20px; |
| 134 | background: #ffeaea; |
| 135 | border: 1px solid #ffcccc; |
| 136 | border-radius: 6px; |
| 137 | color: #d73a49; |
| 138 | } |
| 139 | </style> |
| 140 | </head> |
| 141 | <body> |
| 142 | <div class="demo-runner"> |
| 143 | <nav class="demo-sidebar"> |
| 144 | <h1 style="font-size: 18px; margin: 0 0 20px 0; color: #24292f"> |
| 145 | Component Demos |
| 146 | </h1> |
| 147 | |
| 148 | <input |
| 149 | type="text" |
| 150 | class="search-box" |
| 151 | placeholder="Search components..." |
| 152 | id="demo-search" |
| 153 | /> |
| 154 | |
| 155 | <ul class="demo-nav" id="demo-nav"> |
| 156 | <!-- Component list will be populated dynamically --> |
| 157 | </ul> |
| 158 | </nav> |
| 159 | |
| 160 | <main class="demo-content"> |
| 161 | <div class="demo-header" id="demo-header" style="display: none"> |
| 162 | <h1 class="demo-title" id="demo-title"></h1> |
| 163 | <p class="demo-description" id="demo-description"></p> |
| 164 | </div> |
| 165 | |
| 166 | <div class="demo-container" id="demo-container"> |
| 167 | <div class="demo-welcome"> |
| 168 | <h2>Welcome to Sketch Component Demos</h2> |
| 169 | <p>Select a component from the sidebar to view its demo.</p> |
| 170 | </div> |
| 171 | </div> |
| 172 | </main> |
| 173 | </div> |
| 174 | |
| 175 | <script type="module"> |
| 176 | import { DemoRunner } from "./demo-framework/demo-runner.ts"; |
| 177 | |
| 178 | class DemoRunnerApp { |
| 179 | constructor() { |
| 180 | this.demoRunner = new DemoRunner({ |
| 181 | container: document.getElementById("demo-container"), |
| 182 | onDemoChange: this.onDemoChange.bind(this), |
| 183 | }); |
| 184 | |
| 185 | this.searchBox = document.getElementById("demo-search"); |
| 186 | this.navList = document.getElementById("demo-nav"); |
| 187 | this.demoHeader = document.getElementById("demo-header"); |
| 188 | this.demoTitle = document.getElementById("demo-title"); |
| 189 | this.demoDescription = document.getElementById("demo-description"); |
| 190 | |
| 191 | this.currentComponent = null; |
| 192 | this.availableComponents = []; |
| 193 | |
| 194 | this.init(); |
| 195 | } |
| 196 | |
| 197 | async init() { |
| 198 | try { |
| 199 | // Load available components |
| 200 | this.availableComponents = |
| 201 | await this.demoRunner.getAvailableComponents(); |
| 202 | this.renderNavigation(); |
| 203 | |
| 204 | // Set up search |
| 205 | this.searchBox.addEventListener( |
| 206 | "input", |
| 207 | this.handleSearch.bind(this), |
| 208 | ); |
| 209 | |
| 210 | // Handle URL hash for direct linking |
| 211 | this.handleHashChange(); |
| 212 | window.addEventListener( |
| 213 | "hashchange", |
| 214 | this.handleHashChange.bind(this), |
| 215 | ); |
| 216 | } catch (error) { |
| 217 | console.error("Failed to initialize demo runner:", error); |
| 218 | this.showError("Failed to load demo components"); |
| 219 | } |
| 220 | } |
| 221 | |
| 222 | renderNavigation(filter = "") { |
| 223 | const filteredComponents = this.availableComponents.filter( |
| 224 | (component) => |
| 225 | component.toLowerCase().includes(filter.toLowerCase()), |
| 226 | ); |
| 227 | |
| 228 | this.navList.innerHTML = ""; |
| 229 | |
| 230 | filteredComponents.forEach((component) => { |
| 231 | const li = document.createElement("li"); |
| 232 | const button = document.createElement("button"); |
| 233 | button.textContent = this.formatComponentName(component); |
| 234 | button.addEventListener("click", () => |
| 235 | this.loadComponent(component), |
| 236 | ); |
| 237 | |
| 238 | if (component === this.currentComponent) { |
| 239 | button.classList.add("active"); |
| 240 | } |
| 241 | |
| 242 | li.appendChild(button); |
| 243 | this.navList.appendChild(li); |
| 244 | }); |
| 245 | } |
| 246 | |
| 247 | formatComponentName(component) { |
| 248 | return component |
| 249 | .replace(/^sketch-/, "") |
| 250 | .replace(/-/g, " ") |
| 251 | .replace(/\b\w/g, (l) => l.toUpperCase()); |
| 252 | } |
| 253 | |
| 254 | async loadComponent(componentName) { |
| 255 | if (this.currentComponent === componentName) { |
| 256 | return; |
| 257 | } |
| 258 | |
| 259 | try { |
| 260 | this.showLoading(); |
| 261 | await this.demoRunner.loadDemo(componentName); |
| 262 | this.currentComponent = componentName; |
| 263 | |
| 264 | // Update URL hash |
| 265 | window.location.hash = componentName; |
| 266 | |
| 267 | // Update navigation |
| 268 | this.renderNavigation(this.searchBox.value); |
| 269 | } catch (error) { |
| 270 | console.error(`Failed to load demo for ${componentName}:`, error); |
| 271 | this.showError(`Failed to load demo for ${componentName}`); |
| 272 | } |
| 273 | } |
| 274 | |
| 275 | onDemoChange(componentName, demo) { |
| 276 | // Update header |
| 277 | this.demoTitle.textContent = demo.title; |
| 278 | this.demoDescription.textContent = demo.description || ""; |
| 279 | |
| 280 | if (demo.description) { |
| 281 | this.demoDescription.style.display = "block"; |
| 282 | } else { |
| 283 | this.demoDescription.style.display = "none"; |
| 284 | } |
| 285 | |
| 286 | this.demoHeader.style.display = "block"; |
| 287 | } |
| 288 | |
| 289 | handleSearch(event) { |
| 290 | this.renderNavigation(event.target.value); |
| 291 | } |
| 292 | |
| 293 | handleHashChange() { |
| 294 | const hash = window.location.hash.slice(1); |
| 295 | if (hash && this.availableComponents.includes(hash)) { |
| 296 | this.loadComponent(hash); |
| 297 | } |
| 298 | } |
| 299 | |
| 300 | showLoading() { |
| 301 | document.getElementById("demo-container").innerHTML = ` |
| 302 | <div class="demo-loading"> |
| 303 | Loading demo... |
| 304 | </div> |
| 305 | `; |
| 306 | } |
| 307 | |
| 308 | showError(message) { |
| 309 | document.getElementById("demo-container").innerHTML = ` |
| 310 | <div class="demo-error"> |
| 311 | <strong>Error:</strong> ${message} |
| 312 | </div> |
| 313 | `; |
| 314 | } |
| 315 | } |
| 316 | |
| 317 | // Initialize the demo runner when DOM is ready |
| 318 | if (document.readyState === "loading") { |
| 319 | document.addEventListener( |
| 320 | "DOMContentLoaded", |
| 321 | () => new DemoRunnerApp(), |
| 322 | ); |
| 323 | } else { |
| 324 | new DemoRunnerApp(); |
| 325 | } |
| 326 | </script> |
| 327 | </body> |
| 328 | </html> |