md-slides 1.0.0 โ†’ 1.0.1

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.
package/README.md CHANGED
@@ -1,4 +1,7 @@
1
- # @dprrwt/slides
1
+ # md-slides
2
+
3
+ [![npm version](https://img.shields.io/npm/v/md-slides.svg)](https://www.npmjs.com/package/md-slides)
4
+ [![license](https://img.shields.io/npm/l/md-slides.svg)](https://github.com/dprrwt/md-slides/blob/master/LICENSE)
2
5
 
3
6
  > Convert Markdown to beautiful presentation slides. Zero config, developer-friendly.
4
7
 
@@ -20,13 +23,13 @@ Write your presentation in **Markdown**, present it in the **browser**, deploy i
20
23
  ## ๐Ÿ“ฆ Install
21
24
 
22
25
  ```bash
23
- npm install -g @dprrwt/slides
26
+ npm install -g md-slides
24
27
  ```
25
28
 
26
29
  Or use directly:
27
30
 
28
31
  ```bash
29
- npx @dprrwt/slides init my-talk
32
+ npx md-slides init my-talk
30
33
  ```
31
34
 
32
35
  ## ๐Ÿš€ Quick Start
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "md-slides",
3
- "version": "1.0.0",
3
+ "version": "1.0.1",
4
4
  "description": "Convert Markdown to beautiful presentation slides. Zero config, developer-friendly.",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
package/src/renderer.js CHANGED
@@ -28,9 +28,25 @@ function getScript(slideCount, liveReload = false) {
28
28
 
29
29
  function goTo(n) {
30
30
  if (n < 0 || n >= total) return;
31
- slides[current].classList.remove('active');
32
- slides[current].classList.add(n > current ? 'prev' : '');
31
+ var goingForward = n > current;
32
+
33
+ // Remove active from old slide
34
+ slides[current].classList.remove('active', 'prev');
35
+ if (goingForward) {
36
+ slides[current].classList.add('prev');
37
+ }
38
+
33
39
  current = n;
40
+
41
+ // For backward nav, ensure slide starts from left position
42
+ if (!goingForward) {
43
+ slides[current].classList.add('prev');
44
+ }
45
+
46
+ // Force reflow to re-trigger CSS animations
47
+ void slides[current].offsetWidth;
48
+
49
+ // Activate new slide
34
50
  slides[current].classList.remove('prev');
35
51
  slides[current].classList.add('active');
36
52
 
@@ -166,7 +182,7 @@ export function renderHTML(slidesData, options = {}) {
166
182
  if (slide.directives.class) {
167
183
  classes.push(slide.directives.class);
168
184
  }
169
- if (i === 0) classes.push('active');
185
+ // active class is set by JS goTo() on init โ€” not in HTML
170
186
 
171
187
  let style = '';
172
188
  if (slide.directives.background) {
package/src/themes.js CHANGED
@@ -426,7 +426,7 @@ export const baseStyles = `
426
426
  .slide.active h1,
427
427
  .slide.active h2,
428
428
  .slide.active h3 {
429
- animation: fadeInUp 0.6s ease forwards;
429
+ animation: fadeInUp 0.6s ease both;
430
430
  }
431
431
 
432
432
  .slide.active p,
@@ -435,8 +435,7 @@ export const baseStyles = `
435
435
  .slide.active pre,
436
436
  .slide.active blockquote,
437
437
  .slide.active table {
438
- animation: fadeInUp 0.6s ease 0.15s forwards;
439
- opacity: 0;
438
+ animation: fadeInUp 0.6s ease 0.15s both;
440
439
  }
441
440
  `;
442
441
 
@@ -0,0 +1,733 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>md-slides Demo</title>
7
+ <meta name="author" content="Deepankar Rawat">
8
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.10.0/styles/github-dark.min.css">
9
+ <style>
10
+
11
+ :root {
12
+ --bg: #0a0a0f;
13
+ --bg-slide: #12121a;
14
+ --fg: #e8e8f0;
15
+ --fg-dim: #8888aa;
16
+ --accent: #6c63ff;
17
+ --accent-glow: rgba(108, 99, 255, 0.3);
18
+ --code-bg: #1a1a2e;
19
+ --code-border: #2a2a4a;
20
+ --blockquote-border: #6c63ff;
21
+ --link: #8b83ff;
22
+ --font-heading: 'Inter', system-ui, sans-serif;
23
+ --font-body: 'Inter', system-ui, sans-serif;
24
+ --font-code: 'JetBrains Mono', 'Fira Code', monospace;
25
+ --radius: 12px;
26
+ }
27
+
28
+
29
+ @import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&display=swap');
30
+ @import url('https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500&display=swap');
31
+
32
+ *, *::before, *::after {
33
+ box-sizing: border-box;
34
+ margin: 0;
35
+ padding: 0;
36
+ }
37
+
38
+ html, body {
39
+ height: 100%;
40
+ overflow: hidden;
41
+ background: var(--bg);
42
+ color: var(--fg);
43
+ font-family: var(--font-body);
44
+ -webkit-font-smoothing: antialiased;
45
+ -moz-osx-font-smoothing: grayscale;
46
+ }
47
+
48
+ /* Slide Container */
49
+ .deck {
50
+ position: relative;
51
+ width: 100vw;
52
+ height: 100vh;
53
+ }
54
+
55
+ .slide {
56
+ position: absolute;
57
+ top: 0;
58
+ left: 0;
59
+ width: 100%;
60
+ height: 100%;
61
+ display: flex;
62
+ flex-direction: column;
63
+ justify-content: center;
64
+ padding: 8vh 10vw;
65
+ background: var(--bg-slide);
66
+ opacity: 0;
67
+ transform: translateX(40px);
68
+ transition: opacity 0.5s ease, transform 0.5s ease;
69
+ pointer-events: none;
70
+ overflow-y: auto;
71
+ }
72
+
73
+ .slide.active {
74
+ opacity: 1;
75
+ transform: translateX(0);
76
+ pointer-events: auto;
77
+ z-index: 1;
78
+ }
79
+
80
+ .slide.prev {
81
+ opacity: 0;
82
+ transform: translateX(-40px);
83
+ }
84
+
85
+ /* Layouts */
86
+ .slide.layout-center {
87
+ align-items: center;
88
+ text-align: center;
89
+ justify-content: center;
90
+ }
91
+
92
+ .slide.layout-title {
93
+ justify-content: center;
94
+ }
95
+
96
+ .slide.layout-title h1 {
97
+ font-size: clamp(2.5rem, 6vw, 5rem);
98
+ margin-bottom: 1rem;
99
+ }
100
+
101
+ .slide.layout-quote {
102
+ align-items: center;
103
+ justify-content: center;
104
+ text-align: center;
105
+ padding: 10vh 15vw;
106
+ }
107
+
108
+ .slide.layout-code {
109
+ justify-content: center;
110
+ }
111
+
112
+ .slide.layout-image {
113
+ align-items: center;
114
+ justify-content: center;
115
+ }
116
+
117
+ .slide.layout-image img {
118
+ max-width: 80%;
119
+ max-height: 70vh;
120
+ border-radius: var(--radius);
121
+ }
122
+
123
+ /* Typography */
124
+ h1 {
125
+ font-family: var(--font-heading);
126
+ font-size: clamp(2rem, 5vw, 4rem);
127
+ font-weight: 800;
128
+ letter-spacing: -0.03em;
129
+ line-height: 1.1;
130
+ margin-bottom: 0.8em;
131
+ background: linear-gradient(135deg, var(--fg), var(--accent));
132
+ -webkit-background-clip: text;
133
+ -webkit-text-fill-color: transparent;
134
+ background-clip: text;
135
+ }
136
+
137
+ h2 {
138
+ font-family: var(--font-heading);
139
+ font-size: clamp(1.5rem, 3vw, 2.5rem);
140
+ font-weight: 700;
141
+ letter-spacing: -0.02em;
142
+ line-height: 1.2;
143
+ margin-bottom: 0.6em;
144
+ color: var(--accent);
145
+ }
146
+
147
+ h3 {
148
+ font-family: var(--font-heading);
149
+ font-size: clamp(1.2rem, 2vw, 1.8rem);
150
+ font-weight: 600;
151
+ margin-bottom: 0.5em;
152
+ color: var(--fg);
153
+ }
154
+
155
+ p {
156
+ font-size: clamp(1rem, 1.8vw, 1.4rem);
157
+ line-height: 1.7;
158
+ margin-bottom: 0.8em;
159
+ color: var(--fg-dim);
160
+ }
161
+
162
+ strong {
163
+ color: var(--fg);
164
+ font-weight: 600;
165
+ }
166
+
167
+ em {
168
+ color: var(--accent);
169
+ font-style: italic;
170
+ }
171
+
172
+ a {
173
+ color: var(--link);
174
+ text-decoration: none;
175
+ border-bottom: 1px solid transparent;
176
+ transition: border-color 0.2s;
177
+ }
178
+
179
+ a:hover {
180
+ border-bottom-color: var(--link);
181
+ }
182
+
183
+ /* Lists */
184
+ ul, ol {
185
+ font-size: clamp(1rem, 1.6vw, 1.3rem);
186
+ line-height: 1.8;
187
+ padding-left: 1.5em;
188
+ margin-bottom: 1em;
189
+ color: var(--fg-dim);
190
+ }
191
+
192
+ li {
193
+ margin-bottom: 0.4em;
194
+ }
195
+
196
+ li::marker {
197
+ color: var(--accent);
198
+ }
199
+
200
+ /* Code */
201
+ code {
202
+ font-family: var(--font-code);
203
+ font-size: 0.85em;
204
+ background: var(--code-bg);
205
+ border: 1px solid var(--code-border);
206
+ padding: 0.15em 0.4em;
207
+ border-radius: 4px;
208
+ color: var(--accent);
209
+ }
210
+
211
+ pre {
212
+ margin: 1em 0;
213
+ border-radius: var(--radius);
214
+ overflow: hidden;
215
+ border: 1px solid var(--code-border);
216
+ }
217
+
218
+ pre code {
219
+ display: block;
220
+ padding: 1.5em 2em;
221
+ background: var(--code-bg);
222
+ border: none;
223
+ font-size: clamp(0.8rem, 1.2vw, 1rem);
224
+ line-height: 1.6;
225
+ overflow-x: auto;
226
+ color: var(--fg);
227
+ }
228
+
229
+ /* Blockquotes */
230
+ blockquote {
231
+ border-left: 4px solid var(--blockquote-border);
232
+ padding: 1em 1.5em;
233
+ margin: 1em 0;
234
+ background: var(--accent-glow);
235
+ border-radius: 0 var(--radius) var(--radius) 0;
236
+ }
237
+
238
+ blockquote p {
239
+ font-size: clamp(1.2rem, 2vw, 1.8rem);
240
+ font-style: italic;
241
+ color: var(--fg);
242
+ margin-bottom: 0;
243
+ }
244
+
245
+ /* Tables */
246
+ table {
247
+ width: 100%;
248
+ border-collapse: collapse;
249
+ margin: 1em 0;
250
+ font-size: clamp(0.85rem, 1.3vw, 1.1rem);
251
+ }
252
+
253
+ th, td {
254
+ padding: 0.75em 1em;
255
+ text-align: left;
256
+ border-bottom: 1px solid var(--code-border);
257
+ }
258
+
259
+ th {
260
+ font-weight: 600;
261
+ color: var(--accent);
262
+ }
263
+
264
+ td {
265
+ color: var(--fg-dim);
266
+ }
267
+
268
+ /* Images */
269
+ img {
270
+ max-width: 100%;
271
+ border-radius: var(--radius);
272
+ }
273
+
274
+ /* Progress bar */
275
+ .progress {
276
+ position: fixed;
277
+ bottom: 0;
278
+ left: 0;
279
+ height: 3px;
280
+ background: var(--accent);
281
+ transition: width 0.4s ease;
282
+ z-index: 100;
283
+ box-shadow: 0 0 10px var(--accent-glow);
284
+ }
285
+
286
+ /* Slide number */
287
+ .slide-number {
288
+ position: fixed;
289
+ bottom: 16px;
290
+ right: 24px;
291
+ font-family: var(--font-code);
292
+ font-size: 0.8rem;
293
+ color: var(--fg-dim);
294
+ opacity: 0.5;
295
+ z-index: 100;
296
+ }
297
+
298
+ /* Presenter notes toggle */
299
+ .notes-overlay {
300
+ display: none;
301
+ position: fixed;
302
+ bottom: 0;
303
+ left: 0;
304
+ right: 0;
305
+ background: rgba(0,0,0,0.9);
306
+ color: #ccc;
307
+ padding: 1.5em 2em;
308
+ font-size: 0.9rem;
309
+ line-height: 1.6;
310
+ z-index: 200;
311
+ max-height: 30vh;
312
+ overflow-y: auto;
313
+ border-top: 2px solid var(--accent);
314
+ }
315
+
316
+ .notes-overlay.visible {
317
+ display: block;
318
+ }
319
+
320
+ /* Keyboard hint */
321
+ .hint {
322
+ position: fixed;
323
+ bottom: 40px;
324
+ left: 50%;
325
+ transform: translateX(-50%);
326
+ font-family: var(--font-code);
327
+ font-size: 0.75rem;
328
+ color: var(--fg-dim);
329
+ opacity: 0;
330
+ transition: opacity 0.5s;
331
+ z-index: 100;
332
+ }
333
+
334
+ .hint.visible {
335
+ opacity: 0.4;
336
+ }
337
+
338
+ /* Print / PDF */
339
+ @media print {
340
+ .slide {
341
+ position: relative;
342
+ opacity: 1;
343
+ transform: none;
344
+ page-break-after: always;
345
+ min-height: 100vh;
346
+ }
347
+ .progress, .slide-number, .hint, .notes-overlay {
348
+ display: none;
349
+ }
350
+ }
351
+
352
+ /* Animations */
353
+ @keyframes fadeInUp {
354
+ from { opacity: 0; transform: translateY(20px); }
355
+ to { opacity: 1; transform: translateY(0); }
356
+ }
357
+
358
+ .slide.active h1,
359
+ .slide.active h2,
360
+ .slide.active h3 {
361
+ animation: fadeInUp 0.6s ease both;
362
+ }
363
+
364
+ .slide.active p,
365
+ .slide.active ul,
366
+ .slide.active ol,
367
+ .slide.active pre,
368
+ .slide.active blockquote,
369
+ .slide.active table {
370
+ animation: fadeInUp 0.6s ease 0.15s both;
371
+ }
372
+
373
+ </style>
374
+ </head>
375
+ <body>
376
+ <div class="deck">
377
+ <section class="slide layout-title">
378
+ <div class="slide-content">
379
+ <h1>md-slides โœจ</h1>
380
+ <p>Markdown โ†’ Beautiful Slides</p>
381
+ <p><em>Zero config. Developer-friendly.</em></p>
382
+
383
+ </div>
384
+ </section>
385
+ <section class="slide layout-default" data-notes="This tool exists because making slides should be as easy as writing markdown.">
386
+ <div class="slide-content">
387
+ <h2>Why md-slides?</h2>
388
+ <ul>
389
+ <li><strong>Write in Markdown</strong> โ€” your editor, your flow</li>
390
+ <li><strong>4 themes</strong> โ€” Dark, Light, Minimal, Neon</li>
391
+ <li><strong>Live reload</strong> โ€” see changes instantly</li>
392
+ <li><strong>Single HTML output</strong> โ€” deploy anywhere</li>
393
+ <li><strong>Speaker notes</strong> โ€” press <code>S</code> to toggle</li>
394
+ </ul>
395
+
396
+ </div>
397
+ </section>
398
+ <section class="slide layout-code">
399
+ <div class="slide-content">
400
+ <h2>Getting Started</h2>
401
+ <pre><code class="language-bash"># Install globally
402
+ npm install -g @dprrwt/slides
403
+
404
+ # Create a new presentation
405
+ slides init my-talk
406
+
407
+ # Start editing and previewing
408
+ cd my-talk
409
+ slides preview
410
+ </code></pre>
411
+
412
+ </div>
413
+ </section>
414
+ <section class="slide layout-code">
415
+ <div class="slide-content">
416
+ <h2>Slide Syntax</h2>
417
+ <p>Separate slides with <code>---</code> on its own line:</p>
418
+ <pre><code class="language-markdown"># Title Slide
419
+
420
+ Content here.
421
+
422
+ ---
423
+
424
+ ## Next Slide
425
+
426
+ More content.
427
+
428
+ ---
429
+
430
+ &gt; A quote slide.
431
+ </code></pre>
432
+
433
+ </div>
434
+ </section>
435
+ <section class="slide layout-code">
436
+ <div class="slide-content">
437
+ <h2>Code Highlighting</h2>
438
+ <p>190+ languages supported out of the box:</p>
439
+ <pre><code class="language-typescript">interface Slide {
440
+ html: string;
441
+ notes: string;
442
+ layout: &#39;title&#39; | &#39;center&#39; | &#39;code&#39; | &#39;quote&#39;;
443
+ }
444
+
445
+ function present(slides: Slide[]): void {
446
+ slides.forEach((slide, i) =&gt; {
447
+ render(slide, { index: i, total: slides.length });
448
+ });
449
+ }
450
+ </code></pre>
451
+
452
+ </div>
453
+ </section>
454
+ <section class="slide layout-default">
455
+ <div class="slide-content">
456
+ <h2>Smart Layouts</h2>
457
+ <p>Slides auto-detect their layout:</p>
458
+ <table>
459
+ <thead>
460
+ <tr>
461
+ <th>Content</th>
462
+ <th>Layout</th>
463
+ </tr>
464
+ </thead>
465
+ <tbody><tr>
466
+ <td>Just a heading</td>
467
+ <td><strong>Center</strong></td>
468
+ </tr>
469
+ <tr>
470
+ <td>H1 with content</td>
471
+ <td><strong>Title</strong></td>
472
+ </tr>
473
+ <tr>
474
+ <td>Blockquote</td>
475
+ <td><strong>Quote</strong></td>
476
+ </tr>
477
+ <tr>
478
+ <td>Code block</td>
479
+ <td><strong>Code</strong></td>
480
+ </tr>
481
+ <tr>
482
+ <td>Everything else</td>
483
+ <td><strong>Default</strong></td>
484
+ </tr>
485
+ </tbody></table>
486
+
487
+ </div>
488
+ </section>
489
+ <section class="slide layout-quote" data-notes="This is a quote slide โ€” detected automatically from the blockquote.">
490
+ <div class="slide-content">
491
+ <blockquote>
492
+ <p>&quot;The best presentations are written, not designed.&quot;</p>
493
+ </blockquote>
494
+
495
+ </div>
496
+ </section>
497
+ <section class="slide layout-code">
498
+ <div class="slide-content">
499
+ <h2>Themes</h2>
500
+ <p>Set via frontmatter or CLI:</p>
501
+ <pre><code class="language-yaml">---
502
+ theme: neon
503
+ ---
504
+ </code></pre>
505
+ <pre><code class="language-bash">slides build --theme light
506
+ slides preview --theme minimal
507
+ </code></pre>
508
+ <p><strong>Built-in:</strong> <code>dark</code> ยท <code>light</code> ยท <code>minimal</code> ยท <code>neon</code></p>
509
+
510
+ </div>
511
+ </section>
512
+ <section class="slide layout-code" data-notes="Only you can see this. Press S.">
513
+ <div class="slide-content">
514
+ <h2>Speaker Notes</h2>
515
+ <p>Add notes with HTML comments:</p>
516
+ <pre><code class="language-markdown">## My Slide
517
+
518
+ Content visible to audience.
519
+
520
+ </code></pre>
521
+ <p>Press <strong>S</strong> now to see the notes panel โ†’</p>
522
+ <!-- notes: ๐Ÿ‘‹ You found the notes! These are only visible to the presenter. Use them for talking points, reminders, or timing cues. -->
523
+ </div>
524
+ </section>
525
+ <section class="slide layout-default">
526
+ <div class="slide-content">
527
+ <h2>Navigation</h2>
528
+ <table>
529
+ <thead>
530
+ <tr>
531
+ <th>Key</th>
532
+ <th>Action</th>
533
+ </tr>
534
+ </thead>
535
+ <tbody><tr>
536
+ <td><code>โ†’</code> <code>โ†“</code> <code>Space</code></td>
537
+ <td>Next slide</td>
538
+ </tr>
539
+ <tr>
540
+ <td><code>โ†</code> <code>โ†‘</code></td>
541
+ <td>Previous slide</td>
542
+ </tr>
543
+ <tr>
544
+ <td><code>S</code></td>
545
+ <td>Speaker notes</td>
546
+ </tr>
547
+ <tr>
548
+ <td><code>F</code></td>
549
+ <td>Fullscreen</td>
550
+ </tr>
551
+ <tr>
552
+ <td><code>Home</code> / <code>End</code></td>
553
+ <td>First / Last</td>
554
+ </tr>
555
+ </tbody></table>
556
+ <p><strong>Touch:</strong> Swipe left/right</p>
557
+ <p><strong>Click:</strong> Right half โ†’ next, Left half โ†’ prev</p>
558
+
559
+ </div>
560
+ </section>
561
+ <section class="slide layout-code">
562
+ <div class="slide-content">
563
+ <h2>Deploy Anywhere</h2>
564
+ <p>Output is a <strong>single <code>index.html</code></strong> file:</p>
565
+ <pre><code class="language-bash"># Build
566
+ slides build
567
+
568
+ # Deploy to GitHub Pages
569
+ cd dist
570
+ git init &amp;&amp; git add . &amp;&amp; git commit -m &quot;slides&quot;
571
+ git push origin gh-pages
572
+ </code></pre>
573
+ <p>Or just email the HTML file. It&#39;s self-contained.</p>
574
+
575
+ </div>
576
+ </section>
577
+ <section class="slide layout-title">
578
+ <div class="slide-content">
579
+ <h1>Ship It ๐Ÿš€</h1>
580
+ <pre><code class="language-bash">npm install -g @dprrwt/slides
581
+ </code></pre>
582
+ <p><strong>GitHub:</strong> <a href="https://github.com/dprrwt/md-slides">dprrwt/md-slides</a></p>
583
+ <p><strong>Author:</strong> <a href="https://dprrwt.me">dprrwt.me</a></p>
584
+
585
+ </div>
586
+ </section>
587
+ </div>
588
+
589
+ <div class="progress" style="width: 0%"></div>
590
+ <div class="slide-number">1 / 12</div>
591
+ <div class="notes-overlay"></div>
592
+ <div class="hint">โ† โ†’ navigate ยท S notes ยท F fullscreen</div>
593
+
594
+ <script>
595
+
596
+ (function() {
597
+ let current = 0;
598
+ const total = 12;
599
+ const slides = document.querySelectorAll('.slide');
600
+ const progress = document.querySelector('.progress');
601
+ const slideNum = document.querySelector('.slide-number');
602
+ const notesOverlay = document.querySelector('.notes-overlay');
603
+ const hint = document.querySelector('.hint');
604
+ let notesVisible = false;
605
+
606
+ // Parse hash
607
+ const hash = parseInt(window.location.hash.replace('#', ''), 10);
608
+ if (!isNaN(hash) && hash >= 0 && hash < total) {
609
+ current = hash;
610
+ }
611
+
612
+ function goTo(n) {
613
+ if (n < 0 || n >= total) return;
614
+ var goingForward = n > current;
615
+
616
+ // Remove active from old slide
617
+ slides[current].classList.remove('active', 'prev');
618
+ if (goingForward) {
619
+ slides[current].classList.add('prev');
620
+ }
621
+
622
+ current = n;
623
+
624
+ // For backward nav, ensure slide starts from left position
625
+ if (!goingForward) {
626
+ slides[current].classList.add('prev');
627
+ }
628
+
629
+ // Force reflow to re-trigger CSS animations
630
+ void slides[current].offsetWidth;
631
+
632
+ // Activate new slide
633
+ slides[current].classList.remove('prev');
634
+ slides[current].classList.add('active');
635
+
636
+ progress.style.width = ((current + 1) / total * 100) + '%';
637
+ slideNum.textContent = (current + 1) + ' / ' + total;
638
+ window.location.hash = current;
639
+
640
+ // Update notes
641
+ const noteText = slides[current].dataset.notes || '';
642
+ notesOverlay.textContent = noteText || '(no notes for this slide)';
643
+ }
644
+
645
+ function next() { goTo(current + 1); }
646
+ function prev() { goTo(current - 1); }
647
+ function first() { goTo(0); }
648
+ function last() { goTo(total - 1); }
649
+
650
+ // Keyboard
651
+ document.addEventListener('keydown', function(e) {
652
+ hint.classList.remove('visible');
653
+ switch(e.key) {
654
+ case 'ArrowRight':
655
+ case 'ArrowDown':
656
+ case ' ':
657
+ case 'l':
658
+ case 'j':
659
+ e.preventDefault();
660
+ next();
661
+ break;
662
+ case 'ArrowLeft':
663
+ case 'ArrowUp':
664
+ case 'h':
665
+ case 'k':
666
+ e.preventDefault();
667
+ prev();
668
+ break;
669
+ case 'Home':
670
+ e.preventDefault();
671
+ first();
672
+ break;
673
+ case 'End':
674
+ e.preventDefault();
675
+ last();
676
+ break;
677
+ case 's':
678
+ case 'n':
679
+ notesVisible = !notesVisible;
680
+ notesOverlay.classList.toggle('visible', notesVisible);
681
+ break;
682
+ case 'f':
683
+ if (document.fullscreenElement) {
684
+ document.exitFullscreen();
685
+ } else {
686
+ document.documentElement.requestFullscreen();
687
+ }
688
+ break;
689
+ case 'Escape':
690
+ notesOverlay.classList.remove('visible');
691
+ notesVisible = false;
692
+ break;
693
+ }
694
+ });
695
+
696
+ // Touch / swipe
697
+ let touchStartX = 0;
698
+ let touchStartY = 0;
699
+ document.addEventListener('touchstart', function(e) {
700
+ touchStartX = e.touches[0].clientX;
701
+ touchStartY = e.touches[0].clientY;
702
+ });
703
+ document.addEventListener('touchend', function(e) {
704
+ const dx = e.changedTouches[0].clientX - touchStartX;
705
+ const dy = e.changedTouches[0].clientY - touchStartY;
706
+ if (Math.abs(dx) > Math.abs(dy) && Math.abs(dx) > 50) {
707
+ dx > 0 ? prev() : next();
708
+ }
709
+ });
710
+
711
+ // Click (right half = next, left half = prev)
712
+ document.addEventListener('click', function(e) {
713
+ if (e.target.tagName === 'A' || e.target.tagName === 'BUTTON') return;
714
+ if (e.clientX > window.innerWidth / 2) {
715
+ next();
716
+ } else {
717
+ prev();
718
+ }
719
+ });
720
+
721
+ // Init
722
+ goTo(current);
723
+
724
+ // Show hint briefly
725
+ setTimeout(function() { hint.classList.add('visible'); }, 1000);
726
+ setTimeout(function() { hint.classList.remove('visible'); }, 5000);
727
+
728
+
729
+ })();
730
+
731
+ </script>
732
+ </body>
733
+ </html>