Skip to content

Commit a307f37

Browse files
committed
Add inline speaker notes editor (press N)
1 parent ea900b3 commit a307f37

1 file changed

Lines changed: 115 additions & 3 deletions

File tree

the-future-of-ruby-documentation.html

Lines changed: 115 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,13 @@
103103
@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); } }
104104
@media (max-width: 600px) { :root { --title-size: clamp(1.25rem, 7vw, 2.5rem); } .two-col { flex-direction: column !important; } }
105105
@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; }
106113
.speaker-notes { display: none; }
107114
.blackout-overlay { position: fixed; inset: 0; background: #000; z-index: 10000; display: none; }
108115
.blackout-overlay.active { display: block; }
@@ -115,6 +122,13 @@
115122
<button class="edit-toggle" id="editToggle" title="Edit mode (E)"></button>
116123
<div class="edit-banner" id="editBanner">EDIT MODE — Click any text to edit · Ctrl+S to save</div>
117124
<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>
118132

119133
<!-- SLIDE 1: TITLE -->
120134
<section class="slide title-slide visible">
@@ -565,7 +579,7 @@ <h1 class="reveal" style="font-size:clamp(2rem,6vw,4.5rem)">Thank you!</h1>
565579
}
566580
setupKeyboardNav() {
567581
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;
569583
switch(e.key) {
570584
case 'ArrowDown': case 'ArrowRight': case ' ': case 'PageDown':
571585
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>
622636
}
623637
}
624638
class InlineEditor {
625-
constructor() {
639+
constructor(notesEditor) {
640+
this.notesEditor = notesEditor;
626641
this.isActive = false;
627642
this.setupHotzone();
628643
this.setupKeyboard();
@@ -665,13 +680,16 @@ <h1 class="reveal" style="font-size:clamp(2rem,6vw,4.5rem)">Thank you!</h1>
665680
}
666681
}
667682
async exportFile() {
683+
if (this.notesEditor) this.notesEditor.syncToDOM();
668684
const editableEls = Array.from(document.querySelectorAll('[contenteditable]'));
669685
editableEls.forEach(el => el.removeAttribute('contenteditable'));
670686
document.body.classList.remove('edit-active');
671687
const toggle = document.getElementById('editToggle');
672688
const banner = document.getElementById('editBanner');
673689
if (toggle) { toggle.classList.remove('active', 'show'); }
674690
if (banner) { banner.classList.remove('active'); }
691+
const notesPanel = document.getElementById('notesPanel');
692+
if (notesPanel) notesPanel.classList.remove('active');
675693
const html = '<!DOCTYPE html>\n' + document.documentElement.outerHTML;
676694
document.body.classList.add('edit-active');
677695
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>
707725
URL.revokeObjectURL(a.href);
708726
}
709727
}
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+
}
710821
class PresenterMode {
711822
constructor(slidePresentation) {
712823
this.sp = slidePresentation;
@@ -900,7 +1011,8 @@ <h1 class="reveal" style="font-size:clamp(2rem,6vw,4.5rem)">Thank you!</h1>
9001011
}
9011012

9021013
const sp = new SlidePresentation();
903-
new InlineEditor();
1014+
const notesEditor = new NotesEditor(sp);
1015+
const editor = new InlineEditor(notesEditor);
9041016
new PresenterMode(sp);
9051017
</script>
9061018

0 commit comments

Comments
 (0)