diff --git a/ventures/mdecks/README.md b/ventures/mdecks/README.md index 216d974..a7aee8b 100644 --- a/ventures/mdecks/README.md +++ b/ventures/mdecks/README.md @@ -12,7 +12,8 @@ People who make a quick throwaway deck every week — sprint demos, status updat 1. Open the page and type markdown-ish text in the editor on the left. It comes preloaded with a sample deck so you can see real output immediately. 2. Watch the preview on the right re-typeset live. Navigate with the arrow keys (or swipe on touch screens); a progress bar tracks your place. -3. Press **Copy deck as HTML** to copy the whole deck as one self-contained HTML file. Open it in any browser and present with the arrow keys — offline, no dependencies. +3. A slide with too much text shrinks to fit the screen, down to 70% of the normal size. If it still doesn't fit, the preview names it so you can split it with `---` (until then it scrolls). The copied deck fits its slides the same way. +4. Press **Copy deck as HTML** to copy the whole deck as one self-contained HTML file. Open it in any browser and present with the arrow keys — offline, no dependencies. The syntax: diff --git a/ventures/mdecks/app.js b/ventures/mdecks/app.js index f52697e..00bbf8a 100644 --- a/ventures/mdecks/app.js +++ b/ventures/mdecks/app.js @@ -8,6 +8,7 @@ var prevBtn = document.getElementById("prev"); var nextBtn = document.getElementById("next"); var copyBtn = document.getElementById("copy"); + var noteEl = document.getElementById("fitnote"); var SAMPLE = [ "# .mdeck", @@ -132,10 +133,13 @@ if (current >= slides.length) current = slides.length - 1; if (current < 0) current = 0; render(); + scheduleLint(); } function render() { slideEl.innerHTML = renderSlide(slides[current]); + fit(slideEl, wrap); + wrap.scrollTop = 0; counterEl.textContent = (current + 1) + " / " + slides.length; var pct = slides.length <= 1 ? 100 : (current / (slides.length - 1)) * 100; barEl.style.width = pct + "%"; @@ -143,6 +147,50 @@ nextBtn.disabled = current === slides.length - 1; } + // Fit each slide to the screen: shrink its text, never below FLOOR, until nothing has to scroll. + // A slide that still doesn't fit keeps scrolling and is named under the preview. + var FLOOR = 0.7; + var lintTimer = null; + + function fits(box) { + return box.scrollHeight <= box.clientHeight + 1; + } + + // The text scale the slide got, or 0 when it is too long even at FLOOR. + function fit(el, box) { + el.style.setProperty("--fit", "1"); + if (fits(box)) return 1; + el.style.setProperty("--fit", String(FLOOR)); + if (!fits(box)) return 0; + var lo = FLOOR, hi = 1; + for (var k = 0; k < 6; k++) { + var mid = (lo + hi) / 2; + el.style.setProperty("--fit", String(mid)); + if (fits(box)) lo = mid; else hi = mid; + } + el.style.setProperty("--fit", String(lo)); + return lo; + } + + function lint() { + var tooLong = []; + for (var k = 0; k < slides.length; k++) { + slideEl.innerHTML = renderSlide(slides[k]); + if (!fit(slideEl, wrap)) tooLong.push(k + 1); + } + render(); + noteEl.hidden = tooLong.length === 0; + noteEl.textContent = !tooLong.length ? "" : + (tooLong.length === 1 ? "Slide " + tooLong[0] + " is" : "Slides " + tooLong.join(", ") + " are") + + " too long to fit, even with smaller text. Split " + (tooLong.length === 1 ? "it" : "them") + + " with --- or cut some words."; + } + + function scheduleLint() { + clearTimeout(lintTimer); + lintTimer = setTimeout(lint, 200); + } + function go(delta) { var next = current + delta; if (next < 0 || next > slides.length - 1) return; @@ -151,6 +199,7 @@ } src.addEventListener("input", parse); + window.addEventListener("resize", scheduleLint); prevBtn.addEventListener("click", function () { go(-1); }); nextBtn.addEventListener("click", function () { go(1); }); @@ -184,12 +233,12 @@ "body{font-family:ui-sans-serif,system-ui,-apple-system,'Segoe UI',Roboto,sans-serif;background:#0f1115;color:#e8eaf0;display:flex;flex-direction:column}" + ".bar{height:5px;background:#1e222b}.bar>div{height:100%;width:0;background:linear-gradient(90deg,#6ea8fe,#9d7bff);transition:width .18s ease}" + "main{flex:1;overflow-y:auto}" + - ".s{display:none;padding:clamp(24px,6vw,64px);min-height:100%;flex-direction:column;justify-content:center}" + + ".s{display:none;--fit:1;padding:clamp(24px,6vw,64px);min-height:100%;flex-direction:column;justify-content:center;overflow-wrap:anywhere}" + ".s.on{display:flex}" + - "h1{font-size:clamp(1.8rem,5vw,3.2rem);margin:0 0 .5em;line-height:1.15}" + - "h2{font-size:clamp(1.2rem,3vw,1.9rem);color:#9aa1af;font-weight:500;margin:0 0 .6em}" + - "ul{margin:0;padding-left:1.3em;font-size:clamp(1.1rem,2.5vw,1.5rem);line-height:1.7}" + - "p{font-size:clamp(1.1rem,2.5vw,1.5rem);line-height:1.7;margin:.3em 0}" + + "h1{font-size:calc(clamp(1.8rem,5vw,3.2rem)*var(--fit));margin:0 0 .5em;line-height:1.15}" + + "h2{font-size:calc(clamp(1.2rem,3vw,1.9rem)*var(--fit));color:#9aa1af;font-weight:500;margin:0 0 .6em}" + + "ul{margin:0;padding-left:1.3em;font-size:calc(clamp(1.1rem,2.5vw,1.5rem)*var(--fit));line-height:1.7}" + + "p{font-size:calc(clamp(1.1rem,2.5vw,1.5rem)*var(--fit));line-height:1.7;margin:.3em 0}" + "code{background:rgba(110,168,254,.12);border-radius:5px;padding:1px 6px;font-family:ui-monospace,Menlo,Consolas,monospace;font-size:.9em}" + "a{color:#6ea8fe;text-decoration:none}a:hover{text-decoration:underline}" + ".empty{color:#9aa1af;font-style:italic}" + @@ -198,7 +247,10 @@ slidesHtml + "\n\n
\n