1import { Controller } from "@hotwired/stimulus" 2 3// Lesson reader page. Owns: 4// - the Aâ/A+ font-scale buttons in the topbar (mutates --book-fs) 5// - the â/â¾ light/dark theme toggle 6// - timing the page (so the "Mark complete" POST can include time spent) 7// All of the actual content rendering happens server-side via 8// Courses::LessonBodyRenderer. 9export default class extends Controller { 10 static values = { 11 courseSlug: String, 12 lessonSlug: String 13 } 14 15 connect() { 16 this.startedAt = performance.now() 17 const saved = parseFloat(localStorage.getItem("courses.fontScale") || "1") 18 if (Number.isFinite(saved) && saved !== 1) { 19 this.element.style.setProperty("--book-fs", String(saved)) 20 } 21 try { 22 const savedTheme = localStorage.getItem("courses.theme") 23 if (savedTheme === "dark" || savedTheme === "light") { 24 document.body.dataset.theme = savedTheme 25 } 26 } catch (_) {} 27 } 28 29 stampTimeSpent(event) { 30 const form = event.currentTarget.closest("form") 31 if (!form) return 32 const seconds = Math.round((performance.now() - this.startedAt) / 1000) 33 const input = document.createElement("input") 34 input.type = "hidden" 35 input.name = "time_spent_seconds" 36 input.value = String(seconds) 37 form.appendChild(input) 38 } 39 40 increaseFont() { this.bumpFont(+0.05) } 41 decreaseFont() { this.bumpFont(-0.05) } 42 43 bumpFont(delta) { 44 const cur = parseFloat(getComputedStyle(this.element).getPropertyValue("--book-fs")) || 1 45 const next = Math.min(1.25, Math.max(0.85, +(cur + delta).toFixed(2))) 46 this.element.style.setProperty("--book-fs", String(next)) 47 try { localStorage.setItem("courses.fontScale", String(next)) } catch (_) {} 48 } 49 50 toggleTheme(event) { 51 // Flip `data-theme` on `<body>` so every rule reading `--book-*` rebinds 52 // in one go â see `.lb-courses-body[data-theme="dark"]` in 53 // `app/views/layouts/courses/_styles.html.erb`. 54 const next = document.body.dataset.theme === "dark" ? "light" : "dark" 55 document.body.dataset.theme = next 56 const btn = event?.currentTarget 57 if (btn) btn.textContent = next === "dark" ? "â¾" : "â" 58 try { localStorage.setItem("courses.theme", next) } catch (_) {} 59 } 60 61 toggleTutor(event) { 62 // Tutor drawer is a TODO from book.jsx â for now flip `data-on` on the 63 // button so the active styling lights up (see `.lb-iconbtn-tutor`). 64 const btn = event?.currentTarget 65 if (!btn) return 66 btn.dataset.on = btn.dataset.on === "true" ? "false" : "true" 67 } 68}
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.