Description
Write markdown with `---` separators between slides, get a navigable fullscreen presentation. Arrow key navigation, slide counter, speaker notes, syntax-highlighted code blocks, and theming. Like a mini Marp/Slidev in the browser — no install, no server, no account. Export as PDF via `window.print()`.
Features
Slide Authoring
`---` separators — Each `---` on its own line starts a new slide
Standard Markdown — Full GFM support: headings, lists, bold/italic, links, images, tables, blockquotes
Code blocks — Syntax-highlighted with language detection (reuse tokenizer from [Feat] Code Screenshot Generator #112 or `marked` from [Feat] Tool: Markdown to PDF Converter #54 )
Speaker notes — Content after `???` on a slide is hidden from the presentation, shown in presenter view
Slide-level directives — HTML comments for per-slide config:
`` — Center all content
`` — Custom background color
`` — Background image
`` — Slide transition
Images — Centered by default, ` ` for background images
Math — Basic LaTeX math rendering for inline `$...$` and block `$$...$$` (lightweight custom renderer for common symbols)
Incremental lists — `> -` prefix for items that appear one-by-one on keypress
Presentation Mode
Fullscreen — F11 or button to enter fullscreen presentation
Navigation :
Arrow keys (left/right) or space/backspace
Click left/right edges of the slide
Touch swipe on mobile
Page up/down
Slide counter — "3 / 15" indicator (hideable)
Progress bar — Thin bar at bottom showing position
Presenter view — Open in a second window/tab:
Current slide + next slide preview
Speaker notes for current slide
Timer (elapsed time, optional countdown)
Slide navigator grid
Keyboard shortcuts :
`F` — Toggle fullscreen
`P` — Open presenter view
`G` — Go to slide number
`B` — Black screen (pause)
`O` — Overview / grid view of all slides
`Esc` — Exit presentation mode
Themes
Built-in themes :
Default (clean, white background, dark text)
Dark (dark background, light text)
Corporate (navy + white, serif headings)
Minimal (lots of whitespace, sans-serif)
Hacker (black + green terminal style)
Academic (LaTeX-inspired, serif fonts)
Custom CSS — Override any theme via a CSS block at the top of the document
Consistent sizing — All slides 16:9 aspect ratio, scaled to fit viewport
Transitions
Slide transitions : none (instant), fade, slide-left, slide-right, slide-up, zoom
Per-slide override via directive
Export
PDF — `window.print()` with print CSS that renders each slide as a page (16:9 landscape)
HTML — Self-contained single HTML file with all slides embedded
PNG — Export individual slides as images via Canvas
Markdown — The source markdown itself (with a "copy source" button)
Implementation
Dependencies
Package
License
Gzipped
Purpose
`marked`
MIT
~12 KB
Markdown parsing (shared with #54 )
The `marked` library is already planned for the Markdown to PDF tool (#54 ). Reusing it here avoids duplicate dependencies. Syntax highlighting uses either the custom tokenizer from #112 or `highlight.js` from #54 .
Core Logic
interface Slide {
content : string ; // Markdown content
notes : string ; // Speaker notes (after ???)
directives : SlideDirectives ;
html ?: string ; // Rendered HTML (cached)
}
interface SlideDirectives {
class ?: string ;
background ?: string ;
backgroundImage ?: string ;
transition ?: string ;
}
function parseSlides ( markdown : string ) : Slide [ ] {
const rawSlides = markdown . split ( / \n - - - \n / ) ;
return rawSlides . map ( raw => {
// Split content and notes
const [ content , ...notesParts ] = raw . split ( / \n \? \? \? \n / ) ;
const notes = notesParts . join ( '\n' ) . trim ( ) ;
// Extract directives from HTML comments
const directives : SlideDirectives = { } ;
const directivePattern = / < ! - - \s * ( \w + ) : \s * ( .+ ?) \s * - - > / g;
let match ;
while ( ( match = directivePattern . exec ( content ) ) !== null ) {
directives [ match [ 1 ] as keyof SlideDirectives ] = match [ 2 ] ;
}
// Clean content (remove directive comments)
const cleanContent = content . replace ( directivePattern , '' ) . trim ( ) ;
return { content : cleanContent , notes, directives } ;
} ) ;
}
function renderSlide ( slide : Slide , theme : Theme ) : HTMLElement {
const el = document . createElement ( 'div' ) ;
el . className = `slide ${ slide . directives . class ?? '' } ` ;
// Apply background
if ( slide . directives . background ) {
el . style . backgroundColor = slide . directives . background ;
}
if ( slide . directives . backgroundImage ) {
el . style . backgroundImage = `url(${ slide . directives . backgroundImage } )` ;
el . style . backgroundSize = 'cover' ;
el . style . backgroundPosition = 'center' ;
}
// Render markdown to HTML
el . innerHTML = marked . parse ( slide . content ) ;
// Apply theme styles
applyTheme ( el , theme ) ;
return el ;
}
// Presentation controller
class PresentationController {
private slides : Slide [ ] ;
private currentIndex : number = 0 ;
private container : HTMLElement ;
private presenterWindow : Window | null = null ;
navigate ( direction : 'next' | 'prev' | number ) : void {
const target = typeof direction === 'number'
? direction
: direction === 'next'
? Math . min ( this . currentIndex + 1 , this . slides . length - 1 )
: Math . max ( this . currentIndex - 1 , 0 ) ;
if ( target === this . currentIndex ) return ;
const transition = this . slides [ target ] . directives . transition ?? 'fade' ;
this . transitionTo ( target , transition ) ;
this . currentIndex = target ;
this . updatePresenterView ( ) ;
}
transitionTo ( index : number , type : string ) : void {
const current = this . container . querySelector ( '.slide.active' ) as HTMLElement ;
const next = renderSlide ( this . slides [ index ] , this . theme ) ;
next . classList . add ( 'slide' , 'incoming' ) ;
this . container . appendChild ( next ) ;
// CSS transition
requestAnimationFrame ( ( ) => {
current ?. classList . add ( `exit-${ type } ` ) ;
next . classList . remove ( 'incoming' ) ;
next . classList . add ( 'active' , `enter-${ type } ` ) ;
current ?. addEventListener ( 'transitionend' , ( ) => current . remove ( ) , { once : true } ) ;
} ) ;
}
openPresenterView ( ) : void {
this . presenterWindow = window . open ( '' , 'presenter' , 'width=800,height=600' ) ;
this . updatePresenterView ( ) ;
}
private updatePresenterView ( ) : void {
if ( ! this . presenterWindow ) return ;
const slide = this . slides [ this . currentIndex ] ;
const nextSlide = this . slides [ this . currentIndex + 1 ] ;
this . presenterWindow . document . body . innerHTML = `
<div class="presenter-layout">
<div class="current-slide">${ marked . parse ( slide . content ) } </div>
<div class="next-slide">${ nextSlide ? marked . parse ( nextSlide . content ) : '<p>End</p>' } </div>
<div class="notes">${ marked . parse ( slide . notes || '*No notes*' ) } </div>
<div class="counter">${ this . currentIndex + 1 } / ${ this . slides . length } </div>
<div class="timer" id="timer">00:00</div>
</div>
` ;
}
}
Slide CSS (16:9 Aspect Ratio)
.slide-container {
position : relative;
width : 100vw ;
height : 100vh ;
overflow : hidden;
}
.slide {
position : absolute;
inset : 0 ;
display : flex;
flex-direction : column;
justify-content : center;
padding : 5% 8% ;
aspect-ratio : 16 / 9 ;
/* Scale to fit viewport while maintaining 16:9 */
max-width : min (100vw , 100vh * 16 / 9 );
max-height : min (100vh , 100vw * 9 / 16 );
margin : auto;
font-size : clamp (16px , 2.5vw , 32px );
}
.slide h1 { font-size : 2.5em ; margin-bottom : 0.5em ; }
.slide h2 { font-size : 1.8em ; margin-bottom : 0.4em ; }
.slide ul , .slide ol { font-size : 1.1em ; line-height : 1.8 ; }
.slide code { font-size : 0.85em ; }
.slide pre { font-size : 0.75em ; padding : 1em ; border-radius : 8px ; }
.slide .center { text-align : center; align-items : center; }
/* Transitions */
.slide .exit-fade { opacity : 0 ; transition : opacity 0.4s ; }
.slide .enter-fade { animation : fadeIn 0.4s ; }
.slide .exit-slide-left { transform : translateX (-100% ); transition : transform 0.4s ; }
.slide .enter-slide-left { animation : slideFromRight 0.4s ; }
@keyframes fadeIn { from { opacity : 0 ; } to { opacity : 1 ; } }
@keyframes slideFromRight { from { transform : translateX (100% ); } to { transform : translateX (0 ); } }
/* Print: each slide = one page */
@media print {
.slide { page-break-after : always; width : 100% ; height : 100vh ; }
.slide-counter , .progress-bar , .controls { display : none; }
}
Privacy
All rendering client-side. Presentation content (which may contain confidential information) never leaves the browser.
UI
Editor mode (default):
Left: Markdown editor with line numbers and slide separators highlighted
Right: Live slide preview (current slide rendered)
Bottom: Slide strip (thumbnails of all slides, click to jump)
Toolbar: Theme selector, Present button, Export dropdown
Presentation mode (fullscreen):
Single slide filling the screen
Slide counter (bottom-right, subtle)
Progress bar (bottom, thin line)
Invisible navigation zones (left/right click areas)
Presenter view (separate window):
Current slide, next slide preview, speaker notes, timer, slide counter
Overview mode (O key):
Grid of all slide thumbnails, click to jump
Responsive editor layout
Files to Create/Modify
File
Purpose
`tools/slide-deck/package.json`
Workspace package, `marked` dependency (shared with #54 )
`tools/slide-deck/src/parser.ts`
Slide parsing, directive extraction, notes separation
`tools/slide-deck/src/renderer.ts`
Slide HTML rendering, theme application
`tools/slide-deck/src/presenter.ts`
PresentationController, navigation, transitions, presenter view
`tools/slide-deck/src/themes.ts`
Theme definitions (6 built-in themes)
`tools/slide-deck/src/tool.ts`
Public API
`tools/slide-deck/src/page.ts`
Editor layout, slide strip, toolbar
`tools/slide-deck/src/meta.ts`
ToolMeta
`tools/slide-deck/src/index.ts`
Exports
`tools/slide-deck/tests/parser.unit.test.ts`
Slide splitting, directives, notes extraction
`tools/slide-deck/tests/presenter.unit.test.ts`
Navigation logic, slide transitions
`tools/slide-deck/tests/page.unit.test.ts`
DOM rendering tests
`src/index.ts`
Register route
`src/index.html`
Add to tool grid
Description
Write markdown with `---` separators between slides, get a navigable fullscreen presentation. Arrow key navigation, slide counter, speaker notes, syntax-highlighted code blocks, and theming. Like a mini Marp/Slidev in the browser — no install, no server, no account. Export as PDF via `window.print()`.
Features
Slide Authoring
Presentation Mode
Themes
Transitions
Export
Implementation
Dependencies
The `marked` library is already planned for the Markdown to PDF tool (#54). Reusing it here avoids duplicate dependencies. Syntax highlighting uses either the custom tokenizer from #112 or `highlight.js` from #54.
Core Logic
Slide CSS (16:9 Aspect Ratio)
Privacy
All rendering client-side. Presentation content (which may contain confidential information) never leaves the browser.
UI
Okey):Files to Create/Modify