No deck loaded.
;
+ let dead = false;
+ (async () => {
+ // window.__DECK__ keeps this route drivable by a headless renderer, should one ever exist.
+ let d = window.__DECK__ || null;
+ let sid = id;
+ if (!d && handoff) {
+ const raw = window.localStorage.getItem(handoff);
+ window.localStorage.removeItem(handoff);
+ if (raw) { const p = JSON.parse(raw); d = p.deck; sid = p.id || sid; }
+ }
+ if (!d && sid) d = (await getDeck(sid)).deck;
+ if (dead) return;
+ if (!d?.slides?.length) { setState('error'); setMsg('There is no deck to print.'); return; }
+ if (sid && !String(sid).startsWith('new:')) {
+ const idx = await workspaceFileIndex(sid).catch(() => null);
+ if (!dead && idx) setResolveSrc(() => buildSrcResolver(idx));
+ }
+ if (!dead) { setDeck(d); setState('waiting'); }
+ })().catch((e) => {
+ if (!dead) { setState('error'); setMsg(e.message || 'Could not load this deck.'); }
+ });
+ return () => { dead = true; };
+ }, [handoff, id]);
+
+ useEffect(() => {
+ if (state !== 'waiting' || !rootRef.current) return undefined;
+ let dead = false;
+ settle(rootRef.current, want, (s) => { if (!dead) setSeen(s); }).then((ok) => {
+ if (dead) return;
+ if (ok) { setState('ready'); window.print(); } else setState('incomplete');
+ // Not settled means DO NOT print. Quietly printing empty chart boxes is the failure this
+ // detector exists to prevent.
+ });
+ return () => { dead = true; };
+ }, [state, want]);
+
+ // Fit the 1920px page to the window on screen; inert in print.
+ useEffect(() => {
+ const fit = () => {
+ const el = rootRef.current;
+ if (el) el.style.setProperty('--sl-print-scale', String(Math.min(1, (window.innerWidth - 48) / 1920)));
+ };
+ fit();
+ window.addEventListener('resize', fit);
+ return () => window.removeEventListener('resize', fit);
+ }, [deck]);
+
+ const total = want.svg + want.code;
+ const pending = Math.max(0, total - (seen.svg + seen.code));
+
return (
-
- {(deck.slides || []).map((s) => (
-
-
+ <>
+
+ {state === 'loading' && Loading deck…}
+ {state === 'waiting' && (
+ {pending ? `Rendering — ${pending} of ${total} charts and diagrams left` : 'Preparing…'}
+ )}
+ {state === 'ready' && (
+ <>
+ Print dialog open — choose “Save as PDF” as the destination.
+ Keep Margins on Default; changing it rescales the slides to paper size.
+
+ >
+ )}
+ {state === 'incomplete' && (
+ <>
+ {pending} chart{pending === 1 ? '' : 's'} or diagram{pending === 1 ? '' : 's'} didn’t
+ finish rendering, so this was not sent to the printer.
+
+ >
+ )}
+ {state === 'error' && {msg}}
+
+ {deck && (
+
+ {deck.slides.map((s, i) => (
+
+
+
+ ))}
- ))}
-
+ )}
+ >
);
}
diff --git a/kits/slides/app/src/styles/app.css b/kits/slides/app/src/styles/app.css
index e0060cd..30a9a4d 100644
--- a/kits/slides/app/src/styles/app.css
+++ b/kits/slides/app/src/styles/app.css
@@ -855,3 +855,52 @@
}
.sk-deck-note { margin: 0; font-size: 13px; color: var(--mute); }
@media (max-width: 820px) { .sk-deck-rail { display: none; } }
+
+
+/* ── print surface ────────────────────────────────────────────────────────────
+ @page fixes the geometry: without it Chrome prints Letter, scales to ~0.67 and
+ clips the right third of every slide. print-color-adjust overrides the print
+ dialog's "Background graphics" checkbox, which is OFF by default — without it
+ every slide background disappears and a dark deck prints as white on white. */
+@page { size: 1920px 1080px; margin: 0; }
+
+@media screen {
+ .sl-print-root {
+ background: #0b0e14; padding: 24px 0;
+ display: flex; flex-direction: column; align-items: center; gap: 24px;
+ }
+ .sl-print-page {
+ width: 1920px; height: 1080px; position: relative; overflow: hidden; flex: 0 0 auto;
+ transform: scale(var(--sl-print-scale, 1)); transform-origin: top center;
+ box-shadow: 0 8px 30px rgba(0, 0, 0, .45);
+ }
+ .sl-print-bar {
+ position: sticky; top: 0; z-index: 10; display: flex; gap: 12px; align-items: center;
+ padding: 10px 16px; background: #11151f; color: #e7ebf3;
+ font: 500 13px/1.4 system-ui, -apple-system, sans-serif;
+ }
+ .sl-print-bar .btn { background: #232a3a; color: #e7ebf3; border-color: #2f3850; }
+}
+
+@media print {
+ html, body { margin: 0; padding: 0; background: #fff; }
+ .sl-print-bar { display: none !important; }
+ .sl-print-root { display: block; padding: 0; background: none; }
+ .sl-print-page {
+ width: 1920px; height: 1080px; position: relative; overflow: hidden;
+ transform: none !important; box-shadow: none !important;
+ break-after: page; page-break-after: always;
+ break-inside: avoid; page-break-inside: avoid;
+ }
+ .sl-print-page:last-child { break-after: auto; page-break-after: auto; }
+ .sl-print-root, .sl-print-root * {
+ -webkit-print-color-adjust: exact !important;
+ print-color-adjust: exact !important;
+ }
+}
+
+.dlg-detail {
+ margin: 10px 0 0; padding: 10px; max-height: 180px; overflow: auto;
+ background: var(--surface-2); border-radius: 8px;
+ font: 12px/1.5 ui-monospace, SFMono-Regular, Menlo, monospace; white-space: pre-wrap;
+}