/* =========================================================================
* Whisper-Pipeline · Tweaks panel + small mockup helpers
* Vanilla JS, separate from product code.
* ========================================================================= */
(function () {
const STORAGE_KEY = 'wp-tweaks';
const DEFAULTS = /*EDITMODE-BEGIN*/{
"aspect": "gunshi",
"phases": "stepper",
"theme": "dark",
"grain": false,
"tail": true
}/*EDITMODE-END*/;
let state = { ...DEFAULTS };
try { state = { ...state, ...JSON.parse(localStorage.getItem(STORAGE_KEY) || '{}') }; } catch (e) {}
// ─── APPLY STATE ──────────────────────────────────────────────────────
function apply() {
document.documentElement.setAttribute('data-aspect', state.aspect);
document.documentElement.setAttribute('data-theme', state.theme);
document.body.setAttribute('data-grain', state.grain ? 'on' : 'off');
// Phase indicator variant
document.querySelectorAll('[data-phases-wrapper]').forEach(w => {
w.querySelectorAll('[data-phases-variant]').forEach(v => {
v.hidden = v.getAttribute('data-phases-variant') !== state.phases;
});
});
// Tail-auto-scroll toggle reflection (purely visual)
document.querySelectorAll('.tail').forEach(t => {
t.setAttribute('aria-pressed', state.tail ? 'true' : 'false');
});
}
function persist() {
try { localStorage.setItem(STORAGE_KEY, JSON.stringify(state)); } catch (e) {}
try { window.parent.postMessage({ type: '__edit_mode_set_keys', edits: state }, '*'); } catch (e) {}
}
// ─── WAVEFORM BUILDER ────────────────────────────────────────────────
// Procedural pseudo-random bars based on a seed, so static markup
// gets a credible waveform without huge SVG payloads.
function mulberry(seed) {
let t = seed >>> 0;
return function () {
t = (t + 0x6D2B79F5) >>> 0;
let x = t;
x = Math.imul(x ^ (x >>> 15), x | 1);
x ^= x + Math.imul(x ^ (x >>> 7), x | 61);
return ((x ^ (x >>> 14)) >>> 0) / 4294967296;
};
}
function buildWaveform(el) {
const bars = parseInt(el.dataset.bars || '32', 10);
const seed = parseInt(el.dataset.seed || '1', 10);
const variant = el.dataset.waveform || 'default';
const rand = mulberry(seed);
// Bias the curve so it looks like real audio (envelope-ish):
// ramp up at start, fluctuate, fade at end.
function envelope(i) {
const t = i / (bars - 1);
const ramp = Math.min(1, t / 0.12);
const fade = Math.min(1, (1 - t) / 0.08);
return ramp * fade;
}
el.classList.add('waveform');
el.innerHTML = '';
for (let i = 0; i < bars; i++) {
const noise = 0.35 + rand() * 0.65; // 0.35–1.0
const env = envelope(i);
const h = Math.max(2, Math.round(noise * env * 28));
const bar = document.createElement('span');
bar.className = 'bar';
bar.style.height = h + 'px';
el.appendChild(bar);
}
// Variant-specific tone
if (variant === 'muted') el.classList.add('muted');
}
function buildAllWaveforms() {
document.querySelectorAll('[data-waveform]').forEach(buildWaveform);
}
// ─── TAIL TOGGLE (per-page wireup) ───────────────────────────────────
function wireTailToggles() {
document.querySelectorAll('.tail').forEach(t => {
if (t.dataset.bound) return;
t.dataset.bound = '1';
t.addEventListener('click', () => {
state.tail = t.getAttribute('aria-pressed') !== 'true';
apply(); persist();
});
});
}
// ─── PANEL ───────────────────────────────────────────────────────────
function buildPanel() {
const root = document.createElement('div');
root.id = 'tweaks-root';
root.style.display = 'none';
root.innerHTML = `
<div class="tweaks-panel" role="dialog" aria-label="Tweaks">
<div class="tweaks-head">
<span>⌬</span><span>Tweaks</span>
<button class="close" aria-label="Close">✕</button>
</div>
<div class="tweaks-body">
<div class="tweaks-row">
<div class="tweaks-label">Aspect · accent</div>
<div class="swatches" data-control="aspect">
<div class="swatch" data-value="gunshi" title="Strategist · Spectre" style="background:#a878ff"></div>
<div class="swatch" data-value="shugo" title="Guardian · Phosphor" style="background:#39ff7a"></div>
<div class="swatch" data-value="kantoku" title="Taskmaster · Crimson" style="background:#d4203a"></div>
<div class="swatch" data-value="sensei" title="Mentor · Ember" style="background:#ffb442"></div>
</div>
</div>
<div class="tweaks-row">
<div class="tweaks-label">Theme</div>
<div class="segmented" data-control="theme">
<button class="opt" data-value="dark">Dark</button>
<button class="opt" data-value="light">Light</button>
</div>
</div>
<div class="tweaks-row">
<div class="tweaks-label">Phase indicator</div>
<div class="segmented" data-control="phases">
<button class="opt" data-value="stepper">Stepper</button>
<button class="opt" data-value="subway">Subway</button>
<button class="opt" data-value="cards">Cards</button>
</div>
</div>
<div class="tweaks-row">
<div class="toggle">
<span>Grain overlay</span>
<div class="switch" data-control="grain" role="switch" tabindex="0"></div>
</div>
</div>
</div>
</div>
`;
document.body.appendChild(root);
function sync() {
root.querySelectorAll('[data-control="aspect"] .swatch').forEach(el => el.setAttribute('aria-pressed', el.dataset.value === state.aspect ? 'true' : 'false'));
root.querySelectorAll('[data-control="theme"] .opt').forEach(el => el.setAttribute('aria-pressed', el.dataset.value === state.theme ? 'true' : 'false'));
root.querySelectorAll('[data-control="phases"] .opt').forEach(el => el.setAttribute('aria-pressed', el.dataset.value === state.phases ? 'true' : 'false'));
root.querySelector('[data-control="grain"]').setAttribute('aria-pressed', state.grain ? 'true' : 'false');
}
sync();
root.addEventListener('click', (e) => {
const swatch = e.target.closest('[data-control="aspect"] .swatch');
if (swatch) { state.aspect = swatch.dataset.value; apply(); persist(); sync(); return; }
const theme = e.target.closest('[data-control="theme"] .opt');
if (theme) { state.theme = theme.dataset.value; apply(); persist(); sync(); return; }
const opt = e.target.closest('[data-control="phases"] .opt');
if (opt) { state.phases = opt.dataset.value; apply(); persist(); sync(); return; }
const grain = e.target.closest('[data-control="grain"]');
if (grain) { state.grain = !state.grain; apply(); persist(); sync(); return; }
if (e.target.closest('.close')) { hide(); return; }
});
return root;
}
let panel = null;
function show() {
if (!panel) panel = buildPanel();
panel.style.display = 'block';
}
function hide() {
if (panel) panel.style.display = 'none';
try { window.parent.postMessage({ type: '__edit_mode_dismissed' }, '*'); } catch (e) {}
}
// ─── MODAL HELPERS (cheatsheet auto-injects on every page) ──────────
function injectCheatsheet() {
if (document.getElementById('modal-cheatsheet')) return;
const html = `
<div class="modal-backdrop" id="modal-cheatsheet" hidden>
<div class="modal modal--wide" role="dialog" aria-modal="true">
<header class="modal-head">
<span class="caps">Keyboard · whisper-pipeline</span>
<button class="close" type="button" data-close-modal aria-label="Close">✕</button>
</header>
<div class="modal-body">
<h2 class="modal-title">The chamber listens to keys.</h2>
<div class="kbd-list">
<div class="group">
<h4>Global</h4>
<div class="item"><span>Show this cheatsheet</span><span class="keys"><kbd>?</kbd></span></div>
<div class="item"><span>Focus search</span><span class="keys"><kbd>/</kbd></span></div>
<div class="item"><span>Go to start</span><span class="keys"><kbd>g</kbd><kbd>s</kbd></span></div>
<div class="item"><span>Go to runs</span><span class="keys"><kbd>g</kbd><kbd>r</kbd></span></div>
<div class="item"><span>Close modal / cancel</span><span class="keys"><kbd>Esc</kbd></span></div>
</div>
<div class="group">
<h4>On start</h4>
<div class="item"><span>Start pipeline</span><span class="keys"><kbd>Ctrl</kbd><kbd>R</kbd></span></div>
<div class="item"><span>Drop audio (focus path)</span><span class="keys"><kbd>p</kbd></span></div>
<div class="item"><span>Toggle skip-render</span><span class="keys"><kbd>s</kbd><kbd>r</kbd></span></div>
<div class="item"><span>Reload resume state</span><span class="keys"><kbd>r</kbd></span></div>
</div>
<div class="group">
<h4>On run-detail</h4>
<div class="item"><span>Abort current run</span><span class="keys"><kbd>Ctrl</kbd><kbd>.</kbd></span></div>
<div class="item"><span>Pause auto-scroll (log)</span><span class="keys"><kbd>t</kbd></span></div>
<div class="item"><span>Copy YouTube URL</span><span class="keys"><kbd>c</kbd><kbd>u</kbd></span></div>
<div class="item"><span>Open MP4 locally</span><span class="keys"><kbd>o</kbd></span></div>
</div>
<div class="group">
<h4>On runs list</h4>
<div class="item"><span>Move selection</span><span class="keys"><kbd>↑</kbd><kbd>↓</kbd></span></div>
<div class="item"><span>Open selected</span><span class="keys"><kbd>↵</kbd></span></div>
<div class="item"><span>Filter: done / aborted</span><span class="keys"><kbd>f</kbd><kbd>d</kbd> / <kbd>f</kbd><kbd>a</kbd></span></div>
</div>
</div>
</div>
<footer class="modal-foot">
<span class="mono dim" style="margin-right:auto; font-size: var(--text-xs); letter-spacing: 0.04em;">All shortcuts also available in palette · <kbd>Ctrl</kbd><kbd>K</kbd></span>
<button class="btn btn--primary" type="button" data-close-modal>Got it</button>
</footer>
</div>
</div>
`;
const div = document.createElement('div');
div.innerHTML = html;
document.body.appendChild(div.firstElementChild);
}
function wireModals() {
document.addEventListener('click', (e) => {
const opener = e.target.closest('[data-open-modal]');
if (opener) {
e.preventDefault();
const id = opener.dataset.openModal;
const el = document.getElementById('modal-' + id);
if (el) el.hidden = false;
return;
}
const closer = e.target.closest('[data-close-modal]');
if (closer) {
document.querySelectorAll('.modal-backdrop').forEach(m => m.hidden = true);
return;
}
const backdrop = e.target.closest('.modal-backdrop');
if (backdrop && e.target === backdrop) { backdrop.hidden = true; }
});
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape') {
document.querySelectorAll('.modal-backdrop').forEach(m => m.hidden = true);
}
if (e.key === '?' && !/^(INPUT|TEXTAREA)$/.test(document.activeElement?.tagName)) {
e.preventDefault();
document.querySelectorAll('.modal-backdrop').forEach(m => m.hidden = true);
const cs = document.getElementById('modal-cheatsheet');
if (cs) cs.hidden = false;
}
});
}
// ─── BOOT ────────────────────────────────────────────────────────────
apply();
buildAllWaveforms();
wireTailToggles();
injectCheatsheet();
wireModals();
window.addEventListener('message', (e) => {
const data = e.data;
if (!data || typeof data !== 'object') return;
if (data.type === '__activate_edit_mode') show();
if (data.type === '__deactivate_edit_mode') hide();
});
try { window.parent.postMessage({ type: '__edit_mode_available' }, '*'); } catch (e) {}
})();