tractatus/public/js/blog-presentation.js
TheFlow 5557126f6a feat: eliminate all GitHub references from agenticgovernance.digital
- Created /source-code.html — sovereign hosting landing page explaining
  why we left GitHub, how to access the code, and the sovereignty model
- Navbar: GitHub link → Source Code link (desktop + mobile)
- Footer: GitHub link → Source Code link
- Docs sidebar: GitHub section → Source Code section with sovereign repo
- Implementer page: all repository links point to /source-code.html,
  clone instructions updated, CI/CD code example genericised
- FAQ: GitHub Discussions button → Contact Us with email icon
- FAQ content: all 4 locales (en/de/fr/mi) rewritten to remove
  GitHub Actions YAML, GitHub URLs, and GitHub-specific patterns
- faq.js fallback content: same changes as locale files
- agent-lightning integration page: updated to source-code.html
- Project model: example URL changed from GitHub to Codeberg
- All locale files updated: navbar.github → navbar.source_code,
  footer GitHub → source_code, FAQ button text updated in 4 languages

Zero GitHub references remain in any HTML, JS, or JSON file
(only github-dark.min.css theme name in highlight.js CDN reference).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 18:14:26 +13:00

427 lines
12 KiB
JavaScript

/**
* Blog Presentation Mode
* Renders blog posts as full-viewport slides with presenter notes
* Zero dependencies — pure JS
*/
/* eslint-disable no-unused-vars */
/**
* Check if presentation mode is requested and initialize
* Called from blog-post.js after post data is loaded
*/
function initPresentationMode(post) {
const urlParams = new URLSearchParams(window.location.search);
if (urlParams.get('mode') !== 'presentation') return false;
const slides = buildSlides(post);
if (slides.length === 0) return false;
renderPresentation(slides);
return true;
}
/**
* Build slides from post data
* Option A: curated slides from post.presentation
* Option B: auto-extract from HTML content using <h2> breaks
*/
function buildSlides(post) {
const slides = [];
// Title slide is always first
const authorName = post.author_name || (post.author && post.author.name) || 'Tractatus Team';
const publishedDate = post.published_at
? new Date(post.published_at).toLocaleDateString('en-US', {
year: 'numeric', month: 'long', day: 'numeric'
})
: '';
slides.push({
type: 'title',
heading: post.title,
subtitle: post.excerpt || '',
meta: [authorName, publishedDate].filter(Boolean).join(' — '),
notes: ''
});
// Option A: curated presentation data
if (post.presentation && post.presentation.enabled &&
post.presentation.slides && post.presentation.slides.length > 0) {
for (const slide of post.presentation.slides) {
slides.push({
type: 'content',
heading: slide.heading,
bullets: slide.bullets || [],
notes: slide.notes || ''
});
}
return slides;
}
// Option B: auto-extract from content HTML
const contentHtml = post.content_html || post.content || '';
if (!contentHtml) return slides;
// Parse the HTML to extract sections by <h2>
const parser = new DOMParser();
const doc = parser.parseFromString(`<div>${contentHtml}</div>`, 'text/html');
const wrapper = doc.body.firstChild;
let currentHeading = null;
let currentElements = [];
const flushSection = () => {
if (!currentHeading) return;
// Extract bullet points from paragraphs
const bullets = [];
for (const el of currentElements) {
if (el.tagName === 'UL' || el.tagName === 'OL') {
const items = el.querySelectorAll('li');
for (const item of items) {
const text = item.textContent.trim();
if (text) bullets.push(text);
}
} else if (el.tagName === 'P') {
const text = el.textContent.trim();
if (text) {
const firstSentence = text.match(/^[^.!?]+[.!?]/);
bullets.push(firstSentence ? firstSentence[0] : text.substring(0, 120));
}
} else if (el.tagName === 'BLOCKQUOTE') {
const text = el.textContent.trim();
if (text) bullets.push(text);
}
}
// Limit to 6 bullets per slide for readability
const slideBullets = bullets.slice(0, 6);
// Use full section text as notes
const allText = currentElements
.map(el => el.textContent.trim())
.filter(Boolean)
.join(' ');
const notes = allText.length > 200
? `${allText.substring(0, 500)}...`
: allText;
slides.push({
type: 'content',
heading: currentHeading,
bullets: slideBullets,
notes
});
};
for (const node of wrapper.childNodes) {
if (node.nodeType !== 1) continue; // skip text nodes
if (node.tagName === 'H2') {
flushSection();
currentHeading = node.textContent.trim();
currentElements = [];
} else if (currentHeading) {
currentElements.push(node);
}
}
flushSection();
return slides;
}
/**
* Escape HTML for safe insertion
*/
function escapeForPresentation(text) {
if (!text) return '';
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
/**
* Render the presentation UI
*/
function renderPresentation(slides) {
// Hide the normal article view
document.body.classList.add('presentation-active');
// Create presentation container
const container = document.createElement('div');
container.className = 'presentation-mode';
container.id = 'presentation-container';
// Progress bar
const progress = document.createElement('div');
progress.className = 'slide-progress';
progress.id = 'slide-progress';
container.appendChild(progress);
// Slides container
const slidesEl = document.createElement('div');
slidesEl.className = 'presentation-slides';
for (let i = 0; i < slides.length; i++) {
const slide = slides[i];
const slideEl = document.createElement('div');
slideEl.className = `presentation-slide${slide.type === 'title' ? ' slide-title' : ''}`;
slideEl.dataset.index = i;
if (i === 0) slideEl.classList.add('active');
if (slide.type === 'title') {
const subtitleHtml = slide.subtitle
? `<p class="slide-subtitle">${escapeForPresentation(slide.subtitle)}</p>`
: '';
const metaHtml = slide.meta
? `<p class="slide-meta">${escapeForPresentation(slide.meta)}</p>`
: '';
slideEl.innerHTML =
`<h1>${escapeForPresentation(slide.heading)}</h1>${subtitleHtml}${metaHtml}`;
} else {
let bulletsHtml = '';
if (slide.bullets && slide.bullets.length > 0) {
const items = slide.bullets
.map(b => `<li>${escapeForPresentation(b)}</li>`)
.join('');
bulletsHtml = `<ul class="slide-bullets">${items}</ul>`;
}
slideEl.innerHTML =
`<h2 class="slide-heading">${escapeForPresentation(slide.heading)}</h2>${bulletsHtml}`;
}
slidesEl.appendChild(slideEl);
}
container.appendChild(slidesEl);
// Notes panel
const notesPanel = document.createElement('div');
notesPanel.className = 'presentation-notes';
notesPanel.id = 'presentation-notes';
notesPanel.innerHTML =
'<div class="presentation-notes-inner">' +
'<div class="presentation-notes-label">Presenter Notes</div>' +
'<div class="presentation-notes-text" id="presentation-notes-text"></div>' +
'</div>';
container.appendChild(notesPanel);
// Navigation bar
const nav = document.createElement('div');
nav.className = 'presentation-nav';
nav.innerHTML =
'<div style="display:flex;gap:0.5rem;align-items:center;">' +
'<button id="pres-exit" title="Exit presentation (Esc)">Exit</button>' +
'<button id="pres-notes" title="Toggle notes (N)">Notes</button>' +
'</div>' +
`<span class="slide-counter" id="slide-counter">1 / ${slides.length}</span>` +
'<div style="display:flex;gap:0.5rem;">' +
'<button id="pres-prev" title="Previous slide" disabled>&larr; Prev</button>' +
'<button id="pres-next" title="Next slide">Next &rarr;</button>' +
'</div>';
container.appendChild(nav);
// Keyboard hints overlay
const hints = document.createElement('div');
hints.className = 'presentation-hints';
hints.id = 'presentation-hints';
hints.innerHTML =
'<h3>Keyboard Shortcuts</h3>' +
'<dl>' +
'<dt>&larr; / &rarr;</dt><dd>Previous / Next slide</dd>' +
'<dt>N</dt><dd>Toggle presenter notes</dd>' +
'<dt>H / ?</dt><dd>Show this help</dd>' +
'<dt>Esc</dt><dd>Exit presentation</dd>' +
'<dt>F</dt><dd>Toggle fullscreen</dd>' +
'</dl>';
container.appendChild(hints);
document.body.appendChild(container);
// Start presentation controller
startController(slides, container);
}
/**
* Presentation controller — manages slide state and navigation
*/
function startController(slides, container) {
let currentIndex = 0;
let notesOpen = false;
let touchStartX = 0;
let touchStartY = 0;
const updateUI = () => {
document.getElementById('slide-counter').textContent =
`${currentIndex + 1} / ${slides.length}`;
const pct = ((currentIndex + 1) / slides.length) * 100;
document.getElementById('slide-progress').style.width = `${pct}%`;
document.getElementById('pres-prev').disabled = (currentIndex === 0);
document.getElementById('pres-next').disabled = (currentIndex === slides.length - 1);
};
const updateNotes = () => {
const notes = slides[currentIndex].notes || '';
document.getElementById('presentation-notes-text').textContent = notes;
};
const goTo = index => {
if (index < 0 || index >= slides.length) return;
const slideEls = container.querySelectorAll('.presentation-slide');
const currentEl = slideEls[currentIndex];
const nextEl = slideEls[index];
// Direction-aware animation
currentEl.classList.remove('active');
currentEl.classList.add('exiting-left');
nextEl.style.transform = index > currentIndex ? 'translateX(40px)' : 'translateX(-40px)';
// Trigger reflow then animate in
void nextEl.offsetWidth; // eslint-disable-line no-void
nextEl.classList.add('active');
nextEl.style.transform = '';
setTimeout(() => {
currentEl.classList.remove('exiting-left');
}, 400);
currentIndex = index;
updateUI();
updateNotes();
};
const next = () => goTo(currentIndex + 1);
const prev = () => goTo(currentIndex - 1);
const exit = () => {
document.removeEventListener('keydown', onKeyDown);
container.removeEventListener('touchstart', onTouchStart);
container.removeEventListener('touchend', onTouchEnd);
document.body.style.overflow = '';
const el = document.getElementById('presentation-container');
if (el) el.remove();
document.body.classList.remove('presentation-active');
// Remove mode=presentation from URL without reload
const url = new URL(window.location);
url.searchParams.delete('mode');
window.history.replaceState({}, '', url.toString());
};
const toggleNotes = () => {
notesOpen = !notesOpen;
const panel = document.getElementById('presentation-notes');
if (notesOpen) {
panel.classList.add('open');
} else {
panel.classList.remove('open');
}
};
const toggleFullscreen = () => {
if (!document.fullscreenElement) {
container.requestFullscreen().catch(() => {});
} else {
document.exitFullscreen().catch(() => {});
}
};
const showHints = () => {
const hintsEl = document.getElementById('presentation-hints');
hintsEl.classList.add('visible');
setTimeout(() => {
hintsEl.classList.remove('visible');
}, 3000);
};
const onKeyDown = e => {
switch (e.key) {
case 'ArrowRight':
case 'ArrowDown':
case ' ':
e.preventDefault();
next();
break;
case 'ArrowLeft':
case 'ArrowUp':
e.preventDefault();
prev();
break;
case 'Escape':
e.preventDefault();
exit();
break;
case 'n':
case 'N':
e.preventDefault();
toggleNotes();
break;
case 'f':
case 'F':
e.preventDefault();
toggleFullscreen();
break;
case 'h':
case 'H':
case '?':
e.preventDefault();
showHints();
break;
case 'Home':
e.preventDefault();
goTo(0);
break;
case 'End':
e.preventDefault();
goTo(slides.length - 1);
break;
}
};
const onTouchStart = e => {
touchStartX = e.changedTouches[0].screenX;
touchStartY = e.changedTouches[0].screenY;
};
const onTouchEnd = e => {
const dx = e.changedTouches[0].screenX - touchStartX;
const dy = e.changedTouches[0].screenY - touchStartY;
// Only trigger if horizontal swipe is dominant and > 50px
if (Math.abs(dx) > Math.abs(dy) && Math.abs(dx) > 50) {
if (dx < 0) {
next();
} else {
prev();
}
}
};
// Bind events
document.addEventListener('keydown', onKeyDown);
container.addEventListener('touchstart', onTouchStart, { passive: true });
container.addEventListener('touchend', onTouchEnd, { passive: true });
document.getElementById('pres-prev').addEventListener('click', prev);
document.getElementById('pres-next').addEventListener('click', next);
document.getElementById('pres-exit').addEventListener('click', exit);
document.getElementById('pres-notes').addEventListener('click', toggleNotes);
// Click on slide area to advance
container.querySelector('.presentation-slides').addEventListener('click', e => {
if (e.target.closest('.presentation-nav') || e.target.closest('.presentation-notes')) return;
next();
});
// Initial state
updateUI();
updateNotes();
showHints();
document.body.style.overflow = 'hidden';
}