My aggregated monorepo of OCaml code, automaintained
0
fork

Configure Feed

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

feat(scrollycode): add interactive playground overlay with x-ocaml

Each tutorial step gets a "Try it" button that opens a full-screen
overlay containing an x-ocaml editor pre-loaded with all code up to
that step. Users can edit and run the code interactively.

- Add playground button to desktop and mobile step layouts
- Add overlay HTML with x-ocaml web component element
- Add JavaScript to collect step code and open/close overlay
- Add themed CSS for overlay, buttons, and editor styling
- Load x-ocaml.js script for interactive editing

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

+184
+184
odoc-scrollycode-extension/src/scrollycode_extension.ml
··· 469 469 470 470 // Initialize first step 471 471 renderStep(0); 472 + 473 + // Playground overlay 474 + var overlay = document.getElementById('sc-playground-overlay'); 475 + var closeBtn = overlay ? overlay.querySelector('.sc-playground-close') : null; 476 + 477 + if (overlay && closeBtn) { 478 + // Close button 479 + closeBtn.addEventListener('click', function() { 480 + overlay.classList.remove('sc-open'); 481 + }); 482 + 483 + // ESC key closes 484 + document.addEventListener('keydown', function(e) { 485 + if (e.key === 'Escape') overlay.classList.remove('sc-open'); 486 + }); 487 + 488 + // Click outside closes 489 + overlay.addEventListener('click', function(e) { 490 + if (e.target === overlay) overlay.classList.remove('sc-open'); 491 + }); 492 + } 493 + 494 + // Try it buttons 495 + container.querySelectorAll('.sc-playground-btn').forEach(function(btn) { 496 + btn.addEventListener('click', function() { 497 + var stepIndex = parseInt(btn.dataset.step, 10); 498 + // Collect code from all steps up to and including this one 499 + var allCode = []; 500 + for (var si = 0; si <= stepIndex; si++) { 501 + var slot = steps[si].querySelector('.sc-code-slot'); 502 + if (slot) { 503 + var lines = slot.querySelectorAll('.sc-line'); 504 + var code = Array.from(lines).map(function(l) { 505 + return l.textContent.replace(/^\d+/, ''); 506 + }).join('\n'); 507 + allCode.push(code); 508 + } 509 + } 510 + var fullCode = allCode.join('\n\n'); 511 + 512 + var editor = document.getElementById('sc-playground-x-ocaml'); 513 + if (editor) { 514 + editor.textContent = fullCode; 515 + // Trigger re-initialization if x-ocaml supports it 516 + if (editor.setSource) editor.setSource(fullCode); 517 + } 518 + 519 + if (overlay) overlay.classList.add('sc-open'); 520 + }); 521 + }); 472 522 } 473 523 474 524 // Initialize all scrollycode containers on the page ··· 773 823 .sc-container.sc-warm .sc-diff-added { background: rgba(80, 200, 80, 0.15); border-left: 3px solid #4caf50; } 774 824 .sc-container.sc-warm .sc-diff-removed { background: rgba(255, 80, 80, 0.12); border-left: 3px solid #ef5350; text-decoration: line-through; opacity: 0.7; } 775 825 .sc-container.sc-warm .sc-diff-same { opacity: 0.5; } 826 + 827 + /* Playground overlay */ 828 + .sc-playground-overlay { 829 + display: none; 830 + position: fixed; 831 + inset: 0; 832 + z-index: 10000; 833 + background: rgba(0,0,0,0.6); 834 + backdrop-filter: blur(4px); 835 + align-items: center; 836 + justify-content: center; 837 + } 838 + .sc-playground-overlay.sc-open { 839 + display: flex; 840 + } 841 + .sc-playground-container { 842 + width: 90vw; 843 + max-width: 900px; 844 + height: 80vh; 845 + background: #1e1b2e; 846 + border-radius: 12px; 847 + display: flex; 848 + flex-direction: column; 849 + overflow: hidden; 850 + box-shadow: 0 25px 80px rgba(0,0,0,0.5); 851 + } 852 + .sc-playground-header { 853 + display: flex; 854 + align-items: center; 855 + justify-content: space-between; 856 + padding: 0.75rem 1rem; 857 + background: rgba(255,255,255,0.05); 858 + border-bottom: 1px solid rgba(255,255,255,0.1); 859 + } 860 + .sc-playground-title { 861 + font-family: 'Fraunces', serif; 862 + font-size: 0.9rem; 863 + color: rgba(255,255,255,0.8); 864 + } 865 + .sc-playground-close { 866 + background: none; 867 + border: none; 868 + color: rgba(255,255,255,0.5); 869 + font-size: 1.5rem; 870 + cursor: pointer; 871 + padding: 0 0.5rem; 872 + line-height: 1; 873 + } 874 + .sc-playground-close:hover { color: #fff; } 875 + .sc-playground-editor { 876 + flex: 1; 877 + overflow: auto; 878 + } 879 + .sc-playground-editor x-ocaml { 880 + display: block; 881 + height: 100%; 882 + } 883 + .sc-container.sc-warm .sc-playground-btn { 884 + display: inline-block; 885 + margin-top: 0.75rem; 886 + padding: 0.4rem 1rem; 887 + border: 1px solid rgba(160,120,90,0.3); 888 + border-radius: 6px; 889 + background: transparent; 890 + color: #a0785a; 891 + font-family: 'Source Serif 4', serif; 892 + font-size: 0.85rem; 893 + cursor: pointer; 894 + transition: all 0.2s; 895 + } 896 + .sc-container.sc-warm .sc-playground-btn:hover { 897 + background: rgba(160,120,90,0.1); 898 + border-color: #a0785a; 899 + } 776 900 |} 777 901 778 902 (** {1 Theme: Dark Terminal} ··· 1086 1210 .sc-container.sc-dark .sc-diff-added { background: rgba(0, 212, 170, 0.12); border-left: 3px solid #00d4aa; } 1087 1211 .sc-container.sc-dark .sc-diff-removed { background: rgba(255, 80, 80, 0.1); border-left: 3px solid #ff6b6b; text-decoration: line-through; opacity: 0.6; } 1088 1212 .sc-container.sc-dark .sc-diff-same { opacity: 0.4; } 1213 + 1214 + /* Playground */ 1215 + .sc-container.sc-dark .sc-playground-btn { 1216 + display: inline-block; 1217 + margin-top: 0.75rem; 1218 + padding: 0.4rem 1rem; 1219 + border: 1px solid rgba(0,212,170,0.3); 1220 + border-radius: 6px; 1221 + background: transparent; 1222 + color: #00d4aa; 1223 + font-family: 'Outfit', sans-serif; 1224 + font-size: 0.85rem; 1225 + cursor: pointer; 1226 + transition: all 0.2s; 1227 + } 1228 + .sc-container.sc-dark .sc-playground-btn:hover { 1229 + background: rgba(0,212,170,0.1); 1230 + border-color: #00d4aa; 1231 + } 1089 1232 |} 1090 1233 1091 1234 (** {1 Theme: Notebook} ··· 1408 1551 .sc-container.sc-notebook .sc-diff-added { background: rgba(0, 102, 204, 0.12); border-left: 3px solid #0066cc; } 1409 1552 .sc-container.sc-notebook .sc-diff-removed { background: rgba(220, 50, 50, 0.1); border-left: 3px solid #dc3232; text-decoration: line-through; opacity: 0.6; } 1410 1553 .sc-container.sc-notebook .sc-diff-same { opacity: 0.4; } 1554 + 1555 + /* Playground */ 1556 + .sc-container.sc-notebook .sc-playground-btn { 1557 + display: inline-block; 1558 + margin-top: 0.75rem; 1559 + padding: 0.4rem 1rem; 1560 + border: 1px solid rgba(0,102,204,0.3); 1561 + border-radius: 6px; 1562 + background: transparent; 1563 + color: #0066cc; 1564 + font-family: 'DM Sans', sans-serif; 1565 + font-size: 0.85rem; 1566 + cursor: pointer; 1567 + transition: all 0.2s; 1568 + } 1569 + .sc-container.sc-notebook .sc-playground-btn:hover { 1570 + background: rgba(0,102,204,0.1); 1571 + border-color: #0066cc; 1572 + } 1411 1573 |} 1412 1574 1413 1575 (** {1 CSS to hide odoc chrome} ··· 1507 1669 in 1508 1670 Buffer.add_string buf (render_diff_html diff); 1509 1671 Buffer.add_string buf " </div>\n"; 1672 + Buffer.add_string buf 1673 + (Printf.sprintf " <button class=\"sc-playground-btn\" data-step=\"%d\">&#9654; Try it</button>\n" i); 1510 1674 Buffer.add_string buf " </div>\n"; 1511 1675 prev_code := Some step.code) 1512 1676 steps; ··· 1578 1742 Buffer.add_string buf " <div class=\"sc-code-slot\">\n"; 1579 1743 Buffer.add_string buf (generate_code_lines step.code step.focus); 1580 1744 Buffer.add_string buf " </div>\n"; 1745 + Buffer.add_string buf 1746 + (Printf.sprintf " <button class=\"sc-playground-btn\" data-step=\"%d\">&#9654; Try it</button>\n" i); 1581 1747 Buffer.add_string buf " </div>\n") 1582 1748 steps; 1583 1749 Buffer.add_string buf " </div>\n"; ··· 1611 1777 (* Mobile stacked layout *) 1612 1778 Buffer.add_string buf (generate_mobile_html steps); 1613 1779 1780 + (* Playground overlay *) 1781 + Buffer.add_string buf {|<div id="sc-playground-overlay" class="sc-playground-overlay"> 1782 + <div class="sc-playground-container"> 1783 + <div class="sc-playground-header"> 1784 + <span class="sc-playground-title">Playground</span> 1785 + <button class="sc-playground-close">&times;</button> 1786 + </div> 1787 + <div class="sc-playground-editor"> 1788 + <x-ocaml id="sc-playground-x-ocaml" run-on="click"></x-ocaml> 1789 + </div> 1790 + </div> 1791 + </div> 1792 + |}; 1793 + 1614 1794 (* JavaScript *) 1615 1795 Buffer.add_string buf "<script>\n"; 1616 1796 Buffer.add_string buf shared_js; 1617 1797 Buffer.add_string buf "</script>\n"; 1798 + 1799 + (* x-ocaml for playground *) 1800 + Buffer.add_string buf {|<script src="/_x-ocaml/x-ocaml.js" src-worker="/_x-ocaml/worker.js" backend="jtw"></script> 1801 + |}; 1618 1802 1619 1803 Buffer.contents buf 1620 1804