Abfahrtsanzeiger Display Basic Library
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
libmonoformat/ts/index.html

613 lines
26 KiB

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width,initial-scale=1.0" />
<title>MonoDisplay Editor</title>
<script>!function(){var t=localStorage.getItem('theme');t&&document.documentElement.setAttribute('data-theme',t)}();</script>
<link href="style.css" rel="stylesheet"/>
</head>
<body>
<div id="topbar">
<span class="app-name">MonoDisplay</span>
<div class="tb-sep"></div>
<button class="tb-btn" onclick="document.getElementById('file-input').click()">
<svg viewBox="0 0 24 24">
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" />
<polyline points="17 8 12 3 7 8" />
<line x1="12" y1="3" x2="12" y2="15" />
</svg>
Load .bin
</button>
<input type="file" id="file-input" accept=".bin" onchange="loadBin(this)" />
<button class="tb-btn" onclick="exportBin()">
<svg viewBox="0 0 24 24">
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" />
<polyline points="7 10 12 15 17 10" />
<line x1="12" y1="15" x2="12" y2="3" />
</svg>
Export .bin
</button>
<div class="tb-sep"></div>
<button class="tb-btn" onclick="addSection()">
<svg viewBox="0 0 24 24">
<line x1="12" y1="5" x2="12" y2="19" />
<line x1="5" y1="12" x2="19" y2="12" />
</svg>
Add section
</button>
<span id="filename">untitled</span>
<div style="flex:1"></div>
<div class="tb-sep"></div>
<button class="tb-btn" id="theme-toggle" onclick="cycleTheme()">⊙ auto</button>
</div>
<div id="main">
<div id="left">
<span class="pv-label">preview · 120 × 60</span>
<div id="display-box">
<canvas id="canvas_root" width="120" height="60"></canvas>
</div>
<span class="pv-label">demos</span>
<div id="demo-btns"></div>
<div id="sec-meta">
<div class="meta-row">
<label>Selected section</label>
<span class="meta-val green" id="meta-name">-</span>
</div>
<div class="meta-row">
<label>Elements</label>
<span class="meta-val" id="meta-count">-</span>
</div>
<div class="meta-row">
<label>Section flags</label>
<label class="flag-check">
<input type="checkbox" id="flag-drawFront" onchange="setSectionFlag('drawFront',this.checked)" />
drawFront
</label>
<label class="flag-check" style="margin-left:8px">
<input type="checkbox" id="flag-clearBuffer" onchange="setSectionFlag('clearBuffer',this.checked)" />
clearBuffer
</label>
</div>
</div>
</div>
<div id="right">
<div id="right-hdr">
<span class="rh-title">sections</span>
</div>
<div id="sections-wrap">
<div class="empty-state">No sections yet.<br />Use <b>Add section</b> to get started.</div>
</div>
</div>
</div>
<!-- confirm dialog -->
<div id="confirm-overlay">
<div id="confirm-box">
<div id="confirm-msg"></div>
<div id="confirm-btns"></div>
</div>
</div>
<script src="./public/mono-display.js"></script>
<script>
const W = 120, H = 60;
let sections = [];
let activeSec = null, activeEl = null;
let secCounter = 0, elCounter = 0;
let currentFilename = 'untitled';
let isDirty = false;
// --- Element type definitions ------------------------------------------------
const EL_TYPES = ['Image2D', 'Animation', 'ClippedText', 'HScrollText', 'CurrentTime'];
const EL_FIELDS = {
Image2D: [
{ k: 'xOffset', l: 'X offset', t: 'number', d: 0 }, { k: 'yOffset', l: 'Y offset', t: 'number', d: 0 },
{ k: 'width', l: 'Width', t: 'number', d: W }, { k: 'height', l: 'Height', t: 'number', d: H },
],
Animation: [
{ k: 'xOffset', l: 'X offset', t: 'number', d: 0 }, { k: 'yOffset', l: 'Y offset', t: 'number', d: 0 },
{ k: 'width', l: 'Width', t: 'number', d: W }, { k: 'height', l: 'Height', t: 'number', d: H },
{ k: 'updateInterval', l: 'Update interval', t: 'number', d: 12 },
],
ClippedText: [
{ k: 'text', l: 'Text', t: 'text', d: 'Hello, World!', full: true },
{ k: 'xOffset', l: 'X offset', t: 'number', d: 0 }, { k: 'yOffset', l: 'Y offset', t: 'number', d: 32 },
{ k: 'width', l: 'Width', t: 'number', d: W }, { k: 'height', l: 'Height', t: 'number', d: 16 },
],
HScrollText: [
{ k: 'text', l: 'Text', t: 'text', d: 'Scrolling text - ', full: true },
{ k: 'xOffset', l: 'X offset', t: 'number', d: 0 }, { k: 'yOffset', l: 'Y offset', t: 'number', d: 32 },
{ k: 'width', l: 'Width', t: 'number', d: W }, { k: 'height', l: 'Height', t: 'number', d: 16 },
{ k: 'scrollSpeed', l: 'Scroll speed', t: 'number', d: 50 },
],
CurrentTime: [
{ k: 'xOffset', l: 'X offset', t: 'number', d: 0 }, { k: 'yOffset', l: 'Y offset', t: 'number', d: 8 },
{ k: 'width', l: 'Width', t: 'number', d: W }, { k: 'height', l: 'Height', t: 'number', d: 16 },
{ k: 'utcOffsetMinutes', l: 'UTC offset (min)', t: 'number', d: 0 },
],
};
const EL_FLAGS = {
Image2D: [], Animation: [], ClippedText: [],
HScrollText: [{ k: 'endless', l: 'Endless' }, { k: 'invertDirection', l: 'Invert direction' }],
CurrentTime: [{ k: 'clock12h', l: '12h mode' }, { k: 'showHours', l: 'Show hours' }, { k: 'showSeconds', l: 'Show seconds' }],
};
// --- Confirm dialog -----------------------------------------------------------
function confirm(msg, buttons) {
// buttons: [{label,primary,action}]
return new Promise(resolve => {
const overlay = document.getElementById('confirm-overlay');
document.getElementById('confirm-msg').textContent = msg;
const btns = document.getElementById('confirm-btns');
btns.innerHTML = '';
buttons.forEach(b => {
const el = document.createElement('button');
el.className = 'cb' + (b.primary ? ' primary' : '');
el.textContent = b.label;
el.onclick = () => { overlay.classList.remove('show'); resolve(b.action); };
btns.appendChild(el);
});
overlay.classList.add('show');
});
}
// --- Dirty tracking -----------------------------------------------------------
function markDirty() {
isDirty = true;
document.getElementById('filename').classList.add('dirty');
}
function markClean() {
isDirty = false;
document.getElementById('filename').classList.remove('dirty');
}
async function guardDirty() {
if (!isDirty || !sections.length) return true;
const action = await confirm(
'You have unsaved changes. Loading a demo will replace the current editor state.',
[{ label: 'Cancel', action: false }, { label: 'Load anyway', primary: true, action: true }]
);
return action;
}
// --- Helpers -----------------------------------------------------------------
function newSec() {
secCounter++;
return {
id: 's' + secCounter, name: 'Section ' + secCounter, open: true,
flags: { drawFront: true, clearBuffer: true }, elements: []
};
}
function newEl(type) {
elCounter++;
const fields = {};
(EL_FIELDS[type] || []).forEach(f => fields[f.k] = f.d);
const flags = {};
(EL_FLAGS[type] || []).forEach(f => flags[f.k] = false);
return { id: 'e' + elCounter, type, fields, flags };
}
function getSec(id) { return sections.find(s => s.id === id) }
function getEl(secId, elId) { const s = getSec(secId); return s && s.elements.find(e => e.id === elId) }
// --- Mutations ----------------------------------------------------------------
function addSection() {
const s = newSec(); sections.push(s);
activeSec = s.id; activeEl = null;
markDirty(); render(); triggerPreview();
}
function removeSection(id) {
sections = sections.filter(s => s.id !== id);
if (activeSec === id) { activeSec = sections.length ? sections[sections.length - 1].id : null; activeEl = null; }
markDirty(); render(); triggerPreview();
}
function toggleSection(id) {
const s = getSec(id); if (!s) return;
s.open = !s.open; activeSec = id; render(); updateMeta();
}
function setSectionFlag(flag, val) {
if (!activeSec) return;
const s = getSec(activeSec); if (!s) return;
s.flags[flag] = val; markDirty(); triggerPreview();
}
function addElement(secId) {
const s = getSec(secId); if (!s) return;
const el = newEl(EL_TYPES[0]); s.elements.push(el);
activeSec = secId; activeEl = { secId, elId: el.id };
markDirty(); render(); triggerPreview();
}
function removeElement(secId, elId) {
const s = getSec(secId); if (!s) return;
s.elements = s.elements.filter(e => e.id !== elId);
if (activeEl && activeEl.secId === secId && activeEl.elId === elId) activeEl = null;
markDirty(); render(); triggerPreview();
}
function selectElement(secId, elId) {
activeSec = secId;
activeEl = activeEl && activeEl.secId === secId && activeEl.elId === elId ? null : { secId, elId };
render();
}
function changeElType(secId, elId, type) {
const el = getEl(secId, elId); if (!el) return;
const fresh = newEl(type);
el.type = type; el.fields = fresh.fields; el.flags = fresh.flags;
markDirty(); render(); triggerPreview();
}
function setElField(secId, elId, key, val) {
const el = getEl(secId, elId); if (!el) return;
el.fields[key] = val; markDirty(); triggerPreview();
}
function setElFlag(secId, elId, key, val) {
const el = getEl(secId, elId); if (!el) return;
el.flags[key] = val; markDirty(); triggerPreview();
}
// --- Load demo into editor state ---------------------------------------------
const DEMO_DEFS = [
{
label: 'Checkerboard', build() {
const s = newSec(); s.name = 'Checkerboard';
const el = newEl('Image2D');
s.elements.push(el);
s.flags = { drawFront: false, clearBuffer: false };
return [s];
}
},
{
label: 'Blink', build() {
const s = newSec(); s.name = 'Blink'; s.flags = { drawFront: true, clearBuffer: true };
const el = newEl('Animation'); el.fields.updateInterval = 12;
s.elements.push(el); return [s];
}
},
{
label: 'Text', build() {
const s = newSec(); s.name = 'Text'; s.flags = { drawFront: true, clearBuffer: true };
const el = newEl('ClippedText'); el.fields.text = 'Hello, World!'; el.fields.yOffset = 32;
s.elements.push(el); return [s];
}
},
{
label: 'Scrolltext', build() {
const s = newSec(); s.name = 'Scrolltext'; s.flags = { drawFront: true, clearBuffer: true };
const el = newEl('HScrollText');
el.fields.text = 'MONO DISPLAY - scrolling ticker - 🚀 ';
el.fields.yOffset = 32; el.fields.scrollSpeed = 50;
el.flags.endless = true; el.flags.invertDirection = false;
s.elements.push(el); return [s];
}
},
{
label: 'Time', build() {
const s = newSec(); s.name = 'Time'; s.flags = { drawFront: true, clearBuffer: true };
const defs = [
{ flags: {}, yOffset: 8 }, { flags: { clock12h: true }, xOffset: 40, yOffset: 16 },
{ flags: { clock12h: true, showHours: true }, yOffset: 24 },
{ flags: { clock12h: true, showHours: false }, xOffset: 40, yOffset: 32 },
{ flags: { clock12h: true, showSeconds: true }, xOffset: 120, yOffset: 32 },
];
defs.forEach(d => {
const el = newEl('CurrentTime');
Object.assign(el.fields, { utcOffsetMinutes: 120, xOffset: d.xOffset || 0, yOffset: d.yOffset });
el.flags = d.flags; s.elements.push(el);
});
return [s];
}
},
];
// --- Preview via MonoDisplayFile (same as original demos) --------------------
const DEMO_PREVIEWS = {
Checkerboard() {
const { MonoDisplayFile, ElementType } = window.MonoDisplay;
const pixels = new Uint8Array(W * H);
for (let y = 0; y < H; y++)for (let x = 0; x < W; x++)pixels[y * W + x] = (x + y) % 2;
return new MonoDisplayFile({
width: W, height: H,
elements_always: [{ type: ElementType.Image2D, pixels, width: W, height: H }]
}).toBuffer();
},
Blink() {
const { MonoDisplayFile, ElementType } = window.MonoDisplay;
return new MonoDisplayFile({
elements_always: {
flags: { drawFront: true, clearBuffer: true },
elements: [{
type: ElementType.Animation, width: W, height: H, updateInterval: 12,
frames: [{ pixels: new Uint8Array(W * H).fill(1) }, { pixels: new Uint8Array(W * H).fill(0) }]
}]
}
}).toBuffer();
},
Text() {
const { MonoDisplayFile, ElementType } = window.MonoDisplay;
return new MonoDisplayFile({
elements_always: {
flags: { drawFront: true, clearBuffer: true },
elements: [{ type: ElementType.ClippedText, text: 'Hello, World!', xOffset: 0, yOffset: 32, width: W, height: 16 }]
}
}).toBuffer();
},
Scrolltext() {
const { MonoDisplayFile, ElementType } = window.MonoDisplay;
return new MonoDisplayFile({
elements_always: {
flags: { drawFront: true, clearBuffer: true },
elements: [{
type: ElementType.HScrollText, text: 'MONO DISPLAY - scrolling ticker - 🚀 ',
xOffset: 0, yOffset: 32, width: W, height: 16, scrollSpeed: 50, flags: { endless: true, invertDirection: false }
}]
}
}).toBuffer();
},
Time() {
const { MonoDisplayFile, ElementType } = window.MonoDisplay;
return new MonoDisplayFile({
elements_always: {
flags: { drawFront: true, clearBuffer: true }, elements: [
{ flags: {}, type: ElementType.CurrentTime, xOffset: 0, yOffset: 8, width: W, height: 16, utcOffsetMinutes: 120 },
{ flags: { clock12h: true }, type: ElementType.CurrentTime, xOffset: 40, yOffset: 16, width: W, height: 16, utcOffsetMinutes: 120 },
{ flags: { clock12h: true, showHours: true }, type: ElementType.CurrentTime, xOffset: 0, yOffset: 24, width: W, height: 16, utcOffsetMinutes: 120 },
{ flags: { clock12h: true, showHours: false }, type: ElementType.CurrentTime, xOffset: 40, yOffset: 32, width: W, height: 16, utcOffsetMinutes: 120 },
{ flags: { clock12h: true, showSeconds: true }, type: ElementType.CurrentTime, xOffset: 80, yOffset: 32, width: W, height: 16, utcOffsetMinutes: 120 },
]
}
}).toBuffer();
},
};
function initDemos() {
const wrap = document.getElementById('demo-btns');
let activeBtn = null;
DEMO_DEFS.forEach(d => {
const btn = document.createElement('button');
btn.className = 'tb-btn'; btn.textContent = d.label;
btn.onclick = async () => {
const ok = await guardDirty();
if (!ok) return;
// load into editor
sections = d.build();
activeSec = sections[0]?.id || null; activeEl = null;
currentFilename = d.label.toLowerCase();
document.getElementById('filename').textContent = currentFilename;
markClean(); render();
// drive preview with full fidelity build
if (!window._mdDriver)
window._mdDriver = new window.MonoDisplay.MonoDisplayDriver('canvas_root', { onColor: '#EC0', offColor: '#000', fps: 25 });
window._mdDriver.load(() => Promise.resolve(DEMO_PREVIEWS[d.label]()));
if (activeBtn) activeBtn.style.color = '';
btn.style.color = '#33ff66'; activeBtn = btn;
};
wrap.appendChild(btn);
});
}
// --- Render -------------------------------------------------------------------
function render() {
updateMeta();
const wrap = document.getElementById('sections-wrap');
if (!sections.length) {
wrap.innerHTML = '<div class="empty-state">No sections yet.<br/>Use <b>Add section</b> to get started.</div>';
return;
}
wrap.innerHTML = sections.map(s => renderSection(s)).join('');
}
function renderSection(s) {
const isActive = activeSec === s.id, isOpen = s.open;
return `
<div class="sec-card${isActive ? ' active' : ''}${isOpen ? ' open' : ''}" id="sc-${s.id}">
<div class="sec-hdr${isActive ? ' active' : ''}" onclick="toggleSection('${s.id}')">
<span class="sec-arrow">▶</span>
<span class="sec-label">${esc(s.name)}</span>
<span class="sec-badge">${s.elements.length} el</span>
<button class="x-btn" title="Remove section" onclick="event.stopPropagation();removeSection('${s.id}')">×</button>
</div>
${isOpen ? `
<div class="el-list">
${s.elements.map(el => renderElement(s.id, el)).join('')}
<button class="add-el" onclick="addElement('${s.id}')">+ add element</button>
</div>`: ''}
</div>`;
}
function renderElement(secId, el) {
const isActive = activeEl && activeEl.secId === secId && activeEl.elId === el.id;
const fields = EL_FIELDS[el.type] || [];
const flags = EL_FLAGS[el.type] || [];
return `
<div class="el-item${isActive ? ' active' : ''}" id="eli-${el.id}">
<div class="el-item-hdr" onclick="selectElement('${secId}','${el.id}')">
<span class="el-type">${el.type}</span>
<span class="el-name">${elSummary(el)}</span>
<button class="x-btn" title="Remove element" onclick="event.stopPropagation();removeElement('${secId}','${el.id}')">×</button>
</div>
${isActive ? `
<div class="el-fields">
<div class="field full">
<label>Type</label>
<select onchange="changeElType('${secId}','${el.id}',this.value)">
${EL_TYPES.map(t => `<option value="${t}"${t === el.type ? ' selected' : ''}>${t}</option>`).join('')}
</select>
</div>
<div class="fields-grid">
${fields.map(f => `
<div class="field${f.full ? ' full' : ''}">
<label>${f.l}</label>
${f.t === 'text'
? `<textarea onchange="setElField('${secId}','${el.id}','${f.k}',this.value)">${esc(el.fields[f.k] ?? f.d)}</textarea>`
: `<input type="number" value="${el.fields[f.k] ?? f.d}" onchange="setElField('${secId}','${el.id}','${f.k}',+this.value)"/>`
}
</div>`).join('')}
</div>
${flags.length ? `
<div class="field full">
<label>Flags</label>
<div class="flags-row">
${flags.map(f => `
<label class="flag-check">
<input type="checkbox" ${el.flags[f.k] ? 'checked' : ''} onchange="setElFlag('${secId}','${el.id}','${f.k}',this.checked)"/>
${f.l}
</label>`).join('')}
</div>
</div>`: ''}
</div>`: ''}
</div>`;
}
function elSummary(el) {
if (el.fields.text) return esc(el.fields.text.slice(0, 24) + (el.fields.text.length > 24 ? '...' : ''));
return `${el.fields.xOffset ?? 0}, ${el.fields.yOffset ?? 0}`;
}
function esc(s) { return String(s).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;') }
function updateMeta() {
const s = activeSec ? getSec(activeSec) : null;
document.getElementById('meta-name').textContent = s ? s.name : '-';
document.getElementById('meta-count').textContent = s ? s.elements.length + ' element(s)' : '-';
document.getElementById('flag-drawFront').checked = s ? !!s.flags.drawFront : false;
document.getElementById('flag-clearBuffer').checked = s ? !!s.flags.clearBuffer : false;
}
// --- Preview ------------------------------------------------------------------
let previewTimer = null;
function triggerPreview() {
clearTimeout(previewTimer);
previewTimer = setTimeout(() => { try { buildPreview(); } catch (e) { console.warn('preview', e); } }, 150);
}
function buildPreview() {
if (!window.MonoDisplay) return;
const { MonoDisplayDriver, MonoDisplayFile, ElementType } = window.MonoDisplay;
if (!window._mdDriver)
window._mdDriver = new MonoDisplayDriver('canvas_root', { onColor: '#EC0', offColor: '#000', fps: 25 });
const s = activeSec ? getSec(activeSec) : sections[0];
if (!s || !s.elements.length) {
window._mdDriver.load(() => Promise.resolve(new MonoDisplayFile({ elements_always: [] }).toBuffer()));
return;
}
const elDefs = s.elements.map(el => ({ type: ElementType[el.type], ...el.fields, flags: el.flags })).filter(e => e.type != null);
window._mdDriver.load(() => Promise.resolve(
new MonoDisplayFile({ elements_always: { flags: s.flags, elements: elDefs } }).toBuffer()
));
}
// --- Load / Export ------------------------------------------------------------
function parsedToSections(parsedSecs) {
const TYPE_MAP = { 1: 'Image2D', 2: 'Animation', 16: 'ClippedText', 17: 'HScrollText', 32: 'CurrentTime' };
return parsedSecs
.filter(s => s.sectionType === 1 || s.sectionType === 2)
.map(s => {
secCounter++;
const elements = (s.elements || []).map(el => {
const typeName = TYPE_MAP[el.type];
if (!typeName) return null;
elCounter++;
let fields = {}, flags = {};
switch (typeName) {
case 'Image2D':
fields = { xOffset: el.xOffset, yOffset: el.yOffset, width: el.image?.width ?? W, height: el.image?.height ?? H };
break;
case 'Animation':
fields = { xOffset: el.xOffset, yOffset: el.yOffset, width: el.width, height: el.height, updateInterval: el.updateInterval };
break;
case 'ClippedText':
fields = { text: el.text, xOffset: el.xOffset, yOffset: el.yOffset, width: el.width, height: el.height };
break;
case 'HScrollText':
fields = { text: el.text, xOffset: el.xOffset, yOffset: el.yOffset, width: el.width, height: el.height, scrollSpeed: el.scrollSpeed };
flags = { endless: !!el.flags?.endless, invertDirection: !!el.flags?.invertDirection };
break;
case 'CurrentTime':
fields = { xOffset: el.xOffset, yOffset: el.yOffset, width: el.width, height: el.height, utcOffsetMinutes: el.utcOffsetMinutes };
flags = { clock12h: !!el.flags?.clock12h, showHours: !!el.flags?.showHours, showSeconds: !!el.flags?.showSeconds };
break;
}
return { id: 'e' + elCounter, type: typeName, fields, flags };
}).filter(Boolean);
return {
id: 's' + secCounter,
name: 'Section ' + secCounter,
open: true,
flags: { drawFront: !!s.flags?.drawFront, clearBuffer: !!s.flags?.clearBuffer },
elements
};
});
}
function loadBin(input) {
const file = input.files[0]; if (!file) return;
currentFilename = file.name;
document.getElementById('filename').textContent = file.name;
const reader = new FileReader();
reader.onload = e => {
const arrayBuf = e.target.result;
const buf = new Uint8Array(arrayBuf);
if (!window._mdDriver && window.MonoDisplay)
window._mdDriver = new window.MonoDisplay.MonoDisplayDriver('canvas_root', { onColor: '#EC0', offColor: '#000', fps: 25 });
if (window._mdDriver) window._mdDriver.load(() => Promise.resolve(buf));
try {
const parsed = new window.MonoDisplay.MonoDisplayParser().parse(arrayBuf);
sections = parsedToSections(parsed.sections);
activeSec = sections.length ? sections[0].id : null;
activeEl = null;
render();
} catch (err) {
console.warn('Could not parse sections from bin:', err);
}
};
reader.readAsArrayBuffer(file);
input.value = ''; markClean();
}
function exportBin() {
if (!window.MonoDisplay) { alert('MonoDisplay library not loaded.'); return; }
const { MonoDisplayFile, ElementType } = window.MonoDisplay;
const s = activeSec ? getSec(activeSec) : sections[0];
if (!s) { alert('Nothing to export.'); return; }
const elDefs = s.elements.map(el => ({ type: ElementType[el.type], ...el.fields, flags: el.flags })).filter(e => e.type != null);
const buf = new MonoDisplayFile({ elements_always: { flags: s.flags, elements: elDefs } }).toBuffer();
const blob = new Blob([buf], { type: 'application/octet-stream' });
const a = document.createElement('a');
a.href = URL.createObjectURL(blob);
a.download = currentFilename.replace(/\.bin$/, '') + '.bin';
a.click(); markClean();
}
// --- Theme --------------------------------------------------------------------
var _THEMES = ['dark', 'light', 'auto'];
function _getTheme() { return document.documentElement.getAttribute('data-theme') || 'auto'; }
function _applyTheme(t) {
if (t === 'auto') { document.documentElement.removeAttribute('data-theme'); localStorage.removeItem('theme'); }
else { document.documentElement.setAttribute('data-theme', t); localStorage.setItem('theme', t); }
_updateThemeBtn();
}
function cycleTheme() {
var cur = _getTheme(), idx = _THEMES.indexOf(cur);
_applyTheme(_THEMES[(idx + 1) % _THEMES.length]);
}
function _detectDR() {
return !!(document.querySelector('meta[name="darkreader"]') ||
document.querySelector('style[data-darkreader-style]') ||
document.documentElement.getAttribute('data-darkreader-mode'));
}
function _updateThemeBtn() {
var btn = document.getElementById('theme-toggle');
if (!btn) return;
var t = _getTheme(), dr = _detectDR();
var lbl = { dark: '☾ dark', light: '☀ light', auto: '⊙ auto' };
btn.textContent = (dr ? 'DR · ' : '') + (lbl[t] || lbl.auto);
btn.title = dr ? 'DarkReader active - controlling theme' : ('Theme: ' + t + ' (click to cycle)');
}
window.addEventListener('DOMContentLoaded', _updateThemeBtn);
// --- Init ---------------------------------------------------------------------
render();
if (window.MonoDisplay) { initDemos(); }
else { const iv = setInterval(() => { if (window.MonoDisplay) { clearInterval(iv); initDemos(); } }, 100); }
</script>
</body>
</html>