this repo has no description

Refactor scrollycode themes into CSS custom properties and add odoc shell plugins

Three-layer architecture separating content, theme, and renderer:

Phase 1 — Scrollycode refactor:
- Strip ~1030 lines of embedded theme CSS from scrollycode_extension.ml
- Create scrollycode_css.ml with structural CSS using CSS custom properties
- Create scrollycode_themes.ml with three theme files (warm/dark/notebook)
registered as support files
- Simplify @scrolly tag: theme suffix now ignored (CSS concern, not content)
- Playground overlay styled via --xo-* custom properties on x-ocaml element

Phase 2 — Extra CSS support:
- Add --extra-css flag to odoc html-generate for injecting additional
<link> tags (used for per-page theme selection)

Phase 3 — Shell plugin system:
- Add Html_shell module with Shell interface and hashtable registry
- Register "default" shell in html_page.ml, "json" shell in
html_fragment_json.ml
- Replace hardcoded if/else in generator.ml with shell registry lookup
- Add --shell NAME flag (--as-json kept as backward-compat alias)

x-ocaml theming:
- Replace hardcoded colors in style.css with var(--xo-*, fallback)
covering editor, gutter, buttons, tooltips, and output areas
- CSS custom properties inherit through shadow DOM boundary, so
consumers theme x-ocaml with pure CSS — no JS injection needed

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

