Precise DOM morphing
morphing typescript dom
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

Benchmark

+812
+807
benchmark/index.html
··· 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>Morphlex Benchmark Suite</title> 7 + <style> 8 + :root { 9 + --primary: #6366f1; 10 + --success: #10b981; 11 + --warning: #f59e0b; 12 + --danger: #ef4444; 13 + --dark: #1f2937; 14 + --light: #f3f4f6; 15 + --white: #ffffff; 16 + } 17 + 18 + * { 19 + margin: 0; 20 + padding: 0; 21 + box-sizing: border-box; 22 + } 23 + 24 + body { 25 + font-family: 26 + system-ui, 27 + -apple-system, 28 + BlinkMacSystemFont, 29 + "Segoe UI", 30 + Roboto, 31 + Oxygen, 32 + Ubuntu, 33 + sans-serif; 34 + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); 35 + min-height: 100vh; 36 + padding: 2rem; 37 + } 38 + 39 + .container { 40 + max-width: 1200px; 41 + margin: 0 auto; 42 + } 43 + 44 + header { 45 + text-align: center; 46 + color: var(--white); 47 + margin-bottom: 2rem; 48 + } 49 + 50 + h1 { 51 + font-size: 3rem; 52 + margin-bottom: 0.5rem; 53 + text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.2); 54 + } 55 + 56 + .subtitle { 57 + font-size: 1.2rem; 58 + opacity: 0.95; 59 + } 60 + 61 + .controls { 62 + background: var(--white); 63 + border-radius: 12px; 64 + padding: 1.5rem; 65 + margin-bottom: 2rem; 66 + box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2); 67 + } 68 + 69 + .control-group { 70 + display: flex; 71 + gap: 1rem; 72 + align-items: center; 73 + flex-wrap: wrap; 74 + } 75 + 76 + label { 77 + font-weight: 600; 78 + color: var(--dark); 79 + } 80 + 81 + input[type="number"] { 82 + padding: 0.5rem; 83 + border: 2px solid var(--light); 84 + border-radius: 6px; 85 + font-size: 1rem; 86 + width: 120px; 87 + } 88 + 89 + button { 90 + padding: 0.75rem 2rem; 91 + background: var(--primary); 92 + color: var(--white); 93 + border: none; 94 + border-radius: 6px; 95 + font-size: 1rem; 96 + font-weight: 600; 97 + cursor: pointer; 98 + transition: 99 + transform 0.2s, 100 + box-shadow 0.2s; 101 + } 102 + 103 + button:hover:not(:disabled) { 104 + transform: translateY(-2px); 105 + box-shadow: 0 5px 15px rgba(99, 102, 241, 0.4); 106 + } 107 + 108 + button:disabled { 109 + background: var(--light); 110 + color: #9ca3af; 111 + cursor: not-allowed; 112 + } 113 + 114 + button.stop { 115 + background: var(--danger); 116 + } 117 + 118 + .status { 119 + margin-left: auto; 120 + padding: 0.5rem 1rem; 121 + border-radius: 20px; 122 + background: var(--light); 123 + font-weight: 600; 124 + color: var(--dark); 125 + } 126 + 127 + .status.running { 128 + background: var(--warning); 129 + color: var(--white); 130 + animation: pulse 1s infinite; 131 + } 132 + 133 + .status.complete { 134 + background: var(--success); 135 + color: var(--white); 136 + } 137 + 138 + @keyframes pulse { 139 + 0%, 140 + 100% { 141 + opacity: 1; 142 + } 143 + 50% { 144 + opacity: 0.7; 145 + } 146 + } 147 + 148 + .results { 149 + background: var(--white); 150 + border-radius: 12px; 151 + padding: 1.5rem; 152 + box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2); 153 + } 154 + 155 + .test-case { 156 + margin-bottom: 2rem; 157 + padding: 1.5rem; 158 + background: var(--light); 159 + border-radius: 8px; 160 + } 161 + 162 + .test-case h3 { 163 + color: var(--primary); 164 + margin-bottom: 0.5rem; 165 + } 166 + 167 + .test-description { 168 + color: #6b7280; 169 + margin-bottom: 1rem; 170 + } 171 + 172 + .library-result { 173 + display: flex; 174 + justify-content: space-between; 175 + align-items: center; 176 + padding: 0.75rem; 177 + margin: 0.5rem 0; 178 + background: var(--white); 179 + border-radius: 6px; 180 + transition: transform 0.2s; 181 + } 182 + 183 + .library-result:hover { 184 + transform: translateX(5px); 185 + box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1); 186 + } 187 + 188 + .library-name { 189 + font-weight: 600; 190 + min-width: 150px; 191 + } 192 + 193 + .metrics { 194 + display: flex; 195 + gap: 2rem; 196 + flex: 1; 197 + justify-content: center; 198 + } 199 + 200 + .metric { 201 + text-align: center; 202 + } 203 + 204 + .metric-value { 205 + font-size: 1.2rem; 206 + font-weight: 700; 207 + color: var(--primary); 208 + } 209 + 210 + .metric-label { 211 + font-size: 0.8rem; 212 + color: #9ca3af; 213 + text-transform: uppercase; 214 + letter-spacing: 0.5px; 215 + } 216 + 217 + .rank { 218 + min-width: 50px; 219 + text-align: center; 220 + font-weight: 700; 221 + } 222 + 223 + .rank-1 { 224 + color: #fbbf24; 225 + font-size: 1.5rem; 226 + } 227 + 228 + .rank-2 { 229 + color: #9ca3af; 230 + font-size: 1.3rem; 231 + } 232 + 233 + .rank-3 { 234 + color: #cd7f32; 235 + font-size: 1.2rem; 236 + } 237 + 238 + .summary { 239 + margin-top: 2rem; 240 + padding: 2rem; 241 + background: linear-gradient(135deg, var(--primary), #8b5cf6); 242 + border-radius: 12px; 243 + color: var(--white); 244 + } 245 + 246 + .summary h2 { 247 + margin-bottom: 1.5rem; 248 + text-align: center; 249 + } 250 + 251 + .summary-grid { 252 + display: grid; 253 + grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); 254 + gap: 1.5rem; 255 + } 256 + 257 + .summary-card { 258 + background: rgba(255, 255, 255, 0.15); 259 + backdrop-filter: blur(10px); 260 + padding: 1.5rem; 261 + border-radius: 8px; 262 + text-align: center; 263 + } 264 + 265 + .summary-library { 266 + font-size: 1.2rem; 267 + font-weight: 700; 268 + margin-bottom: 0.5rem; 269 + } 270 + 271 + .summary-score { 272 + font-size: 2rem; 273 + font-weight: 900; 274 + } 275 + 276 + .progress-bar { 277 + width: 100%; 278 + height: 6px; 279 + background: var(--light); 280 + border-radius: 3px; 281 + overflow: hidden; 282 + margin-top: 1rem; 283 + } 284 + 285 + .progress-fill { 286 + height: 100%; 287 + background: var(--success); 288 + border-radius: 3px; 289 + transition: width 0.3s ease; 290 + } 291 + 292 + #sandbox { 293 + display: none; 294 + } 295 + 296 + .error { 297 + background: #fee; 298 + border: 2px solid var(--danger); 299 + color: var(--danger); 300 + padding: 1rem; 301 + border-radius: 8px; 302 + margin: 1rem 0; 303 + } 304 + 305 + .chart-container { 306 + margin: 2rem 0; 307 + padding: 1.5rem; 308 + background: var(--white); 309 + border-radius: 8px; 310 + } 311 + 312 + .bar-chart { 313 + display: flex; 314 + flex-direction: column; 315 + gap: 0.5rem; 316 + } 317 + 318 + .bar-row { 319 + display: flex; 320 + align-items: center; 321 + gap: 1rem; 322 + } 323 + 324 + .bar-label { 325 + min-width: 120px; 326 + font-weight: 600; 327 + } 328 + 329 + .bar { 330 + height: 30px; 331 + background: linear-gradient(90deg, var(--primary), #8b5cf6); 332 + border-radius: 4px; 333 + display: flex; 334 + align-items: center; 335 + padding: 0 0.5rem; 336 + color: var(--white); 337 + font-weight: 600; 338 + font-size: 0.9rem; 339 + } 340 + </style> 341 + </head> 342 + <body> 343 + <div class="container"> 344 + <header> 345 + <h1>⚡ Morphlex Benchmark Suite</h1> 346 + <p class="subtitle">Performance comparison of DOM morphing libraries</p> 347 + </header> 348 + 349 + <div class="controls"> 350 + <div class="control-group"> 351 + <label for="iterations">Iterations:</label> 352 + <input type="number" id="iterations" value="1000" min="100" max="10000" step="100" /> 353 + 354 + <label for="warmup">Warmup:</label> 355 + <input type="number" id="warmup" value="100" min="10" max="1000" step="10" /> 356 + 357 + <button id="runBtn">🚀 Run Benchmark</button> 358 + <button id="stopBtn" class="stop" style="display: none">⏹ Stop</button> 359 + 360 + <div class="status" id="status">Ready</div> 361 + </div> 362 + <div class="progress-bar"> 363 + <div class="progress-fill" id="progress" style="width: 0%"></div> 364 + </div> 365 + </div> 366 + 367 + <div class="results" id="results"> 368 + <p style="text-align: center; color: #9ca3af; padding: 2rem">Click "Run Benchmark" to start testing...</p> 369 + </div> 370 + </div> 371 + 372 + <div id="sandbox"></div> 373 + 374 + <script type="module"> 375 + // For file:// protocol, we'll use unpkg for morphlex as well 376 + // To test your local morphlex build instead: 377 + // 1. Run 'bun run build' to build morphlex 378 + // 2. Start a local server: 'npx serve ..' or 'python -m http.server' in the morphlex root 379 + // 3. Change the import below to: '../dist/morphlex.min.js' 380 + import { morph as morphlex } from "https://unpkg.com/morphlex@0.0.16/dist/morphlex.min.js" 381 + import { Idiomorph } from "https://unpkg.com/idiomorph@0.7.4/dist/idiomorph.esm.js" 382 + import morphdom from "https://unpkg.com/morphdom@2.7.7/dist/morphdom-esm.js" 383 + // Try loading nanomorph from jsdelivr with ESM 384 + import nanomorph from "https://cdn.jsdelivr.net/npm/nanomorph@5.4.3/+esm" 385 + // Alpine morph requires Alpine.js to be loaded, so we'll skip it 386 + 387 + const sandbox = document.getElementById("sandbox") 388 + let isRunning = false 389 + let stopRequested = false 390 + 391 + class BrowserBenchmark { 392 + constructor(iterations = 1000, warmupIterations = 100) { 393 + this.iterations = iterations 394 + this.warmupIterations = warmupIterations 395 + this.results = [] 396 + } 397 + 398 + async runSingleBenchmark(library, testCase, morphFn) { 399 + // Warmup 400 + for (let i = 0; i < this.warmupIterations; i++) { 401 + const { from, to } = testCase.setup() 402 + sandbox.appendChild(from) 403 + morphFn(from, to) 404 + sandbox.innerHTML = "" 405 + } 406 + 407 + // Allow browser to settle 408 + await new Promise((resolve) => setTimeout(resolve, 10)) 409 + 410 + // Actual benchmark 411 + const times = [] 412 + for (let i = 0; i < this.iterations; i++) { 413 + const { from, to } = testCase.setup() 414 + sandbox.appendChild(from) 415 + 416 + const start = performance.now() 417 + morphFn(from, to) 418 + const end = performance.now() 419 + 420 + times.push(end - start) 421 + sandbox.innerHTML = "" 422 + } 423 + 424 + // Calculate statistics 425 + const totalTime = times.reduce((a, b) => a + b, 0) 426 + const averageTime = totalTime / times.length 427 + const sorted = [...times].sort((a, b) => a - b) 428 + const median = sorted[Math.floor(sorted.length / 2)] 429 + const min = sorted[0] 430 + const max = sorted[sorted.length - 1] 431 + const opsPerSecond = 1000 / averageTime 432 + 433 + return { 434 + library, 435 + testName: testCase.name, 436 + iterations: this.iterations, 437 + totalTime, 438 + averageTime, 439 + median, 440 + min, 441 + max, 442 + opsPerSecond, 443 + } 444 + } 445 + 446 + async runTestCase(testCase, onProgress) { 447 + const results = [] 448 + const libraries = [ 449 + { name: "morphlex", fn: (from, to) => morphlex(from, to) }, 450 + { name: "idiomorph", fn: (from, to) => Idiomorph.morph(from, to) }, 451 + { name: "morphdom", fn: (from, to) => morphdom(from, to.cloneNode(true)) }, 452 + { name: "nanomorph", fn: (from, to) => nanomorph(from, to.cloneNode(true)) }, 453 + ] 454 + 455 + for (const lib of libraries) { 456 + if (stopRequested) break 457 + 458 + try { 459 + const result = await this.runSingleBenchmark(lib.name, testCase, lib.fn) 460 + results.push(result) 461 + this.results.push(result) 462 + if (onProgress) onProgress(result) 463 + } catch (error) { 464 + console.error(`Error running ${lib.name}:`, error) 465 + results.push({ 466 + library: lib.name, 467 + testName: testCase.name, 468 + error: error.message, 469 + }) 470 + } 471 + 472 + // Small delay between libraries 473 + await new Promise((resolve) => setTimeout(resolve, 50)) 474 + } 475 + 476 + return results 477 + } 478 + } 479 + 480 + // Test cases 481 + const testCases = [ 482 + { 483 + name: "Simple Text Update", 484 + description: "Morphing a single text node change", 485 + setup: () => { 486 + const from = document.createElement("div") 487 + from.innerHTML = "<p>Hello World</p>" 488 + const to = document.createElement("div") 489 + to.innerHTML = "<p>Hello Morphlex</p>" 490 + return { from: from.firstElementChild, to: to.firstElementChild } 491 + }, 492 + }, 493 + { 494 + name: "Attribute Changes", 495 + description: "Updating multiple attributes on elements", 496 + setup: () => { 497 + const from = document.createElement("div") 498 + from.innerHTML = ` 499 + <div class="old-class" data-value="1"> 500 + <span id="test" title="old">Content</span> 501 + </div> 502 + ` 503 + const to = document.createElement("div") 504 + to.innerHTML = ` 505 + <div class="new-class" data-value="2" data-new="true"> 506 + <span id="test" title="new" aria-label="label">Content</span> 507 + </div> 508 + ` 509 + return { from: from.firstElementChild, to: to.firstElementChild } 510 + }, 511 + }, 512 + { 513 + name: "List Reordering", 514 + description: "Reordering items in a list with IDs", 515 + setup: () => { 516 + const from = document.createElement("ul") 517 + from.innerHTML = ` 518 + <li id="item-1">First</li> 519 + <li id="item-2">Second</li> 520 + <li id="item-3">Third</li> 521 + <li id="item-4">Fourth</li> 522 + <li id="item-5">Fifth</li> 523 + ` 524 + const to = document.createElement("ul") 525 + to.innerHTML = ` 526 + <li id="item-3">Third</li> 527 + <li id="item-1">First</li> 528 + <li id="item-5">Fifth</li> 529 + <li id="item-2">Second</li> 530 + <li id="item-4">Fourth</li> 531 + ` 532 + return { from, to } 533 + }, 534 + }, 535 + { 536 + name: "Large List", 537 + description: "Morphing a list with 50 items", 538 + setup: () => { 539 + const from = document.createElement("ul") 540 + for (let i = 1; i <= 50; i++) { 541 + const li = document.createElement("li") 542 + li.id = `item-${i}` 543 + li.textContent = `Item ${i}` 544 + from.appendChild(li) 545 + } 546 + const to = document.createElement("ul") 547 + const indices = Array.from({ length: 50 }, (_, i) => i + 1) 548 + // Simple shuffle 549 + for (let i = indices.length - 1; i > 0; i--) { 550 + const j = Math.floor(Math.random() * (i + 1)) 551 + ;[indices[i], indices[j]] = [indices[j], indices[i]] 552 + } 553 + for (const i of indices.slice(0, 45)) { 554 + const li = document.createElement("li") 555 + li.id = `item-${i}` 556 + li.textContent = i % 3 === 0 ? `Modified Item ${i}` : `Item ${i}` 557 + to.appendChild(li) 558 + } 559 + return { from, to } 560 + }, 561 + }, 562 + { 563 + name: "Deep Nesting", 564 + description: "Morphing deeply nested structures", 565 + setup: () => { 566 + const from = document.createElement("div") 567 + from.innerHTML = ` 568 + <div id="root"> 569 + <section id="s1"> 570 + <article id="a1"> 571 + <header id="h1"> 572 + <h1>Title</h1> 573 + <p>Subtitle</p> 574 + </header> 575 + <div id="content"> 576 + <p>Paragraph 1</p> 577 + <p>Paragraph 2</p> 578 + </div> 579 + </article> 580 + </section> 581 + </div> 582 + ` 583 + const to = document.createElement("div") 584 + to.innerHTML = ` 585 + <div id="root"> 586 + <section id="s1"> 587 + <article id="a1"> 588 + <header id="h1"> 589 + <h1>New Title</h1> 590 + <p>New Subtitle</p> 591 + </header> 592 + <div id="content"> 593 + <p>Modified Paragraph 1</p> 594 + <p>Paragraph 2</p> 595 + <p>Paragraph 3</p> 596 + </div> 597 + </article> 598 + </section> 599 + </div> 600 + ` 601 + return { from: from.firstElementChild, to: to.firstElementChild } 602 + }, 603 + }, 604 + ] 605 + 606 + // UI Functions 607 + function updateProgress(current, total) { 608 + const percent = (current / total) * 100 609 + document.getElementById("progress").style.width = `${percent}%` 610 + } 611 + 612 + function renderResult(testResults, container) { 613 + // Sort by performance 614 + const sorted = [...testResults].sort((a, b) => { 615 + if (a.error) return 1 616 + if (b.error) return -1 617 + return a.averageTime - b.averageTime 618 + }) 619 + 620 + sorted.forEach((result, index) => { 621 + const resultEl = document.createElement("div") 622 + resultEl.className = "library-result" 623 + 624 + if (result.error) { 625 + resultEl.innerHTML = ` 626 + <div class="library-name">${result.library}</div> 627 + <div class="error">Error: ${result.error}</div> 628 + ` 629 + } else { 630 + const rankClass = index === 0 ? "rank-1" : index === 1 ? "rank-2" : index === 2 ? "rank-3" : "" 631 + const rankEmoji = index === 0 ? "🏆" : index === 1 ? "🥈" : index === 2 ? "🥉" : `#${index + 1}` 632 + 633 + resultEl.innerHTML = ` 634 + <div class="rank ${rankClass}">${rankEmoji}</div> 635 + <div class="library-name">${result.library}</div> 636 + <div class="metrics"> 637 + <div class="metric"> 638 + <div class="metric-value">${result.averageTime.toFixed(3)}</div> 639 + <div class="metric-label">ms/op</div> 640 + </div> 641 + <div class="metric"> 642 + <div class="metric-value">${Math.round(result.opsPerSecond)}</div> 643 + <div class="metric-label">ops/sec</div> 644 + </div> 645 + <div class="metric"> 646 + <div class="metric-value">${result.median.toFixed(3)}</div> 647 + <div class="metric-label">median ms</div> 648 + </div> 649 + </div> 650 + ` 651 + } 652 + 653 + container.appendChild(resultEl) 654 + }) 655 + } 656 + 657 + function renderSummary(allResults) { 658 + // Group by library 659 + const libraryStats = {} 660 + 661 + allResults.forEach((result) => { 662 + if (!result.error) { 663 + if (!libraryStats[result.library]) { 664 + libraryStats[result.library] = { 665 + totalTime: 0, 666 + count: 0, 667 + wins: 0, 668 + } 669 + } 670 + libraryStats[result.library].totalTime += result.averageTime 671 + libraryStats[result.library].count++ 672 + } 673 + }) 674 + 675 + // Count wins 676 + testCases.forEach((testCase) => { 677 + const testResults = allResults.filter((r) => r.testName === testCase.name && !r.error) 678 + if (testResults.length > 0) { 679 + const winner = testResults.reduce((min, r) => (r.averageTime < min.averageTime ? r : min)) 680 + if (libraryStats[winner.library]) { 681 + libraryStats[winner.library].wins++ 682 + } 683 + } 684 + }) 685 + 686 + // Calculate averages and sort 687 + const summaryData = Object.entries(libraryStats) 688 + .map(([library, stats]) => ({ 689 + library, 690 + avgTime: stats.totalTime / stats.count, 691 + wins: stats.wins, 692 + })) 693 + .sort((a, b) => a.avgTime - b.avgTime) 694 + 695 + // Create summary element 696 + const summaryEl = document.createElement("div") 697 + summaryEl.className = "summary" 698 + summaryEl.innerHTML = "<h2>📊 Overall Performance Summary</h2>" 699 + 700 + const gridEl = document.createElement("div") 701 + gridEl.className = "summary-grid" 702 + 703 + summaryData.forEach((data, index) => { 704 + const cardEl = document.createElement("div") 705 + cardEl.className = "summary-card" 706 + cardEl.innerHTML = ` 707 + <div class="summary-library">${index === 0 ? "🏆 " : ""}${data.library}</div> 708 + <div class="summary-score">${data.avgTime.toFixed(3)}ms</div> 709 + <div style="font-size: 0.9rem; margin-top: 0.5rem;"> 710 + ${data.wins} test${data.wins !== 1 ? "s" : ""} won 711 + </div> 712 + ` 713 + gridEl.appendChild(cardEl) 714 + }) 715 + 716 + summaryEl.appendChild(gridEl) 717 + 718 + // Add bar chart 719 + const chartEl = document.createElement("div") 720 + chartEl.className = "chart-container" 721 + chartEl.innerHTML = "<h3>Relative Performance (lower is better)</h3>" 722 + 723 + const barChartEl = document.createElement("div") 724 + barChartEl.className = "bar-chart" 725 + 726 + const maxTime = Math.max(...summaryData.map((d) => d.avgTime)) 727 + summaryData.forEach((data) => { 728 + const barRow = document.createElement("div") 729 + barRow.className = "bar-row" 730 + barRow.innerHTML = ` 731 + <div class="bar-label">${data.library}</div> 732 + <div class="bar" style="width: ${(data.avgTime / maxTime) * 100}%"> 733 + ${data.avgTime.toFixed(3)}ms 734 + </div> 735 + ` 736 + barChartEl.appendChild(barRow) 737 + }) 738 + 739 + chartEl.appendChild(barChartEl) 740 + summaryEl.appendChild(chartEl) 741 + 742 + return summaryEl 743 + } 744 + 745 + // Event handlers 746 + document.getElementById("runBtn").addEventListener("click", async () => { 747 + if (isRunning) return 748 + 749 + isRunning = true 750 + stopRequested = false 751 + const runBtn = document.getElementById("runBtn") 752 + const stopBtn = document.getElementById("stopBtn") 753 + const statusEl = document.getElementById("status") 754 + const resultsEl = document.getElementById("results") 755 + 756 + runBtn.style.display = "none" 757 + stopBtn.style.display = "inline-block" 758 + statusEl.textContent = "Running..." 759 + statusEl.className = "status running" 760 + resultsEl.innerHTML = "" 761 + 762 + const iterations = parseInt(document.getElementById("iterations").value) 763 + const warmup = parseInt(document.getElementById("warmup").value) 764 + 765 + const benchmark = new BrowserBenchmark(iterations, warmup) 766 + let testIndex = 0 767 + const totalTests = testCases.length * 4 // 4 libraries per test 768 + let completedTests = 0 769 + 770 + for (const testCase of testCases) { 771 + if (stopRequested) break 772 + 773 + const testEl = document.createElement("div") 774 + testEl.className = "test-case" 775 + testEl.innerHTML = ` 776 + <h3>${testCase.name}</h3> 777 + <div class="test-description">${testCase.description}</div> 778 + ` 779 + resultsEl.appendChild(testEl) 780 + 781 + const testResults = await benchmark.runTestCase(testCase, (result) => { 782 + completedTests++ 783 + updateProgress(completedTests, totalTests) 784 + }) 785 + 786 + renderResult(testResults, testEl) 787 + testIndex++ 788 + } 789 + 790 + if (!stopRequested && benchmark.results.length > 0) { 791 + resultsEl.appendChild(renderSummary(benchmark.results)) 792 + } 793 + 794 + isRunning = false 795 + runBtn.style.display = "inline-block" 796 + stopBtn.style.display = "none" 797 + statusEl.textContent = stopRequested ? "Stopped" : "Complete" 798 + statusEl.className = stopRequested ? "status" : "status complete" 799 + updateProgress(100, 100) 800 + }) 801 + 802 + document.getElementById("stopBtn").addEventListener("click", () => { 803 + stopRequested = true 804 + }) 805 + </script> 806 + </body> 807 + </html>
bun.lockb

This is a binary file and will not be displayed.

+5
package.json
··· 25 25 "test:all": "vitest run && vitest run -c vitest.config.browser.ts" 26 26 }, 27 27 "devDependencies": { 28 + "@alpinejs/morph": "^3.15.1", 28 29 "@playwright/test": "^1.56.1", 29 30 "@types/bun": "^1.3.1", 30 31 "@vitest/browser": "^4.0.5", ··· 33 34 "@vitest/ui": "^4.0.5", 34 35 "gzip-size-cli": "^5.1.0", 35 36 "happy-dom": "^20.0.10", 37 + "idiomorph": "^0.7.4", 38 + "morphdom": "^2.7.7", 39 + "nanomorph": "^5.4.3", 36 40 "playwright": "^1.56.1", 37 41 "prettier": "^3.2.5", 38 42 "terser": "^5.28.1", 39 43 "typescript": "^5.4.2", 40 44 "typescript-eslint": "^7.0.2", 45 + "udomdiff": "^1.1.2", 41 46 "vitest": "^4.0.5" 42 47 } 43 48 }