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 mobile responsive stacked layout with diff view

- Add LCS-based line diff computation between consecutive steps
- Generate stacked mobile layout with colored diffs (green=added, red=removed)
- Add responsive CSS to all three themes (warm, dark, notebook)
- Desktop: hide mobile layout; Mobile (<=700px): hide desktop, show stacked diffs
- Each theme uses its own color palette for diff highlighting

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

+254 -1
+254 -1
odoc-scrollycode-extension/src/scrollycode_extension.ml
··· 149 149 s; 150 150 Buffer.contents buf 151 151 152 + (** {1 Diff Computation} *) 153 + 154 + type diff_line = 155 + | Same of string 156 + | Added of string 157 + | Removed of string 158 + 159 + (** Simple LCS-based line diff between two code strings *) 160 + let diff_lines old_code new_code = 161 + let old_lines = String.split_on_char '\n' old_code |> Array.of_list in 162 + let new_lines = String.split_on_char '\n' new_code |> Array.of_list in 163 + let n = Array.length old_lines in 164 + let m = Array.length new_lines in 165 + let dp = Array.make_matrix (n + 1) (m + 1) 0 in 166 + for i = 1 to n do 167 + for j = 1 to m do 168 + if old_lines.(i-1) = new_lines.(j-1) then 169 + dp.(i).(j) <- dp.(i-1).(j-1) + 1 170 + else 171 + dp.(i).(j) <- max dp.(i-1).(j) dp.(i).(j-1) 172 + done 173 + done; 174 + let result = ref [] in 175 + let i = ref n and j = ref m in 176 + while !i > 0 || !j > 0 do 177 + if !i > 0 && !j > 0 && old_lines.(!i-1) = new_lines.(!j-1) then begin 178 + result := Same old_lines.(!i-1) :: !result; 179 + decr i; decr j 180 + end else if !j > 0 && (!i = 0 || dp.(!i).(!j-1) >= dp.(!i-1).(!j)) then begin 181 + result := Added new_lines.(!j-1) :: !result; 182 + decr j 183 + end else begin 184 + result := Removed old_lines.(!i-1) :: !result; 185 + decr i 186 + end 187 + done; 188 + !result 189 + 152 190 (** {1 OCaml Syntax Highlighting} 153 191 154 192 A simple lexer-based highlighter for OCaml code. Produces HTML spans ··· 317 355 Buffer.add_string buf (html_escape (String.make 1 (current ()))); 318 356 advance () 319 357 done; 358 + Buffer.contents buf 359 + 360 + (** Render a diff as HTML with colored lines *) 361 + let render_diff_html diff = 362 + let buf = Buffer.create 1024 in 363 + List.iter (fun line -> 364 + match line with 365 + | Same s -> 366 + Buffer.add_string buf 367 + (Printf.sprintf "<div class=\"sc-diff-line sc-diff-same\">%s</div>\n" 368 + (highlight_ocaml s)) 369 + | Added s -> 370 + Buffer.add_string buf 371 + (Printf.sprintf "<div class=\"sc-diff-line sc-diff-added\">%s</div>\n" 372 + (highlight_ocaml s)) 373 + | Removed s -> 374 + Buffer.add_string buf 375 + (Printf.sprintf "<div class=\"sc-diff-line sc-diff-removed\">%s</div>\n" 376 + (highlight_ocaml s))) 377 + diff; 320 378 Buffer.contents buf 321 379 322 380 (** {1 Shared JavaScript} ··· 663 721 664 722 /* Hidden code slot */ 665 723 .sc-code-slot { display: none; } 724 + 725 + /* Mobile responsive */ 726 + @media (max-width: 700px) { 727 + .sc-container.sc-warm .sc-desktop { display: none !important; } 728 + .sc-container.sc-warm .sc-mobile { display: block !important; } 729 + .sc-container.sc-warm .sc-progress { display: none; } 730 + .sc-container.sc-warm .sc-hero h1 { font-size: 2rem; } 731 + } 732 + @media (min-width: 701px) { 733 + .sc-container.sc-warm .sc-mobile { display: none !important; } 734 + } 735 + .sc-container.sc-warm .sc-mobile-step { 736 + margin: 1.5rem 0; 737 + padding: 1.5rem; 738 + border-radius: 12px; 739 + background: rgba(255,255,255,0.5); 740 + } 741 + .sc-container.sc-warm .sc-mobile-step-num { 742 + font-family: 'Fraunces', serif; 743 + font-size: 0.75rem; 744 + text-transform: uppercase; 745 + letter-spacing: 0.15em; 746 + color: #a0785a; 747 + margin-bottom: 0.5rem; 748 + } 749 + .sc-container.sc-warm .sc-mobile-step h2 { 750 + font-family: 'Fraunces', serif; 751 + font-size: 1.3rem; 752 + color: #3a2e28; 753 + margin: 0 0 0.75rem; 754 + } 755 + .sc-container.sc-warm .sc-mobile-step p { 756 + font-family: 'Source Serif 4', serif; 757 + font-size: 1rem; 758 + color: #5a4a3a; 759 + line-height: 1.6; 760 + margin: 0 0 1rem; 761 + } 762 + .sc-container.sc-warm .sc-diff-block { 763 + background: #1e1b2e; 764 + border-radius: 8px; 765 + padding: 0.75rem; 766 + overflow-x: auto; 767 + font-family: 'Source Code Pro', monospace; 768 + font-size: 0.8rem; 769 + line-height: 1.5; 770 + } 771 + .sc-container.sc-warm .sc-diff-line { padding: 1px 0.5rem; white-space: pre; } 772 + .sc-container.sc-warm .sc-diff-added { background: rgba(80, 200, 80, 0.15); border-left: 3px solid #4caf50; } 773 + .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; } 774 + .sc-container.sc-warm .sc-diff-same { opacity: 0.5; } 666 775 |} 667 776 668 777 (** {1 Theme: Dark Terminal} ··· 922 1031 } 923 1032 924 1033 .sc-code-slot { display: none; } 1034 + 1035 + /* Mobile responsive */ 1036 + @media (max-width: 700px) { 1037 + .sc-container.sc-dark .sc-desktop { display: none !important; } 1038 + .sc-container.sc-dark .sc-mobile { display: block !important; } 1039 + .sc-container.sc-dark .sc-progress { display: none; } 1040 + .sc-container.sc-dark .sc-hero h1 { font-size: 2rem; } 1041 + } 1042 + @media (min-width: 701px) { 1043 + .sc-container.sc-dark .sc-mobile { display: none !important; } 1044 + } 1045 + .sc-container.sc-dark .sc-mobile-step { 1046 + margin: 1.5rem 0; 1047 + padding: 1.5rem; 1048 + border-radius: 10px; 1049 + background: rgba(255,255,255,0.04); 1050 + border: 1px solid rgba(255,255,255,0.08); 1051 + } 1052 + .sc-container.sc-dark .sc-mobile-step-num { 1053 + font-family: 'Outfit', sans-serif; 1054 + font-size: 0.7rem; 1055 + text-transform: uppercase; 1056 + letter-spacing: 0.2em; 1057 + color: #00d4aa; 1058 + margin-bottom: 0.5rem; 1059 + } 1060 + .sc-container.sc-dark .sc-mobile-step h2 { 1061 + font-family: 'Outfit', sans-serif; 1062 + font-size: 1.3rem; 1063 + color: #e8e6e3; 1064 + margin: 0 0 0.75rem; 1065 + } 1066 + .sc-container.sc-dark .sc-mobile-step p { 1067 + font-family: 'Outfit', sans-serif; 1068 + font-size: 0.95rem; 1069 + color: rgba(232,230,227,0.7); 1070 + line-height: 1.6; 1071 + margin: 0 0 1rem; 1072 + } 1073 + .sc-container.sc-dark .sc-diff-block { 1074 + background: #0d1117; 1075 + border-radius: 8px; 1076 + padding: 0.75rem; 1077 + overflow-x: auto; 1078 + font-family: 'JetBrains Mono', monospace; 1079 + font-size: 0.8rem; 1080 + line-height: 1.5; 1081 + border: 1px solid rgba(0,212,170,0.15); 1082 + } 1083 + .sc-container.sc-dark .sc-diff-line { padding: 1px 0.5rem; white-space: pre; } 1084 + .sc-container.sc-dark .sc-diff-added { background: rgba(0, 212, 170, 0.12); border-left: 3px solid #00d4aa; } 1085 + .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; } 1086 + .sc-container.sc-dark .sc-diff-same { opacity: 0.4; } 925 1087 |} 926 1088 927 1089 (** {1 Theme: Notebook} ··· 1189 1351 } 1190 1352 1191 1353 .sc-code-slot { display: none; } 1354 + 1355 + /* Mobile responsive */ 1356 + @media (max-width: 700px) { 1357 + .sc-container.sc-notebook .sc-desktop { display: none !important; } 1358 + .sc-container.sc-notebook .sc-mobile { display: block !important; } 1359 + .sc-container.sc-notebook .sc-progress { display: none; } 1360 + .sc-container.sc-notebook .sc-hero h1 { font-size: 2rem; } 1361 + } 1362 + @media (min-width: 701px) { 1363 + .sc-container.sc-notebook .sc-mobile { display: none !important; } 1364 + } 1365 + .sc-container.sc-notebook .sc-mobile-step { 1366 + margin: 1.5rem 0; 1367 + padding: 1.5rem; 1368 + border-radius: 6px; 1369 + background: #ffffff; 1370 + border: 1px solid #e0ddd8; 1371 + } 1372 + .sc-container.sc-notebook .sc-mobile-step-num { 1373 + font-family: 'DM Sans', sans-serif; 1374 + font-size: 0.7rem; 1375 + text-transform: uppercase; 1376 + letter-spacing: 0.15em; 1377 + color: #0066cc; 1378 + font-weight: 600; 1379 + margin-bottom: 0.5rem; 1380 + } 1381 + .sc-container.sc-notebook .sc-mobile-step h2 { 1382 + font-family: 'Newsreader', serif; 1383 + font-size: 1.3rem; 1384 + color: #1a1a1a; 1385 + margin: 0 0 0.75rem; 1386 + } 1387 + .sc-container.sc-notebook .sc-mobile-step p { 1388 + font-family: 'DM Sans', sans-serif; 1389 + font-size: 0.95rem; 1390 + color: #4a4a4a; 1391 + line-height: 1.6; 1392 + margin: 0 0 1rem; 1393 + } 1394 + .sc-container.sc-notebook .sc-diff-block { 1395 + background: #282c34; 1396 + border-radius: 6px; 1397 + padding: 0.75rem; 1398 + overflow-x: auto; 1399 + font-family: 'IBM Plex Mono', monospace; 1400 + font-size: 0.8rem; 1401 + line-height: 1.5; 1402 + border: 1px solid #e0ddd8; 1403 + } 1404 + .sc-container.sc-notebook .sc-diff-line { padding: 1px 0.5rem; white-space: pre; } 1405 + .sc-container.sc-notebook .sc-diff-added { background: rgba(0, 102, 204, 0.12); border-left: 3px solid #0066cc; } 1406 + .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; } 1407 + .sc-container.sc-notebook .sc-diff-same { opacity: 0.4; } 1192 1408 |} 1193 1409 1194 1410 (** {1 CSS to hide odoc chrome} ··· 1262 1478 lines; 1263 1479 Buffer.contents buf 1264 1480 1481 + (** Generate the mobile stacked layout with diffs between steps *) 1482 + let generate_mobile_html steps = 1483 + let buf = Buffer.create 8192 in 1484 + Buffer.add_string buf "<div class=\"sc-mobile\">\n"; 1485 + let prev_code = ref None in 1486 + List.iteri (fun i step -> 1487 + Buffer.add_string buf 1488 + (Printf.sprintf " <div class=\"sc-mobile-step\">\n"); 1489 + Buffer.add_string buf 1490 + (Printf.sprintf " <div class=\"sc-mobile-step-num\">Step %02d</div>\n" (i + 1)); 1491 + if step.title <> "" then 1492 + Buffer.add_string buf 1493 + (Printf.sprintf " <h2>%s</h2>\n" (html_escape step.title)); 1494 + if step.prose <> "" then 1495 + Buffer.add_string buf 1496 + (Printf.sprintf " <p>%s</p>\n" (html_escape step.prose)); 1497 + (* Diff block *) 1498 + Buffer.add_string buf " <div class=\"sc-diff-block\">\n"; 1499 + let diff = match !prev_code with 1500 + | None -> 1501 + List.map (fun l -> Added l) (String.split_on_char '\n' step.code) 1502 + | Some prev -> 1503 + diff_lines prev step.code 1504 + in 1505 + Buffer.add_string buf (render_diff_html diff); 1506 + Buffer.add_string buf " </div>\n"; 1507 + Buffer.add_string buf " </div>\n"; 1508 + prev_code := Some step.code) 1509 + steps; 1510 + Buffer.add_string buf "</div>\n"; 1511 + Buffer.contents buf 1512 + 1265 1513 (** Generate the full scrollycode HTML for a given theme *) 1266 1514 let generate_html ~theme ~title ~filename steps = 1267 1515 let theme_class, fonts, css = ··· 1303 1551 steps; 1304 1552 Buffer.add_string buf "</nav>\n"; 1305 1553 1306 - (* Tutorial layout *) 1554 + (* Desktop layout *) 1555 + Buffer.add_string buf "<div class=\"sc-desktop\">\n"; 1307 1556 Buffer.add_string buf "<div class=\"sc-tutorial\">\n"; 1308 1557 1309 1558 (* Steps column *) ··· 1354 1603 Buffer.add_string buf " </div>\n"; 1355 1604 1356 1605 Buffer.add_string buf "</div>\n"; 1606 + Buffer.add_string buf "</div>\n"; 1607 + 1608 + (* Mobile stacked layout *) 1609 + Buffer.add_string buf (generate_mobile_html steps); 1357 1610 1358 1611 (* JavaScript *) 1359 1612 Buffer.add_string buf "<script>\n";