Import the exercise builder
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
This commit is contained in:
@@ -0,0 +1,85 @@
|
||||
// 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(); }
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user