|
103 | 103 | @media (max-height: 500px) { :root { --slide-padding: clamp(0.4rem, 2vw, 1rem); --title-size: clamp(1rem, 3.5vw, 1.5rem); --h2-size: clamp(0.9rem, 2.5vw, 1.25rem); --body-size: clamp(0.65rem, 1vw, 0.85rem); } } |
104 | 104 | @media (max-width: 600px) { :root { --title-size: clamp(1.25rem, 7vw, 2.5rem); } .two-col { flex-direction: column !important; } } |
105 | 105 | @media (prefers-reduced-motion: reduce) { *, *::before, *::after { animation-duration: 0.01ms !important; transition-duration: 0.2s !important; } html { scroll-behavior: auto; } } |
| 106 | +.notes-panel { position: fixed; bottom: 0; left: 0; right: 0; height: 180px; background: #1a1a2e; border-top: 2px solid #cc342d; display: none; flex-direction: column; z-index: 9000; font-family: var(--font-body); } |
| 107 | +.notes-panel.active { display: flex; } |
| 108 | +.notes-panel-header { display: flex; justify-content: space-between; align-items: center; padding: 6px 16px; background: #252540; font-size: 12px; color: #888; } |
| 109 | +.notes-panel-header .notes-label { font-weight: 600; color: #ccc; } |
| 110 | +.notes-panel-header .notes-status { font-style: italic; } |
| 111 | +.notes-panel textarea { flex: 1; background: #1a1a2e; color: #e0e0e0; border: none; padding: 12px 16px; font-family: var(--font-body); font-size: 15px; line-height: 1.5; resize: none; outline: none; } |
| 112 | +.notes-panel textarea::placeholder { color: #555; } |
106 | 113 | .speaker-notes { display: none; } |
107 | 114 | .blackout-overlay { position: fixed; inset: 0; background: #000; z-index: 10000; display: none; } |
108 | 115 | .blackout-overlay.active { display: block; } |
|
115 | 122 | <button class="edit-toggle" id="editToggle" title="Edit mode (E)">✎</button> |
116 | 123 | <div class="edit-banner" id="editBanner">EDIT MODE — Click any text to edit · Ctrl+S to save</div> |
117 | 124 | <div class="blackout-overlay" id="blackoutOverlay"></div> |
| 125 | +<div class="notes-panel" id="notesPanel"> |
| 126 | + <div class="notes-panel-header"> |
| 127 | + <span class="notes-label">Speaker Notes (slide <span id="noteSlideNum">1</span>/33)</span> |
| 128 | + <span class="notes-status" id="noteStatus">Press N to toggle</span> |
| 129 | + </div> |
| 130 | + <textarea id="notesTextarea" placeholder="Type your speaker notes here..."></textarea> |
| 131 | +</div> |
118 | 132 |
|
119 | 133 | <!-- SLIDE 1: TITLE --> |
120 | 134 | <section class="slide title-slide visible"> |
@@ -565,7 +579,7 @@ <h1 class="reveal" style="font-size:clamp(2rem,6vw,4.5rem)">Thank you!</h1> |
565 | 579 | } |
566 | 580 | setupKeyboardNav() { |
567 | 581 | document.addEventListener('keydown', (e) => { |
568 | | - if (e.target.getAttribute('contenteditable')) return; |
| 582 | + if (e.target.getAttribute('contenteditable') || e.target.tagName === 'TEXTAREA' || e.target.tagName === 'INPUT') return; |
569 | 583 | switch(e.key) { |
570 | 584 | case 'ArrowDown': case 'ArrowRight': case ' ': case 'PageDown': |
571 | 585 | e.preventDefault(); this.goToSlide(this.currentSlide + 1); break; |
@@ -622,7 +636,8 @@ <h1 class="reveal" style="font-size:clamp(2rem,6vw,4.5rem)">Thank you!</h1> |
622 | 636 | } |
623 | 637 | } |
624 | 638 | class InlineEditor { |
625 | | - constructor() { |
| 639 | + constructor(notesEditor) { |
| 640 | + this.notesEditor = notesEditor; |
626 | 641 | this.isActive = false; |
627 | 642 | this.setupHotzone(); |
628 | 643 | this.setupKeyboard(); |
@@ -665,13 +680,16 @@ <h1 class="reveal" style="font-size:clamp(2rem,6vw,4.5rem)">Thank you!</h1> |
665 | 680 | } |
666 | 681 | } |
667 | 682 | async exportFile() { |
| 683 | + if (this.notesEditor) this.notesEditor.syncToDOM(); |
668 | 684 | const editableEls = Array.from(document.querySelectorAll('[contenteditable]')); |
669 | 685 | editableEls.forEach(el => el.removeAttribute('contenteditable')); |
670 | 686 | document.body.classList.remove('edit-active'); |
671 | 687 | const toggle = document.getElementById('editToggle'); |
672 | 688 | const banner = document.getElementById('editBanner'); |
673 | 689 | if (toggle) { toggle.classList.remove('active', 'show'); } |
674 | 690 | if (banner) { banner.classList.remove('active'); } |
| 691 | + const notesPanel = document.getElementById('notesPanel'); |
| 692 | + if (notesPanel) notesPanel.classList.remove('active'); |
675 | 693 | const html = '<!DOCTYPE html>\n' + document.documentElement.outerHTML; |
676 | 694 | document.body.classList.add('edit-active'); |
677 | 695 | editableEls.forEach(el => el.setAttribute('contenteditable', 'true')); |
@@ -707,6 +725,99 @@ <h1 class="reveal" style="font-size:clamp(2rem,6vw,4.5rem)">Thank you!</h1> |
707 | 725 | URL.revokeObjectURL(a.href); |
708 | 726 | } |
709 | 727 | } |
| 728 | +class NotesEditor { |
| 729 | + constructor(slidePresentation) { |
| 730 | + this.sp = slidePresentation; |
| 731 | + this.panel = document.getElementById('notesPanel'); |
| 732 | + this.textarea = document.getElementById('notesTextarea'); |
| 733 | + this.slideNumEl = document.getElementById('noteSlideNum'); |
| 734 | + this.statusEl = document.getElementById('noteStatus'); |
| 735 | + this.isOpen = false; |
| 736 | + this.storagePrefix = 'slide-notes-'; |
| 737 | + this.loadAllFromStorage(); |
| 738 | + this.setupKeyboard(); |
| 739 | + this.setupAutoSave(); |
| 740 | + this.setupSlideTracking(); |
| 741 | + } |
| 742 | + setupKeyboard() { |
| 743 | + document.addEventListener('keydown', (e) => { |
| 744 | + if (e.target === this.textarea) return; |
| 745 | + if ((e.key === 'n' || e.key === 'N') && !e.target.getAttribute('contenteditable')) this.toggle(); |
| 746 | + }); |
| 747 | + this.textarea.addEventListener('keydown', (e) => { |
| 748 | + if (e.key === 'Escape') { this.toggle(); e.preventDefault(); } |
| 749 | + if (e.ctrlKey && e.key === 's') { |
| 750 | + e.preventDefault(); |
| 751 | + this.syncToDOM(); |
| 752 | + this.statusEl.textContent = 'Notes synced to HTML'; |
| 753 | + clearTimeout(this._statusTimeout); |
| 754 | + this._statusTimeout = setTimeout(() => { this.statusEl.textContent = ''; }, 1500); |
| 755 | + } |
| 756 | + }); |
| 757 | + } |
| 758 | + setupAutoSave() { |
| 759 | + this.textarea.addEventListener('input', () => { |
| 760 | + const idx = this.sp.currentSlide; |
| 761 | + localStorage.setItem(this.storagePrefix + idx, this.textarea.value); |
| 762 | + this.statusEl.textContent = 'Saved'; |
| 763 | + clearTimeout(this._statusTimeout); |
| 764 | + this._statusTimeout = setTimeout(() => { this.statusEl.textContent = ''; }, 1500); |
| 765 | + }); |
| 766 | + } |
| 767 | + setupSlideTracking() { |
| 768 | + let lastSlide = -1; |
| 769 | + setInterval(() => { |
| 770 | + if (this.sp.currentSlide !== lastSlide) { |
| 771 | + lastSlide = this.sp.currentSlide; |
| 772 | + this.loadNoteForSlide(lastSlide); |
| 773 | + } |
| 774 | + }, 300); |
| 775 | + } |
| 776 | + loadNoteForSlide(idx) { |
| 777 | + this.slideNumEl.textContent = idx + 1; |
| 778 | + const stored = localStorage.getItem(this.storagePrefix + idx); |
| 779 | + if (stored !== null) { |
| 780 | + this.textarea.value = stored; |
| 781 | + } else { |
| 782 | + const slide = this.sp.slides[idx]; |
| 783 | + const notesEl = slide ? slide.querySelector('.speaker-notes') : null; |
| 784 | + this.textarea.value = notesEl ? notesEl.textContent.trim() : ''; |
| 785 | + } |
| 786 | + } |
| 787 | + loadAllFromStorage() { |
| 788 | + this.sp.slides.forEach((slide, idx) => { |
| 789 | + if (localStorage.getItem(this.storagePrefix + idx) === null) { |
| 790 | + const notesEl = slide.querySelector('.speaker-notes'); |
| 791 | + if (notesEl && notesEl.textContent.trim()) { |
| 792 | + localStorage.setItem(this.storagePrefix + idx, notesEl.textContent.trim()); |
| 793 | + } |
| 794 | + } |
| 795 | + }); |
| 796 | + } |
| 797 | + toggle() { |
| 798 | + this.isOpen = !this.isOpen; |
| 799 | + this.panel.classList.toggle('active', this.isOpen); |
| 800 | + if (this.isOpen) { |
| 801 | + this.loadNoteForSlide(this.sp.currentSlide); |
| 802 | + this.textarea.focus(); |
| 803 | + } |
| 804 | + } |
| 805 | + syncToDOM() { |
| 806 | + this.sp.slides.forEach((slide, idx) => { |
| 807 | + const stored = localStorage.getItem(this.storagePrefix + idx); |
| 808 | + if (stored !== null) { |
| 809 | + let notesEl = slide.querySelector('.speaker-notes'); |
| 810 | + if (!notesEl) { |
| 811 | + notesEl = document.createElement('div'); |
| 812 | + notesEl.className = 'speaker-notes'; |
| 813 | + notesEl.hidden = true; |
| 814 | + slide.appendChild(notesEl); |
| 815 | + } |
| 816 | + notesEl.textContent = stored; |
| 817 | + } |
| 818 | + }); |
| 819 | + } |
| 820 | +} |
710 | 821 | class PresenterMode { |
711 | 822 | constructor(slidePresentation) { |
712 | 823 | this.sp = slidePresentation; |
@@ -900,7 +1011,8 @@ <h1 class="reveal" style="font-size:clamp(2rem,6vw,4.5rem)">Thank you!</h1> |
900 | 1011 | } |
901 | 1012 |
|
902 | 1013 | const sp = new SlidePresentation(); |
903 | | -new InlineEditor(); |
| 1014 | +const notesEditor = new NotesEditor(sp); |
| 1015 | +const editor = new InlineEditor(notesEditor); |
904 | 1016 | new PresenterMode(sp); |
905 | 1017 | </script> |
906 | 1018 |
|
|
0 commit comments