c18510b3c3
The drag-and-drop workspace for building BI122G exercise sheets, served by the Workshop at /projects/exercise-builder. Plain HTML, CSS and ES modules; no build step. It runs entirely in the browser — work in progress lives in localStorage and finished sheets leave as downloads, so nothing is written on the server. This directory is canonical. It began as a deployment target for deploy_builder.sh syncing from the BI122G course folder, and that arrangement has ended; see the README. Includes the sheet's light/dark switch: the stylesheet already carried both themes and followed the reader's system setting, so this adds the control that overrides it — a corner toggle built by sheet.js, applied before first paint by a head script the exporter writes, and hidden in print. The builder's toolbar previews the same attribute on the canvas. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ed5kU3HfZq9dfj7EYj5J2v
86 lines
3.3 KiB
JavaScript
86 lines
3.3 KiB
JavaScript
// The light/dark switch. The stylesheet already carries both themes and
|
|
// already follows the reader's system setting on its own — all this adds is
|
|
// a way to override that, by setting the data-theme attribute the CSS is
|
|
// written to look for. Progressive enhancement, like the copy behaviour
|
|
// below: without it the sheet still tracks the system setting, and the
|
|
// control is not printed.
|
|
//
|
|
// No attribute is set unless the reader asks for one, so "follow my system"
|
|
// stays the state a sheet arrives in.
|
|
(function () {
|
|
var KEY = 'bi122g-sheet-theme';
|
|
var root = document.documentElement;
|
|
|
|
// A sheet is often read from a file:// URL or a locked-down browser, where
|
|
// storage throws rather than returning null. Losing the preference is fine;
|
|
// losing the switch is not.
|
|
function stored() { try { return localStorage.getItem(KEY); } catch (e) { return null; } }
|
|
function remember(v) { try { localStorage.setItem(KEY, v); } catch (e) {} }
|
|
|
|
var system = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)');
|
|
var saved = stored();
|
|
var choice = saved === 'dark' || saved === 'light' ? saved : null;
|
|
// Normally already set by the head script the exporter writes (see
|
|
// document.js) — repeated here so the switch also works in any page that
|
|
// includes this file on its own.
|
|
if (choice) root.setAttribute('data-theme', choice);
|
|
|
|
function current() { return choice || (system && system.matches ? 'dark' : 'light'); }
|
|
|
|
var btn = document.createElement('button');
|
|
btn.className = 'themetoggle';
|
|
btn.type = 'button';
|
|
|
|
function label() {
|
|
var next = current() === 'dark' ? 'light' : 'dark';
|
|
btn.textContent = current() === 'dark' ? '☀' : '☾';
|
|
btn.setAttribute('title', 'Switch to ' + next + ' mode');
|
|
btn.setAttribute('aria-label', 'Switch to ' + next + ' mode');
|
|
}
|
|
|
|
btn.addEventListener('click', function () {
|
|
choice = current() === 'dark' ? 'light' : 'dark';
|
|
root.setAttribute('data-theme', choice);
|
|
remember(choice);
|
|
label();
|
|
});
|
|
|
|
// still following the system: keep the glyph honest if it changes mid-read
|
|
if (system && system.addEventListener) {
|
|
system.addEventListener('change', function () { if (!choice) label(); });
|
|
}
|
|
|
|
label();
|
|
document.body.appendChild(btn);
|
|
})();
|
|
|
|
// Click anywhere on a command box to copy it. Progressive enhancement only:
|
|
// the page is complete without this, and nothing of it appears in print.
|
|
document.querySelectorAll('.cmd').forEach(function (block) {
|
|
var payload = block.textContent.trim();
|
|
|
|
block.setAttribute('role', 'button');
|
|
block.setAttribute('tabindex', '0');
|
|
block.setAttribute('aria-label', 'Copy command: ' + payload);
|
|
|
|
function copy() {
|
|
navigator.clipboard.writeText(payload).then(function () {
|
|
block.classList.add('copied');
|
|
setTimeout(function () { block.classList.remove('copied'); }, 1400);
|
|
}).catch(function () {
|
|
// clipboard blocked (insecure context, or permission denied) — select
|
|
// the text instead so the reader can copy it by hand
|
|
var range = document.createRange();
|
|
range.selectNodeContents(block);
|
|
var sel = window.getSelection();
|
|
sel.removeAllRanges();
|
|
sel.addRange(range);
|
|
});
|
|
}
|
|
|
|
block.addEventListener('click', copy);
|
|
block.addEventListener('keydown', function (e) {
|
|
if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); copy(); }
|
|
});
|
|
});
|