tractatus/public/js/reading-mode.js
TheFlow 240cc7d3a9 feat: reading-mode toggle for long research papers
Three density modes: Overview (~3 min), Standard (~12 min), Deep (~25 min).
Applied to researcher.html — 7 sections tagged with data-reading-level.
Persists user preference in localStorage. Auto-injects toggle UI.
Reusable component for architectural-alignment papers.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-17 07:15:44 +12:00

114 lines
3.9 KiB
JavaScript

/**
* Reading Mode Toggle
* Provides overview/standard/deep density modes for long research papers.
*
* Usage: Add data-reading-level="overview|standard|deep" to content sections.
* Sections without the attribute are always visible.
*
* The toggle is auto-injected at the top of any page that includes this script
* and has elements with data-reading-level attributes.
*/
(function() {
'use strict';
var STORAGE_KEY = 'tractatus-reading-mode';
var MODES = ['overview', 'standard', 'deep'];
var MODE_LABELS = {
overview: { label: 'Overview', desc: 'Key points only' },
standard: { label: 'Standard', desc: 'Main content' },
deep: { label: 'Deep', desc: 'Full detail' }
};
function init() {
// Only activate if page has reading-level content
var leveledContent = document.querySelectorAll('[data-reading-level]');
if (leveledContent.length === 0) return;
var savedMode = localStorage.getItem(STORAGE_KEY) || 'standard';
// Inject toggle UI after the first h1 or at the top of main content
var heroOrH1 = document.querySelector('.hero-section, .page-hero, h1');
var insertPoint = (heroOrH1 && heroOrH1.parentElement)
|| document.querySelector('main')
|| document.body;
var toggle = document.createElement('div');
toggle.className = 'reading-mode-toggle';
toggle.setAttribute('role', 'radiogroup');
toggle.setAttribute('aria-label', 'Reading depth');
toggle.innerHTML = '<span class="reading-mode-label">Reading depth:</span>' +
MODES.map(function(mode) {
return '<button class="reading-mode-btn ' + (mode === savedMode ? 'active' : '') + '" ' +
'data-mode="' + mode + '" ' +
'role="radio" ' +
'aria-checked="' + (mode === savedMode) + '" ' +
'title="' + MODE_LABELS[mode].desc + '">' +
MODE_LABELS[mode].label +
'</button>';
}).join('') +
'<span class="reading-mode-estimate"></span>';
// Insert after the hero/h1 section
if (insertPoint.nextSibling) {
insertPoint.parentNode.insertBefore(toggle, insertPoint.nextSibling);
} else {
insertPoint.parentNode.appendChild(toggle);
}
// Apply initial mode
applyMode(savedMode);
// Handle clicks
toggle.addEventListener('click', function(e) {
var btn = e.target.closest('.reading-mode-btn');
if (!btn) return;
var mode = btn.dataset.mode;
localStorage.setItem(STORAGE_KEY, mode);
// Update button states
var buttons = toggle.querySelectorAll('.reading-mode-btn');
for (var i = 0; i < buttons.length; i++) {
var b = buttons[i];
if (b.dataset.mode === mode) {
b.classList.add('active');
b.setAttribute('aria-checked', 'true');
} else {
b.classList.remove('active');
b.setAttribute('aria-checked', 'false');
}
}
applyMode(mode);
});
}
function applyMode(mode) {
var elements = document.querySelectorAll('[data-reading-level]');
var modeIndex = MODES.indexOf(mode);
for (var i = 0; i < elements.length; i++) {
var el = elements[i];
var level = el.dataset.readingLevel;
var levelIndex = MODES.indexOf(level);
// overview shows only overview; standard shows overview+standard; deep shows all
var visible = levelIndex <= modeIndex;
el.style.display = visible ? '' : 'none';
el.setAttribute('aria-hidden', !visible);
}
// Update reading time estimate if present
var counter = document.querySelector('.reading-mode-estimate');
if (counter) {
var labels = { overview: '~3 min read', standard: '~12 min read', deep: '~25 min read' };
counter.textContent = labels[mode] || '';
}
}
// Init on DOM ready
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
})();