md-slides 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,237 @@
1
+ /**
2
+ * HTML Renderer for md-slides
3
+ * Takes parsed slides and produces a complete HTML presentation
4
+ */
5
+
6
+ import { getTheme, baseStyles } from './themes.js';
7
+
8
+ /**
9
+ * Generate the keyboard navigation + live reload script
10
+ */
11
+ function getScript(slideCount, liveReload = false) {
12
+ return `
13
+ (function() {
14
+ let current = 0;
15
+ const total = ${slideCount};
16
+ const slides = document.querySelectorAll('.slide');
17
+ const progress = document.querySelector('.progress');
18
+ const slideNum = document.querySelector('.slide-number');
19
+ const notesOverlay = document.querySelector('.notes-overlay');
20
+ const hint = document.querySelector('.hint');
21
+ let notesVisible = false;
22
+
23
+ // Parse hash
24
+ const hash = parseInt(window.location.hash.replace('#', ''), 10);
25
+ if (!isNaN(hash) && hash >= 0 && hash < total) {
26
+ current = hash;
27
+ }
28
+
29
+ function goTo(n) {
30
+ if (n < 0 || n >= total) return;
31
+ slides[current].classList.remove('active');
32
+ slides[current].classList.add(n > current ? 'prev' : '');
33
+ current = n;
34
+ slides[current].classList.remove('prev');
35
+ slides[current].classList.add('active');
36
+
37
+ progress.style.width = ((current + 1) / total * 100) + '%';
38
+ slideNum.textContent = (current + 1) + ' / ' + total;
39
+ window.location.hash = current;
40
+
41
+ // Update notes
42
+ const noteText = slides[current].dataset.notes || '';
43
+ notesOverlay.textContent = noteText || '(no notes for this slide)';
44
+ }
45
+
46
+ function next() { goTo(current + 1); }
47
+ function prev() { goTo(current - 1); }
48
+ function first() { goTo(0); }
49
+ function last() { goTo(total - 1); }
50
+
51
+ // Keyboard
52
+ document.addEventListener('keydown', function(e) {
53
+ hint.classList.remove('visible');
54
+ switch(e.key) {
55
+ case 'ArrowRight':
56
+ case 'ArrowDown':
57
+ case ' ':
58
+ case 'l':
59
+ case 'j':
60
+ e.preventDefault();
61
+ next();
62
+ break;
63
+ case 'ArrowLeft':
64
+ case 'ArrowUp':
65
+ case 'h':
66
+ case 'k':
67
+ e.preventDefault();
68
+ prev();
69
+ break;
70
+ case 'Home':
71
+ e.preventDefault();
72
+ first();
73
+ break;
74
+ case 'End':
75
+ e.preventDefault();
76
+ last();
77
+ break;
78
+ case 's':
79
+ case 'n':
80
+ notesVisible = !notesVisible;
81
+ notesOverlay.classList.toggle('visible', notesVisible);
82
+ break;
83
+ case 'f':
84
+ if (document.fullscreenElement) {
85
+ document.exitFullscreen();
86
+ } else {
87
+ document.documentElement.requestFullscreen();
88
+ }
89
+ break;
90
+ case 'Escape':
91
+ notesOverlay.classList.remove('visible');
92
+ notesVisible = false;
93
+ break;
94
+ }
95
+ });
96
+
97
+ // Touch / swipe
98
+ let touchStartX = 0;
99
+ let touchStartY = 0;
100
+ document.addEventListener('touchstart', function(e) {
101
+ touchStartX = e.touches[0].clientX;
102
+ touchStartY = e.touches[0].clientY;
103
+ });
104
+ document.addEventListener('touchend', function(e) {
105
+ const dx = e.changedTouches[0].clientX - touchStartX;
106
+ const dy = e.changedTouches[0].clientY - touchStartY;
107
+ if (Math.abs(dx) > Math.abs(dy) && Math.abs(dx) > 50) {
108
+ dx > 0 ? prev() : next();
109
+ }
110
+ });
111
+
112
+ // Click (right half = next, left half = prev)
113
+ document.addEventListener('click', function(e) {
114
+ if (e.target.tagName === 'A' || e.target.tagName === 'BUTTON') return;
115
+ if (e.clientX > window.innerWidth / 2) {
116
+ next();
117
+ } else {
118
+ prev();
119
+ }
120
+ });
121
+
122
+ // Init
123
+ goTo(current);
124
+
125
+ // Show hint briefly
126
+ setTimeout(function() { hint.classList.add('visible'); }, 1000);
127
+ setTimeout(function() { hint.classList.remove('visible'); }, 5000);
128
+
129
+ ${liveReload ? `
130
+ // Live reload via WebSocket
131
+ (function connectWS() {
132
+ const ws = new WebSocket('ws://localhost:' + (parseInt(location.port) + 1));
133
+ ws.onmessage = function(e) {
134
+ if (e.data === 'reload') {
135
+ location.reload();
136
+ }
137
+ };
138
+ ws.onclose = function() {
139
+ setTimeout(connectWS, 1000);
140
+ };
141
+ })();
142
+ ` : ''}
143
+ })();
144
+ `;
145
+ }
146
+
147
+ /**
148
+ * Build complete HTML document from parsed slides
149
+ */
150
+ export function renderHTML(slidesData, options = {}) {
151
+ const {
152
+ theme = 'dark',
153
+ title = 'Presentation',
154
+ author = '',
155
+ liveReload = false,
156
+ } = options;
157
+
158
+ const { metadata, slides } = slidesData;
159
+ const finalTitle = metadata.title || title;
160
+ const finalAuthor = metadata.author || author;
161
+ const finalTheme = metadata.theme || theme;
162
+
163
+ const slideElements = slides.map((slide, i) => {
164
+ const classes = ['slide'];
165
+ classes.push(`layout-${slide.layout}`);
166
+ if (slide.directives.class) {
167
+ classes.push(slide.directives.class);
168
+ }
169
+ if (i === 0) classes.push('active');
170
+
171
+ let style = '';
172
+ if (slide.directives.background) {
173
+ const bg = slide.directives.background;
174
+ if (bg.startsWith('#') || bg.startsWith('rgb')) {
175
+ style = `background: ${bg};`;
176
+ } else {
177
+ style = `background: url('${bg}') center/cover no-repeat;`;
178
+ }
179
+ }
180
+
181
+ const notes = slide.notes ? ` data-notes="${escapeAttr(slide.notes)}"` : '';
182
+
183
+ return ` <section class="${classes.join(' ')}"${style ? ` style="${style}"` : ''}${notes}>
184
+ <div class="slide-content">
185
+ ${slide.html}
186
+ </div>
187
+ </section>`;
188
+ }).join('\n');
189
+
190
+ return `<!DOCTYPE html>
191
+ <html lang="en">
192
+ <head>
193
+ <meta charset="UTF-8">
194
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
195
+ <title>${escapeHTML(finalTitle)}</title>
196
+ ${finalAuthor ? `<meta name="author" content="${escapeAttr(finalAuthor)}">` : ''}
197
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.10.0/styles/github-dark.min.css">
198
+ <style>
199
+ ${getTheme(finalTheme)}
200
+ ${baseStyles}
201
+ </style>
202
+ </head>
203
+ <body>
204
+ <div class="deck">
205
+ ${slideElements}
206
+ </div>
207
+
208
+ <div class="progress" style="width: 0%"></div>
209
+ <div class="slide-number">1 / ${slides.length}</div>
210
+ <div class="notes-overlay"></div>
211
+ <div class="hint">← → navigate · S notes · F fullscreen</div>
212
+
213
+ <script>
214
+ ${getScript(slides.length, liveReload)}
215
+ </script>
216
+ </body>
217
+ </html>`;
218
+ }
219
+
220
+ function escapeHTML(str) {
221
+ return str
222
+ .replace(/&/g, '&amp;')
223
+ .replace(/</g, '&lt;')
224
+ .replace(/>/g, '&gt;')
225
+ .replace(/"/g, '&quot;');
226
+ }
227
+
228
+ function escapeAttr(str) {
229
+ return str
230
+ .replace(/&/g, '&amp;')
231
+ .replace(/"/g, '&quot;')
232
+ .replace(/'/g, '&#39;')
233
+ .replace(/</g, '&lt;')
234
+ .replace(/>/g, '&gt;');
235
+ }
236
+
237
+ export default { renderHTML };
package/src/themes.js ADDED
@@ -0,0 +1,452 @@
1
+ /**
2
+ * Built-in themes for md-slides
3
+ * Each theme provides CSS variables and base styles
4
+ */
5
+
6
+ const themes = {
7
+ dark: {
8
+ name: 'Dark',
9
+ css: `
10
+ :root {
11
+ --bg: #0a0a0f;
12
+ --bg-slide: #12121a;
13
+ --fg: #e8e8f0;
14
+ --fg-dim: #8888aa;
15
+ --accent: #6c63ff;
16
+ --accent-glow: rgba(108, 99, 255, 0.3);
17
+ --code-bg: #1a1a2e;
18
+ --code-border: #2a2a4a;
19
+ --blockquote-border: #6c63ff;
20
+ --link: #8b83ff;
21
+ --font-heading: 'Inter', system-ui, sans-serif;
22
+ --font-body: 'Inter', system-ui, sans-serif;
23
+ --font-code: 'JetBrains Mono', 'Fira Code', monospace;
24
+ --radius: 12px;
25
+ }
26
+ `,
27
+ },
28
+
29
+ light: {
30
+ name: 'Light',
31
+ css: `
32
+ :root {
33
+ --bg: #f5f5f7;
34
+ --bg-slide: #ffffff;
35
+ --fg: #1d1d1f;
36
+ --fg-dim: #86868b;
37
+ --accent: #0071e3;
38
+ --accent-glow: rgba(0, 113, 227, 0.15);
39
+ --code-bg: #f5f5f7;
40
+ --code-border: #d2d2d7;
41
+ --blockquote-border: #0071e3;
42
+ --link: #0071e3;
43
+ --font-heading: 'Inter', system-ui, sans-serif;
44
+ --font-body: 'Inter', system-ui, sans-serif;
45
+ --font-code: 'JetBrains Mono', 'Fira Code', monospace;
46
+ --radius: 12px;
47
+ }
48
+ `,
49
+ },
50
+
51
+ minimal: {
52
+ name: 'Minimal',
53
+ css: `
54
+ :root {
55
+ --bg: #fafafa;
56
+ --bg-slide: #ffffff;
57
+ --fg: #222222;
58
+ --fg-dim: #999999;
59
+ --accent: #222222;
60
+ --accent-glow: rgba(0, 0, 0, 0.05);
61
+ --code-bg: #f6f6f6;
62
+ --code-border: #eeeeee;
63
+ --blockquote-border: #cccccc;
64
+ --link: #444444;
65
+ --font-heading: 'Georgia', serif;
66
+ --font-body: 'Georgia', serif;
67
+ --font-code: 'Menlo', 'Monaco', monospace;
68
+ --radius: 4px;
69
+ }
70
+ `,
71
+ },
72
+
73
+ neon: {
74
+ name: 'Neon',
75
+ css: `
76
+ :root {
77
+ --bg: #000000;
78
+ --bg-slide: #0a0a0a;
79
+ --fg: #ffffff;
80
+ --fg-dim: #666688;
81
+ --accent: #00ff88;
82
+ --accent-glow: rgba(0, 255, 136, 0.2);
83
+ --code-bg: #0d0d1a;
84
+ --code-border: #1a3a2a;
85
+ --blockquote-border: #00ff88;
86
+ --link: #00ccff;
87
+ --font-heading: 'Inter', system-ui, sans-serif;
88
+ --font-body: 'Inter', system-ui, sans-serif;
89
+ --font-code: 'JetBrains Mono', 'Fira Code', monospace;
90
+ --radius: 8px;
91
+ }
92
+ `,
93
+ },
94
+ };
95
+
96
+ export const baseStyles = `
97
+ @import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&display=swap');
98
+ @import url('https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500&display=swap');
99
+
100
+ *, *::before, *::after {
101
+ box-sizing: border-box;
102
+ margin: 0;
103
+ padding: 0;
104
+ }
105
+
106
+ html, body {
107
+ height: 100%;
108
+ overflow: hidden;
109
+ background: var(--bg);
110
+ color: var(--fg);
111
+ font-family: var(--font-body);
112
+ -webkit-font-smoothing: antialiased;
113
+ -moz-osx-font-smoothing: grayscale;
114
+ }
115
+
116
+ /* Slide Container */
117
+ .deck {
118
+ position: relative;
119
+ width: 100vw;
120
+ height: 100vh;
121
+ }
122
+
123
+ .slide {
124
+ position: absolute;
125
+ top: 0;
126
+ left: 0;
127
+ width: 100%;
128
+ height: 100%;
129
+ display: flex;
130
+ flex-direction: column;
131
+ justify-content: center;
132
+ padding: 8vh 10vw;
133
+ background: var(--bg-slide);
134
+ opacity: 0;
135
+ transform: translateX(40px);
136
+ transition: opacity 0.5s ease, transform 0.5s ease;
137
+ pointer-events: none;
138
+ overflow-y: auto;
139
+ }
140
+
141
+ .slide.active {
142
+ opacity: 1;
143
+ transform: translateX(0);
144
+ pointer-events: auto;
145
+ z-index: 1;
146
+ }
147
+
148
+ .slide.prev {
149
+ opacity: 0;
150
+ transform: translateX(-40px);
151
+ }
152
+
153
+ /* Layouts */
154
+ .slide.layout-center {
155
+ align-items: center;
156
+ text-align: center;
157
+ justify-content: center;
158
+ }
159
+
160
+ .slide.layout-title {
161
+ justify-content: center;
162
+ }
163
+
164
+ .slide.layout-title h1 {
165
+ font-size: clamp(2.5rem, 6vw, 5rem);
166
+ margin-bottom: 1rem;
167
+ }
168
+
169
+ .slide.layout-quote {
170
+ align-items: center;
171
+ justify-content: center;
172
+ text-align: center;
173
+ padding: 10vh 15vw;
174
+ }
175
+
176
+ .slide.layout-code {
177
+ justify-content: center;
178
+ }
179
+
180
+ .slide.layout-image {
181
+ align-items: center;
182
+ justify-content: center;
183
+ }
184
+
185
+ .slide.layout-image img {
186
+ max-width: 80%;
187
+ max-height: 70vh;
188
+ border-radius: var(--radius);
189
+ }
190
+
191
+ /* Typography */
192
+ h1 {
193
+ font-family: var(--font-heading);
194
+ font-size: clamp(2rem, 5vw, 4rem);
195
+ font-weight: 800;
196
+ letter-spacing: -0.03em;
197
+ line-height: 1.1;
198
+ margin-bottom: 0.8em;
199
+ background: linear-gradient(135deg, var(--fg), var(--accent));
200
+ -webkit-background-clip: text;
201
+ -webkit-text-fill-color: transparent;
202
+ background-clip: text;
203
+ }
204
+
205
+ h2 {
206
+ font-family: var(--font-heading);
207
+ font-size: clamp(1.5rem, 3vw, 2.5rem);
208
+ font-weight: 700;
209
+ letter-spacing: -0.02em;
210
+ line-height: 1.2;
211
+ margin-bottom: 0.6em;
212
+ color: var(--accent);
213
+ }
214
+
215
+ h3 {
216
+ font-family: var(--font-heading);
217
+ font-size: clamp(1.2rem, 2vw, 1.8rem);
218
+ font-weight: 600;
219
+ margin-bottom: 0.5em;
220
+ color: var(--fg);
221
+ }
222
+
223
+ p {
224
+ font-size: clamp(1rem, 1.8vw, 1.4rem);
225
+ line-height: 1.7;
226
+ margin-bottom: 0.8em;
227
+ color: var(--fg-dim);
228
+ }
229
+
230
+ strong {
231
+ color: var(--fg);
232
+ font-weight: 600;
233
+ }
234
+
235
+ em {
236
+ color: var(--accent);
237
+ font-style: italic;
238
+ }
239
+
240
+ a {
241
+ color: var(--link);
242
+ text-decoration: none;
243
+ border-bottom: 1px solid transparent;
244
+ transition: border-color 0.2s;
245
+ }
246
+
247
+ a:hover {
248
+ border-bottom-color: var(--link);
249
+ }
250
+
251
+ /* Lists */
252
+ ul, ol {
253
+ font-size: clamp(1rem, 1.6vw, 1.3rem);
254
+ line-height: 1.8;
255
+ padding-left: 1.5em;
256
+ margin-bottom: 1em;
257
+ color: var(--fg-dim);
258
+ }
259
+
260
+ li {
261
+ margin-bottom: 0.4em;
262
+ }
263
+
264
+ li::marker {
265
+ color: var(--accent);
266
+ }
267
+
268
+ /* Code */
269
+ code {
270
+ font-family: var(--font-code);
271
+ font-size: 0.85em;
272
+ background: var(--code-bg);
273
+ border: 1px solid var(--code-border);
274
+ padding: 0.15em 0.4em;
275
+ border-radius: 4px;
276
+ color: var(--accent);
277
+ }
278
+
279
+ pre {
280
+ margin: 1em 0;
281
+ border-radius: var(--radius);
282
+ overflow: hidden;
283
+ border: 1px solid var(--code-border);
284
+ }
285
+
286
+ pre code {
287
+ display: block;
288
+ padding: 1.5em 2em;
289
+ background: var(--code-bg);
290
+ border: none;
291
+ font-size: clamp(0.8rem, 1.2vw, 1rem);
292
+ line-height: 1.6;
293
+ overflow-x: auto;
294
+ color: var(--fg);
295
+ }
296
+
297
+ /* Blockquotes */
298
+ blockquote {
299
+ border-left: 4px solid var(--blockquote-border);
300
+ padding: 1em 1.5em;
301
+ margin: 1em 0;
302
+ background: var(--accent-glow);
303
+ border-radius: 0 var(--radius) var(--radius) 0;
304
+ }
305
+
306
+ blockquote p {
307
+ font-size: clamp(1.2rem, 2vw, 1.8rem);
308
+ font-style: italic;
309
+ color: var(--fg);
310
+ margin-bottom: 0;
311
+ }
312
+
313
+ /* Tables */
314
+ table {
315
+ width: 100%;
316
+ border-collapse: collapse;
317
+ margin: 1em 0;
318
+ font-size: clamp(0.85rem, 1.3vw, 1.1rem);
319
+ }
320
+
321
+ th, td {
322
+ padding: 0.75em 1em;
323
+ text-align: left;
324
+ border-bottom: 1px solid var(--code-border);
325
+ }
326
+
327
+ th {
328
+ font-weight: 600;
329
+ color: var(--accent);
330
+ }
331
+
332
+ td {
333
+ color: var(--fg-dim);
334
+ }
335
+
336
+ /* Images */
337
+ img {
338
+ max-width: 100%;
339
+ border-radius: var(--radius);
340
+ }
341
+
342
+ /* Progress bar */
343
+ .progress {
344
+ position: fixed;
345
+ bottom: 0;
346
+ left: 0;
347
+ height: 3px;
348
+ background: var(--accent);
349
+ transition: width 0.4s ease;
350
+ z-index: 100;
351
+ box-shadow: 0 0 10px var(--accent-glow);
352
+ }
353
+
354
+ /* Slide number */
355
+ .slide-number {
356
+ position: fixed;
357
+ bottom: 16px;
358
+ right: 24px;
359
+ font-family: var(--font-code);
360
+ font-size: 0.8rem;
361
+ color: var(--fg-dim);
362
+ opacity: 0.5;
363
+ z-index: 100;
364
+ }
365
+
366
+ /* Presenter notes toggle */
367
+ .notes-overlay {
368
+ display: none;
369
+ position: fixed;
370
+ bottom: 0;
371
+ left: 0;
372
+ right: 0;
373
+ background: rgba(0,0,0,0.9);
374
+ color: #ccc;
375
+ padding: 1.5em 2em;
376
+ font-size: 0.9rem;
377
+ line-height: 1.6;
378
+ z-index: 200;
379
+ max-height: 30vh;
380
+ overflow-y: auto;
381
+ border-top: 2px solid var(--accent);
382
+ }
383
+
384
+ .notes-overlay.visible {
385
+ display: block;
386
+ }
387
+
388
+ /* Keyboard hint */
389
+ .hint {
390
+ position: fixed;
391
+ bottom: 40px;
392
+ left: 50%;
393
+ transform: translateX(-50%);
394
+ font-family: var(--font-code);
395
+ font-size: 0.75rem;
396
+ color: var(--fg-dim);
397
+ opacity: 0;
398
+ transition: opacity 0.5s;
399
+ z-index: 100;
400
+ }
401
+
402
+ .hint.visible {
403
+ opacity: 0.4;
404
+ }
405
+
406
+ /* Print / PDF */
407
+ @media print {
408
+ .slide {
409
+ position: relative;
410
+ opacity: 1;
411
+ transform: none;
412
+ page-break-after: always;
413
+ min-height: 100vh;
414
+ }
415
+ .progress, .slide-number, .hint, .notes-overlay {
416
+ display: none;
417
+ }
418
+ }
419
+
420
+ /* Animations */
421
+ @keyframes fadeInUp {
422
+ from { opacity: 0; transform: translateY(20px); }
423
+ to { opacity: 1; transform: translateY(0); }
424
+ }
425
+
426
+ .slide.active h1,
427
+ .slide.active h2,
428
+ .slide.active h3 {
429
+ animation: fadeInUp 0.6s ease forwards;
430
+ }
431
+
432
+ .slide.active p,
433
+ .slide.active ul,
434
+ .slide.active ol,
435
+ .slide.active pre,
436
+ .slide.active blockquote,
437
+ .slide.active table {
438
+ animation: fadeInUp 0.6s ease 0.15s forwards;
439
+ opacity: 0;
440
+ }
441
+ `;
442
+
443
+ export function getTheme(name) {
444
+ const theme = themes[name] || themes.dark;
445
+ return theme.css;
446
+ }
447
+
448
+ export function getThemeNames() {
449
+ return Object.keys(themes);
450
+ }
451
+
452
+ export default themes;