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
@@ -0,0 +1,97 @@
1
+ import * as fs from "fs";
2
+ import * as path from "path";
3
+
4
+ export async function runInit() {
5
+ console.log("Initializing md project structure...");
6
+
7
+ const dirs = [
8
+ "chapters",
9
+ "chapters/01-chapter",
10
+ "chapters/01-chapter/lessons",
11
+ "chapters/01-chapter/lessons/01-lesson",
12
+ "chapters/01-chapter/lessons/01-lesson/sims",
13
+ "chapters/01-chapter/lessons/01-lesson/media",
14
+ "chapters/01-chapter/lessons/01-lesson/quizzes",
15
+ "chapters/01-chapter/lessons/01-lesson/content",
16
+ ];
17
+
18
+ for (const dir of dirs) {
19
+ const fullPath = path.resolve(process.cwd(), dir);
20
+ if (!fs.existsSync(fullPath)) {
21
+ fs.mkdirSync(fullPath, { recursive: true });
22
+ console.log(` Created directory: ${dir}`);
23
+ }
24
+ }
25
+
26
+ const chapterTsPath = path.resolve(process.cwd(), "chapters/01-chapter/chapter.ts");
27
+ if (!fs.existsSync(chapterTsPath)) {
28
+ fs.writeFileSync(chapterTsPath, `import { chapter } from "mr-md";
29
+ import { firstLesson } from "./lessons/01-lesson/lesson.js";
30
+
31
+ export const firstChapter = chapter("First Chapter", ctx => {
32
+ ctx.lesson(firstLesson);
33
+ });
34
+
35
+ if (import.meta.main) {
36
+ firstChapter.build();
37
+ }
38
+ `, "utf-8");
39
+ console.log(" Created: chapters/01-chapter/chapter.ts");
40
+ }
41
+
42
+ const lessonTsPath = path.resolve(process.cwd(), "chapters/01-chapter/lessons/01-lesson/lesson.ts");
43
+ if (!fs.existsSync(lessonTsPath)) {
44
+ fs.writeFileSync(lessonTsPath, `import { lesson } from "mr-md";
45
+
46
+ export const firstLesson = lesson("First Lesson", { contentBase: import.meta.dir }, ctx => {
47
+ ctx.markdown("Welcome to your first lesson!");
48
+ });
49
+ `, "utf-8");
50
+ console.log(" Created: chapters/01-chapter/lessons/01-lesson/lesson.ts");
51
+ }
52
+
53
+ const packageJsonPath = path.resolve(process.cwd(), "package.json");
54
+ let pkg: any = {};
55
+ if (fs.existsSync(packageJsonPath)) {
56
+ try {
57
+ pkg = JSON.parse(fs.readFileSync(packageJsonPath, "utf-8"));
58
+ } catch (e) {
59
+ console.error(" Failed to parse existing package.json, ignoring.");
60
+ }
61
+ } else {
62
+ pkg = {
63
+ name: "my-md-project",
64
+ version: "1.0.0",
65
+ private: true
66
+ };
67
+ }
68
+
69
+ pkg.scripts = {
70
+ ...(pkg.scripts || {}),
71
+ build: "bun chapters/01-chapter/chapter.ts",
72
+ dev: "md dev",
73
+ g: "md g",
74
+ generate: "md generate"
75
+ };
76
+
77
+ let mrMdVersion = "latest";
78
+ try {
79
+ // Find mr-md's own package.json to get its version
80
+ const __dirname = path.dirname(new URL(import.meta.url).pathname);
81
+ const ownPkgPath = path.resolve(__dirname, "../../package.json");
82
+ const ownPkg = JSON.parse(fs.readFileSync(ownPkgPath, "utf-8"));
83
+ mrMdVersion = ownPkg.version;
84
+ } catch (e) {
85
+ // Fallback if unable to read
86
+ }
87
+
88
+ pkg.dependencies = {
89
+ ...(pkg.dependencies || {}),
90
+ "mr-md": mrMdVersion
91
+ };
92
+
93
+ fs.writeFileSync(packageJsonPath, JSON.stringify(pkg, null, 2) + "\n", "utf-8");
94
+ console.log(" Updated: package.json");
95
+
96
+ console.log("\nDone! You can now run `npm run dev` or `bun run dev` to start the local development server.");
97
+ }
package/src/cli.ts ADDED
@@ -0,0 +1,29 @@
1
+ #!/usr/bin/env bun
2
+
3
+ const args = process.argv.slice(2);
4
+ const command = args[0];
5
+
6
+ if (!command) {
7
+ console.error("Usage: md <command> [args]");
8
+ console.error("Commands:");
9
+ console.error(" init Scaffold a new mr-md project");
10
+ console.error(" g Generate resources (ch, lesson, quiz)");
11
+ console.error(" dev Start local dev server");
12
+ process.exit(1);
13
+ }
14
+
15
+ switch (command) {
16
+ case "init":
17
+ import("./cli/init.js").then((m) => m.runInit());
18
+ break;
19
+ case "g":
20
+ case "generate":
21
+ import("./cli/generate.js").then((m) => m.runGenerate(args.slice(1)));
22
+ break;
23
+ case "dev":
24
+ import("./cli/dev.js").then((m) => m.runDev(args.slice(1)));
25
+ break;
26
+ default:
27
+ console.error(`Unknown command: ${command}`);
28
+ process.exit(1);
29
+ }
package/src/client/app.js CHANGED
@@ -1,5 +1,5 @@
1
1
  function bkSimDoc(js, props, loop, dependencies) {
2
- const scriptTags = (dependencies || []).map(url => '<script src="' + url.replace(/"/g, '&quot;') + '"></' + 'script>').join("\n");
2
+ const scriptTags = (dependencies || []).map(url => '<script src="' + url.replace(/"/g, '&quot;') + '"></' + 'script>').join("\\n");
3
3
  return (
4
4
  "<!DOCTYPE html><html><head>" +
5
5
  scriptTags +
@@ -15,12 +15,12 @@ function bkSimDoc(js, props, loop, dependencies) {
15
15
  ";window.bkSetupCalled=false;" +
16
16
  'window.bkCanvasPoint=function(e,c){const r=(c||e.currentTarget||e.target).getBoundingClientRect(),w=(c&&c.__bkLogicalW)||800,h=(c&&c.__bkLogicalH)||500;return{x:(e.clientX-r.left)*w/r.width,y:(e.clientY-r.top)*h/r.height}};' +
17
17
  'window.bkFitCanvas=function(c,reqW,reqH,o){if(!c)return{scale:1,width:reqW,height:reqH,cssScale:1};const d=window.devicePixelRatio||1;const w=reqW;const h=reqH;c.__bkLogicalW=w;c.__bkLogicalH=h;c.style.width=w+"px";c.style.height=h+"px";c.style.position="relative";c.style.left="auto";c.style.top="auto";c.style.transformOrigin="center center";const sx=window.innerWidth/w,sy=window.innerHeight/h,cssS=Math.max(sx,sy);c.style.transform="scale("+cssS+")";const pw=Math.max(1,Math.round(w*d)),ph=Math.max(1,Math.round(h*d));if(!o||o.bitmap!==false){if(c.width!==pw||c.height!==ph){c.width=pw;c.height=ph}}return{scale:d,width:w,height:h,cssScale:cssS}};' +
18
- 'window.bkSetup=function(w,h,f){window.bkSetupCalled=true;const c=document.getElementById("c");if(!c)return;const ctx=c.getContext("2d");function l(){const fit=window.bkFitCanvas(c,w,h);if(window.innerWidth>=32&&window.innerHeight>=32){ctx.save();ctx.scale(fit.scale,fit.scale);f(ctx,fit.width,fit.height);ctx.restore()}requestAnimationFrame(l)}l()};' +
18
+ 'window.bkSetup=function(w,h,f){window.bkSetupCalled=true;const c=document.getElementById("c");if(!c)return;const ctx=c.getContext("2d");let loopId=null;let fit=window.bkFitCanvas(c,w,h);function l(){if(window.innerWidth>=32&&window.innerHeight>=32){ctx.save();ctx.scale(fit.scale,fit.scale);f(ctx,fit.width,fit.height);ctx.restore()}if(window.__loop){loopId=requestAnimationFrame(l)}else{loopId=null}}function i(){if(window.innerWidth>=32&&window.innerHeight>=32){fit=window.bkFitCanvas(c,w,h);l()}else{requestAnimationFrame(i)}}i();window.addEventListener("resize",function(){fit=window.bkFitCanvas(c,w,h);if(!window.__loop&&window.innerWidth>=32&&window.innerHeight>=32&&!loopId){ctx.save();ctx.scale(fit.scale,fit.scale);f(ctx,fit.width,fit.height);ctx.restore()}});window.addEventListener("message",function(event){if(!event.data)return;if(event.data.type==="bk:play"){window.__loop=true;if(!loopId)loopId=requestAnimationFrame(l)}else if(event.data.type==="bk:pause"){window.__loop=false}});};' +
19
19
  'window.addEventListener("message",function(event){if(!event.data||event.data.type!=="bk:set-props")return;window.__simProps=Object.assign({},window.__simProps,event.data.props);window.dispatchEvent(new CustomEvent("bk:props",{detail:window.__simProps}));});' +
20
20
  "try{" +
21
21
  js +
22
22
  '}catch(e){console.error("Simulation Error:",e);document.body.innerHTML="<div style=\'padding: 20px; color: red; font-family: monospace;\'>Error: "+e.message+"</div>"}' +
23
- "if(!window.bkSetupCalled){function fallbackScale(){window.bkFitCanvas(document.getElementById('c'),800,500,{bitmap:false});requestAnimationFrame(fallbackScale)}fallbackScale()}" +
23
+ "if(!window.bkSetupCalled){function fallbackScale(){window.bkFitCanvas(document.getElementById('c'),800,500,{bitmap:false});}fallbackScale();window.addEventListener('resize', fallbackScale);}" +
24
24
  "</" + "script></body></html>"
25
25
  );
26
26
  }
@@ -41,64 +41,55 @@ function bkReadSimProps(figure) {
41
41
  return props;
42
42
  }
43
43
 
44
- function bkRestartSim(iframe, config, props) {
45
- iframe.srcdoc = bkSimDoc(config.js, props, config.loop, config.dependencies);
46
- }
47
-
48
44
  function bkWireSimControls() {
49
- document.querySelectorAll(".bk-object").forEach((figure) => {
50
- const configEl = figure.querySelector(".bk-sim-config");
45
+ const handler = (e) => {
46
+ const input = e.target.closest?.("[data-bk-prop]");
47
+ if (!input) return;
48
+ const figure = input.closest(".bk-object");
49
+ if (!figure) return;
51
50
  const iframe = figure.querySelector("iframe");
52
- if (!configEl || !iframe) return;
53
-
54
- let config;
55
- try {
56
- config = JSON.parse(configEl.textContent || "{}");
57
- } catch {
58
- return;
59
- }
60
-
61
- figure.querySelectorAll("[data-bk-prop]").forEach((input) => {
62
- input.addEventListener("input", () => {
63
- const props = bkReadSimProps(figure);
64
- iframe.contentWindow?.postMessage({ type: "bk:set-props", props }, "*");
65
- });
66
- input.addEventListener("change", () => {
67
- const props = bkReadSimProps(figure);
68
- iframe.contentWindow?.postMessage({ type: "bk:set-props", props }, "*");
69
- });
70
- });
71
- });
51
+ if (!iframe) return;
52
+ const props = bkReadSimProps(figure);
53
+ iframe.contentWindow?.postMessage({ type: "bk:set-props", props }, "*");
54
+ };
55
+ document.addEventListener("input", handler, { passive: true });
56
+ document.addEventListener("change", handler, { passive: true });
72
57
  }
73
58
 
74
59
  function bkWireMaximizeControls() {
75
- document.querySelectorAll(".bk-object-maximize").forEach((btn) => {
76
- btn.addEventListener("click", () => {
77
- const obj = btn.closest(".bk-object");
78
- if (!obj) return;
79
- const isMax = obj.classList.toggle("bk-object--maximized");
80
- if (isMax) {
81
- document.body.style.overflow = "hidden";
82
- btn.innerHTML =
83
- '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M8 3v3a2 2 0 0 1-2 2H3m18 0h-3a2 2 0 0 1-2-2V3m0 18v-3a2 2 0 0 1 2-2h3M3 16h3a2 2 0 0 1 2 2v3"/></svg>';
84
- } else {
85
- document.body.style.overflow = "";
86
- btn.innerHTML =
87
- '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M8 3H5a2 2 0 0 0-2 2v3m18 0V5a2 2 0 0 0-2-2h-3m0 18h3a2 2 0 0 0 2-2v-3M3 16v3a2 2 0 0 0 2 2h3"/></svg>';
88
- }
89
- });
60
+ document.addEventListener("click", (e) => {
61
+ const btn = e.target.closest?.(".bk-object-maximize");
62
+ if (!btn) return;
63
+ const obj = btn.closest(".bk-object");
64
+ if (!obj) return;
65
+ const isMax = obj.classList.toggle("bk-object--maximized");
66
+ if (isMax) {
67
+ document.body.style.overflow = "hidden";
68
+ btn.innerHTML =
69
+ '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M8 3v3a2 2 0 0 1-2 2H3m18 0h-3a2 2 0 0 1-2-2V3m0 18v-3a2 2 0 0 1 2-2h3M3 16h3a2 2 0 0 1 2 2v3"/></svg>';
70
+ } else {
71
+ document.body.style.overflow = "";
72
+ btn.innerHTML =
73
+ '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M8 3H5a2 2 0 0 0-2 2v3m18 0V5a2 2 0 0 0-2-2h-3m0 18h3a2 2 0 0 0 2-2v-3M3 16v3a2 2 0 0 0 2 2h3"/></svg>';
74
+ }
90
75
  });
91
76
  }
92
77
 
93
78
  function bkWireInteractiveFrames() {
94
- document.querySelectorAll(".bk-object").forEach((obj) => {
95
- const activate = () => {
96
- const frame = obj.querySelector(".bk-embed-interactive");
97
- if (frame) frame.classList.add("is-interactive");
98
- };
99
- obj.addEventListener("pointerdown", activate, { passive: true });
100
- obj.addEventListener("focusin", activate, { passive: true });
101
- });
79
+ const activate = (e) => {
80
+ const obj = e.target.closest?.(".bk-object");
81
+ if (!obj) return;
82
+ const frame = obj.querySelector(".bk-embed-interactive");
83
+ if (frame) {
84
+ frame.classList.add("is-interactive");
85
+ const iframe = frame.querySelector("iframe");
86
+ if (iframe && iframe.contentWindow) {
87
+ iframe.contentWindow.postMessage({ type: "bk:play" }, "*");
88
+ }
89
+ }
90
+ };
91
+ document.addEventListener("pointerdown", activate, { passive: true });
92
+ document.addEventListener("focusin", activate, { passive: true });
102
93
 
103
94
  const exitInteractive = (e) => {
104
95
  document
@@ -107,11 +98,56 @@ function bkWireInteractiveFrames() {
107
98
  const container = frame.closest(".bk-object") || frame;
108
99
  if (!container.contains(e.target)) {
109
100
  frame.classList.remove("is-interactive");
101
+ const iframe = frame.querySelector("iframe");
102
+ if (iframe && iframe.contentWindow) {
103
+ iframe.contentWindow.postMessage({ type: "bk:pause" }, "*");
104
+ }
110
105
  }
111
106
  });
112
107
  };
113
108
  document.addEventListener("pointerdown", exitInteractive, { passive: true });
114
109
  document.addEventListener("focusin", exitInteractive, { passive: true });
110
+
111
+ const obs = new IntersectionObserver((entries) => {
112
+ entries.forEach((e) => {
113
+ const frame = e.target;
114
+ const iframe = frame.querySelector("iframe");
115
+ if (!e.isIntersecting) {
116
+ if (frame.classList.contains("is-interactive")) {
117
+ frame.classList.remove("is-interactive");
118
+ }
119
+ if (iframe && iframe.contentWindow) {
120
+ iframe.contentWindow.postMessage({ type: "bk:pause" }, "*");
121
+ }
122
+ } else {
123
+ if (frame.dataset.isAnimation === "true") {
124
+ if (iframe && iframe.contentWindow) {
125
+ iframe.contentWindow.postMessage({ type: "bk:play" }, "*");
126
+ }
127
+ }
128
+ }
129
+ });
130
+ }, { threshold: 0 });
131
+
132
+ document.querySelectorAll(".bk-embed-interactive").forEach((frame) => {
133
+ obs.observe(frame);
134
+ });
135
+
136
+ document.addEventListener('contentvisibilityautostatechange', (e) => {
137
+ const frame = e.target;
138
+ if (frame && frame.classList && frame.classList.contains('bk-embed-interactive')) {
139
+ const iframe = frame.querySelector('iframe');
140
+ if (!iframe || !iframe.contentWindow) return;
141
+
142
+ if (e.skipped) {
143
+ iframe.contentWindow.postMessage({ type: "bk:pause" }, "*");
144
+ } else {
145
+ if (frame.dataset.isAnimation === "true" || frame.classList.contains("is-interactive")) {
146
+ iframe.contentWindow.postMessage({ type: "bk:play" }, "*");
147
+ }
148
+ }
149
+ }
150
+ }, { capture: true });
115
151
  }
116
152
 
117
153
  function bkWireSidebarToggle() {
@@ -128,14 +164,58 @@ function bkWireSidebarToggle() {
128
164
  );
129
165
  }
130
166
 
167
+ function bkBroadcastTheme(targetWindow) {
168
+ const root = document.documentElement;
169
+ const styles = getComputedStyle(root);
170
+ const state = {
171
+ theme: root.dataset.theme || "light",
172
+ palette: root.dataset.palette || "ink",
173
+ ui: root.dataset.ui || "standard",
174
+ colors: {
175
+ bg: styles.getPropertyValue('--bg').trim(),
176
+ paper: styles.getPropertyValue('--paper').trim(),
177
+ line: styles.getPropertyValue('--line').trim(),
178
+ 'line-strong': styles.getPropertyValue('--line-strong').trim(),
179
+ text: styles.getPropertyValue('--text').trim(),
180
+ 'text-light': styles.getPropertyValue('--text-light').trim(),
181
+ accent: styles.getPropertyValue('--accent').trim(),
182
+ 'accent-soft': styles.getPropertyValue('--accent-soft').trim()
183
+ }
184
+ };
185
+ if (targetWindow) {
186
+ targetWindow.postMessage({ type: "bk:theme-sync", state }, "*");
187
+ } else {
188
+ document.querySelectorAll(".bk-embed-interactive iframe").forEach(iframe => {
189
+ if (iframe.contentWindow) iframe.contentWindow.postMessage({ type: "bk:theme-sync", state }, "*");
190
+ });
191
+ }
192
+ }
193
+
194
+ window.addEventListener("message", function(e) {
195
+ if (e.data && e.data.type === "bk:request-theme") {
196
+ // Small delay to ensure CSS has applied if this happens right on load
197
+ requestAnimationFrame(() => bkBroadcastTheme(e.source));
198
+ }
199
+ });
200
+
201
+ // Watch for OS theme changes if on auto
202
+ window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', () => {
203
+ if (document.documentElement.dataset.theme === "auto") {
204
+ bkBroadcastTheme();
205
+ }
206
+ });
207
+
131
208
  function bkWireThemeControls() {
132
209
  const root = document.documentElement;
133
210
  const button = document.getElementById("bk-settings-button");
134
211
  const panel = document.getElementById("bk-theme-panel");
135
212
  const themeBtns = document.querySelectorAll("#bk-theme-icons button");
136
- const paletteBtns = document.querySelectorAll("#bk-palette-icons button");
137
- const savedTheme = localStorage.getItem("bk-theme");
213
+ const paletteBtns = document.querySelectorAll("#bk-palette-icons button");
214
+ const uiBtns = document.querySelectorAll("#bk-ui-icons button");
215
+ let savedTheme = localStorage.getItem("bk-theme");
216
+ if (!savedTheme) savedTheme = "auto";
138
217
  const savedPalette = localStorage.getItem("bk-palette");
218
+ const savedUi = localStorage.getItem("bk-ui");
139
219
 
140
220
  function updateThemeBtn(val) {
141
221
  themeBtns.forEach(b => {
@@ -144,23 +224,55 @@ function bkWireThemeControls() {
144
224
  });
145
225
  }
146
226
 
227
+ const proPalettes = {
228
+ ink: "elixir",
229
+ field: "trunk",
230
+ ember: "lava"
231
+ };
232
+
233
+ const proIcons = {
234
+ elixir: '<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="M9 3v4.4L4.8 14.5a2.5 2.5 0 0 0 2.2 3.5h10a2.5 2.5 0 0 0 2.2-3.5L15 7.4V3"></path><path d="M9 14h6"></path><path d="M10 3h4"></path></svg>',
235
+ trunk: '<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 3 4 8 5-5 5 15H2L8 3z"></path></svg>',
236
+ lava: '<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="m3 21 4.5-9"></path><path d="M16.5 12 21 21"></path><path d="m11 21 1.4-2.8"></path><path d="m15 21-1.4-2.8"></path><path d="M11 6a3 3 0 0 1 2-2 3 3 0 0 1 2 2"></path><path d="M12 12a3 3 0 0 0 3-3"></path><path d="M9 12a3 3 0 0 1-3-3"></path><path d="M12 21v-3.5"></path><path d="M10 18h4"></path></svg>'
237
+ };
238
+
239
+ const normalIcons = {
240
+ ink: '<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>',
241
+ field: '<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>',
242
+ ember: '<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>'
243
+ };
244
+
147
245
  function updatePaletteBtn(val) {
148
246
  paletteBtns.forEach(b => {
149
- if(b.dataset.palette === val) b.classList.add("active");
247
+ const basePalette = b.dataset.palette;
248
+
249
+ if (val === proPalettes[basePalette]) {
250
+ b.innerHTML = proIcons[val];
251
+ } else {
252
+ b.innerHTML = normalIcons[basePalette];
253
+ }
254
+
255
+ if(basePalette === val || proPalettes[basePalette] === val) b.classList.add("active");
256
+ else b.classList.remove("active");
257
+ });
258
+ }
259
+
260
+ function updateUiBtn(val) {
261
+ uiBtns.forEach(b => {
262
+ if(b.dataset.ui === val) b.classList.add("active");
150
263
  else b.classList.remove("active");
151
264
  });
152
265
  }
153
266
 
154
267
  if (savedTheme) {
155
268
  updateThemeBtn(savedTheme);
156
- savedTheme === "auto"
157
- ? root.removeAttribute("data-theme")
158
- : root.setAttribute("data-theme", savedTheme);
159
269
  }
160
270
  if (savedPalette) {
161
271
  const normalizedPalette = savedPalette === "green" ? "field" : savedPalette;
162
272
  updatePaletteBtn(normalizedPalette);
163
- root.setAttribute("data-palette", normalizedPalette);
273
+ }
274
+ if (savedUi) {
275
+ updateUiBtn(savedUi);
164
276
  }
165
277
 
166
278
  button &&
@@ -190,18 +302,43 @@ function bkWireThemeControls() {
190
302
  const val = btn.dataset.theme;
191
303
  localStorage.setItem("bk-theme", val);
192
304
  updateThemeBtn(val);
193
- val === "auto"
194
- ? root.removeAttribute("data-theme")
195
- : root.setAttribute("data-theme", val);
305
+ root.setAttribute("data-theme", val);
306
+ bkBroadcastTheme();
196
307
  });
197
308
  });
198
309
 
199
- paletteBtns.forEach(btn => {
310
+ paletteBtns.forEach(btn => {
311
+ btn.addEventListener("click", (e) => {
312
+ const baseVal = btn.dataset.palette;
313
+ let currentVal = root.getAttribute("data-palette") || "ink";
314
+ let newVal = baseVal;
315
+
316
+ if (e.detail === 2) {
317
+ if (currentVal === proPalettes[baseVal]) {
318
+ newVal = baseVal;
319
+ } else {
320
+ newVal = proPalettes[baseVal];
321
+ }
322
+ } else {
323
+ if (currentVal === proPalettes[baseVal]) {
324
+ newVal = proPalettes[baseVal];
325
+ }
326
+ }
327
+
328
+ localStorage.setItem("bk-palette", newVal);
329
+ updatePaletteBtn(newVal);
330
+ root.setAttribute("data-palette", newVal);
331
+ bkBroadcastTheme();
332
+ });
333
+ });
334
+
335
+ uiBtns.forEach(btn => {
200
336
  btn.addEventListener("click", () => {
201
- const val = btn.dataset.palette;
202
- localStorage.setItem("bk-palette", val);
203
- updatePaletteBtn(val);
204
- root.setAttribute("data-palette", val);
337
+ const val = btn.dataset.ui;
338
+ localStorage.setItem("bk-ui", val);
339
+ updateUiBtn(val);
340
+ root.setAttribute("data-ui", val);
341
+ bkBroadcastTheme();
205
342
  });
206
343
  });
207
344
  }
@@ -251,23 +388,28 @@ function bkWireCodeCopy() {
251
388
  btn.innerHTML = '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path></svg>';
252
389
  btn.setAttribute("aria-label", "Copy code");
253
390
  btn.title = "Copy code";
254
-
255
- btn.addEventListener("click", async () => {
256
- try {
257
- await navigator.clipboard.writeText(code.textContent || "");
258
- btn.innerHTML = '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"></polyline></svg>';
259
- btn.classList.add("copied");
260
- setTimeout(() => {
261
- btn.innerHTML = '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path></svg>';
262
- btn.classList.remove("copied");
263
- }, 2000);
264
- } catch (err) {
265
- console.error("Failed to copy", err);
266
- }
267
- });
268
-
269
391
  container.appendChild(btn);
270
392
  });
393
+
394
+ document.addEventListener("click", async (e) => {
395
+ const btn = e.target.closest?.(".bk-copy-btn");
396
+ if (!btn) return;
397
+ const container = btn.closest(".bk-code-block");
398
+ if (!container) return;
399
+ const code = container.querySelector("code");
400
+ if (!code) return;
401
+ try {
402
+ await navigator.clipboard.writeText(code.textContent || "");
403
+ btn.innerHTML = '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"></polyline></svg>';
404
+ btn.classList.add("copied");
405
+ setTimeout(() => {
406
+ btn.innerHTML = '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path></svg>';
407
+ btn.classList.remove("copied");
408
+ }, 2000);
409
+ } catch (err) {
410
+ console.error("Failed to copy", err);
411
+ }
412
+ });
271
413
  }
272
414
 
273
415
  // Active sidebar link on scroll
@@ -279,36 +421,69 @@ document.addEventListener("DOMContentLoaded", () => {
279
421
  bkWireInteractiveFrames();
280
422
  bkWireCodeCopy();
281
423
 
282
- const sections = document.querySelectorAll(
283
- '[id^="heading-"], [id^="section-"], [id^="quiz-"]',
284
- );
285
424
  const navLinks = document.querySelectorAll(".bk-nav-item");
425
+ if (!navLinks.length) return;
286
426
 
287
- if (!sections.length || !navLinks.length) return;
427
+ const nav = document.querySelector(".bk-nav");
428
+ let pill = document.querySelector(".bk-nav-active-pill");
429
+ if (nav && !pill) {
430
+ pill = document.createElement("div");
431
+ pill.className = "bk-nav-active-pill";
432
+ nav.prepend(pill);
433
+ }
288
434
 
289
- const obs = new IntersectionObserver(
290
- (entries) => {
291
- let activeId = null;
292
- entries.forEach((e) => {
293
- if (e.isIntersecting) {
294
- activeId = e.target.id;
295
- }
296
- });
435
+ const sections = [];
436
+ navLinks.forEach((l) => {
437
+ const id = l.dataset.id;
438
+ if (id) {
439
+ const el = document.getElementById(id);
440
+ if (el) sections.push({ id, el, link: l, isAbove: false });
441
+ }
442
+ });
297
443
 
298
- if (activeId) {
299
- navLinks.forEach((l) => {
300
- if (l.dataset.id === activeId) {
301
- l.classList.add("active");
302
- } else {
303
- l.classList.remove("active");
304
- }
305
- });
444
+ if (!sections.length) return;
445
+
446
+ function setActive(idx) {
447
+ sections.forEach((s, i) => {
448
+ if (i === idx) {
449
+ s.link.classList.add("active");
450
+ if (pill) {
451
+ pill.style.top = s.link.offsetTop + "px";
452
+ pill.style.height = s.link.offsetHeight + "px";
453
+ pill.style.opacity = "1";
454
+ }
455
+ } else {
456
+ s.link.classList.remove("active");
306
457
  }
307
- },
308
- { rootMargin: "-20% 0px -60% 0px", threshold: 0 },
309
- );
458
+ });
459
+ }
310
460
 
311
- sections.forEach((s) => {
312
- obs.observe(s);
461
+ function updateActive() {
462
+ let activeIdx = 0;
463
+ for (let i = 0; i < sections.length; i++) {
464
+ if (sections[i].isAbove) activeIdx = i;
465
+ }
466
+ setActive(activeIdx);
467
+ }
468
+
469
+ const mainScrollContainer = document.querySelector(".bk-main");
470
+ const sectionObs = new IntersectionObserver((entries) => {
471
+ for (const entry of entries) {
472
+ const section = sections.find(s => s.el === entry.target);
473
+ if (!section) continue;
474
+ if (entry.isIntersecting) {
475
+ section.isAbove = true;
476
+ } else {
477
+ section.isAbove = entry.boundingClientRect.top < (entry.rootBounds?.top ?? 0);
478
+ }
479
+ }
480
+ updateActive();
481
+ }, {
482
+ root: mainScrollContainer || null,
483
+ rootMargin: "0px 0px -75% 0px",
484
+ threshold: 0
313
485
  });
486
+
487
+ sections.forEach(s => sectionObs.observe(s.el));
488
+ setActive(0);
314
489
  });
package/src/index.ts CHANGED
@@ -1,4 +1,4 @@
1
- export { ChapterBuilder, chapter, CourseBuilder, course, LessonBuilder, lesson } from "./builder.js";
1
+ export { ChapterBuilder, chapter, LessonBuilder, lesson } from "./builder.js";
2
2
  export { render } from "./renderer/index.js";
3
3
  export type {
4
4
  AnimationBlock,