mdecks: slides shrink to fit, and the preview names any that can't

Both the preview and the copied HTML shrink a slide's text until nothing has
to scroll, down to 70% of its size. A slide that still doesn't fit keeps
scrolling, and a note under the preview names it: "Slide 3 is too long to
fit, even with smaller text. Split it with --- or cut some words." Long words
and links now wrap instead of running off the side. Checked in Chrome: a
nine-point slide fitted at 87% in the preview, a thirty-point one was named,
and the copied deck fits the same way on a laptop and a phone.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CYHXFvWMCs1GUmRmpySqQ6
This commit is contained in:
teahubxyzandClaude Opus 5.5 committed 2026-10-15 01:45:45 -07:00
1 parent 1a5b105956
commit 3aac83357b
3 files changed
+72 -12

No files matched your search

+2 -1
View File
@@ -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:
+58 -6
View File
@@ -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</main>\n<div class=\"n\" id=\"n\"></div>\n<script>\n" +
"var s=document.querySelectorAll('.s'),i=0,n=document.getElementById('n'),b=document.getElementById('b');\n" +
"function show(){for(var k=0;k<s.length;k++)s[k].classList.toggle('on',k===i);n.textContent=(i+1)+' / '+s.length;b.style.width=(s.length<=1?100:i/(s.length-1)*100)+'%';}\n" +
"function show(){for(var k=0;k<s.length;k++)s[k].classList.toggle('on',k===i);n.textContent=(i+1)+' / '+s.length;b.style.width=(s.length<=1?100:i/(s.length-1)*100)+'%';fit(s[i]);}\n" +
"var F=0.7,m=document.querySelector('main');function ok(){return m.scrollHeight<=m.clientHeight+1;}\n" +
"function fit(e){e.style.setProperty('--fit','1');if(ok())return;e.style.setProperty('--fit',String(F));if(!ok())return;var lo=F,hi=1;for(var k=0;k<6;k++){var md=(lo+hi)/2;e.style.setProperty('--fit',String(md));if(ok())lo=md;else hi=md;}e.style.setProperty('--fit',String(lo));}\n" +
"window.addEventListener('resize',function(){fit(s[i]);});\n" +
"function go(d){var j=i+d;if(j<0||j>s.length-1)return;i=j;show();}\n" +
"document.addEventListener('keydown',function(e){if(e.key==='ArrowRight'||e.key===' '||e.key==='PageDown'){e.preventDefault();go(1);}else if(e.key==='ArrowLeft'||e.key==='PageUp'){e.preventDefault();go(-1);}else if(e.key==='Home'){i=0;show();}else if(e.key==='End'){i=s.length-1;show();}});\n" +
"var x=null;document.addEventListener('touchstart',function(e){x=e.touches[0].clientX;},{passive:true});" +
+12 -5
View File
@@ -183,19 +183,21 @@
touch-action: pan-y;
}
.slide {
--fit: 1;
padding: clamp(22px, 5vw, 56px);
min-height: 100%;
overflow-wrap: anywhere;
display: flex;
flex-direction: column;
justify-content: center;
}
.slide h1 {
font-size: clamp(1.6rem, 4.5vw, 3rem);
font-size: calc(clamp(1.6rem, 4.5vw, 3rem) * var(--fit));
margin: 0 0 0.5em;
line-height: 1.15;
}
.slide h2 {
font-size: clamp(1.2rem, 3vw, 1.9rem);
font-size: calc(clamp(1.2rem, 3vw, 1.9rem) * var(--fit));
margin: 0 0 0.6em;
color: var(--ink-dim);
font-weight: 500;
@@ -203,11 +205,11 @@
.slide ul {
margin: 0;
padding-left: 1.3em;
font-size: clamp(1rem, 2.2vw, 1.35rem);
font-size: calc(clamp(1rem, 2.2vw, 1.35rem) * var(--fit));
line-height: 1.7;
}
.slide p {
font-size: clamp(1rem, 2.2vw, 1.35rem);
font-size: calc(clamp(1rem, 2.2vw, 1.35rem) * var(--fit));
line-height: 1.7;
margin: 0.3em 0;
}
@@ -226,6 +228,11 @@
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
font-size: 0.9em;
}
.fit-note {
font-size: 0.78rem;
color: #f2c46d;
margin-top: 8px;
}
.export-note {
font-size: 0.78rem;
color: var(--ink-dim);
@@ -406,7 +413,7 @@
<span class="counter" id="counter">1 / 1</span>
</div>
<div class="progress" role="progressbar" aria-label="Deck progress"><div id="bar"></div></div>
<div class="slide-wrap" id="wrap"><article class="slide" id="slide"></article></div>
<div class="slide-wrap" id="wrap"><article class="slide" id="slide"></article></div><p class="fit-note" id="fitnote" role="status" hidden></p>
<p class="export-note">The copied file is a self-contained deck: open it in any browser and use &larr; / &rarr; to present.</p>
</section>
</main>