+1016 -1143
+1
src/dune
··· 1 1 (library 2 2 (public_name odoc-scrollycode-extension.impl) 3 3 (name scrollycode_extension) 4 + (modules scrollycode_extension scrollycode_css scrollycode_themes) 4 5 (libraries odoc.extension_api odoc.model odoc.document)) 5 6 6 7 (plugin
+448
src/scrollycode_css.ml
··· 1 + (** Structural CSS for the scrollycode extension. 2 + 3 + This CSS defines layout, animations, and structure using CSS custom 4 + properties for all theming. Theme files set the custom property values. 5 + 6 + Custom property contract: 7 + - Typography: --sc-font-display, --sc-font-body, --sc-font-code 8 + - Colors: --sc-bg, --sc-text, --sc-text-dim, --sc-accent, --sc-accent-soft, 9 + --sc-code-bg, --sc-code-text, --sc-code-gutter, --sc-border, 10 + --sc-focus-bg, --sc-panel-radius 11 + - Syntax: --sc-hl-keyword, --sc-hl-type, --sc-hl-string, --sc-hl-comment, 12 + --sc-hl-number, --sc-hl-module, --sc-hl-operator, --sc-hl-punct *) 13 + 14 + let structural_css = 15 + {| 16 + /* === Override odoc page chrome for scrollycode pages === */ 17 + .odoc-nav, .odoc-tocs, .odoc-search { display: none !important; } 18 + .odoc-preamble > h1, .odoc-preamble > h2, .odoc-preamble > h3 { display: none !important; } 19 + .at-tags > li > .at-tag { display: none !important; } 20 + .odoc-preamble, .odoc-content { 21 + max-width: none !important; 22 + padding: 0 !important; 23 + margin: 0 !important; 24 + display: block !important; 25 + } 26 + .at-tags { 27 + list-style: none !important; 28 + padding: 0 !important; 29 + margin: 0 !important; 30 + } 31 + .at-tags > li { 32 + display: block !important; 33 + margin: 0 !important; 34 + padding: 0 !important; 35 + } 36 + body.odoc, .odoc { 37 + padding: 0 !important; 38 + margin: 0 !important; 39 + max-width: none !important; 40 + background: inherit; 41 + } 42 + 43 + /* === Container === */ 44 + .sc-container { 45 + font-family: var(--sc-font-body); 46 + background: var(--sc-bg); 47 + color: var(--sc-text); 48 + } 49 + 50 + /* === Hero === */ 51 + .sc-container .sc-hero { 52 + background: var(--sc-bg); 53 + padding: 5rem 2rem 3rem; 54 + } 55 + 56 + .sc-container .sc-hero h1 { 57 + font-family: var(--sc-font-display); 58 + font-size: clamp(2.2rem, 5vw, 3.4rem); 59 + font-weight: 800; 60 + color: var(--sc-text); 61 + letter-spacing: -0.03em; 62 + line-height: 1.1; 63 + margin-bottom: 0.75rem; 64 + } 65 + 66 + .sc-container .sc-hero p { 67 + color: var(--sc-text-dim); 68 + font-size: 1.05rem; 69 + max-width: 48ch; 70 + line-height: 1.6; 71 + } 72 + 73 + /* === Tutorial layout === */ 74 + .sc-container .sc-tutorial { 75 + display: flex; 76 + gap: 0; 77 + background: var(--sc-bg); 78 + position: relative; 79 + } 80 + 81 + .sc-container .sc-steps-col { 82 + flex: 1; 83 + min-width: 0; 84 + padding: 2rem 2.5rem 50vh 2.5rem; 85 + } 86 + 87 + .sc-container .sc-code-col { 88 + width: 52%; 89 + flex-shrink: 0; 90 + } 91 + 92 + /* === Step === */ 93 + .sc-container .sc-step { 94 + min-height: 70vh; 95 + display: flex; 96 + flex-direction: column; 97 + justify-content: center; 98 + padding: 2rem 0; 99 + } 100 + 101 + .sc-container .sc-step-number { 102 + font-family: var(--sc-font-code); 103 + font-size: 0.7rem; 104 + font-weight: 600; 105 + letter-spacing: 0.1em; 106 + color: var(--sc-accent); 107 + text-transform: uppercase; 108 + margin-bottom: 0.5rem; 109 + } 110 + 111 + .sc-container .sc-step h2 { 112 + font-family: var(--sc-font-display); 113 + font-size: 1.5rem; 114 + font-weight: 700; 115 + color: var(--sc-text); 116 + letter-spacing: -0.02em; 117 + margin-bottom: 0.75rem; 118 + line-height: 1.25; 119 + } 120 + 121 + .sc-container .sc-step p { 122 + color: var(--sc-text-dim); 123 + font-size: 0.95rem; 124 + line-height: 1.7; 125 + max-width: 44ch; 126 + } 127 + 128 + /* === Code panel === */ 129 + .sc-container .sc-code-panel { 130 + position: sticky; 131 + top: 10vh; 132 + height: 80vh; 133 + margin: 0 2rem 0 0; 134 + background: var(--sc-code-bg); 135 + border-radius: var(--sc-panel-radius); 136 + overflow: hidden; 137 + display: flex; 138 + flex-direction: column; 139 + box-shadow: 0 20px 60px rgba(0, 0, 0, 0.2), 0 0 0 1px rgba(255,255,255,0.03) inset; 140 + } 141 + 142 + .sc-container .sc-code-header { 143 + display: flex; 144 + align-items: center; 145 + padding: 0.85rem 1.25rem; 146 + background: rgba(255,255,255,0.03); 147 + border-bottom: 1px solid rgba(255,255,255,0.06); 148 + gap: 0.6rem; 149 + } 150 + 151 + .sc-container .sc-dots { 152 + display: flex; 153 + gap: 6px; 154 + } 155 + 156 + .sc-container .sc-dots span { 157 + width: 10px; 158 + height: 10px; 159 + border-radius: 50%; 160 + } 161 + 162 + .sc-container .sc-dots span:nth-child(1) { background: #ff5f57; } 163 + .sc-container .sc-dots span:nth-child(2) { background: #ffbd2e; } 164 + .sc-container .sc-dots span:nth-child(3) { background: #28c840; } 165 + 166 + .sc-container .sc-filename { 167 + font-family: var(--sc-font-code); 168 + font-size: 0.72rem; 169 + color: rgba(255,255,255,0.35); 170 + letter-spacing: 0.04em; 171 + flex: 1; 172 + text-align: center; 173 + } 174 + 175 + .sc-container .sc-step-badge { 176 + font-family: var(--sc-font-code); 177 + font-size: 0.65rem; 178 + color: rgba(255,255,255,0.25); 179 + letter-spacing: 0.06em; 180 + } 181 + 182 + .sc-container .sc-code-body { 183 + flex: 1; 184 + overflow-y: auto; 185 + padding: 1.25rem 0; 186 + font-family: var(--sc-font-code); 187 + font-size: 0.82rem; 188 + line-height: 1.7; 189 + color: var(--sc-code-text); 190 + } 191 + 192 + /* === Lines === */ 193 + .sc-container .sc-line { 194 + padding: 0 1.25rem; 195 + white-space: pre; 196 + transition: opacity 0.3s ease; 197 + opacity: 0.35; 198 + } 199 + 200 + .sc-container .sc-line.sc-focused { 201 + opacity: 1; 202 + background: var(--sc-focus-bg, rgba(255,255,255,0.04)); 203 + } 204 + 205 + .sc-container .sc-line-number { 206 + display: inline-block; 207 + width: 3ch; 208 + text-align: right; 209 + margin-right: 1.5ch; 210 + color: var(--sc-code-gutter); 211 + user-select: none; 212 + } 213 + 214 + /* === Syntax highlighting === */ 215 + .sc-container .hl-keyword { color: var(--sc-hl-keyword); font-weight: 500; } 216 + .sc-container .hl-type { color: var(--sc-hl-type); } 217 + .sc-container .hl-string { color: var(--sc-hl-string); } 218 + .sc-container .hl-comment { color: var(--sc-hl-comment); font-style: italic; } 219 + .sc-container .hl-number { color: var(--sc-hl-number); } 220 + .sc-container .hl-module { color: var(--sc-hl-module); } 221 + .sc-container .hl-operator { color: var(--sc-hl-operator); } 222 + .sc-container .hl-punct { color: var(--sc-hl-punct); } 223 + 224 + /* === Progress pips === */ 225 + .sc-container .sc-progress { 226 + position: fixed; 227 + left: 1.5rem; 228 + top: 50%; 229 + transform: translateY(-50%); 230 + display: flex; 231 + flex-direction: column; 232 + gap: 8px; 233 + z-index: 100; 234 + } 235 + 236 + .sc-container .sc-pip { 237 + width: 6px; 238 + height: 6px; 239 + border-radius: 50%; 240 + background: var(--sc-border); 241 + transition: all 0.3s ease; 242 + } 243 + 244 + .sc-container .sc-pip.sc-active { 245 + background: var(--sc-accent); 246 + box-shadow: 0 0 8px color-mix(in srgb, var(--sc-accent) 40%, transparent); 247 + transform: scale(1.4); 248 + } 249 + 250 + /* === Animations === */ 251 + @keyframes sc-line-exit { 252 + 0% { opacity: 1; transform: translateX(0); } 253 + 100% { opacity: 0; transform: translateX(-30px); } 254 + } 255 + 256 + @keyframes sc-line-enter { 257 + 0% { opacity: 0; transform: translateX(30px); } 258 + 100% { opacity: 1; transform: translateX(0); } 259 + } 260 + 261 + .sc-container .sc-line.sc-exiting { 262 + animation: sc-line-exit 0.2s cubic-bezier(0.22, 1, 0.36, 1) forwards; 263 + } 264 + 265 + .sc-container .sc-line.sc-entering { 266 + animation: sc-line-enter 0.25s cubic-bezier(0.22, 1, 0.36, 1) both; 267 + } 268 + 269 + /* Hidden code slot */ 270 + .sc-code-slot { display: none; } 271 + 272 + /* === Mobile responsive === */ 273 + @media (max-width: 700px) { 274 + .sc-container { padding: 0 1rem; } 275 + .sc-container .sc-desktop { display: none !important; } 276 + .sc-container .sc-mobile { display: block !important; } 277 + .sc-container .sc-progress { display: none; } 278 + .sc-container .sc-hero h1 { font-size: 2rem; } 279 + } 280 + @media (min-width: 701px) { 281 + .sc-container .sc-mobile { display: none !important; } 282 + } 283 + 284 + .sc-container .sc-mobile-step { 285 + margin: 1.5rem 0; 286 + padding: 1.5rem; 287 + border-radius: 12px; 288 + background: var(--sc-mobile-step-bg, rgba(255,255,255,0.5)); 289 + } 290 + 291 + .sc-container .sc-mobile-step-num { 292 + font-family: var(--sc-font-display); 293 + font-size: 0.75rem; 294 + text-transform: uppercase; 295 + letter-spacing: 0.15em; 296 + color: var(--sc-accent); 297 + margin-bottom: 0.5rem; 298 + } 299 + 300 + .sc-container .sc-mobile-step h2 { 301 + font-family: var(--sc-font-display); 302 + font-size: 1.3rem; 303 + color: var(--sc-text); 304 + margin: 0 0 0.75rem; 305 + } 306 + 307 + .sc-container .sc-mobile-step p { 308 + font-family: var(--sc-font-body); 309 + font-size: 1rem; 310 + color: var(--sc-text-dim); 311 + line-height: 1.6; 312 + margin: 0 0 1rem; 313 + } 314 + 315 + .sc-container .sc-diff-block { 316 + background: var(--sc-code-bg); 317 + border-radius: 8px; 318 + padding: 0.75rem; 319 + overflow-x: auto; 320 + font-family: var(--sc-font-code); 321 + font-size: 0.8rem; 322 + line-height: 1.5; 323 + } 324 + 325 + .sc-container .sc-diff-line { padding: 1px 0.5rem; white-space: pre; } 326 + .sc-container .sc-diff-added { background: rgba(80, 200, 80, 0.15); border-left: 3px solid #4caf50; } 327 + .sc-container .sc-diff-removed { background: rgba(255, 80, 80, 0.12); border-left: 3px solid #ef5350; text-decoration: line-through; opacity: 0.7; } 328 + .sc-container .sc-diff-same { opacity: 0.5; } 329 + 330 + /* === Playground overlay === */ 331 + .sc-playground-overlay { 332 + display: none; 333 + position: fixed; 334 + inset: 0; 335 + z-index: 10000; 336 + background: rgba(0,0,0,0.6); 337 + backdrop-filter: blur(4px); 338 + align-items: center; 339 + justify-content: center; 340 + } 341 + .sc-playground-overlay.sc-open { 342 + display: flex; 343 + } 344 + .sc-playground-container { 345 + width: 90vw; 346 + max-width: 900px; 347 + height: 80vh; 348 + background: var(--sc-code-bg, #1e1b2e); 349 + border-radius: 12px; 350 + display: flex; 351 + flex-direction: column; 352 + overflow: hidden; 353 + box-shadow: 0 25px 80px rgba(0,0,0,0.5); 354 + } 355 + .sc-playground-header { 356 + display: flex; 357 + align-items: center; 358 + justify-content: space-between; 359 + padding: 0.85rem 1.5rem; 360 + background: rgba(255,255,255,0.05); 361 + border-bottom: 1px solid rgba(255,255,255,0.1); 362 + flex-shrink: 0; 363 + } 364 + .sc-playground-title { 365 + font-family: var(--sc-font-code, monospace); 366 + font-size: 0.8rem; 367 + font-weight: 500; 368 + letter-spacing: 0.04em; 369 + color: rgba(255,255,255,0.6); 370 + text-transform: uppercase; 371 + } 372 + .sc-playground-close { 373 + background: none; 374 + border: none; 375 + color: rgba(255,255,255,0.4); 376 + font-size: 1.25rem; 377 + cursor: pointer; 378 + padding: 0 0.25rem; 379 + line-height: 1; 380 + } 381 + .sc-playground-close:hover { color: #fff; } 382 + .sc-playground-editor { 383 + flex: 1; 384 + overflow: auto; 385 + min-height: 0; 386 + } 387 + .sc-playground-editor x-ocaml { 388 + display: block; 389 + height: 100%; 390 + font-size: 0.85rem; 391 + 392 + /* Map scrollycode theme properties to x-ocaml custom properties */ 393 + --xo-font: var(--sc-font-code, monospace); 394 + --xo-font-size: inherit; 395 + --xo-bg: var(--sc-code-bg, #1a1a2e); 396 + --xo-text: var(--sc-code-text, #d4d0c8); 397 + --xo-gutter-bg: var(--sc-code-bg, #1a1a2e); 398 + --xo-gutter-text: var(--sc-code-gutter, #3a3a52); 399 + --xo-gutter-border: rgba(255,255,255,0.06); 400 + --xo-focus-outline: rgba(255,255,255,0.2); 401 + --xo-active-line: rgba(255,255,255,0.04); 402 + --xo-active-line-focused: rgba(255,255,255,0.06); 403 + --xo-selection: rgba(255,255,255,0.12); 404 + --xo-selection-focused: rgba(255,255,255,0.15); 405 + --xo-content-padding-left: 16px; 406 + --xo-gutter-element-padding: 0 12px 0 8px; 407 + --xo-line-numbers-min-width: 40px; 408 + 409 + /* Run button */ 410 + --xo-btn-bg: var(--sc-code-bg, #1a1a2e); 411 + --xo-btn-border: var(--sc-code-gutter, #3a3a52); 412 + --xo-btn-text: var(--sc-code-text, #d4d0c8); 413 + --xo-btn-hover-bg: var(--sc-accent, #c25832); 414 + --xo-btn-hover-text: #fff; 415 + 416 + /* Tooltips */ 417 + --xo-tooltip-bg: var(--sc-code-bg, #1a1a2e); 418 + --xo-tooltip-text: var(--sc-code-text, #d4d0c8); 419 + --xo-tooltip-border: rgba(255,255,255,0.15); 420 + 421 + /* Output areas */ 422 + --xo-stdout-bg: rgba(255,255,255,0.05); 423 + --xo-stdout-text: var(--sc-code-text, #d4d0c8); 424 + --xo-stderr-bg: rgba(235,86,86,0.1); 425 + --xo-stderr-text: #f08080; 426 + --xo-meta-bg: rgba(255,255,255,0.03); 427 + --xo-meta-text: var(--sc-code-gutter, #3a3a52); 428 + } 429 + 430 + /* === Playground button === */ 431 + .sc-container .sc-playground-btn { 432 + display: inline-block; 433 + margin-top: 0.75rem; 434 + padding: 0.4rem 1rem; 435 + border: 1px solid color-mix(in srgb, var(--sc-accent) 30%, transparent); 436 + border-radius: 6px; 437 + background: transparent; 438 + color: var(--sc-accent); 439 + font-family: var(--sc-font-body); 440 + font-size: 0.85rem; 441 + cursor: pointer; 442 + transition: all 0.2s; 443 + } 444 + .sc-container .sc-playground-btn:hover { 445 + background: color-mix(in srgb, var(--sc-accent) 10%, transparent); 446 + border-color: var(--sc-accent); 447 + } 448 + |}
+35 -1140
src/scrollycode_extension.ml
··· 1 1 (** Scrollycode Extension for odoc 2 2 3 - Provides scroll-driven code tutorials with three visual themes: 4 - - warm: Earthy, bookish aesthetic (Fraunces + Source Serif) 5 - - dark: Cinematic terminal aesthetic (JetBrains Mono + Outfit) 6 - - notebook: Clean editorial aesthetic (Newsreader + DM Sans) 3 + Provides scroll-driven code tutorials. Theme styling is handled 4 + externally via CSS custom properties defined in {!Scrollycode_css} 5 + and set by theme files in {!Scrollycode_themes}. 7 6 8 - Authoring format uses @scrolly.<theme> custom tags with an ordered 7 + Authoring format uses @scrolly custom tags with an ordered 9 8 list inside, where each list item is a tutorial step containing 10 - a bold title, prose paragraphs, and a code block. *) 9 + a bold title, prose paragraphs, and a code block. 10 + 11 + For backward compatibility, @scrolly.warm / @scrolly.dark / 12 + @scrolly.notebook are still accepted but the theme suffix is 13 + ignored — theme selection is now a CSS concern. *) 11 14 12 15 module Comment = Odoc_model.Comment 13 16 module Location_ = Odoc_model.Location_ ··· 538 541 })(); 539 542 |} 540 543 541 - (** {1 Theme: Warm Workshop} 542 - 543 - Earthy, bookish. Cream background, burnt sienna accents. 544 - Fraunces display + Source Serif 4 body. 545 - Dark navy code panel with warm syntax highlighting. *) 546 - 547 - let warm_css = 548 - {| 549 - .sc-container.sc-warm { 550 - --sc-bg: #f5f0e6; 551 - --sc-text: #2c2416; 552 - --sc-text-dim: #8a7c6a; 553 - --sc-accent: #c25832; 554 - --sc-accent-soft: rgba(194, 88, 50, 0.08); 555 - --sc-code-bg: #1a1a2e; 556 - --sc-code-text: #d4d0c8; 557 - --sc-code-gutter: #3a3a52; 558 - --sc-border: rgba(44, 36, 22, 0.1); 559 - --sc-focus-bg: rgba(194, 88, 50, 0.06); 560 - --sc-panel-radius: 12px; 561 - font-family: 'Source Serif 4', Georgia, serif; 562 - } 563 - 564 - .sc-container.sc-warm .sc-hero { 565 - background: var(--sc-bg); 566 - text-align: center; 567 - padding: 5rem 2rem 3rem; 568 - border-bottom: 1px solid var(--sc-border); 569 - } 570 - 571 - .sc-container.sc-warm .sc-hero h1 { 572 - font-family: 'Fraunces', serif; 573 - font-size: clamp(2.2rem, 5vw, 3.4rem); 574 - font-weight: 800; 575 - font-style: italic; 576 - color: var(--sc-text); 577 - letter-spacing: -0.03em; 578 - line-height: 1.1; 579 - margin-bottom: 0.75rem; 580 - } 581 - 582 - .sc-container.sc-warm .sc-hero p { 583 - color: var(--sc-text-dim); 584 - font-size: 1.05rem; 585 - max-width: 48ch; 586 - margin: 0 auto; 587 - line-height: 1.6; 588 - } 589 - 590 - .sc-container.sc-warm .sc-tutorial { 591 - display: flex; 592 - gap: 0; 593 - background: var(--sc-bg); 594 - position: relative; 595 - } 596 - 597 - .sc-container.sc-warm .sc-steps-col { 598 - flex: 1; 599 - min-width: 0; 600 - padding: 2rem 2.5rem 50vh 2.5rem; 601 - } 602 - 603 - .sc-container.sc-warm .sc-code-col { 604 - width: 52%; 605 - flex-shrink: 0; 606 - } 607 - 608 - .sc-container.sc-warm .sc-step { 609 - min-height: 70vh; 610 - display: flex; 611 - flex-direction: column; 612 - justify-content: center; 613 - padding: 2rem 0; 614 - } 615 - 616 - .sc-container.sc-warm .sc-step-number { 617 - font-family: 'Source Code Pro', monospace; 618 - font-size: 0.7rem; 619 - font-weight: 600; 620 - letter-spacing: 0.1em; 621 - color: var(--sc-accent); 622 - text-transform: uppercase; 623 - margin-bottom: 0.5rem; 624 - } 625 - 626 - .sc-container.sc-warm .sc-step h2 { 627 - font-family: 'Fraunces', serif; 628 - font-size: 1.5rem; 629 - font-weight: 700; 630 - color: var(--sc-text); 631 - letter-spacing: -0.02em; 632 - margin-bottom: 0.75rem; 633 - line-height: 1.25; 634 - } 635 - 636 - .sc-container.sc-warm .sc-step p { 637 - color: var(--sc-text-dim); 638 - font-size: 0.95rem; 639 - line-height: 1.7; 640 - max-width: 44ch; 641 - } 642 - 643 - .sc-container.sc-warm .sc-code-panel { 644 - position: sticky; 645 - top: 10vh; 646 - height: 80vh; 647 - margin: 0 2rem 0 0; 648 - background: var(--sc-code-bg); 649 - border-radius: var(--sc-panel-radius); 650 - overflow: hidden; 651 - display: flex; 652 - flex-direction: column; 653 - box-shadow: 0 20px 60px rgba(26, 26, 46, 0.3), 0 0 0 1px rgba(255,255,255,0.03) inset; 654 - } 655 - 656 - .sc-container.sc-warm .sc-code-header { 657 - display: flex; 658 - align-items: center; 659 - padding: 0.85rem 1.25rem; 660 - background: rgba(255,255,255,0.03); 661 - border-bottom: 1px solid rgba(255,255,255,0.06); 662 - gap: 0.6rem; 663 - } 664 - 665 - .sc-container.sc-warm .sc-dots { 666 - display: flex; 667 - gap: 6px; 668 - } 669 - 670 - .sc-container.sc-warm .sc-dots span { 671 - width: 10px; 672 - height: 10px; 673 - border-radius: 50%; 674 - } 675 - 676 - .sc-container.sc-warm .sc-dots span:nth-child(1) { background: #ff5f57; } 677 - .sc-container.sc-warm .sc-dots span:nth-child(2) { background: #ffbd2e; } 678 - .sc-container.sc-warm .sc-dots span:nth-child(3) { background: #28c840; } 679 - 680 - .sc-container.sc-warm .sc-filename { 681 - font-family: 'Source Code Pro', monospace; 682 - font-size: 0.72rem; 683 - color: rgba(255,255,255,0.35); 684 - letter-spacing: 0.04em; 685 - flex: 1; 686 - text-align: center; 687 - } 688 - 689 - .sc-container.sc-warm .sc-step-badge { 690 - font-family: 'Source Code Pro', monospace; 691 - font-size: 0.65rem; 692 - color: rgba(255,255,255,0.25); 693 - letter-spacing: 0.06em; 694 - } 695 - 696 - .sc-container.sc-warm .sc-code-body { 697 - flex: 1; 698 - overflow-y: auto; 699 - padding: 1.25rem 0; 700 - font-family: 'Source Code Pro', monospace; 701 - font-size: 0.82rem; 702 - line-height: 1.7; 703 - color: var(--sc-code-text); 704 - } 705 - 706 - .sc-container.sc-warm .sc-line { 707 - padding: 0 1.25rem; 708 - white-space: pre; 709 - transition: opacity 0.3s ease; 710 - opacity: 0.35; 711 - } 712 - 713 - .sc-container.sc-warm .sc-line.sc-focused { 714 - opacity: 1; 715 - background: rgba(194, 88, 50, 0.06); 716 - } 717 - 718 - .sc-container.sc-warm .sc-line-number { 719 - display: inline-block; 720 - width: 3ch; 721 - text-align: right; 722 - margin-right: 1.5ch; 723 - color: var(--sc-code-gutter); 724 - user-select: none; 725 - } 726 - 727 - /* Syntax highlighting */ 728 - .sc-container.sc-warm .hl-keyword { color: #f0a6a0; font-weight: 500; } 729 - .sc-container.sc-warm .hl-type { color: #8ec8e8; } 730 - .sc-container.sc-warm .hl-string { color: #b8d89a; } 731 - .sc-container.sc-warm .hl-comment { color: #6a6a82; font-style: italic; } 732 - .sc-container.sc-warm .hl-number { color: #ddb97a; } 733 - .sc-container.sc-warm .hl-module { color: #e8c87a; } 734 - .sc-container.sc-warm .hl-operator { color: #c8a8d8; } 735 - .sc-container.sc-warm .hl-punct { color: #7a7a92; } 736 - 737 - /* Progress pips */ 738 - .sc-container.sc-warm .sc-progress { 739 - position: fixed; 740 - left: 1.5rem; 741 - top: 50%; 742 - transform: translateY(-50%); 743 - display: flex; 744 - flex-direction: column; 745 - gap: 8px; 746 - z-index: 100; 747 - } 748 - 749 - .sc-container.sc-warm .sc-pip { 750 - width: 6px; 751 - height: 6px; 752 - border-radius: 50%; 753 - background: var(--sc-border); 754 - transition: all 0.3s ease; 755 - } 756 - 757 - .sc-container.sc-warm .sc-pip.sc-active { 758 - background: var(--sc-accent); 759 - box-shadow: 0 0 8px rgba(194, 88, 50, 0.4); 760 - transform: scale(1.4); 761 - } 762 - 763 - /* Animations */ 764 - @keyframes sc-line-exit { 765 - 0% { opacity: 1; transform: translateX(0); } 766 - 100% { opacity: 0; transform: translateX(-30px); } 767 - } 768 - 769 - @keyframes sc-line-enter { 770 - 0% { opacity: 0; transform: translateX(30px); } 771 - 100% { opacity: 1; transform: translateX(0); } 772 - } 773 - 774 - .sc-container.sc-warm .sc-line.sc-exiting { 775 - animation: sc-line-exit 0.2s cubic-bezier(0.22, 1, 0.36, 1) forwards; 776 - } 777 - 778 - .sc-container.sc-warm .sc-line.sc-entering { 779 - animation: sc-line-enter 0.25s cubic-bezier(0.22, 1, 0.36, 1) both; 780 - } 781 - 782 - /* Hidden code slot */ 783 - .sc-code-slot { display: none; } 784 - 785 - /* Mobile responsive */ 786 - @media (max-width: 700px) { 787 - .sc-container.sc-warm { padding: 0 1rem; } 788 - .sc-container.sc-warm .sc-desktop { display: none !important; } 789 - .sc-container.sc-warm .sc-mobile { display: block !important; } 790 - .sc-container.sc-warm .sc-progress { display: none; } 791 - .sc-container.sc-warm .sc-hero h1 { font-size: 2rem; } 792 - } 793 - @media (min-width: 701px) { 794 - .sc-container.sc-warm .sc-mobile { display: none !important; } 795 - } 796 - .sc-container.sc-warm .sc-mobile-step { 797 - margin: 1.5rem 0; 798 - padding: 1.5rem; 799 - border-radius: 12px; 800 - background: rgba(255,255,255,0.5); 801 - } 802 - .sc-container.sc-warm .sc-mobile-step-num { 803 - font-family: 'Fraunces', serif; 804 - font-size: 0.75rem; 805 - text-transform: uppercase; 806 - letter-spacing: 0.15em; 807 - color: #a0785a; 808 - margin-bottom: 0.5rem; 809 - } 810 - .sc-container.sc-warm .sc-mobile-step h2 { 811 - font-family: 'Fraunces', serif; 812 - font-size: 1.3rem; 813 - color: #3a2e28; 814 - margin: 0 0 0.75rem; 815 - } 816 - .sc-container.sc-warm .sc-mobile-step p { 817 - font-family: 'Source Serif 4', serif; 818 - font-size: 1rem; 819 - color: #5a4a3a; 820 - line-height: 1.6; 821 - margin: 0 0 1rem; 822 - } 823 - .sc-container.sc-warm .sc-diff-block { 824 - background: #1e1b2e; 825 - border-radius: 8px; 826 - padding: 0.75rem; 827 - overflow-x: auto; 828 - font-family: 'Source Code Pro', monospace; 829 - font-size: 0.8rem; 830 - line-height: 1.5; 831 - } 832 - .sc-container.sc-warm .sc-diff-line { padding: 1px 0.5rem; white-space: pre; } 833 - .sc-container.sc-warm .sc-diff-added { background: rgba(80, 200, 80, 0.15); border-left: 3px solid #4caf50; } 834 - .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; } 835 - .sc-container.sc-warm .sc-diff-same { opacity: 0.5; } 836 - 837 - /* Playground overlay */ 838 - .sc-playground-overlay { 839 - display: none; 840 - position: fixed; 841 - inset: 0; 842 - z-index: 10000; 843 - background: rgba(0,0,0,0.6); 844 - backdrop-filter: blur(4px); 845 - align-items: center; 846 - justify-content: center; 847 - } 848 - .sc-playground-overlay.sc-open { 849 - display: flex; 850 - } 851 - .sc-playground-container { 852 - width: 90vw; 853 - max-width: 900px; 854 - height: 80vh; 855 - background: #1e1b2e; 856 - border-radius: 12px; 857 - display: flex; 858 - flex-direction: column; 859 - overflow: hidden; 860 - box-shadow: 0 25px 80px rgba(0,0,0,0.5); 861 - } 862 - .sc-playground-header { 863 - display: flex; 864 - align-items: center; 865 - justify-content: space-between; 866 - padding: 0.75rem 1rem; 867 - background: rgba(255,255,255,0.05); 868 - border-bottom: 1px solid rgba(255,255,255,0.1); 869 - } 870 - .sc-playground-title { 871 - font-family: 'Fraunces', serif; 872 - font-size: 0.9rem; 873 - color: rgba(255,255,255,0.8); 874 - } 875 - .sc-playground-close { 876 - background: none; 877 - border: none; 878 - color: rgba(255,255,255,0.5); 879 - font-size: 1.5rem; 880 - cursor: pointer; 881 - padding: 0 0.5rem; 882 - line-height: 1; 883 - } 884 - .sc-playground-close:hover { color: #fff; } 885 - .sc-playground-editor { 886 - flex: 1; 887 - overflow: auto; 888 - } 889 - .sc-playground-editor x-ocaml { 890 - display: block; 891 - height: 100%; 892 - } 893 - .sc-container.sc-warm .sc-playground-btn { 894 - display: inline-block; 895 - margin-top: 0.75rem; 896 - padding: 0.4rem 1rem; 897 - border: 1px solid rgba(160,120,90,0.3); 898 - border-radius: 6px; 899 - background: transparent; 900 - color: #a0785a; 901 - font-family: 'Source Serif 4', serif; 902 - font-size: 0.85rem; 903 - cursor: pointer; 904 - transition: all 0.2s; 905 - } 906 - .sc-container.sc-warm .sc-playground-btn:hover { 907 - background: rgba(160,120,90,0.1); 908 - border-color: #a0785a; 909 - } 910 - |} 911 - 912 - (** {1 Theme: Dark Terminal} 913 - 914 - Cinematic dark theme. Near-black background, phosphor green and amber. 915 - JetBrains Mono + Outfit geometric sans. 916 - Code panel is hero-sized, prose is a narrow overlay strip. *) 917 - 918 - let dark_css = 919 - {| 920 - .sc-container.sc-dark { 921 - --sc-bg: #0a0a0f; 922 - --sc-text: #e8e6f0; 923 - --sc-text-dim: #6e6b80; 924 - --sc-accent: #4ade80; 925 - --sc-accent-alt: #fbbf24; 926 - --sc-code-bg: #0f0f18; 927 - --sc-code-text: #c8c5d8; 928 - --sc-code-gutter: #2a2a3e; 929 - --sc-border: rgba(255, 255, 255, 0.06); 930 - --sc-panel-radius: 0; 931 - font-family: 'Outfit', sans-serif; 932 - background: var(--sc-bg); 933 - color: var(--sc-text); 934 - } 935 - 936 - .sc-container.sc-dark .sc-hero { 937 - background: var(--sc-bg); 938 - text-align: left; 939 - padding: 8rem 4rem 4rem; 940 - max-width: 800px; 941 - position: relative; 942 - } 943 - 944 - .sc-container.sc-dark .sc-hero::before { 945 - content: ''; 946 - position: absolute; 947 - top: 0; 948 - left: 0; 949 - right: 0; 950 - bottom: 0; 951 - background: radial-gradient(ellipse at 20% 50%, rgba(74, 222, 128, 0.04) 0%, transparent 60%); 952 - pointer-events: none; 953 - } 954 - 955 - .sc-container.sc-dark .sc-hero h1 { 956 - font-family: 'Outfit', sans-serif; 957 - font-size: clamp(2.8rem, 6vw, 4.5rem); 958 - font-weight: 800; 959 - color: var(--sc-text); 960 - letter-spacing: -0.04em; 961 - line-height: 1.0; 962 - margin-bottom: 1.25rem; 963 - } 964 - 965 - .sc-container.sc-dark .sc-hero h1 em { 966 - font-style: normal; 967 - color: var(--sc-accent); 968 - } 969 - 970 - .sc-container.sc-dark .sc-hero p { 971 - color: var(--sc-text-dim); 972 - font-size: 1.1rem; 973 - max-width: 50ch; 974 - line-height: 1.6; 975 - font-weight: 300; 976 - } 977 - 978 - .sc-container.sc-dark .sc-tutorial { 979 - display: flex; 980 - gap: 0; 981 - position: relative; 982 - } 983 - 984 - .sc-container.sc-dark .sc-steps-col { 985 - width: 38%; 986 - flex-shrink: 0; 987 - padding: 2rem 2.5rem 50vh 4rem; 988 - border-right: 1px solid var(--sc-border); 989 - } 990 - 991 - .sc-container.sc-dark .sc-code-col { 992 - flex: 1; 993 - min-width: 0; 994 - } 995 - 996 - .sc-container.sc-dark .sc-step { 997 - min-height: 70vh; 998 - display: flex; 999 - flex-direction: column; 1000 - justify-content: center; 1001 - padding: 2rem 0; 1002 - } 1003 - 1004 - .sc-container.sc-dark .sc-step-number { 1005 - font-family: 'JetBrains Mono', monospace; 1006 - font-size: 0.65rem; 1007 - font-weight: 700; 1008 - letter-spacing: 0.15em; 1009 - color: var(--sc-accent); 1010 - text-transform: uppercase; 1011 - margin-bottom: 0.75rem; 1012 - display: flex; 1013 - align-items: center; 1014 - gap: 0.75rem; 1015 - } 1016 - 1017 - .sc-container.sc-dark .sc-step-number::after { 1018 - content: ''; 1019 - flex: 1; 1020 - height: 1px; 1021 - background: var(--sc-border); 1022 - } 1023 - 1024 - .sc-container.sc-dark .sc-step h2 { 1025 - font-family: 'Outfit', sans-serif; 1026 - font-size: 1.4rem; 1027 - font-weight: 700; 1028 - color: var(--sc-text); 1029 - letter-spacing: -0.02em; 1030 - margin-bottom: 0.75rem; 1031 - line-height: 1.2; 1032 - } 1033 - 1034 - .sc-container.sc-dark .sc-step p { 1035 - color: var(--sc-text-dim); 1036 - font-size: 0.9rem; 1037 - line-height: 1.7; 1038 - max-width: 40ch; 1039 - font-weight: 300; 1040 - } 1041 - 1042 - .sc-container.sc-dark .sc-code-panel { 1043 - position: sticky; 1044 - top: 0; 1045 - height: 100vh; 1046 - background: var(--sc-code-bg); 1047 - display: flex; 1048 - flex-direction: column; 1049 - border-left: 1px solid var(--sc-border); 1050 - } 1051 - 1052 - .sc-container.sc-dark .sc-code-header { 1053 - display: flex; 1054 - align-items: center; 1055 - padding: 1rem 1.5rem; 1056 - border-bottom: 1px solid var(--sc-border); 1057 - gap: 1rem; 1058 - } 1059 - 1060 - .sc-container.sc-dark .sc-dots { 1061 - display: flex; 1062 - gap: 6px; 1063 - } 1064 - 1065 - .sc-container.sc-dark .sc-dots span { 1066 - width: 8px; 1067 - height: 8px; 1068 - border-radius: 50%; 1069 - background: var(--sc-code-gutter); 1070 - } 1071 - 1072 - .sc-container.sc-dark .sc-filename { 1073 - font-family: 'JetBrains Mono', monospace; 1074 - font-size: 0.7rem; 1075 - color: var(--sc-text-dim); 1076 - letter-spacing: 0.04em; 1077 - flex: 1; 1078 - } 1079 - 1080 - .sc-container.sc-dark .sc-step-badge { 1081 - font-family: 'JetBrains Mono', monospace; 1082 - font-size: 0.6rem; 1083 - color: var(--sc-accent); 1084 - letter-spacing: 0.08em; 1085 - background: rgba(74, 222, 128, 0.08); 1086 - padding: 0.25em 0.75em; 1087 - border-radius: 3px; 1088 - } 1089 - 1090 - .sc-container.sc-dark .sc-code-body { 1091 - flex: 1; 1092 - overflow-y: auto; 1093 - padding: 1.5rem 0; 1094 - font-family: 'JetBrains Mono', monospace; 1095 - font-size: 0.8rem; 1096 - line-height: 1.75; 1097 - color: var(--sc-code-text); 1098 - } 1099 - 1100 - .sc-container.sc-dark .sc-line { 1101 - padding: 0 1.5rem; 1102 - white-space: pre; 1103 - transition: opacity 0.3s ease, background 0.3s ease; 1104 - opacity: 0.25; 1105 - } 1106 - 1107 - .sc-container.sc-dark .sc-line.sc-focused { 1108 - opacity: 1; 1109 - background: rgba(74, 222, 128, 0.04); 1110 - border-left: 2px solid var(--sc-accent); 1111 - padding-left: calc(1.5rem - 2px); 1112 - } 1113 - 1114 - .sc-container.sc-dark .sc-line-number { 1115 - display: inline-block; 1116 - width: 3ch; 1117 - text-align: right; 1118 - margin-right: 2ch; 1119 - color: var(--sc-code-gutter); 1120 - user-select: none; 1121 - } 1122 - 1123 - /* Syntax highlighting — neon palette */ 1124 - .sc-container.sc-dark .hl-keyword { color: #ff7eb3; font-weight: 500; } 1125 - .sc-container.sc-dark .hl-type { color: #7dd3fc; } 1126 - .sc-container.sc-dark .hl-string { color: #4ade80; } 1127 - .sc-container.sc-dark .hl-comment { color: #4a4a62; font-style: italic; } 1128 - .sc-container.sc-dark .hl-number { color: #fbbf24; } 1129 - .sc-container.sc-dark .hl-module { color: #c4b5fd; } 1130 - .sc-container.sc-dark .hl-operator { color: #67e8f9; } 1131 - .sc-container.sc-dark .hl-punct { color: #4a4a62; } 1132 - 1133 - /* Progress pips */ 1134 - .sc-container.sc-dark .sc-progress { 1135 - position: fixed; 1136 - right: 1.5rem; 1137 - top: 50%; 1138 - transform: translateY(-50%); 1139 - display: flex; 1140 - flex-direction: column; 1141 - gap: 10px; 1142 - z-index: 100; 1143 - } 1144 - 1145 - .sc-container.sc-dark .sc-pip { 1146 - width: 3px; 1147 - height: 20px; 1148 - border-radius: 2px; 1149 - background: var(--sc-border); 1150 - transition: all 0.3s ease; 1151 - } 1152 - 1153 - .sc-container.sc-dark .sc-pip.sc-active { 1154 - background: var(--sc-accent); 1155 - box-shadow: 0 0 12px rgba(74, 222, 128, 0.5); 1156 - height: 30px; 1157 - } 1158 - 1159 - /* Animations */ 1160 - .sc-container.sc-dark .sc-line.sc-exiting { 1161 - animation: sc-line-exit 0.2s cubic-bezier(0.22, 1, 0.36, 1) forwards; 1162 - } 1163 - 1164 - .sc-container.sc-dark .sc-line.sc-entering { 1165 - animation: sc-line-enter 0.25s cubic-bezier(0.22, 1, 0.36, 1) both; 1166 - } 1167 - 1168 - .sc-code-slot { display: none; } 1169 - 1170 - /* Mobile responsive */ 1171 - @media (max-width: 700px) { 1172 - .sc-container.sc-dark { padding: 0 1rem; } 1173 - .sc-container.sc-dark .sc-desktop { display: none !important; } 1174 - .sc-container.sc-dark .sc-mobile { display: block !important; } 1175 - .sc-container.sc-dark .sc-progress { display: none; } 1176 - .sc-container.sc-dark .sc-hero h1 { font-size: 2rem; } 1177 - } 1178 - @media (min-width: 701px) { 1179 - .sc-container.sc-dark .sc-mobile { display: none !important; } 1180 - } 1181 - .sc-container.sc-dark .sc-mobile-step { 1182 - margin: 1.5rem 0; 1183 - padding: 1.5rem; 1184 - border-radius: 10px; 1185 - background: rgba(255,255,255,0.04); 1186 - border: 1px solid rgba(255,255,255,0.08); 1187 - } 1188 - .sc-container.sc-dark .sc-mobile-step-num { 1189 - font-family: 'Outfit', sans-serif; 1190 - font-size: 0.7rem; 1191 - text-transform: uppercase; 1192 - letter-spacing: 0.2em; 1193 - color: #00d4aa; 1194 - margin-bottom: 0.5rem; 1195 - } 1196 - .sc-container.sc-dark .sc-mobile-step h2 { 1197 - font-family: 'Outfit', sans-serif; 1198 - font-size: 1.3rem; 1199 - color: #e8e6e3; 1200 - margin: 0 0 0.75rem; 1201 - } 1202 - .sc-container.sc-dark .sc-mobile-step p { 1203 - font-family: 'Outfit', sans-serif; 1204 - font-size: 0.95rem; 1205 - color: rgba(232,230,227,0.7); 1206 - line-height: 1.6; 1207 - margin: 0 0 1rem; 1208 - } 1209 - .sc-container.sc-dark .sc-diff-block { 1210 - background: #0d1117; 1211 - border-radius: 8px; 1212 - padding: 0.75rem; 1213 - overflow-x: auto; 1214 - font-family: 'JetBrains Mono', monospace; 1215 - font-size: 0.8rem; 1216 - line-height: 1.5; 1217 - border: 1px solid rgba(0,212,170,0.15); 1218 - } 1219 - .sc-container.sc-dark .sc-diff-line { padding: 1px 0.5rem; white-space: pre; } 1220 - .sc-container.sc-dark .sc-diff-added { background: rgba(0, 212, 170, 0.12); border-left: 3px solid #00d4aa; } 1221 - .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; } 1222 - .sc-container.sc-dark .sc-diff-same { opacity: 0.4; } 1223 - 1224 - /* Playground */ 1225 - .sc-container.sc-dark .sc-playground-btn { 1226 - display: inline-block; 1227 - margin-top: 0.75rem; 1228 - padding: 0.4rem 1rem; 1229 - border: 1px solid rgba(0,212,170,0.3); 1230 - border-radius: 6px; 1231 - background: transparent; 1232 - color: #00d4aa; 1233 - font-family: 'Outfit', sans-serif; 1234 - font-size: 0.85rem; 1235 - cursor: pointer; 1236 - transition: all 0.2s; 1237 - } 1238 - .sc-container.sc-dark .sc-playground-btn:hover { 1239 - background: rgba(0,212,170,0.1); 1240 - border-color: #00d4aa; 1241 - } 1242 - |} 1243 - 1244 - (** {1 Theme: Notebook} 1245 - 1246 - Clean editorial. Soft white, blue-violet accent. 1247 - Newsreader display + DM Sans body. 1248 - Vertical layout with code blocks inline but sticky. *) 1249 - 1250 - let notebook_css = 1251 - {| 1252 - .sc-container.sc-notebook { 1253 - --sc-bg: #fafbfe; 1254 - --sc-text: #1a1a2e; 1255 - --sc-text-dim: #64648a; 1256 - --sc-accent: #6366f1; 1257 - --sc-accent-soft: rgba(99, 102, 241, 0.06); 1258 - --sc-code-bg: #1e1e32; 1259 - --sc-code-text: #d1d0e0; 1260 - --sc-code-gutter: #3a3a52; 1261 - --sc-border: rgba(99, 102, 241, 0.08); 1262 - --sc-panel-radius: 16px; 1263 - font-family: 'DM Sans', sans-serif; 1264 - } 1265 - 1266 - .sc-container.sc-notebook .sc-hero { 1267 - background: var(--sc-bg); 1268 - text-align: left; 1269 - padding: 6rem 0 3rem; 1270 - max-width: 640px; 1271 - margin: 0 auto; 1272 - border-bottom: 2px solid var(--sc-accent); 1273 - position: relative; 1274 - } 1275 - 1276 - .sc-container.sc-notebook .sc-hero::after { 1277 - content: ''; 1278 - position: absolute; 1279 - bottom: -2px; 1280 - left: 0; 1281 - width: 120px; 1282 - height: 2px; 1283 - background: var(--sc-accent); 1284 - box-shadow: 0 0 16px rgba(99, 102, 241, 0.4); 1285 - } 1286 - 1287 - .sc-container.sc-notebook .sc-hero h1 { 1288 - font-family: 'Newsreader', serif; 1289 - font-size: clamp(2rem, 4vw, 2.8rem); 1290 - font-weight: 600; 1291 - color: var(--sc-text); 1292 - letter-spacing: -0.02em; 1293 - line-height: 1.15; 1294 - margin-bottom: 0.75rem; 1295 - } 1296 - 1297 - .sc-container.sc-notebook .sc-hero p { 1298 - color: var(--sc-text-dim); 1299 - font-size: 1rem; 1300 - max-width: 52ch; 1301 - line-height: 1.6; 1302 - font-weight: 400; 1303 - } 1304 - 1305 - .sc-container.sc-notebook .sc-tutorial { 1306 - display: flex; 1307 - gap: 0; 1308 - background: var(--sc-bg); 1309 - max-width: 1200px; 1310 - margin: 0 auto; 1311 - position: relative; 1312 - } 1313 - 1314 - .sc-container.sc-notebook .sc-steps-col { 1315 - flex: 1; 1316 - min-width: 0; 1317 - padding: 2rem 3rem 50vh 0; 1318 - max-width: 420px; 1319 - } 1320 - 1321 - .sc-container.sc-notebook .sc-code-col { 1322 - flex: 1; 1323 - min-width: 0; 1324 - } 1325 - 1326 - .sc-container.sc-notebook .sc-step { 1327 - min-height: 60vh; 1328 - display: flex; 1329 - flex-direction: column; 1330 - justify-content: center; 1331 - padding: 1.5rem 0; 1332 - position: relative; 1333 - } 1334 - 1335 - .sc-container.sc-notebook .sc-step::before { 1336 - content: ''; 1337 - position: absolute; 1338 - left: -1.5rem; 1339 - top: 50%; 1340 - transform: translateY(-50%); 1341 - width: 3px; 1342 - height: 0; 1343 - background: var(--sc-accent); 1344 - border-radius: 2px; 1345 - transition: height 0.4s cubic-bezier(0.22, 1, 0.36, 1); 1346 - } 1347 - 1348 - .sc-container.sc-notebook .sc-step-number { 1349 - font-family: 'DM Sans', sans-serif; 1350 - font-size: 0.68rem; 1351 - font-weight: 700; 1352 - letter-spacing: 0.12em; 1353 - color: var(--sc-accent); 1354 - text-transform: uppercase; 1355 - margin-bottom: 0.5rem; 1356 - display: flex; 1357 - align-items: center; 1358 - gap: 0.5rem; 1359 - } 1360 - 1361 - .sc-container.sc-notebook .sc-step h2 { 1362 - font-family: 'Newsreader', serif; 1363 - font-size: 1.3rem; 1364 - font-weight: 600; 1365 - color: var(--sc-text); 1366 - letter-spacing: -0.01em; 1367 - margin-bottom: 0.6rem; 1368 - line-height: 1.3; 1369 - } 1370 - 1371 - .sc-container.sc-notebook .sc-step p { 1372 - color: var(--sc-text-dim); 1373 - font-size: 0.88rem; 1374 - line-height: 1.7; 1375 - max-width: 42ch; 1376 - } 1377 - 1378 - .sc-container.sc-notebook .sc-code-panel { 1379 - position: sticky; 1380 - top: 8vh; 1381 - height: 84vh; 1382 - margin: 0 0 0 2rem; 1383 - background: var(--sc-code-bg); 1384 - border-radius: var(--sc-panel-radius); 1385 - overflow: hidden; 1386 - display: flex; 1387 - flex-direction: column; 1388 - box-shadow: 1389 - 0 24px 80px rgba(30, 30, 50, 0.15), 1390 - 0 0 0 1px rgba(99, 102, 241, 0.08); 1391 - } 1392 - 1393 - .sc-container.sc-notebook .sc-code-header { 1394 - display: flex; 1395 - align-items: center; 1396 - padding: 0.75rem 1.25rem; 1397 - background: rgba(99, 102, 241, 0.04); 1398 - border-bottom: 1px solid rgba(255,255,255,0.04); 1399 - gap: 0.75rem; 1400 - } 1401 - 1402 - .sc-container.sc-notebook .sc-dots { 1403 - display: flex; 1404 - gap: 5px; 1405 - } 1406 - 1407 - .sc-container.sc-notebook .sc-dots span { 1408 - width: 9px; 1409 - height: 9px; 1410 - border-radius: 50%; 1411 - background: rgba(255,255,255,0.08); 1412 - } 1413 - 1414 - .sc-container.sc-notebook .sc-filename { 1415 - font-family: 'DM Mono', monospace; 1416 - font-size: 0.7rem; 1417 - color: rgba(255,255,255,0.3); 1418 - letter-spacing: 0.04em; 1419 - flex: 1; 1420 - text-align: center; 1421 - } 1422 - 1423 - .sc-container.sc-notebook .sc-step-badge { 1424 - font-family: 'DM Mono', monospace; 1425 - font-size: 0.6rem; 1426 - color: var(--sc-accent); 1427 - letter-spacing: 0.06em; 1428 - } 1429 - 1430 - .sc-container.sc-notebook .sc-code-body { 1431 - flex: 1; 1432 - overflow-y: auto; 1433 - padding: 1.25rem 0; 1434 - font-family: 'DM Mono', 'Source Code Pro', monospace; 1435 - font-size: 0.78rem; 1436 - line-height: 1.75; 1437 - color: var(--sc-code-text); 1438 - } 1439 - 1440 - .sc-container.sc-notebook .sc-line { 1441 - padding: 0 1.25rem; 1442 - white-space: pre; 1443 - transition: opacity 0.3s ease; 1444 - opacity: 0.3; 1445 - } 1446 - 1447 - .sc-container.sc-notebook .sc-line.sc-focused { 1448 - opacity: 1; 1449 - background: rgba(99, 102, 241, 0.05); 1450 - } 1451 - 1452 - .sc-container.sc-notebook .sc-line-number { 1453 - display: inline-block; 1454 - width: 3ch; 1455 - text-align: right; 1456 - margin-right: 1.5ch; 1457 - color: var(--sc-code-gutter); 1458 - user-select: none; 1459 - } 1460 - 1461 - /* Syntax highlighting — cool tones */ 1462 - .sc-container.sc-notebook .hl-keyword { color: #a78bfa; font-weight: 500; } 1463 - .sc-container.sc-notebook .hl-type { color: #67e8f9; } 1464 - .sc-container.sc-notebook .hl-string { color: #86efac; } 1465 - .sc-container.sc-notebook .hl-comment { color: #4a4a62; font-style: italic; } 1466 - .sc-container.sc-notebook .hl-number { color: #fde68a; } 1467 - .sc-container.sc-notebook .hl-module { color: #f9a8d4; } 1468 - .sc-container.sc-notebook .hl-operator { color: #93c5fd; } 1469 - .sc-container.sc-notebook .hl-punct { color: #4a4a62; } 1470 - 1471 - /* Progress pips */ 1472 - .sc-container.sc-notebook .sc-progress { 1473 - position: fixed; 1474 - left: 2rem; 1475 - top: 50%; 1476 - transform: translateY(-50%); 1477 - display: flex; 1478 - flex-direction: column; 1479 - gap: 6px; 1480 - z-index: 100; 1481 - } 1482 - 1483 - .sc-container.sc-notebook .sc-pip { 1484 - width: 8px; 1485 - height: 8px; 1486 - border-radius: 3px; 1487 - background: var(--sc-border); 1488 - transition: all 0.3s ease; 1489 - } 1490 - 1491 - .sc-container.sc-notebook .sc-pip.sc-active { 1492 - background: var(--sc-accent); 1493 - box-shadow: 0 0 10px rgba(99, 102, 241, 0.4); 1494 - border-radius: 2px; 1495 - width: 8px; 1496 - height: 16px; 1497 - } 1498 - 1499 - /* Animations */ 1500 - .sc-container.sc-notebook .sc-line.sc-exiting { 1501 - animation: sc-line-exit 0.2s cubic-bezier(0.22, 1, 0.36, 1) forwards; 1502 - } 1503 - 1504 - .sc-container.sc-notebook .sc-line.sc-entering { 1505 - animation: sc-line-enter 0.25s cubic-bezier(0.22, 1, 0.36, 1) both; 1506 - } 1507 - 1508 - .sc-code-slot { display: none; } 1509 - 1510 - /* Mobile responsive */ 1511 - @media (max-width: 700px) { 1512 - .sc-container.sc-notebook { padding: 0 1rem; } 1513 - .sc-container.sc-notebook .sc-desktop { display: none !important; } 1514 - .sc-container.sc-notebook .sc-mobile { display: block !important; } 1515 - .sc-container.sc-notebook .sc-progress { display: none; } 1516 - .sc-container.sc-notebook .sc-hero h1 { font-size: 2rem; } 1517 - } 1518 - @media (min-width: 701px) { 1519 - .sc-container.sc-notebook .sc-mobile { display: none !important; } 1520 - } 1521 - .sc-container.sc-notebook .sc-mobile-step { 1522 - margin: 1.5rem 0; 1523 - padding: 1.5rem; 1524 - border-radius: 6px; 1525 - background: #ffffff; 1526 - border: 1px solid #e0ddd8; 1527 - } 1528 - .sc-container.sc-notebook .sc-mobile-step-num { 1529 - font-family: 'DM Sans', sans-serif; 1530 - font-size: 0.7rem; 1531 - text-transform: uppercase; 1532 - letter-spacing: 0.15em; 1533 - color: #0066cc; 1534 - font-weight: 600; 1535 - margin-bottom: 0.5rem; 1536 - } 1537 - .sc-container.sc-notebook .sc-mobile-step h2 { 1538 - font-family: 'Newsreader', serif; 1539 - font-size: 1.3rem; 1540 - color: #1a1a1a; 1541 - margin: 0 0 0.75rem; 1542 - } 1543 - .sc-container.sc-notebook .sc-mobile-step p { 1544 - font-family: 'DM Sans', sans-serif; 1545 - font-size: 0.95rem; 1546 - color: #4a4a4a; 1547 - line-height: 1.6; 1548 - margin: 0 0 1rem; 1549 - } 1550 - .sc-container.sc-notebook .sc-diff-block { 1551 - background: #282c34; 1552 - border-radius: 6px; 1553 - padding: 0.75rem; 1554 - overflow-x: auto; 1555 - font-family: 'IBM Plex Mono', monospace; 1556 - font-size: 0.8rem; 1557 - line-height: 1.5; 1558 - border: 1px solid #e0ddd8; 1559 - } 1560 - .sc-container.sc-notebook .sc-diff-line { padding: 1px 0.5rem; white-space: pre; } 1561 - .sc-container.sc-notebook .sc-diff-added { background: rgba(0, 102, 204, 0.12); border-left: 3px solid #0066cc; } 1562 - .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; } 1563 - .sc-container.sc-notebook .sc-diff-same { opacity: 0.4; } 1564 - 1565 - /* Playground */ 1566 - .sc-container.sc-notebook .sc-playground-btn { 1567 - display: inline-block; 1568 - margin-top: 0.75rem; 1569 - padding: 0.4rem 1rem; 1570 - border: 1px solid rgba(0,102,204,0.3); 1571 - border-radius: 6px; 1572 - background: transparent; 1573 - color: #0066cc; 1574 - font-family: 'DM Sans', sans-serif; 1575 - font-size: 0.85rem; 1576 - cursor: pointer; 1577 - transition: all 0.2s; 1578 - } 1579 - .sc-container.sc-notebook .sc-playground-btn:hover { 1580 - background: rgba(0,102,204,0.1); 1581 - border-color: #0066cc; 1582 - } 1583 - |} 1584 - 1585 - (** {1 CSS to hide odoc chrome} 1586 - 1587 - When a scrollycode block is rendered, we want it to take over 1588 - the page. This CSS hides the odoc navigation, breadcrumbs, etc. *) 1589 - 1590 - let chrome_override_css = 1591 - {| 1592 - /* Override odoc page chrome for scrollycode pages */ 1593 - .odoc-nav, .odoc-tocs, .odoc-search { display: none !important; } 1594 - .odoc-preamble > h1, .odoc-preamble > h2, .odoc-preamble > h3 { display: none !important; } 1595 - .at-tags > li > .at-tag { display: none !important; } 1596 - .odoc-preamble, .odoc-content { 1597 - max-width: none !important; 1598 - padding: 0 !important; 1599 - margin: 0 !important; 1600 - display: block !important; 1601 - } 1602 - .at-tags { 1603 - list-style: none !important; 1604 - padding: 0 !important; 1605 - margin: 0 !important; 1606 - } 1607 - .at-tags > li { 1608 - display: block !important; 1609 - margin: 0 !important; 1610 - padding: 0 !important; 1611 - } 1612 - body.odoc, .odoc { 1613 - padding: 0 !important; 1614 - margin: 0 !important; 1615 - max-width: none !important; 1616 - background: inherit; 1617 - } 1618 - |} 1619 - 1620 - (** {1 Google Fonts links} *) 1621 - 1622 - let warm_fonts = 1623 - {|<link rel="preconnect" href="https://fonts.googleapis.com"> 1624 - <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> 1625 - <link href="https://fonts.googleapis.com/css2?family=Fraunces:ital,opsz,wght@0,9..144,300..900;1,9..144,300..900&family=Source+Code+Pro:ital,wght@0,300..900;1,300..900&family=Source+Serif+4:ital,opsz,wght@0,8..60,300..900;1,8..60,300..900&display=swap" rel="stylesheet">|} 1626 - 1627 - let dark_fonts = 1628 - {|<link rel="preconnect" href="https://fonts.googleapis.com"> 1629 - <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> 1630 - <link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@300..800&family=Outfit:wght@300..900&display=swap" rel="stylesheet">|} 1631 - 1632 - let notebook_fonts = 1633 - {|<link rel="preconnect" href="https://fonts.googleapis.com"> 1634 - <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> 1635 - <link href="https://fonts.googleapis.com/css2?family=DM+Mono:wght@300;400;500&family=DM+Sans:ital,opsz,wght@0,9..40,300..900;1,9..40,300..900&family=Newsreader:ital,opsz,wght@0,6..72,300..800;1,6..72,300..800&display=swap" rel="stylesheet">|} 1636 - 1637 544 (** {1 HTML Generation} *) 1638 545 1639 546 (** Generate the code lines HTML for a step's code slot *) ··· 1687 594 Buffer.add_string buf "</div>\n"; 1688 595 Buffer.contents buf 1689 596 1690 - (** Generate the full scrollycode HTML for a given theme *) 1691 - let generate_html ~theme ~title ~filename steps = 1692 - let theme_class, fonts, css = 1693 - match theme with 1694 - | "warm" -> ("sc-warm", warm_fonts, warm_css) 1695 - | "dark" -> ("sc-dark", dark_fonts, dark_css) 1696 - | "notebook" -> ("sc-notebook", notebook_fonts, notebook_css) 1697 - | _ -> ("sc-warm", warm_fonts, warm_css) 1698 - in 597 + (** Generate the full scrollycode HTML. 598 + Theme styling is handled externally via CSS — this produces 599 + theme-agnostic semantic HTML. *) 600 + let generate_html ~title ~filename steps = 1699 601 let buf = Buffer.create 16384 in 1700 602 1701 - (* Fonts *) 1702 - Buffer.add_string buf fonts; 1703 - Buffer.add_char buf '\n'; 1704 - 1705 - (* CSS *) 1706 - Buffer.add_string buf "<style>\n"; 1707 - Buffer.add_string buf chrome_override_css; 1708 - Buffer.add_string buf css; 1709 - Buffer.add_string buf "</style>\n"; 1710 - 1711 - (* Container *) 1712 - Buffer.add_string buf 1713 - (Printf.sprintf "<div class=\"sc-container %s\">\n" theme_class); 603 + (* Container — no theme class, CSS custom properties handle theming *) 604 + Buffer.add_string buf "<div class=\"sc-container\">\n"; 1714 605 1715 606 (* Hero *) 1716 607 Buffer.add_string buf "<div class=\"sc-hero\">\n"; ··· 1817 708 module Scrolly : Odoc_extension_api.Extension = struct 1818 709 let prefix = "scrolly" 1819 710 1820 - let to_document ~tag content = 1821 - (* Extract theme from tag: scrolly.warm, scrolly.dark, scrolly.notebook *) 1822 - let theme = 1823 - match String.index_opt tag '.' with 1824 - | None -> "warm" 1825 - | Some i -> String.sub tag (i + 1) (String.length tag - i - 1) 1826 - in 711 + let to_document ~tag:_ content = 1827 712 let tutorial_title, steps = extract_steps content in 1828 - let filename = 1829 - match theme with 1830 - | "dark" -> "main.ml" 1831 - | "notebook" -> "test.ml" 1832 - | _ -> "parser.ml" 1833 - in 1834 - let html = generate_html ~theme ~title:tutorial_title ~filename steps in 713 + let filename = "main.ml" in 714 + let html = generate_html ~title:tutorial_title ~filename steps in 1835 715 let block : Block.t = 1836 716 [ 1837 717 { ··· 1840 720 }; 1841 721 ] 1842 722 in 1843 - { Odoc_extension_api.content = block; overrides = []; resources = []; assets = [] } 723 + { 724 + Odoc_extension_api.content = block; 725 + overrides = []; 726 + resources = [ 727 + Css_url "extensions/scrollycode.css"; 728 + ]; 729 + assets = []; 730 + } 1844 731 end 1845 732 1846 - let () = Odoc_extension_api.Registry.register (module Scrolly) 733 + (* Register extension and structural CSS support file. 734 + Force-link Scrollycode_themes to ensure theme support files are registered. *) 735 + let () = 736 + ignore (Scrollycode_themes.warm_css : string); 737 + Odoc_extension_api.Registry.register (module Scrolly); 738 + Odoc_extension_api.Registry.register_support_file ~prefix:"scrolly" { 739 + filename = "extensions/scrollycode.css"; 740 + content = Scrollycode_css.structural_css; 741 + }
+529
src/scrollycode_themes.ml
··· 1 + (** Theme CSS files for the scrollycode extension. 2 + 3 + Each theme sets the CSS custom property values defined in 4 + {!Scrollycode_css.structural_css} and imports its Google Fonts. *) 5 + 6 + (** Warm Workshop theme. 7 + Cream background, burnt sienna accents, Fraunces + Source Serif 4. *) 8 + let warm_css = 9 + {|@import url('https://fonts.googleapis.com/css2?family=Fraunces:ital,opsz,wght@0,9..144,300..900;1,9..144,300..900&family=Source+Code+Pro:ital,wght@0,300..900;1,300..900&family=Source+Serif+4:ital,opsz,wght@0,8..60,300..900;1,8..60,300..900&display=swap'); 10 + 11 + .sc-container { 12 + /* Typography */ 13 + --sc-font-display: 'Fraunces', serif; 14 + --sc-font-body: 'Source Serif 4', Georgia, serif; 15 + --sc-font-code: 'Source Code Pro', monospace; 16 + 17 + /* Colors */ 18 + --sc-bg: #f5f0e6; 19 + --sc-text: #2c2416; 20 + --sc-text-dim: #8a7c6a; 21 + --sc-accent: #c25832; 22 + --sc-accent-soft: rgba(194, 88, 50, 0.08); 23 + --sc-code-bg: #1a1a2e; 24 + --sc-code-text: #d4d0c8; 25 + --sc-code-gutter: #3a3a52; 26 + --sc-border: rgba(44, 36, 22, 0.1); 27 + --sc-focus-bg: rgba(194, 88, 50, 0.06); 28 + --sc-panel-radius: 12px; 29 + 30 + /* Syntax highlighting */ 31 + --sc-hl-keyword: #f0a6a0; 32 + --sc-hl-type: #8ec8e8; 33 + --sc-hl-string: #b8d89a; 34 + --sc-hl-comment: #6a6a82; 35 + --sc-hl-number: #ddb97a; 36 + --sc-hl-module: #e8c87a; 37 + --sc-hl-operator: #c8a8d8; 38 + --sc-hl-punct: #7a7a92; 39 + 40 + /* Mobile */ 41 + --sc-mobile-step-bg: rgba(255,255,255,0.5); 42 + } 43 + 44 + /* Warm hero: centered */ 45 + .sc-container .sc-hero { 46 + text-align: center; 47 + border-bottom: 1px solid var(--sc-border); 48 + } 49 + 50 + .sc-container .sc-hero h1 { 51 + font-style: italic; 52 + } 53 + 54 + .sc-container .sc-hero p { 55 + margin: 0 auto; 56 + } 57 + 58 + /* Warm code panel: navy shadow */ 59 + .sc-container .sc-code-panel { 60 + box-shadow: 0 20px 60px rgba(26, 26, 46, 0.3), 0 0 0 1px rgba(255,255,255,0.03) inset; 61 + } 62 + 63 + /* Warm dots: traffic light colors */ 64 + .sc-container .sc-dots span:nth-child(1) { background: #ff5f57; } 65 + .sc-container .sc-dots span:nth-child(2) { background: #ffbd2e; } 66 + .sc-container .sc-dots span:nth-child(3) { background: #28c840; } 67 + 68 + /* Warm diff */ 69 + .sc-container .sc-diff-block { 70 + background: #1e1b2e; 71 + } 72 + .sc-container .sc-diff-added { background: rgba(80, 200, 80, 0.15); border-left: 3px solid #4caf50; } 73 + .sc-container .sc-diff-removed { background: rgba(255, 80, 80, 0.12); border-left: 3px solid #ef5350; text-decoration: line-through; opacity: 0.7; } 74 + .sc-container .sc-diff-same { opacity: 0.5; } 75 + |} 76 + 77 + (** Dark Terminal theme. 78 + Near-black background, phosphor green and amber, JetBrains Mono + Outfit. *) 79 + let dark_css = 80 + {|@import url('https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@300..800&family=Outfit:wght@300..900&display=swap'); 81 + 82 + .sc-container { 83 + /* Typography */ 84 + --sc-font-display: 'Outfit', sans-serif; 85 + --sc-font-body: 'Outfit', sans-serif; 86 + --sc-font-code: 'JetBrains Mono', monospace; 87 + 88 + /* Colors */ 89 + --sc-bg: #0a0a0f; 90 + --sc-text: #e8e6f0; 91 + --sc-text-dim: #6e6b80; 92 + --sc-accent: #4ade80; 93 + --sc-accent-soft: rgba(74, 222, 128, 0.06); 94 + --sc-code-bg: #0f0f18; 95 + --sc-code-text: #c8c5d8; 96 + --sc-code-gutter: #2a2a3e; 97 + --sc-border: rgba(255, 255, 255, 0.06); 98 + --sc-focus-bg: rgba(74, 222, 128, 0.04); 99 + --sc-panel-radius: 0; 100 + 101 + /* Syntax highlighting — neon palette */ 102 + --sc-hl-keyword: #ff7eb3; 103 + --sc-hl-type: #7dd3fc; 104 + --sc-hl-string: #4ade80; 105 + --sc-hl-comment: #4a4a62; 106 + --sc-hl-number: #fbbf24; 107 + --sc-hl-module: #c4b5fd; 108 + --sc-hl-operator: #67e8f9; 109 + --sc-hl-punct: #4a4a62; 110 + 111 + /* Mobile */ 112 + --sc-mobile-step-bg: rgba(255,255,255,0.04); 113 + } 114 + 115 + /* Dark hero: left-aligned, larger */ 116 + .sc-container .sc-hero { 117 + text-align: left; 118 + padding: 8rem 4rem 4rem; 119 + max-width: 800px; 120 + position: relative; 121 + } 122 + 123 + .sc-container .sc-hero::before { 124 + content: ''; 125 + position: absolute; 126 + top: 0; left: 0; right: 0; bottom: 0; 127 + background: radial-gradient(ellipse at 20% 50%, rgba(74, 222, 128, 0.04) 0%, transparent 60%); 128 + pointer-events: none; 129 + } 130 + 131 + .sc-container .sc-hero h1 { 132 + font-size: clamp(2.8rem, 6vw, 4.5rem); 133 + font-weight: 800; 134 + letter-spacing: -0.04em; 135 + line-height: 1.0; 136 + margin-bottom: 1.25rem; 137 + } 138 + 139 + .sc-container .sc-hero h1 em { 140 + font-style: normal; 141 + color: var(--sc-accent); 142 + } 143 + 144 + .sc-container .sc-hero p { 145 + max-width: 50ch; 146 + font-weight: 300; 147 + } 148 + 149 + /* Dark layout: narrow prose, wide code */ 150 + .sc-container .sc-steps-col { 151 + width: 38%; 152 + flex: none; 153 + padding: 2rem 2.5rem 50vh 4rem; 154 + border-right: 1px solid var(--sc-border); 155 + } 156 + 157 + .sc-container .sc-code-col { 158 + flex: 1; 159 + width: auto; 160 + } 161 + 162 + /* Dark step number: with line */ 163 + .sc-container .sc-step-number { 164 + font-weight: 700; 165 + letter-spacing: 0.15em; 166 + display: flex; 167 + align-items: center; 168 + gap: 0.75rem; 169 + } 170 + 171 + .sc-container .sc-step-number::after { 172 + content: ''; 173 + flex: 1; 174 + height: 1px; 175 + background: var(--sc-border); 176 + } 177 + 178 + .sc-container .sc-step h2 { 179 + font-size: 1.4rem; 180 + line-height: 1.2; 181 + } 182 + 183 + .sc-container .sc-step p { 184 + font-size: 0.9rem; 185 + max-width: 40ch; 186 + font-weight: 300; 187 + } 188 + 189 + /* Dark code panel: full viewport height */ 190 + .sc-container .sc-code-panel { 191 + top: 0; 192 + height: 100vh; 193 + margin: 0; 194 + border-radius: 0; 195 + border-left: 1px solid var(--sc-border); 196 + box-shadow: none; 197 + } 198 + 199 + .sc-container .sc-code-header { 200 + padding: 1rem 1.5rem; 201 + border-bottom: 1px solid var(--sc-border); 202 + gap: 1rem; 203 + } 204 + 205 + .sc-container .sc-dots span { 206 + width: 8px; 207 + height: 8px; 208 + background: var(--sc-code-gutter); 209 + } 210 + 211 + .sc-container .sc-filename { 212 + color: var(--sc-text-dim); 213 + text-align: left; 214 + } 215 + 216 + .sc-container .sc-step-badge { 217 + color: var(--sc-accent); 218 + background: rgba(74, 222, 128, 0.08); 219 + padding: 0.25em 0.75em; 220 + border-radius: 3px; 221 + } 222 + 223 + .sc-container .sc-code-body { 224 + padding: 1.5rem 0; 225 + line-height: 1.75; 226 + } 227 + 228 + .sc-container .sc-line { 229 + padding: 0 1.5rem; 230 + transition: opacity 0.3s ease, background 0.3s ease; 231 + opacity: 0.25; 232 + } 233 + 234 + .sc-container .sc-line.sc-focused { 235 + border-left: 2px solid var(--sc-accent); 236 + padding-left: calc(1.5rem - 2px); 237 + } 238 + 239 + .sc-container .sc-line-number { 240 + margin-right: 2ch; 241 + } 242 + 243 + /* Dark progress: right side, bar style */ 244 + .sc-container .sc-progress { 245 + left: auto; 246 + right: 1.5rem; 247 + gap: 10px; 248 + } 249 + 250 + .sc-container .sc-pip { 251 + width: 3px; 252 + height: 20px; 253 + border-radius: 2px; 254 + } 255 + 256 + .sc-container .sc-pip.sc-active { 257 + box-shadow: 0 0 12px rgba(74, 222, 128, 0.5); 258 + height: 30px; 259 + transform: none; 260 + } 261 + 262 + /* Dark mobile */ 263 + .sc-container .sc-mobile-step { 264 + border-radius: 10px; 265 + border: 1px solid rgba(255,255,255,0.08); 266 + } 267 + 268 + .sc-container .sc-mobile-step-num { 269 + font-family: var(--sc-font-body); 270 + letter-spacing: 0.2em; 271 + color: #00d4aa; 272 + } 273 + 274 + .sc-container .sc-mobile-step h2 { 275 + color: #e8e6e3; 276 + } 277 + 278 + .sc-container .sc-mobile-step p { 279 + color: rgba(232,230,227,0.7); 280 + } 281 + 282 + .sc-container .sc-diff-block { 283 + background: #0d1117; 284 + border: 1px solid rgba(0,212,170,0.15); 285 + } 286 + .sc-container .sc-diff-added { background: rgba(0, 212, 170, 0.12); border-left: 3px solid #00d4aa; } 287 + .sc-container .sc-diff-removed { background: rgba(255, 80, 80, 0.1); border-left: 3px solid #ff6b6b; text-decoration: line-through; opacity: 0.6; } 288 + .sc-container .sc-diff-same { opacity: 0.4; } 289 + 290 + .sc-container .sc-playground-btn { 291 + border-color: rgba(0,212,170,0.3); 292 + color: #00d4aa; 293 + } 294 + .sc-container .sc-playground-btn:hover { 295 + background: rgba(0,212,170,0.1); 296 + border-color: #00d4aa; 297 + } 298 + |} 299 + 300 + (** Notebook theme. 301 + Soft white, blue-violet accent, Newsreader + DM Sans. *) 302 + let notebook_css = 303 + {|@import url('https://fonts.googleapis.com/css2?family=DM+Mono:wght@300;400;500&family=DM+Sans:ital,opsz,wght@0,9..40,300..900;1,9..40,300..900&family=Newsreader:ital,opsz,wght@0,6..72,300..800;1,6..72,300..800&display=swap'); 304 + 305 + .sc-container { 306 + /* Typography */ 307 + --sc-font-display: 'Newsreader', serif; 308 + --sc-font-body: 'DM Sans', sans-serif; 309 + --sc-font-code: 'DM Mono', 'Source Code Pro', monospace; 310 + 311 + /* Colors */ 312 + --sc-bg: #fafbfe; 313 + --sc-text: #1a1a2e; 314 + --sc-text-dim: #64648a; 315 + --sc-accent: #6366f1; 316 + --sc-accent-soft: rgba(99, 102, 241, 0.06); 317 + --sc-code-bg: #1e1e32; 318 + --sc-code-text: #d1d0e0; 319 + --sc-code-gutter: #3a3a52; 320 + --sc-border: rgba(99, 102, 241, 0.08); 321 + --sc-focus-bg: rgba(99, 102, 241, 0.05); 322 + --sc-panel-radius: 16px; 323 + 324 + /* Syntax highlighting — cool tones */ 325 + --sc-hl-keyword: #a78bfa; 326 + --sc-hl-type: #67e8f9; 327 + --sc-hl-string: #86efac; 328 + --sc-hl-comment: #4a4a62; 329 + --sc-hl-number: #fde68a; 330 + --sc-hl-module: #f9a8d4; 331 + --sc-hl-operator: #93c5fd; 332 + --sc-hl-punct: #4a4a62; 333 + 334 + /* Mobile */ 335 + --sc-mobile-step-bg: #ffffff; 336 + } 337 + 338 + /* Notebook hero: left-aligned, accent underline */ 339 + .sc-container .sc-hero { 340 + text-align: left; 341 + padding: 6rem 0 3rem; 342 + max-width: 640px; 343 + margin: 0 auto; 344 + border-bottom: 2px solid var(--sc-accent); 345 + position: relative; 346 + } 347 + 348 + .sc-container .sc-hero::after { 349 + content: ''; 350 + position: absolute; 351 + bottom: -2px; left: 0; 352 + width: 120px; 353 + height: 2px; 354 + background: var(--sc-accent); 355 + box-shadow: 0 0 16px rgba(99, 102, 241, 0.4); 356 + } 357 + 358 + .sc-container .sc-hero h1 { 359 + font-size: clamp(2rem, 4vw, 2.8rem); 360 + font-weight: 600; 361 + letter-spacing: -0.02em; 362 + line-height: 1.15; 363 + } 364 + 365 + .sc-container .sc-hero p { 366 + font-size: 1rem; 367 + max-width: 52ch; 368 + font-weight: 400; 369 + } 370 + 371 + /* Notebook layout: constrained width */ 372 + .sc-container .sc-tutorial { 373 + max-width: 1200px; 374 + margin: 0 auto; 375 + } 376 + 377 + .sc-container .sc-steps-col { 378 + padding: 2rem 3rem 50vh 0; 379 + max-width: 420px; 380 + } 381 + 382 + .sc-container .sc-code-col { 383 + flex: 1; 384 + width: auto; 385 + } 386 + 387 + /* Notebook step: accent line indicator */ 388 + .sc-container .sc-step { 389 + min-height: 60vh; 390 + padding: 1.5rem 0; 391 + position: relative; 392 + } 393 + 394 + .sc-container .sc-step::before { 395 + content: ''; 396 + position: absolute; 397 + left: -1.5rem; 398 + top: 50%; 399 + transform: translateY(-50%); 400 + width: 3px; 401 + height: 0; 402 + background: var(--sc-accent); 403 + border-radius: 2px; 404 + transition: height 0.4s cubic-bezier(0.22, 1, 0.36, 1); 405 + } 406 + 407 + .sc-container .sc-step-number { 408 + font-family: var(--sc-font-body); 409 + font-weight: 700; 410 + letter-spacing: 0.12em; 411 + display: flex; 412 + align-items: center; 413 + gap: 0.5rem; 414 + } 415 + 416 + .sc-container .sc-step h2 { 417 + font-size: 1.3rem; 418 + font-weight: 600; 419 + letter-spacing: -0.01em; 420 + margin-bottom: 0.6rem; 421 + line-height: 1.3; 422 + } 423 + 424 + .sc-container .sc-step p { 425 + font-size: 0.88rem; 426 + max-width: 42ch; 427 + } 428 + 429 + /* Notebook code panel */ 430 + .sc-container .sc-code-panel { 431 + top: 8vh; 432 + height: 84vh; 433 + margin: 0 0 0 2rem; 434 + box-shadow: 435 + 0 24px 80px rgba(30, 30, 50, 0.15), 436 + 0 0 0 1px rgba(99, 102, 241, 0.08); 437 + } 438 + 439 + .sc-container .sc-code-header { 440 + background: rgba(99, 102, 241, 0.04); 441 + border-bottom: 1px solid rgba(255,255,255,0.04); 442 + gap: 0.75rem; 443 + } 444 + 445 + .sc-container .sc-dots span { 446 + width: 9px; 447 + height: 9px; 448 + background: rgba(255,255,255,0.08); 449 + } 450 + 451 + .sc-container .sc-code-body { 452 + font-size: 0.78rem; 453 + line-height: 1.75; 454 + } 455 + 456 + .sc-container .sc-line { 457 + opacity: 0.3; 458 + } 459 + 460 + /* Notebook progress: left side, square pips */ 461 + .sc-container .sc-progress { 462 + left: 2rem; 463 + gap: 6px; 464 + } 465 + 466 + .sc-container .sc-pip { 467 + width: 8px; 468 + height: 8px; 469 + border-radius: 3px; 470 + } 471 + 472 + .sc-container .sc-pip.sc-active { 473 + box-shadow: 0 0 10px rgba(99, 102, 241, 0.4); 474 + border-radius: 2px; 475 + width: 8px; 476 + height: 16px; 477 + transform: none; 478 + } 479 + 480 + /* Notebook mobile */ 481 + .sc-container .sc-mobile-step { 482 + border-radius: 6px; 483 + border: 1px solid #e0ddd8; 484 + } 485 + 486 + .sc-container .sc-mobile-step-num { 487 + font-family: var(--sc-font-body); 488 + font-weight: 600; 489 + color: #0066cc; 490 + } 491 + 492 + .sc-container .sc-mobile-step h2 { 493 + color: #1a1a1a; 494 + } 495 + 496 + .sc-container .sc-mobile-step p { 497 + color: #4a4a4a; 498 + } 499 + 500 + .sc-container .sc-diff-block { 501 + background: #282c34; 502 + border: 1px solid #e0ddd8; 503 + font-family: 'IBM Plex Mono', monospace; 504 + } 505 + .sc-container .sc-diff-added { background: rgba(0, 102, 204, 0.12); border-left: 3px solid #0066cc; } 506 + .sc-container .sc-diff-removed { background: rgba(220, 50, 50, 0.1); border-left: 3px solid #dc3232; text-decoration: line-through; opacity: 0.6; } 507 + .sc-container .sc-diff-same { opacity: 0.4; } 508 + 509 + .sc-container .sc-playground-btn { 510 + border-color: rgba(0,102,204,0.3); 511 + color: #0066cc; 512 + } 513 + .sc-container .sc-playground-btn:hover { 514 + background: rgba(0,102,204,0.1); 515 + border-color: #0066cc; 516 + } 517 + |} 518 + 519 + (** Register all theme CSS files as support files *) 520 + let () = 521 + let register name content = 522 + Odoc_extension_api.Registry.register_support_file ~prefix:"scrolly" { 523 + filename = "extensions/scrollycode-" ^ name ^ ".css"; 524 + content; 525 + } 526 + in 527 + register "warm" warm_css; 528 + register "dark" dark_css; 529 + register "notebook" notebook_css
+1 -1
test/dark_repl.mld
··· 1 1 {0 Building a REPL} 2 2 3 - @scrolly.dark Building a REPL in OCaml 3 + @scrolly Building a REPL in OCaml 4 4 {ol 5 5 {li 6 6 {b The Expression Type}
+1 -1
test/notebook_testing.mld
··· 1 1 {0 Building a Test Framework} 2 2 3 - @scrolly.notebook Building a Test Framework in OCaml 3 + @scrolly Building a Test Framework in OCaml 4 4 {ol 5 5 {li 6 6 {b A Single Assertion}
+1 -1
test/warm_parser.mld
··· 1 1 {0 Building a JSON Parser} 2 2 3 - @scrolly.warm Building a JSON Parser in OCaml 3 + @scrolly Building a JSON Parser in OCaml 4 4 {ol 5 5 {li 6 6 {b Defining the Value Type}