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
89 lines
2.9 KiB
JavaScript
89 lines
2.9 KiB
JavaScript
/* =====================================================================
|
|
Assembling and reading whole sheet files.
|
|
|
|
The preview iframe and the exported file are built from the same
|
|
renderer, so what you arrange on the canvas is what lands on disk.
|
|
The only difference is that the preview carries editing chrome and
|
|
omits the click-to-copy script, because on the canvas a click selects.
|
|
===================================================================== */
|
|
|
|
import { renderBody } from './render.js';
|
|
|
|
let cache = null;
|
|
|
|
export async function assets() {
|
|
if (!cache) {
|
|
const [css, js, logo] = await Promise.all([
|
|
fetch('./sheet.css').then((r) => r.text()),
|
|
fetch('./sheet.js').then((r) => r.text()),
|
|
fetch('./logo.txt').then((r) => r.text()).catch(() => ''),
|
|
]);
|
|
cache = { css, js, logo: logo.trim() };
|
|
}
|
|
return cache;
|
|
}
|
|
|
|
const esc = (s) => String(s).replace(/[&<>]/g,
|
|
(c) => ({ '&': '&', '<': '<', '>': '>' }[c]));
|
|
|
|
function title(doc) {
|
|
const code = (doc.meta.courseCode || '').trim() || 'Exercise';
|
|
return `${code} · ${doc.meta.exercise} · ${doc.meta.title}`;
|
|
}
|
|
|
|
/** A complete, self-contained sheet — what Export writes. */
|
|
export function wholeFile(doc, { css, js }) {
|
|
return [
|
|
'<!DOCTYPE html>',
|
|
'<html lang="en">',
|
|
'<head>',
|
|
'<meta charset="utf-8">',
|
|
'<meta name="viewport" content="width=device-width, initial-scale=1">',
|
|
`<title>${esc(title(doc))}</title>`,
|
|
'',
|
|
// A reader who overrode their system setting has to have it applied
|
|
// before the first paint, or every load of a dark sheet starts with a
|
|
// white flash. That rules out sheet.js, which runs at the end of the
|
|
// body — so the read happens here and the switch itself is still built
|
|
// down there. The key is shared with sheet.js; keep the two in step.
|
|
'<script>',
|
|
'try{var t=localStorage.getItem("bi122g-sheet-theme");',
|
|
'if(t==="dark"||t==="light")document.documentElement.setAttribute("data-theme",t)}catch(e){}',
|
|
'<\/script>',
|
|
'',
|
|
'<style>',
|
|
css.trim(),
|
|
'</style>',
|
|
'</head>',
|
|
'<body>',
|
|
renderBody(doc),
|
|
'',
|
|
'<script>',
|
|
js.trim(),
|
|
'<\/script>',
|
|
'</body>',
|
|
'</html>',
|
|
'',
|
|
].join('\n');
|
|
}
|
|
|
|
/** An empty shell for the canvas; the body is filled on every update. */
|
|
export function previewShell({ css }, chromeCSS) {
|
|
return [
|
|
'<!DOCTYPE html>',
|
|
'<html lang="en">',
|
|
'<head><meta charset="utf-8">',
|
|
'<style>', css.trim(), chromeCSS, '</style>',
|
|
'</head><body></body></html>',
|
|
].join('\n');
|
|
}
|
|
|
|
export function download(name, text, type = 'text/html;charset=utf-8') {
|
|
const url = URL.createObjectURL(new Blob([text], { type }));
|
|
const a = Object.assign(document.createElement('a'), { href: url, download: name });
|
|
document.body.appendChild(a);
|
|
a.click();
|
|
a.remove();
|
|
setTimeout(() => URL.revokeObjectURL(url), 2000);
|
|
}
|