Light mode is a remap of the sixteen EGA slots, not a second stylesheet. Every rule in bbs.css already goes through those variables, so the whole board inverts by redefining them and nothing below has to know. Warm greys on sepia, monochrome. Not an EGA palette and not pretending to be one: the amber-on-paper end of the same machine. Tones are picked by contrast ratio against the ground rather than by eye, and the ordering of the sixteen is preserved, so what was recessive stays recessive: --dgrey quiet, --blue chrome, --yellow the hotkeys, --lred the loudest thing on screen. Freed from EGA fidelity, --dgrey is lifted to 4.5:1 rather than the 2.7:1 its counterpart sits at on black. The switch is a command, not a button, because a board had no widgets. It names where it is going, so it reads [L]ight on a dark screen and [D]ark on a light one and the key moves with the label. L and D were the only two letters free across all 71 pages. It is injected by bbs.js rather than written into the template, so a reader without JS is not offered a switch that cannot work. The logo takes a second set of ramps: on paper presence is ink rather than light, so they run pale to dark and the feet stay the lightest step, which keeps the weight where the dark version puts it. The session's ramp index is shared, so the two themes are the same draw rendered two ways. Dark stays the default and the no-JS result. The preference is in localStorage rather than sessionStorage, since it is a preference and not a welcome screen, and it is applied from <head> before first paint. Also fixes a hardcoded #000 on the guestbook form, which would have been a black box on paper. Verified in a browser: 70 pages and 321 frame rows still exactly 80 columns, the command flips both ways, the ramp follows, and the choice survives a navigation. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
228 lines
8.4 KiB
JavaScript
228 lines
8.4 KiB
JavaScript
var BBS = {};
|
|
|
|
/* One ANSI ramp per session, and the dark/paper switch.
|
|
*
|
|
* Boards rotated their welcome screens; this draws a palette instead. It is
|
|
* held in sessionStorage so it stays put while you read and changes when you
|
|
* come back, rather than flickering on every page load.
|
|
*
|
|
* Every dark ramp is three of the sixteen EGA indices, ordered dark to bright,
|
|
* and none of them bottom out on the frame blue, which would dissolve the
|
|
* logo's feet into the chrome. Without JS the CSS defaults apply and nothing
|
|
* is lost.
|
|
*
|
|
* On paper the ordering reverses: presence is ink rather than light, so the
|
|
* feet are the palest step and the crown the darkest, which keeps the weight
|
|
* where the dark version puts it. None of them start at the frame tone, for
|
|
* the same reason as above.
|
|
*
|
|
* This runs from <head>, before first paint, so neither the ramp nor the
|
|
* theme flashes. Dark is the default and the no-JS result; paper is opt in
|
|
* and remembered in localStorage rather than per session, because it is a
|
|
* preference and not a welcome screen.
|
|
*/
|
|
(function () {
|
|
var RAMPS = [
|
|
['#0000aa', '#00aaaa', '#55ffff'], // ice
|
|
['#0000aa', '#00aaaa', '#ffffff'], // ice/white
|
|
['#00aaaa', '#55ffff', '#ffffff'], // shallow
|
|
['#555555', '#00aaaa', '#55ffff'], // slate cyan
|
|
['#00aa00', '#55ff55', '#ffffff'], // moss
|
|
['#555555', '#00aa00', '#55ff55'], // slate green
|
|
['#00aa00', '#00aaaa', '#55ffff'], // verdigris
|
|
['#aa0000', '#aa5500', '#ffff55'], // ember
|
|
['#aa0000', '#ff5555', '#ffff55'], // fire
|
|
['#aa5500', '#ff5555', '#ffff55'], // rust
|
|
['#aa5500', '#ffff55', '#ffffff'], // amber
|
|
['#555555', '#aaaaaa', '#ffffff'], // mono
|
|
['#555555', '#aaaaaa', '#55ffff'], // cold mono
|
|
['#aa00aa', '#ff55ff', '#ffffff'], // violet
|
|
['#aa0000', '#aa00aa', '#ff55ff'] // sunset
|
|
];
|
|
/* The paper ramps, pale to dark, off the same warm grey scale as bbs.css. */
|
|
var PAPER = [
|
|
['#887a64', '#655c50', '#38342e'], // paper
|
|
['#887a64', '#534d43', '#1e1c19'], // ink
|
|
['#756b5b', '#534d43', '#2b2824'], // umber
|
|
['#756b5b', '#47423a', '#1e1c19'], // strong
|
|
['#655c50', '#47423a', '#2b2824'], // close
|
|
['#887a64', '#756b5b', '#534d43'], // soft
|
|
['#534d43', '#38342e', '#1e1c19'], // heavy
|
|
['#38342e', '#655c50', '#887a64'], // sunlit, the one that runs the other way
|
|
['#a0917c', '#756b5b', '#2b2824'] // wide
|
|
];
|
|
var i;
|
|
try {
|
|
i = sessionStorage.getItem('ramp');
|
|
if (i === null) {
|
|
i = Math.floor(Math.random() * RAMPS.length);
|
|
sessionStorage.setItem('ramp', i);
|
|
}
|
|
} catch (e) {
|
|
i = Math.floor(Math.random() * RAMPS.length);
|
|
}
|
|
|
|
BBS.ramp = function (theme) {
|
|
var set = theme === 'light' ? PAPER : RAMPS;
|
|
var r = set[i % set.length];
|
|
var s = document.documentElement.style;
|
|
s.setProperty('--l1', r[0]);
|
|
s.setProperty('--l2', r[1]);
|
|
s.setProperty('--l3', r[2]);
|
|
};
|
|
|
|
var t;
|
|
try { t = localStorage.getItem('theme'); } catch (e) { /* private mode */ }
|
|
if (t === 'light') document.documentElement.setAttribute('data-theme', 'light');
|
|
BBS.ramp(t === 'light' ? 'light' : 'dark');
|
|
})();
|
|
|
|
/* The prompt accepts commands.
|
|
*
|
|
* Numbers mean different things depending on the screen, which is how a board
|
|
* worked: at the main menu they select an area, inside an area they select a
|
|
* message. Nothing has to disambiguate because no two things on one screen
|
|
* claim the same key. Anything typeable carries data-key and the chrome that
|
|
* marks it, so the brackets are a promise rather than decoration.
|
|
*/
|
|
(function () {
|
|
// The script sits in <head> so the ramp applies before first paint; the
|
|
// command handler has to wait for a DOM to read its keys out of.
|
|
if (document.readyState === 'loading') {
|
|
document.addEventListener('DOMContentLoaded', init);
|
|
} else {
|
|
init();
|
|
}
|
|
|
|
function init() {
|
|
var buf = '', timer;
|
|
var out = document.getElementById('typed');
|
|
if (!out) return;
|
|
var hint = document.createElement('span');
|
|
hint.className = 'hint';
|
|
out.parentNode.insertBefore(hint, out.nextSibling.nextSibling);
|
|
|
|
function keys() {
|
|
var m = {};
|
|
[].forEach.call(document.querySelectorAll('[data-key]'), function (a) {
|
|
m[String(a.dataset.key).trim().toUpperCase()] = {
|
|
href: a.getAttribute('href'),
|
|
cmd: a.dataset.cmd,
|
|
label: a.textContent.trim().replace(/\s+/g, ' ').slice(0, 44)
|
|
};
|
|
});
|
|
return m;
|
|
}
|
|
var MAP = keys();
|
|
|
|
// Echo what the number is pointing at, so a pause is informative rather than
|
|
// just a delay: you can see it is right and wait, keep typing, or hit Enter.
|
|
function show() {
|
|
out.textContent = buf;
|
|
var hit = MAP[buf];
|
|
hint.textContent = hit ? ' ' + hit.label : '';
|
|
}
|
|
function go(k) {
|
|
var hit = MAP[k];
|
|
if (!hit) return false;
|
|
if (hit.cmd === 'theme') { flip(); return true; }
|
|
location.href = hit.href;
|
|
return true;
|
|
}
|
|
|
|
/* Dark or paper, as a command rather than a button: a board had no widgets,
|
|
it had a letter you typed. The entry names where you are going, so it is
|
|
[L]ight on a dark screen and [D]ark on a light one, and the key moves with
|
|
the label. Injected rather than written into the template, so a reader
|
|
without JS is not offered a switch that cannot work. */
|
|
var root = document.documentElement;
|
|
var slot = document.getElementById('themecmd');
|
|
|
|
function theme() {
|
|
return root.getAttribute('data-theme') === 'light' ? 'light' : 'dark';
|
|
}
|
|
function draw() {
|
|
if (!slot) return;
|
|
var next = theme() === 'light' ? 'dark' : 'light';
|
|
var k = next.charAt(0).toUpperCase();
|
|
slot.innerHTML = '<a href="#" data-cmd="theme" data-key="' + k +
|
|
'"><span class="key">[<b>' + k + '</b>]</span>' + next.slice(1) + '</a>';
|
|
slot.firstChild.addEventListener('click', function (e) {
|
|
e.preventDefault();
|
|
flip();
|
|
});
|
|
MAP = keys();
|
|
}
|
|
function flip() {
|
|
var next = theme() === 'light' ? 'dark' : 'light';
|
|
if (next === 'light') root.setAttribute('data-theme', 'light');
|
|
else root.removeAttribute('data-theme');
|
|
try { localStorage.setItem('theme', next); } catch (e) { /* private mode */ }
|
|
BBS.ramp(next);
|
|
draw();
|
|
}
|
|
draw();
|
|
|
|
addEventListener('keydown', function (e) {
|
|
if (e.metaKey || e.ctrlKey || e.altKey) return;
|
|
var k = e.key;
|
|
|
|
clearTimeout(timer);
|
|
if (k === 'Enter') {
|
|
if (buf && !go(buf)) { out.textContent = ''; }
|
|
buf = ''; show(); e.preventDefault(); return;
|
|
}
|
|
if (k === 'Backspace') { buf = buf.slice(0, -1); show(); e.preventDefault(); return; }
|
|
if (k === 'Escape') { buf = ''; show(); e.preventDefault(); return; }
|
|
|
|
if (/^[0-9]$/.test(k)) {
|
|
buf += k;
|
|
show();
|
|
clearTimeout(timer);
|
|
var longer = Object.keys(MAP).some(function (x) {
|
|
return x.length > buf.length && x.indexOf(buf) === 0;
|
|
});
|
|
if (!longer) {
|
|
// Nothing longer can match, so settle it now: go, or drop what was
|
|
// typed so a mistyped number never sticks at the prompt.
|
|
if (MAP[buf]) { go(buf); } else { buf = ''; show(); }
|
|
} else if (MAP[buf]) {
|
|
// Ambiguous: "1" is an area and "12" is an entry further down the same
|
|
// screen. Terminal menus settled this on a short pause rather than
|
|
// making every selection need Enter, so this does too.
|
|
// 1200ms, because the message index runs past 40 and reaching for a
|
|
// second digit takes longer than a confirmation would. Enter commits
|
|
// at once for anyone who does not want to wait.
|
|
timer = setTimeout(function () { go(buf); }, 1200);
|
|
}
|
|
e.preventDefault();
|
|
return;
|
|
}
|
|
if (/^[a-zA-Z]$/.test(k)) {
|
|
if (go(k.toUpperCase())) e.preventDefault();
|
|
}
|
|
});
|
|
}
|
|
})();
|
|
|
|
/* The address is stored rot13 so it is not sitting in the HTML for a scraper to
|
|
* lift. Same trick as tobias.raayoni-last.com. Without JS the fallback text
|
|
* still reads, spelled out, so nothing is lost but the click.
|
|
*/
|
|
(function () {
|
|
function rot13(s) {
|
|
return s.replace(/[a-zA-Z]/g, function (c) {
|
|
return String.fromCharCode((c <= 'Z' ? 90 : 122) >= (c = c.charCodeAt(0) + 13) ? c : c - 26);
|
|
});
|
|
}
|
|
function init() {
|
|
[].forEach.call(document.querySelectorAll('.mail[data-r]'), function (el) {
|
|
var a = document.createElement('a');
|
|
a.href = 'mailto:' + rot13(el.dataset.r);
|
|
a.textContent = rot13(el.dataset.r);
|
|
el.replaceWith(a);
|
|
});
|
|
}
|
|
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
|
|
else init();
|
|
})();
|