mr-md 1.0.4 → 2.0.0-beta

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. package/README.md +10 -5
  2. package/dist/builder.d.ts +6 -20
  3. package/dist/builder.d.ts.map +1 -1
  4. package/dist/builder.js +38 -97
  5. package/dist/cli/dev.d.ts +2 -0
  6. package/dist/cli/dev.d.ts.map +1 -0
  7. package/dist/cli/dev.js +92 -0
  8. package/dist/cli/generate.d.ts +2 -0
  9. package/dist/cli/generate.d.ts.map +1 -0
  10. package/dist/cli/generate.js +171 -0
  11. package/dist/cli/init.d.ts +2 -0
  12. package/dist/cli/init.d.ts.map +1 -0
  13. package/dist/cli/init.js +89 -0
  14. package/dist/cli.d.ts +3 -0
  15. package/dist/cli.d.ts.map +1 -0
  16. package/dist/cli.js +27 -0
  17. package/dist/client/app.js +282 -107
  18. package/dist/index.d.ts +1 -1
  19. package/dist/index.d.ts.map +1 -1
  20. package/dist/index.js +1 -1
  21. package/dist/renderer/blocks.d.ts.map +1 -1
  22. package/dist/renderer/blocks.js +88 -16
  23. package/dist/renderer/html-neo.d.ts +7 -0
  24. package/dist/renderer/html-neo.d.ts.map +1 -0
  25. package/dist/renderer/html-neo.js +173 -0
  26. package/dist/renderer/html.d.ts.map +1 -1
  27. package/dist/renderer/html.js +36 -7
  28. package/dist/renderer/index-neo.d.ts +4 -0
  29. package/dist/renderer/index-neo.d.ts.map +1 -0
  30. package/dist/renderer/index-neo.js +469 -0
  31. package/dist/renderer/index.d.ts +1 -2
  32. package/dist/renderer/index.d.ts.map +1 -1
  33. package/dist/renderer/index.js +29 -379
  34. package/dist/renderer/markdown.d.ts +1 -1
  35. package/dist/renderer/markdown.d.ts.map +1 -1
  36. package/dist/renderer/markdown.js +3 -3
  37. package/dist/renderer/utils.d.ts +1 -1
  38. package/dist/renderer/utils.d.ts.map +1 -1
  39. package/dist/renderer/utils.js +41 -34
  40. package/dist/styles/theme-neo.css +1369 -0
  41. package/dist/styles/theme.css +412 -127
  42. package/dist/types.d.ts +8 -10
  43. package/dist/types.d.ts.map +1 -1
  44. package/package.json +8 -7
  45. package/src/builder.ts +49 -125
  46. package/src/cli/dev.ts +102 -0
  47. package/src/cli/generate.ts +191 -0
  48. package/src/cli/init.ts +97 -0
  49. package/src/cli.ts +29 -0
  50. package/src/client/app.js +282 -107
  51. package/src/index.ts +1 -1
  52. package/src/renderer/blocks.ts +89 -15
  53. package/src/renderer/html.ts +36 -7
  54. package/src/renderer/index.ts +30 -394
  55. package/src/renderer/markdown.ts +3 -2
  56. package/src/renderer/utils.ts +43 -36
  57. package/src/styles/theme.css +412 -127
  58. package/src/types.ts +8 -12
@@ -37,8 +37,8 @@ function renderBlockInner(block, idx, options) {
37
37
  switch (block.type) {
38
38
  case "heading": {
39
39
  const md = resolveContent(block.src, options, "md");
40
- const { html, title, headings } = mdToHtml(md);
41
- const label = block.title || title || headings[0]?.text || (typeof block.src === "string" && !block.src.includes(".md") ? block.src.replace(/^#+\s*/, '') : "Heading");
40
+ const { html, title } = mdToHtml(md);
41
+ const label = block.title || title || (typeof block.src === "string" && !block.src.includes(".md") ? block.src : "Heading");
42
42
  const id = `heading-${idx}`;
43
43
  return {
44
44
  html: `<section id="${id}" class="bk-section bk-heading">${html}</section>`,
@@ -51,14 +51,14 @@ function renderBlockInner(block, idx, options) {
51
51
  const navItems = headings.map(h => ({
52
52
  id: h.id,
53
53
  label: h.text,
54
- kind: h.level === 2 ? "heading" : "section"
54
+ kind: h.level === 1 ? "heading" : "section"
55
55
  }));
56
56
  return { html: `<div class="bk-markdown">${html}</div>`, navItems: navItems.length > 0 ? navItems : undefined };
57
57
  }
58
58
  case "section": {
59
59
  const md = resolveContent(block.src, options, "md");
60
- const { html, title, headings } = mdToHtml(md);
61
- const label = block.label || title || headings[0]?.text || (typeof block.src === "string" && !block.src.includes(".md") ? block.src.replace(/^#+\s*/, '') : "Section");
60
+ const { html, title } = mdToHtml(md);
61
+ const label = block.label || title || (typeof block.src === "string" && !block.src.includes(".md") ? block.src : "Section");
62
62
  const id = `section-${idx}`;
63
63
  return {
64
64
  html: `<section id="${id}" class="bk-section bk-subsection">${html}</section>`,
@@ -115,6 +115,8 @@ function renderBlockInner(block, idx, options) {
115
115
  const propsJson = escapeScriptJson(block.props ?? {});
116
116
  const simSrc = resolveContent(block.src, options, "js");
117
117
  const simConfig = { js: simSrc, loop: false, dependencies: block.dependencies };
118
+ const id = `sim-${idx}`;
119
+ const label = block.label || "Interactive Simulation";
118
120
  return {
119
121
  html: blockChrome(block.controls === "observe" ? "Simulation" : "Interactive Lab", block.label, block.caption, `${renderSimulationControls(block)}
120
122
  <div class="bk-embed-frame bk-embed-interactive">
@@ -123,21 +125,24 @@ function renderBlockInner(block, idx, options) {
123
125
  </div>
124
126
  <iframe srcdoc="${iframeDoc(simSrc, propsJson, false, block.dependencies)}"
125
127
  sandbox="allow-scripts"
128
+ loading="lazy"
126
129
  style="width:100%;height:100%;border:none;display:block;">
127
130
  </iframe>
128
131
  </div>
129
- <script type="application/json" class="bk-sim-config">${escapeScriptJson(simConfig)}</script>`, block.accent ?? "blue"),
132
+ <script type="application/json" class="bk-sim-config">${escapeScriptJson(simConfig)}</script>`, block.accent ?? "blue", true, id),
133
+ navItems: [{ id, label, kind: "simulation" }],
130
134
  };
131
135
  }
132
136
  case "animation": {
133
137
  const animSrc = resolveContent(block.src, options, "js");
134
138
  return {
135
- html: blockChrome("Animation", block.label, block.caption, `<div class="bk-embed-frame bk-embed-interactive">
139
+ html: blockChrome("Animation", block.label, block.caption, `<div class="bk-embed-frame bk-embed-interactive" data-is-animation="true">
136
140
  <div class="bk-embed-overlay" tabindex="0" role="button" aria-label="Activate interactive animation">
137
141
  <span class="bk-embed-overlay-text">Click to interact</span>
138
142
  </div>
139
143
  <iframe srcdoc="${iframeDoc(animSrc, "{}", block.loop)}"
140
144
  sandbox="allow-scripts"
145
+ loading="lazy"
141
146
  style="width:100%;height:100%;border:none;display:block;">
142
147
  </iframe>
143
148
  </div>`, block.accent ?? "neutral"),
@@ -210,8 +215,7 @@ function renderBlockInner(block, idx, options) {
210
215
  if (!trimmed.startsWith("{") && !trimmed.startsWith("[")) {
211
216
  throw new Error("Quiz file not found or invalid JSON format");
212
217
  }
213
- const parsed = JSON.parse(trimmed);
214
- quiz = Array.isArray(parsed) ? { questions: parsed } : parsed;
218
+ quiz = JSON.parse(trimmed);
215
219
  }
216
220
  catch (e) {
217
221
  const msg = e instanceof Error ? e.message : String(e);
@@ -266,7 +270,7 @@ function renderQuestion(q, quizId, qi) {
266
270
  }
267
271
  // Wraps a JS string in a minimal iframe document
268
272
  function iframeDoc(js, props, loop, dependencies) {
269
- const scriptTags = (dependencies ?? []).map((url) => `<script src="${escAttr(url)}"></script>`).join("\n");
273
+ const scriptTags = (dependencies ?? []).map((url) => `<script src="${escAttr(url)}"></script>`).join("\\n");
270
274
  const doc = `<!DOCTYPE html><html><head>
271
275
  ${scriptTags}
272
276
  <style>
@@ -280,6 +284,26 @@ ${scriptTags}
280
284
  window.__simProps=${props};
281
285
  window.__loop=${loop ?? false};
282
286
  window.bkSetupCalled = false;
287
+ window.__bkTheme = { colors: {}, theme: "light", palette: "ink", ui: "standard" };
288
+ window.bkColor = function(name) { return window.__bkTheme.colors[name] || "#000000"; };
289
+ window.bkUi = function() { return window.__bkTheme.ui; };
290
+ window.bkThemeMode = function() {
291
+ const rootTheme = window.__bkTheme.theme;
292
+ if (rootTheme === "auto") {
293
+ return window.matchMedia('(prefers-color-scheme: dark)').matches ? "dark" : "light";
294
+ }
295
+ return rootTheme;
296
+ };
297
+ window.addEventListener("message", function(e) {
298
+ if (e.data && e.data.type === "bk:theme-sync") {
299
+ window.__bkTheme = e.data.state;
300
+ window.dispatchEvent(new CustomEvent("bk:theme-changed", { detail: window.__bkTheme }));
301
+ }
302
+ });
303
+ if (window.parent && window.parent !== window) {
304
+ window.parent.postMessage({ type: "bk:request-theme" }, "*");
305
+ }
306
+
283
307
  window.bkCanvasPoint = function(event, canvas) {
284
308
  const c = canvas || event.currentTarget || event.target;
285
309
  const rect = c.getBoundingClientRect();
@@ -330,19 +354,67 @@ window.bkSetup = function(requestedW, requestedH, loopFn) {
330
354
  if (!canvas) return;
331
355
  const ctx = canvas.getContext("2d");
332
356
 
357
+ let loopId = null;
358
+ let cachedFit = window.bkFitCanvas(canvas, requestedW, requestedH);
359
+
333
360
  function loop() {
334
- const fit = window.bkFitCanvas(canvas, requestedW, requestedH);
335
361
  if (window.innerWidth >= 32 && window.innerHeight >= 32) {
336
362
  ctx.save();
337
- ctx.scale(fit.scale, fit.scale);
363
+ ctx.scale(cachedFit.scale, cachedFit.scale);
338
364
 
339
- loopFn(ctx, fit.width, fit.height);
365
+ loopFn(ctx, cachedFit.width, cachedFit.height);
340
366
 
341
367
  ctx.restore();
342
368
  }
343
- requestAnimationFrame(loop);
369
+ if (window.__loop) {
370
+ loopId = requestAnimationFrame(loop);
371
+ } else {
372
+ loopId = null;
373
+ }
344
374
  }
345
- loop();
375
+
376
+ function initDraw() {
377
+ if (window.innerWidth >= 32 && window.innerHeight >= 32) {
378
+ cachedFit = window.bkFitCanvas(canvas, requestedW, requestedH);
379
+ loop();
380
+ } else {
381
+ requestAnimationFrame(initDraw);
382
+ }
383
+ }
384
+ initDraw();
385
+
386
+ window.addEventListener("resize", () => {
387
+ cachedFit = window.bkFitCanvas(canvas, requestedW, requestedH);
388
+ if (!window.__loop && window.innerWidth >= 32 && window.innerHeight >= 32) {
389
+ if (!loopId) {
390
+ ctx.save();
391
+ ctx.scale(cachedFit.scale, cachedFit.scale);
392
+ loopFn(ctx, cachedFit.width, cachedFit.height);
393
+ ctx.restore();
394
+ }
395
+ }
396
+ });
397
+
398
+ window.addEventListener("bk:theme-changed", () => {
399
+ if (!window.__loop && window.innerWidth >= 32 && window.innerHeight >= 32) {
400
+ if (!loopId) {
401
+ ctx.save();
402
+ ctx.scale(cachedFit.scale, cachedFit.scale);
403
+ loopFn(ctx, cachedFit.width, cachedFit.height);
404
+ ctx.restore();
405
+ }
406
+ }
407
+ });
408
+
409
+ window.addEventListener("message", (event) => {
410
+ if (!event.data) return;
411
+ if (event.data.type === "bk:play") {
412
+ window.__loop = true;
413
+ if (!loopId) loopId = requestAnimationFrame(loop);
414
+ } else if (event.data.type === "bk:pause") {
415
+ window.__loop = false; // will stop at next frame
416
+ }
417
+ });
346
418
  };
347
419
 
348
420
  window.addEventListener("message", (event) => {
@@ -359,9 +431,9 @@ try {
359
431
  if (!window.bkSetupCalled) {
360
432
  function fallbackScale() {
361
433
  window.bkFitCanvas(document.getElementById("c"), 800, 500, { bitmap: false });
362
- requestAnimationFrame(fallbackScale);
363
434
  }
364
435
  fallbackScale();
436
+ window.addEventListener("resize", fallbackScale);
365
437
  }
366
438
  </script>
367
439
  </body></html>`;
@@ -0,0 +1,7 @@
1
+ import type { BuildOptions, Lesson } from "../types.js";
2
+ import type { NavItem } from "./utils.js";
3
+ declare function renderNavItem(item: NavItem): string;
4
+ declare function renderPage(lesson: Lesson, navItems: NavItem[], bodyHtml: string, opts: BuildOptions): string;
5
+ declare function copyCoreAssets(outDir: string): void;
6
+ export { copyCoreAssets, renderNavItem, renderPage };
7
+ //# sourceMappingURL=html-neo.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"html-neo.d.ts","sourceRoot":"","sources":["../../src/renderer/html-neo.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAExD,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,YAAY,CAAC;AAI1C,iBAAS,aAAa,CAAC,IAAI,EAAE,OAAO,GAAG,MAAM,CAU5C;AAsBD,iBAAS,UAAU,CAClB,MAAM,EAAE,MAAM,EACd,QAAQ,EAAE,OAAO,EAAE,EACnB,QAAQ,EAAE,MAAM,EAChB,IAAI,EAAE,YAAY,GAChB,MAAM,CA0HR;AAID,iBAAS,cAAc,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAa5C;AAED,OAAO,EAAE,cAAc,EAAE,aAAa,EAAE,UAAU,EAAE,CAAC"}
@@ -0,0 +1,173 @@
1
+ import * as fs from "fs";
2
+ import * as path from "path";
3
+ import { fileURLToPath } from "url";
4
+ const __filename = fileURLToPath(import.meta.url);
5
+ const __dirname = path.dirname(__filename);
6
+ import { escHtml } from "./blocks.js";
7
+ // ─── Page shell ───────────────────────────────────────────────────────────────
8
+ function renderNavItem(item) {
9
+ const kindClass = item.kind === "heading"
10
+ ? "bk-nav-heading"
11
+ : item.kind === "quiz"
12
+ ? "bk-nav-quiz"
13
+ : item.kind === "simulation"
14
+ ? "bk-nav-sim"
15
+ : "bk-nav-sub";
16
+ return `<a href="#${item.id}" class="bk-nav-item ${kindClass}" data-id="${item.id}">${escHtml(item.label)}</a>`;
17
+ }
18
+ function renderEndNav(lesson) {
19
+ const { prevSlug, prevTitle, nextSlug, nextTitle } = lesson.meta;
20
+ if (!prevSlug && !nextSlug)
21
+ return "";
22
+ return `<nav class="bk-end-nav" aria-label="Lesson navigation" style="grid-template-columns: repeat(2, minmax(0, 1fr));">
23
+ ${prevSlug ? `
24
+ <a class="bk-end-link bk-end-link--prev" href="${prevSlug}.html">
25
+ <span>Previous Lesson</span>
26
+ <strong>${escHtml(prevTitle || "Previous")}</strong>
27
+ </a>
28
+ ` : `<div class="bk-end-link" style="visibility:hidden"></div>`}
29
+ ${nextSlug ? `
30
+ <a class="bk-end-link bk-end-link--next" href="${nextSlug}.html">
31
+ <span>Next Lesson</span>
32
+ <strong>${escHtml(nextTitle || "Next")}</strong>
33
+ </a>
34
+ ` : `<div class="bk-end-link" style="visibility:hidden"></div>`}
35
+ </nav>`;
36
+ }
37
+ function renderPage(lesson, navItems, bodyHtml, opts) {
38
+ const theme = opts.theme ?? "auto";
39
+ const schemeAttr = `data-theme="${theme}"`;
40
+ const preset = opts.preset ?? {};
41
+ const layout = preset.layout ?? "lesson";
42
+ const density = preset.density ?? "comfortable";
43
+ const tone = preset.tone ?? "scholarly";
44
+ const palette = opts.palette ?? "ink";
45
+ const navHtml = navItems.map(renderNavItem).join("\n");
46
+ const endNavHtml = renderEndNav(lesson);
47
+ return `<!DOCTYPE html>
48
+ <html lang="en" data-palette="${palette}" ${schemeAttr}>
49
+ <head>
50
+ <meta charset="UTF-8">
51
+ <meta name="viewport" content="width=device-width, initial-scale=1">
52
+ <title>${escHtml(lesson.meta.title)}</title>
53
+ ${lesson.meta.description ? `<meta name="description" content="${escHtml(lesson.meta.description)}">` : ""}
54
+ <link rel="preconnect" href="https://fonts.googleapis.com">
55
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
56
+ <link rel="preconnect" href="https://cdn.jsdelivr.net" crossorigin>
57
+ <link href="https://fonts.googleapis.com/css2?family=Archivo:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500;600&family=Syne:wght@600;700;800&display=swap" rel="stylesheet">
58
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/katex@0.16.47/dist/katex.min.css">
59
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/highlight.js@11.11.1/styles/github-dark.min.css">
60
+ ${opts.head ?? ""}
61
+ <link rel="stylesheet" href="assets/theme.css">
62
+ <style>
63
+ ${opts.font ? `:root { --font-sans: ${opts.font}, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; }` : ""}
64
+ </style>
65
+ <script type="speculationrules">
66
+ {
67
+ "prefetch": [{
68
+ "where": { "href_matches": "/*" },
69
+ "eagerness": "eager"
70
+ }],
71
+ "prerender": [{
72
+ "where": { "href_matches": "/*" },
73
+ "eagerness": "moderate"
74
+ }]
75
+ }
76
+ </script>
77
+ <link rel="expect" href="#bk-content" blocking="render">
78
+ <script blocking="render">
79
+ const savedTheme = localStorage.getItem("bk-theme");
80
+ const savedPalette = localStorage.getItem("bk-palette");
81
+ if (savedTheme) document.documentElement.setAttribute("data-theme", savedTheme);
82
+ if (savedPalette) {
83
+ const normalizedPalette = savedPalette === "green" ? "field" : savedPalette;
84
+ document.documentElement.setAttribute("data-palette", normalizedPalette);
85
+ }
86
+ </script>
87
+ </head>
88
+ <body class="bk-layout-${layout} bk-density-${density} bk-tone-${tone}">
89
+ <div class="bk-shell">
90
+ <aside class="bk-sidebar">
91
+ <div class="bk-sidebar-inner">
92
+ <div class="bk-sidebar-header">
93
+ ${lesson.meta.parentSlug ? `<div style="margin-top: 8px;"><a href="${lesson.meta.parentSlug}.html" class="bk-back-link" aria-label="Back to Chapter" style="margin-bottom: 12px;"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M19 12H5"/><path d="M12 19l-7-7 7-7"/></svg>Back to Chapter</a></div>` : `<div style="margin-top: 8px;"></div>`}
94
+ <div class="bk-sidebar-title">${escHtml(lesson.meta.title)}</div>
95
+ ${lesson.meta.author ? `<div class="bk-sidebar-author">By ${escHtml(lesson.meta.author)}</div>` : ""}
96
+ ${lesson.meta.tags?.length ? `<div class="bk-tag-row">${lesson.meta.tags.map((tag) => `<span>${escHtml(tag)}</span>`).join("")}</div>` : ""}
97
+ </div>
98
+ <nav class="bk-nav">${navHtml}</nav>
99
+ <div class="bk-sidebar-footer">
100
+ <button class="bk-icon-btn bk-settings-button" id="bk-settings-button" type="button" aria-expanded="false" aria-controls="bk-theme-panel" title="Display settings">
101
+ <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="3"></circle><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"></path></svg>
102
+ <span class="bk-sr-only">Display settings</span>
103
+ </button>
104
+ <div class="bk-theme-panel" id="bk-theme-panel" aria-label="Display settings" hidden>
105
+ <div class="bk-theme-row">
106
+ <span>Theme</span>
107
+ <div class="bk-segmented-control" id="bk-theme-icons">
108
+ <button type="button" class="bk-segment-btn ${theme === "light" ? "active" : ""}" data-theme="light" title="Light" aria-label="Light theme">
109
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="5"></circle><line x1="12" y1="1" x2="12" y2="3"></line><line x1="12" y1="21" x2="12" y2="23"></line><line x1="4.22" y1="4.22" x2="5.64" y2="5.64"></line><line x1="18.36" y1="18.36" x2="19.78" y2="19.78"></line><line x1="1" y1="12" x2="3" y2="12"></line><line x1="21" y1="12" x2="23" y2="12"></line><line x1="4.22" y1="19.78" x2="5.64" y2="18.36"></line><line x1="18.36" y1="5.64" x2="19.78" y2="4.22"></line></svg>
110
+ </button>
111
+ <button type="button" class="bk-segment-btn ${theme === "auto" ? "active" : (!theme ? "active" : "")}" data-theme="auto" title="System" aria-label="System theme">
112
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="3" width="20" height="14" rx="2" ry="2"></rect><line x1="8" y1="21" x2="16" y2="21"></line><line x1="12" y1="17" x2="12" y2="21"></line></svg>
113
+ </button>
114
+ <button type="button" class="bk-segment-btn ${theme === "dark" ? "active" : ""}" data-theme="dark" title="Dark" aria-label="Dark theme">
115
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"></path></svg>
116
+ </button>
117
+ </div>
118
+ </div>
119
+ <div class="bk-theme-row">
120
+ <span>Palette</span>
121
+ <div class="bk-segmented-control" id="bk-palette-icons">
122
+ <button type="button" class="bk-segment-btn ${palette === "ink" ? "active" : ""}" data-palette="ink" title="Ink" aria-label="Ink palette">
123
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 2.69l5.66 5.66a8 8 0 1 1-11.31 0z"></path></svg>
124
+ </button>
125
+ <button type="button" class="bk-segment-btn ${palette === "field" ? "active" : ""}" data-palette="field" title="Field" aria-label="Field palette">
126
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M11 20A7 7 0 0 1 9.8 6.1C15.5 5 17 4.48 19 2c1 2 2 4.18 2 8 0 5.5-4.78 10-10 10Z"></path><path d="M2 21c0-3 1.85-5.36 5.08-6C9.5 14.52 12 13 13 12"></path></svg>
127
+ </button>
128
+ <button type="button" class="bk-segment-btn ${palette === "ember" ? "active" : ""}" data-palette="ember" title="Ember" aria-label="Ember palette">
129
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M8.5 14.5A2.5 2.5 0 0011 12c0-1.38-.5-2-1-3-1.072-2.143-.224-4.054 2-6 .5 2.5 2 4.9 4 6.5 2 1.6 3 3.5 3 5.5a7 7 0 11-14 0c0-1.153.433-2.294 1-3a2.5 2.5 0 002.5 2.5z"></path></svg>
130
+ </button>
131
+ </div>
132
+ </div>
133
+ </div>
134
+ </div>
135
+ </div>
136
+ </aside>
137
+ <button class="bk-sidebar-collapse-floating" id="bk-sidebar-collapse" aria-label="Collapse sidebar">
138
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M15 18l-6-6 6-6"/></svg>
139
+ </button>
140
+ <main class="bk-main">
141
+ <button class="bk-sidebar-expand" id="bk-sidebar-expand" type="button" aria-label="Expand sidebar">
142
+ <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M9 18l6-6-6-6"/></svg>
143
+ </button>
144
+ <article class="bk-content" id="bk-content">
145
+ <header class="bk-hero">
146
+ <p class="bk-eyebrow">Interactive Lesson</p>
147
+ <h1 style="view-transition-name: title-${lesson.meta.slug}">${escHtml(lesson.meta.title)}</h1>
148
+ ${lesson.meta.description ? `<p class="bk-deck">${escHtml(lesson.meta.description)}</p>` : ""}
149
+ </header>
150
+ ${bodyHtml}
151
+ ${endNavHtml}
152
+ </article>
153
+ </main>
154
+ </div>
155
+ <script src="assets/app.js" defer></script>
156
+ </body>
157
+ </html>`;
158
+ }
159
+ // ─── Core Assets ────────────────────────────────────────────────────────────────
160
+ function copyCoreAssets(outDir) {
161
+ const assetsDir = path.join(outDir, "assets");
162
+ if (!fs.existsSync(assetsDir))
163
+ fs.mkdirSync(assetsDir, { recursive: true });
164
+ const cssPath = path.join(__dirname, "../styles/theme.css");
165
+ if (fs.existsSync(cssPath)) {
166
+ fs.copyFileSync(cssPath, path.join(assetsDir, "theme.css"));
167
+ }
168
+ const jsPath = path.join(__dirname, "../client/app.js");
169
+ if (fs.existsSync(jsPath)) {
170
+ fs.copyFileSync(jsPath, path.join(assetsDir, "app.js"));
171
+ }
172
+ }
173
+ export { copyCoreAssets, renderNavItem, renderPage };
@@ -1 +1 @@
1
- {"version":3,"file":"html.d.ts","sourceRoot":"","sources":["../../src/renderer/html.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAExD,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,YAAY,CAAC;AAI1C,iBAAS,aAAa,CAAC,IAAI,EAAE,OAAO,GAAG,MAAM,CAQ5C;AAsBD,iBAAS,UAAU,CAClB,MAAM,EAAE,MAAM,EACd,QAAQ,EAAE,OAAO,EAAE,EACnB,QAAQ,EAAE,MAAM,EAChB,IAAI,EAAE,YAAY,GAChB,MAAM,CAkGR;AAID,iBAAS,OAAO,IAAI,MAAM,CAKzB;AAID,iBAAS,YAAY,IAAI,MAAM,CAE9B;AAED,OAAO,EAAE,YAAY,EAAE,OAAO,EAAE,aAAa,EAAE,UAAU,EAAE,CAAC"}
1
+ {"version":3,"file":"html.d.ts","sourceRoot":"","sources":["../../src/renderer/html.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAExD,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,YAAY,CAAC;AAI1C,iBAAS,aAAa,CAAC,IAAI,EAAE,OAAO,GAAG,MAAM,CAU5C;AAsBD,iBAAS,UAAU,CAClB,MAAM,EAAE,MAAM,EACd,QAAQ,EAAE,OAAO,EAAE,EACnB,QAAQ,EAAE,MAAM,EAChB,IAAI,EAAE,YAAY,GAChB,MAAM,CA6HR;AAID,iBAAS,OAAO,IAAI,MAAM,CAKzB;AAID,iBAAS,YAAY,IAAI,MAAM,CAE9B;AAED,OAAO,EAAE,YAAY,EAAE,OAAO,EAAE,aAAa,EAAE,UAAU,EAAE,CAAC"}
@@ -10,7 +10,9 @@ function renderNavItem(item) {
10
10
  ? "bk-nav-heading"
11
11
  : item.kind === "quiz"
12
12
  ? "bk-nav-quiz"
13
- : "bk-nav-sub";
13
+ : item.kind === "simulation"
14
+ ? "bk-nav-sim"
15
+ : "bk-nav-sub";
14
16
  return `<a href="#${item.id}" class="bk-nav-item ${kindClass}" data-id="${item.id}">${escHtml(item.label)}</a>`;
15
17
  }
16
18
  function renderEndNav(lesson) {
@@ -33,23 +35,36 @@ function renderEndNav(lesson) {
33
35
  </nav>`;
34
36
  }
35
37
  function renderPage(lesson, navItems, bodyHtml, opts) {
36
- const theme = opts.theme ?? "auto";
37
- const schemeAttr = theme === "auto" ? "" : `data-theme="${theme}"`;
38
+ const theme = opts.theme ?? "light";
39
+ const schemeAttr = `data-theme="${theme}"`;
38
40
  const preset = opts.preset ?? {};
39
41
  const layout = preset.layout ?? "lesson";
40
42
  const density = preset.density ?? "comfortable";
41
43
  const tone = preset.tone ?? "scholarly";
42
44
  const palette = opts.palette ?? "ink";
45
+ const ui = opts.ui ?? "standard";
43
46
  const navHtml = navItems.map(renderNavItem).join("\n");
44
47
  const endNavHtml = renderEndNav(lesson);
45
48
  return `<!DOCTYPE html>
46
- <html lang="en" data-palette="${palette}" ${schemeAttr}>
49
+ <html lang="en" data-palette="${palette}" data-ui="${ui}" ${schemeAttr}>
47
50
  <head>
51
+ <script>
52
+ (function() {
53
+ var t = localStorage.getItem("bk-theme");
54
+ var p = localStorage.getItem("bk-palette");
55
+ var u = localStorage.getItem("bk-ui");
56
+ var root = document.documentElement;
57
+ if (t) root.setAttribute("data-theme", t);
58
+ if (p) root.setAttribute("data-palette", p === "green" ? "field" : p);
59
+ if (u) root.setAttribute("data-ui", u);
60
+ })();
61
+ </script>
48
62
  <meta charset="UTF-8">
49
63
  <meta name="viewport" content="width=device-width, initial-scale=1">
50
64
  <title>${escHtml(lesson.meta.title)}</title>
51
65
  ${lesson.meta.description ? `<meta name="description" content="${escHtml(lesson.meta.description)}">` : ""}
52
66
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/katex@0.16.47/dist/katex.min.css">
67
+ <link href="https://fonts.googleapis.com/css2?family=Fraunces:opsz,wght@9..144,650;9..144,760&family=IBM+Plex+Sans:wght@400;500;600;700&family=IBM+Plex+Mono:wght@400;500&family=Archivo:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500;600&family=Syne:wght@600;700;800&family=Playfair+Display:ital,wght@0,400..700;1,400..700&family=Lora:ital,wght@0,400..700;1,400..700&display=swap" rel="stylesheet">
53
68
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/highlight.js@11.11.1/styles/github-dark.min.css">
54
69
  ${opts.head ?? ""}
55
70
  <style>
@@ -80,12 +95,12 @@ ${pageCSS()}
80
95
  <button type="button" class="bk-segment-btn ${theme === "light" ? "active" : ""}" data-theme="light" title="Light" aria-label="Light theme">
81
96
  <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="5"></circle><line x1="12" y1="1" x2="12" y2="3"></line><line x1="12" y1="21" x2="12" y2="23"></line><line x1="4.22" y1="4.22" x2="5.64" y2="5.64"></line><line x1="18.36" y1="18.36" x2="19.78" y2="19.78"></line><line x1="1" y1="12" x2="3" y2="12"></line><line x1="21" y1="12" x2="23" y2="12"></line><line x1="4.22" y1="19.78" x2="5.64" y2="18.36"></line><line x1="18.36" y1="5.64" x2="19.78" y2="4.22"></line></svg>
82
97
  </button>
98
+ <button type="button" class="bk-segment-btn ${theme === "auto" ? "active" : (!theme ? "active" : "")}" data-theme="auto" title="System" aria-label="System theme">
99
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="3" width="20" height="14" rx="2" ry="2"></rect><line x1="8" y1="21" x2="16" y2="21"></line><line x1="12" y1="17" x2="12" y2="21"></line></svg>
100
+ </button>
83
101
  <button type="button" class="bk-segment-btn ${theme === "dark" ? "active" : ""}" data-theme="dark" title="Dark" aria-label="Dark theme">
84
102
  <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"></path></svg>
85
103
  </button>
86
- <button type="button" class="bk-segment-btn ${theme === "auto" ? "active" : ""}" data-theme="auto" title="System" aria-label="System theme">
87
- <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="3" width="20" height="14" rx="2" ry="2"></rect><line x1="8" y1="21" x2="16" y2="21"></line><line x1="12" y1="17" x2="12" y2="21"></line></svg>
88
- </button>
89
104
  </div>
90
105
  </div>
91
106
  <div class="bk-theme-row">
@@ -102,6 +117,20 @@ ${pageCSS()}
102
117
  </button>
103
118
  </div>
104
119
  </div>
120
+ <div class="bk-theme-row">
121
+ <span>UI</span>
122
+ <div class="bk-segmented-control" id="bk-ui-icons">
123
+ <button type="button" class="bk-segment-btn ${ui === 'standard' ? 'active' : ''}" data-ui="standard" title="Standard" aria-label="Standard UI">
124
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="18" height="18" rx="4" ry="4"></rect><line x1="9" y1="3" x2="9" y2="21"></line></svg>
125
+ </button>
126
+ <button type="button" class="bk-segment-btn ${ui === 'neo' ? 'active' : ''}" data-ui="neo" title="Neo Brutalist" aria-label="Neo UI">
127
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="square" stroke-linejoin="miter"><rect x="3" y="3" width="18" height="18"></rect><path d="M3 10h18"></path><path d="M10 10v11"></path></svg>
128
+ </button>
129
+ <button type="button" class="bk-segment-btn ${ui === 'playful' ? 'active' : ''}" data-ui="playful" title="Playful" aria-label="Playful UI">
130
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="18" height="18" rx="6" ry="6"></rect><circle cx="8.5" cy="8.5" r="1.5" fill="currentColor"></circle><circle cx="15.5" cy="15.5" r="1.5" fill="currentColor"></circle></svg>
131
+ </button>
132
+ </div>
133
+ </div>
105
134
  </div>
106
135
  </div>
107
136
  </div>
@@ -0,0 +1,4 @@
1
+ import type { BuildOptions, Chapter, Lesson } from "../types.js";
2
+ export declare function render(lesson: Lesson, opts?: BuildOptions): string;
3
+ export declare function renderChapter(chapter: Chapter, opts?: BuildOptions): string;
4
+ //# sourceMappingURL=index-neo.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index-neo.d.ts","sourceRoot":"","sources":["../../src/renderer/index-neo.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAOjE,wBAAgB,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,GAAE,YAAiB,GAAG,MAAM,CAatE;AAID,wBAAgB,aAAa,CAC5B,OAAO,EAAE,OAAO,EAChB,IAAI,GAAE,YAAiB,GACrB,MAAM,CA6cR"}