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
@@ -15,13 +15,14 @@ export function render(lesson, opts = {}) {
15
15
  }
16
16
  // ─── Chapter Rendering ────────────────────────────────────────────────────────
17
17
  export function renderChapter(chapter, opts = {}) {
18
- const theme = opts.theme ?? "auto";
19
- const schemeAttr = theme === "auto" ? "" : `data-theme="${theme}"`;
18
+ const theme = opts.theme ?? "light";
19
+ const schemeAttr = `data-theme="${theme}"`;
20
20
  const preset = opts.preset ?? {};
21
21
  const layout = preset.layout ?? "lesson";
22
22
  const density = preset.density ?? "comfortable";
23
23
  const tone = preset.tone ?? "scholarly";
24
24
  const palette = opts.palette ?? "ink";
25
+ const ui = opts.ui ?? "standard";
25
26
  const navHtml = chapter.lessons
26
27
  .map((l) => `<a href="${escAttr(l.meta.slug)}.html" class="bk-nav-item bk-nav-chapter">${escHtml(l.meta.title)}</a>`)
27
28
  .join("\n");
@@ -259,16 +260,16 @@ export function renderChapter(chapter, opts = {}) {
259
260
  border-bottom-color: var(--ink);
260
261
  }
261
262
 
262
- @media (prefers-color-scheme: dark) {
263
- .bk-timeline-content {
264
- background: var(--panel);
265
- }
266
- .bk-status-unread .bk-timeline-content {
267
- background: transparent;
268
- }
269
- .bk-status-unread:hover .bk-timeline-content {
270
- background: var(--panel);
271
- }
263
+
264
+
265
+ :root[data-theme="dark"] .bk-timeline-content {
266
+ background: var(--panel);
267
+ }
268
+ :root[data-theme="dark"] .bk-status-unread .bk-timeline-content {
269
+ background: transparent;
270
+ }
271
+ :root[data-theme="dark"] .bk-status-unread:hover .bk-timeline-content {
272
+ background: var(--panel);
272
273
  }
273
274
 
274
275
  @media (max-width: 600px) {
@@ -296,9 +297,10 @@ export function renderChapter(chapter, opts = {}) {
296
297
  }
297
298
  `;
298
299
  return `<!DOCTYPE html>
299
- <html lang="en" data-palette="${palette}" ${schemeAttr}>
300
+ <html lang="en" data-palette="${palette}" data-ui="${ui}" ${schemeAttr}>
300
301
  <head>
301
302
  <meta charset="UTF-8">
303
+ <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">
302
304
  <meta name="viewport" content="width=device-width, initial-scale=1">
303
305
  <title>${escHtml(chapter.meta.title)}</title>
304
306
  ${chapter.meta.description ? `<meta name="description" content="${escHtml(chapter.meta.description)}">` : ""}
@@ -330,12 +332,12 @@ ${chapterStyles}
330
332
  <button type="button" class="bk-segment-btn ${theme === "light" ? "active" : ""}" data-theme="light" title="Light" aria-label="Light theme">
331
333
  <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>
332
334
  </button>
335
+ <button type="button" class="bk-segment-btn ${theme === "auto" ? "active" : (!theme ? "active" : "")}" data-theme="auto" title="System" aria-label="System theme">
336
+ <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>
337
+ </button>
333
338
  <button type="button" class="bk-segment-btn ${theme === "dark" ? "active" : ""}" data-theme="dark" title="Dark" aria-label="Dark theme">
334
339
  <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>
335
340
  </button>
336
- <button type="button" class="bk-segment-btn ${theme === "auto" ? "active" : ""}" data-theme="auto" title="System" aria-label="System theme">
337
- <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>
338
- </button>
339
341
  </div>
340
342
  </div>
341
343
  <div class="bk-theme-row">
@@ -352,369 +354,17 @@ ${chapterStyles}
352
354
  </button>
353
355
  </div>
354
356
  </div>
355
- </div>
356
- </div>
357
- </div>
358
- </aside>
359
- <button class="bk-sidebar-collapse-floating" id="bk-sidebar-collapse" aria-label="Collapse sidebar">
360
- <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>
361
- </button>
362
- <main class="bk-main">
363
- <button class="bk-sidebar-expand" id="bk-sidebar-expand" type="button" aria-label="Expand sidebar">
364
- <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>
365
- </button>
366
- <article class="bk-content" style="max-width: 1000px; margin: 0 auto;">
367
- <header class="bk-hero" style="border-bottom: none;">
368
- <p class="bk-eyebrow">Chapter</p>
369
- <h1>${escHtml(chapter.meta.title)}</h1>
370
- ${chapter.meta.description ? `<p class="bk-deck">${escHtml(chapter.meta.description)}</p>` : ""}
371
- </header>
372
- ${timelineHtml}
373
- </article>
374
- </main>
375
- </div>
376
- <script>
377
- ${clientScript()}
378
- </script>
379
- </body>
380
- </html>`;
381
- }
382
- // ─── Course Rendering ────────────────────────────────────────────────────────
383
- export function renderCourse(course, opts = {}) {
384
- const theme = opts.theme ?? "auto";
385
- const schemeAttr = theme === "auto" ? "" : `data-theme="${theme}"`;
386
- const preset = opts.preset ?? {};
387
- const layout = preset.layout ?? "lesson";
388
- const density = preset.density ?? "comfortable";
389
- const tone = preset.tone ?? "scholarly";
390
- const palette = opts.palette ?? "ink";
391
- const navHtml = course.chapters
392
- .map((c) => `<a href="${escAttr(c.meta.slug)}.html" class="bk-nav-item bk-nav-chapter">${escHtml(c.meta.title)}</a>`)
393
- .join("\n");
394
- const timelineHtml = `
395
- <div class="bk-chapter-timeline-wrapper">
396
- <div class="bk-chapter-timeline">
397
- ${course.chapters
398
- .map((chapter, idx) => `
399
- <a href="${escAttr(chapter.meta.slug)}.html" class="bk-timeline-card bk-status-${chapter.meta.status ?? "unread"}">
400
- <div class="bk-timeline-node"></div>
401
- <div class="bk-timeline-content">
402
- <h3 class="bk-timeline-title" style="view-transition-name: title-${escAttr(chapter.meta.slug)}">${escHtml(chapter.meta.title)}</h3>
403
- ${chapter.meta.description ? `<p class="bk-timeline-desc">${escHtml(chapter.meta.description)}</p>` : ""}
404
- <span class="bk-timeline-action">${chapter.meta.status === "completed" ? "Review" : "Start Chapter"} <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="M5 12h14m-7-7 7 7-7 7"/></svg></span>
405
- </div>
406
- </a>
407
- `)
408
- .join("")}
409
- </div>
410
- </div>
411
- <script>
412
- if (!CSS.supports('(animation-timeline: view()) and (animation-range: entry)')) {
413
- const observer = new IntersectionObserver(
414
- (entries) => {
415
- for (const entry of entries) {
416
- if (entry.isIntersecting) {
417
- entry.target.classList.add('js-visible');
418
- entry.target.classList.remove('js-hidden');
419
- observer.unobserve(entry.target);
420
- }
421
- }
422
- },
423
- { threshold: 0.1 }
424
- );
425
- document.querySelectorAll('.bk-timeline-card').forEach((el) => {
426
- el.classList.add('js-hidden');
427
- observer.observe(el);
428
- });
429
- }
430
- </script>`;
431
- const chapterStyles = `
432
- .bk-chapter-timeline-wrapper {
433
- position: relative;
434
- width: 100%;
435
- padding: 1rem 0;
436
- z-index: 1;
437
- }
438
-
439
- .bk-chapter-timeline {
440
- display: flex;
441
- flex-direction: column;
442
- gap: 3rem;
443
- padding: 3rem 0;
444
- position: relative;
445
- max-width: 800px;
446
- margin: 0 auto;
447
- }
448
-
449
- /* Minimalist vertical connecting line */
450
- .bk-chapter-timeline::before {
451
- content: '';
452
- position: absolute;
453
- left: 20px;
454
- top: 3rem;
455
- bottom: 3rem;
456
- width: 1px;
457
- background: var(--line);
458
- z-index: 0;
459
- transform: translateX(-50%);
460
- }
461
-
462
- .bk-timeline-card {
463
- display: flex;
464
- align-items: stretch;
465
- gap: 2rem;
466
- text-decoration: none !important;
467
- border: none !important;
468
- color: inherit;
469
- position: relative;
470
- z-index: 1;
471
- opacity: 1;
472
- transform: none;
473
- }
474
-
475
- /* Subtle scroll-driven animations */
476
- @media (prefers-reduced-motion: no-preference) {
477
- @supports ((animation-timeline: view()) and (animation-range: entry)) {
478
- .bk-timeline-card {
479
- animation-name: slide-fade-in;
480
- animation-fill-mode: both;
481
- animation-timing-function: cubic-bezier(0.16, 1, 0.3, 1);
482
- animation-timeline: view(block);
483
- animation-range: entry 5% cover 20%;
484
- }
485
- @keyframes slide-fade-in {
486
- 0% { opacity: 0; transform: translateY(20px); }
487
- 100% { opacity: 1; transform: translateY(0); }
488
- }
489
- }
490
- }
491
-
492
- /* Fallback for browsers without animation-timeline support */
493
- .bk-timeline-card.js-hidden {
494
- opacity: 0;
495
- transform: translateY(20px);
496
- }
497
- .bk-timeline-card.js-visible {
498
- opacity: 1;
499
- transform: translateY(0);
500
- transition: opacity 0.6s cubic-bezier(0.16, 1, 0.3, 1), transform 0.6s cubic-bezier(0.16, 1, 0.3, 1);
501
- }
502
-
503
- .bk-timeline-node {
504
- width: 40px;
505
- height: 40px;
506
- border-radius: 50%;
507
- background: var(--paper);
508
- border: 1px solid var(--accent);
509
- display: flex;
510
- align-items: center;
511
- justify-content: center;
512
- flex-shrink: 0;
513
- transition: all 0.3s ease;
514
- position: relative;
515
- box-shadow: 0 2px 4px rgba(0,0,0,0.02);
516
- }
517
- .bk-timeline-node::after {
518
- content: '';
519
- width: 10px;
520
- height: 10px;
521
- border-radius: 50%;
522
- background: var(--accent);
523
- transition: all 0.3s ease;
524
- }
525
- .bk-timeline-card:hover .bk-timeline-node {
526
- background: var(--accent);
527
- }
528
- .bk-timeline-card:hover .bk-timeline-node::after {
529
- background: var(--paper);
530
- }
531
-
532
- .bk-timeline-content {
533
- background: var(--paper);
534
- padding: 2rem;
535
- border-radius: 12px;
536
- border: 1px solid var(--line);
537
- box-shadow: 0 4px 12px rgba(0, 0, 0, 0.02);
538
- flex: 1;
539
- transition: all 0.3s ease;
540
- display: flex;
541
- flex-direction: column;
542
- justify-content: center;
543
- position: relative;
544
- }
545
- .bk-timeline-card:hover .bk-timeline-content {
546
- border-color: color-mix(in srgb, var(--accent) 30%, var(--line));
547
- box-shadow: 0 8px 24px rgba(0, 0, 0, 0.04);
548
- transform: translateY(-2px);
549
- }
550
-
551
- .bk-timeline-title {
552
- margin: 0 0 0.5rem 0;
553
- font-family: var(--font-display);
554
- font-size: 1.6rem;
555
- color: var(--ink);
556
- width: fit-content;
557
- transition: color 0.2s ease;
558
- }
559
- .bk-timeline-card:hover .bk-timeline-title {
560
- color: var(--accent);
561
- }
562
-
563
- .bk-timeline-desc {
564
- margin: 0 0 1.5rem 0;
565
- color: var(--muted);
566
- line-height: 1.6;
567
- font-size: 1.05rem;
568
- }
569
-
570
- .bk-timeline-action {
571
- font-weight: 500;
572
- color: var(--accent);
573
- display: inline-flex;
574
- align-items: center;
575
- gap: 0.4rem;
576
- font-size: 0.95rem;
577
- align-self: flex-start;
578
- transition: all 0.2s ease;
579
- border-bottom: 1px solid transparent;
580
- }
581
- .bk-timeline-card:hover .bk-timeline-action {
582
- gap: 0.6rem;
583
- border-bottom-color: var(--accent);
584
- }
585
-
586
- .bk-status-unread .bk-timeline-node {
587
- border: 1px solid var(--line-strong);
588
- }
589
- .bk-status-unread .bk-timeline-node::after {
590
- background: var(--line-strong);
591
- transform: scale(0.8);
592
- }
593
- .bk-status-unread .bk-timeline-content {
594
- background: transparent;
595
- box-shadow: none;
596
- border: 1px solid transparent;
597
- }
598
- .bk-status-unread .bk-timeline-title {
599
- color: var(--muted);
600
- }
601
- .bk-status-unread .bk-timeline-desc {
602
- color: color-mix(in srgb, var(--muted) 80%, transparent);
603
- }
604
- .bk-status-unread .bk-timeline-action {
605
- color: var(--muted);
606
- }
607
- .bk-status-unread:hover .bk-timeline-node {
608
- border-color: var(--ink);
609
- background: var(--paper);
610
- }
611
- .bk-status-unread:hover .bk-timeline-node::after {
612
- background: var(--ink);
613
- transform: scale(1);
614
- }
615
- .bk-status-unread:hover .bk-timeline-content {
616
- background: var(--paper);
617
- border-color: var(--line);
618
- box-shadow: 0 4px 12px rgba(0, 0, 0, 0.02);
619
- }
620
- .bk-status-unread:hover .bk-timeline-title {
621
- color: var(--ink);
622
- }
623
- .bk-status-unread:hover .bk-timeline-action {
624
- color: var(--ink);
625
- border-bottom-color: var(--ink);
626
- }
627
-
628
- @media (prefers-color-scheme: dark) {
629
- .bk-timeline-content {
630
- background: var(--panel);
631
- }
632
- .bk-status-unread .bk-timeline-content {
633
- background: transparent;
634
- }
635
- .bk-status-unread:hover .bk-timeline-content {
636
- background: var(--panel);
637
- }
638
- }
639
-
640
- @media (max-width: 600px) {
641
- .bk-chapter-timeline::before {
642
- left: 16px;
643
- }
644
- .bk-timeline-card {
645
- gap: 1.5rem;
646
- }
647
- .bk-timeline-node {
648
- width: 32px;
649
- height: 32px;
650
- }
651
- .bk-timeline-node::after {
652
- width: 8px;
653
- height: 8px;
654
- }
655
- .bk-timeline-content {
656
- padding: 1.5rem;
657
- border-radius: 10px;
658
- }
659
- .bk-timeline-title {
660
- font-size: 1.4rem;
661
- }
662
- }
663
- `;
664
- return `<!DOCTYPE html>
665
- <html lang="en" data-palette="${palette}" ${schemeAttr}>
666
- <head>
667
- <meta charset="UTF-8">
668
- <meta name="viewport" content="width=device-width, initial-scale=1">
669
- <title>${escHtml(course.meta.title)}</title>
670
- ${course.meta.description ? `<meta name="description" content="${escHtml(course.meta.description)}">` : ""}
671
- ${opts.head ?? ""}
672
- <style>
673
- ${opts.font ? `:root { --font-sans: ${opts.font}, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; }` : ""}
674
- ${pageCSS()}
675
- ${chapterStyles}
676
- </style>
677
- </head>
678
- <body class="bk-layout-${layout} bk-density-${density} bk-tone-${tone}">
679
- <div class="bk-shell">
680
- <aside class="bk-sidebar">
681
- <div class="bk-sidebar-inner">
682
- <div class="bk-sidebar-header">
683
- <div style="margin-top: 8px;"></div>
684
- <div class="bk-sidebar-title">${escHtml(course.meta.title)}</div>
685
- </div>
686
- <nav class="bk-nav">${navHtml}</nav>
687
- <div class="bk-sidebar-footer">
688
- <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">
689
- <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>
690
- <span class="bk-sr-only">Display settings</span>
691
- </button>
692
- <div class="bk-theme-panel" id="bk-theme-panel" aria-label="Display settings" hidden>
693
- <div class="bk-theme-row">
694
- <span>Theme</span>
695
- <div class="bk-segmented-control" id="bk-theme-icons">
696
- <button type="button" class="bk-segment-btn ${theme === "light" ? "active" : ""}" data-theme="light" title="Light" aria-label="Light theme">
697
- <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>
698
- </button>
699
- <button type="button" class="bk-segment-btn ${theme === "dark" ? "active" : ""}" data-theme="dark" title="Dark" aria-label="Dark theme">
700
- <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>
701
- </button>
702
- <button type="button" class="bk-segment-btn ${theme === "auto" ? "active" : ""}" data-theme="auto" title="System" aria-label="System theme">
703
- <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>
704
- </button>
705
- </div>
706
- </div>
707
357
  <div class="bk-theme-row">
708
- <span>Palette</span>
709
- <div class="bk-segmented-control" id="bk-palette-icons">
710
- <button type="button" class="bk-segment-btn ${palette === "ink" ? "active" : ""}" data-palette="ink" title="Ink" aria-label="Ink palette">
711
- <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>
358
+ <span>UI</span>
359
+ <div class="bk-segmented-control" id="bk-ui-icons">
360
+ <button type="button" class="bk-segment-btn ${ui === 'standard' ? 'active' : ''}" data-ui="standard" title="Standard" aria-label="Standard UI">
361
+ <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>
712
362
  </button>
713
- <button type="button" class="bk-segment-btn ${palette === "field" ? "active" : ""}" data-palette="field" title="Field" aria-label="Field palette">
714
- <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 12"></path></svg>
363
+ <button type="button" class="bk-segment-btn ${ui === 'neo' ? 'active' : ''}" data-ui="neo" title="Neo Brutalist" aria-label="Neo UI">
364
+ <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>
715
365
  </button>
716
- <button type="button" class="bk-segment-btn ${palette === "ember" ? "active" : ""}" data-palette="ember" title="Ember" aria-label="Ember palette">
717
- <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>
366
+ <button type="button" class="bk-segment-btn ${ui === 'playful' ? 'active' : ''}" data-ui="playful" title="Playful" aria-label="Playful UI">
367
+ <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>
718
368
  </button>
719
369
  </div>
720
370
  </div>
@@ -731,9 +381,9 @@ ${chapterStyles}
731
381
  </button>
732
382
  <article class="bk-content" style="max-width: 1000px; margin: 0 auto;">
733
383
  <header class="bk-hero" style="border-bottom: none;">
734
- <p class="bk-eyebrow">Course</p>
735
- <h1>${escHtml(course.meta.title)}</h1>
736
- ${course.meta.description ? `<p class="bk-deck">${escHtml(course.meta.description)}</p>` : ""}
384
+ <p class="bk-eyebrow">Chapter</p>
385
+ <h1>${escHtml(chapter.meta.title)}</h1>
386
+ ${chapter.meta.description ? `<p class="bk-deck">${escHtml(chapter.meta.description)}</p>` : ""}
737
387
  </header>
738
388
  ${timelineHtml}
739
389
  </article>
@@ -8,7 +8,7 @@ declare function mdToHtml(md: string): {
8
8
  level: number;
9
9
  }[];
10
10
  };
11
- declare function blockChrome(kind: string, label: string | undefined, caption: string | undefined, body: string, accent?: string, allowMaximize?: boolean): string;
11
+ declare function blockChrome(kind: string, label: string | undefined, caption: string | undefined, body: string, accent?: string, allowMaximize?: boolean, id?: string): string;
12
12
  declare function mdInline(text: string): string;
13
13
  declare function renderSimulationControls(block: Extract<Block, {
14
14
  type: "simulation";
@@ -1 +1 @@
1
- {"version":3,"file":"markdown.d.ts","sourceRoot":"","sources":["../../src/renderer/markdown.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,aAAa,CAAC;AAYzC,iBAAS,QAAQ,CAAC,EAAE,EAAE,MAAM,GAAG;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,EAAE,CAAA;CAAE,CAmFtH;AAeD,iBAAS,WAAW,CACnB,IAAI,EAAE,MAAM,EACZ,KAAK,EAAE,MAAM,GAAG,SAAS,EACzB,OAAO,EAAE,MAAM,GAAG,SAAS,EAC3B,IAAI,EAAE,MAAM,EACZ,MAAM,SAAY,EAClB,aAAa,UAAO,GAClB,MAAM,CAYR;AAED,iBAAS,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAmBtC;AAED,iBAAS,wBAAwB,CAChC,KAAK,EAAE,OAAO,CAAC,KAAK,EAAE;IAAE,IAAI,EAAE,YAAY,CAAA;CAAE,CAAC,GAC3C,MAAM,CAiCR;AAED,iBAAS,gBAAgB,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,CAIhD;AAED,OAAO,EACN,WAAW,EACX,gBAAgB,EAChB,QAAQ,EACR,QAAQ,EACR,wBAAwB,GACxB,CAAC"}
1
+ {"version":3,"file":"markdown.d.ts","sourceRoot":"","sources":["../../src/renderer/markdown.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,aAAa,CAAC;AAYzC,iBAAS,QAAQ,CAAC,EAAE,EAAE,MAAM,GAAG;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,EAAE,CAAA;CAAE,CAmFtH;AAeD,iBAAS,WAAW,CACnB,IAAI,EAAE,MAAM,EACZ,KAAK,EAAE,MAAM,GAAG,SAAS,EACzB,OAAO,EAAE,MAAM,GAAG,SAAS,EAC3B,IAAI,EAAE,MAAM,EACZ,MAAM,SAAY,EAClB,aAAa,UAAO,EACpB,EAAE,CAAC,EAAE,MAAM,GACT,MAAM,CAYR;AAED,iBAAS,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAmBtC;AAED,iBAAS,wBAAwB,CAChC,KAAK,EAAE,OAAO,CAAC,KAAK,EAAE;IAAE,IAAI,EAAE,YAAY,CAAA;CAAE,CAAC,GAC3C,MAAM,CAiCR;AAED,iBAAS,gBAAgB,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,CAIhD;AAED,OAAO,EACN,WAAW,EACX,gBAAgB,EAChB,QAAQ,EACR,QAAQ,EACR,wBAAwB,GACxB,CAAC"}
@@ -46,7 +46,7 @@ function mdToHtml(md) {
46
46
  const renderer = new marked.Renderer();
47
47
  renderer.heading = ({ tokens, depth, text }) => {
48
48
  const id = `bk-heading-${headingIdCounter++}`;
49
- if (depth === 2 || depth === 3) {
49
+ if (depth === 1 || depth === 2) {
50
50
  const plainText = text.replace(/<[^>]+>/g, "");
51
51
  headings.push({ id, text: plainText, level: depth });
52
52
  }
@@ -84,8 +84,8 @@ function escHtml(s) {
84
84
  function escAttr(s) {
85
85
  return escHtml(s);
86
86
  }
87
- function blockChrome(kind, label, caption, body, accent = "neutral", allowMaximize = true) {
88
- return `<figure class="bk-object bk-object--${escAttr(accent)}">
87
+ function blockChrome(kind, label, caption, body, accent = "neutral", allowMaximize = true, id) {
88
+ return `<figure ${id ? `id="${escAttr(id)}" ` : ""}class="bk-object bk-object--${escAttr(accent)}">
89
89
  <div class="bk-object-header">
90
90
  <span class="bk-object-kicker">${escHtml(kind)}</span>
91
91
  ${label ? `<span class="bk-object-title">${escHtml(label)}</span>` : ""}
@@ -2,7 +2,7 @@ import type { BuildOptions } from "../types.js";
2
2
  export interface NavItem {
3
3
  id: string;
4
4
  label: string;
5
- kind: "heading" | "section" | "quiz";
5
+ kind: "heading" | "section" | "quiz" | "simulation";
6
6
  }
7
7
  declare function resolveContent(src: string, options: BuildOptions, expectedType?: "md" | "js" | "json" | "text"): string;
8
8
  declare function resolveAssetSrc(src: string, options: BuildOptions): string;
@@ -1 +1 @@
1
- {"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../../src/renderer/utils.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAEhD,MAAM,WAAW,OAAO;IACvB,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,SAAS,GAAG,SAAS,GAAG,MAAM,CAAC;CACrC;AAID,iBAAS,cAAc,CACtB,GAAG,EAAE,MAAM,EACX,OAAO,EAAE,YAAY,EACrB,YAAY,GAAE,IAAI,GAAG,IAAI,GAAG,MAAM,GAAG,MAAe,GAClD,MAAM,CA0CR;AAED,iBAAS,eAAe,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,EAAE,YAAY,GAAG,MAAM,CAgDnE;AAED,OAAO,EAAE,eAAe,EAAE,cAAc,EAAE,CAAC"}
1
+ {"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../../src/renderer/utils.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAEhD,MAAM,WAAW,OAAO;IACvB,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,SAAS,GAAG,SAAS,GAAG,MAAM,GAAG,YAAY,CAAC;CACpD;AAID,iBAAS,cAAc,CACtB,GAAG,EAAE,MAAM,EACX,OAAO,EAAE,YAAY,EACrB,YAAY,GAAE,IAAI,GAAG,IAAI,GAAG,MAAM,GAAG,MAAe,GAClD,MAAM,CAqDR;AAED,iBAAS,eAAe,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,EAAE,YAAY,GAAG,MAAM,CA0CnE;AAED,OAAO,EAAE,eAAe,EAAE,cAAc,EAAE,CAAC"}
@@ -1,5 +1,7 @@
1
1
  import * as fs from "fs";
2
2
  import * as path from "path";
3
+ import * as crypto from "crypto";
4
+ import { spawnSync } from "child_process";
3
5
  // ─── Smart Content Resolution ────────────────────────────────────────────────
4
6
  function resolveContent(src, options, expectedType = "text") {
5
7
  if (src.includes("\n"))
@@ -17,15 +19,27 @@ function resolveContent(src, options, expectedType = "text") {
17
19
  let filePath = path.isAbsolute(src)
18
20
  ? src
19
21
  : path.resolve(options.contentBase ?? ".", src);
20
- if (!fs.existsSync(filePath) && !path.isAbsolute(src)) {
21
- const cwdFilePath = path.resolve(process.cwd(), src);
22
- if (fs.existsSync(cwdFilePath)) {
23
- filePath = cwdFilePath;
22
+ // Graceful fallback: If it starts with "/" but doesn't exist at the root,
23
+ // the user likely meant it relative to the lesson folder (contentBase).
24
+ if (src.startsWith("/") && !fs.existsSync(filePath)) {
25
+ const fallbackPath = path.resolve(options.contentBase ?? ".", src.slice(1));
26
+ if (fs.existsSync(fallbackPath)) {
27
+ filePath = fallbackPath;
24
28
  }
25
29
  }
26
30
  if (fs.existsSync(filePath)) {
27
31
  const stat = fs.statSync(filePath);
28
32
  if (stat.isFile()) {
33
+ if (expectedType === "js" && (filePath.endsWith(".js") || filePath.endsWith(".ts") || filePath.endsWith(".jsx") || filePath.endsWith(".tsx"))) {
34
+ const out = spawnSync("bun", ["build", "--target=browser", filePath]);
35
+ if (out.status === 0) {
36
+ return out.stdout.toString("utf-8");
37
+ }
38
+ else {
39
+ console.warn(`\n ⚠ Bun build failed for ${filePath}:\n${out.stderr.toString("utf-8")}`);
40
+ // fallback to reading raw
41
+ }
42
+ }
29
43
  return fs.readFileSync(filePath, "utf-8");
30
44
  }
31
45
  }
@@ -38,45 +52,38 @@ function resolveContent(src, options, expectedType = "text") {
38
52
  function resolveAssetSrc(src, options) {
39
53
  if (/^(https?:|data:)/.test(src))
40
54
  return src;
41
- const isWebAbsolute = src.startsWith("/") && !fs.existsSync(src);
42
- if (isWebAbsolute)
43
- return src;
55
+ let isWebAbsolute = src.startsWith("/") && !fs.existsSync(src);
44
56
  let filePath = path.isAbsolute(src)
45
57
  ? src
46
58
  : path.resolve(options.contentBase ?? ".", src);
47
- if (!fs.existsSync(filePath) && !path.isAbsolute(src)) {
48
- const cwdFilePath = path.resolve(process.cwd(), src);
49
- if (fs.existsSync(cwdFilePath)) {
50
- filePath = cwdFilePath;
59
+ if (src.startsWith("/") && !fs.existsSync(filePath)) {
60
+ const fallbackPath = path.resolve(options.contentBase ?? ".", src.slice(1));
61
+ if (fs.existsSync(fallbackPath)) {
62
+ filePath = fallbackPath;
63
+ isWebAbsolute = false; // We found it locally, so don't treat it as a web URL
51
64
  }
52
65
  }
66
+ if (isWebAbsolute)
67
+ return src;
53
68
  if (!fs.existsSync(filePath)) {
54
69
  if (options.strict !== false)
55
70
  throw new Error(`Missing media asset: ${filePath}`);
56
71
  return src;
57
72
  }
58
- const ext = path.extname(filePath).toLowerCase();
59
- const mime = ext === ".svg"
60
- ? "image/svg+xml"
61
- : ext === ".png"
62
- ? "image/png"
63
- : ext === ".jpg" || ext === ".jpeg"
64
- ? "image/jpeg"
65
- : ext === ".webp"
66
- ? "image/webp"
67
- : ext === ".gif"
68
- ? "image/gif"
69
- : ext === ".avif"
70
- ? "image/avif"
71
- : ext === ".mp4"
72
- ? "video/mp4"
73
- : ext === ".webm"
74
- ? "video/webm"
75
- : ext === ".mp3"
76
- ? "audio/mpeg"
77
- : ext === ".wav"
78
- ? "audio/wav"
79
- : "application/octet-stream";
80
- return `data:${mime};base64,${fs.readFileSync(filePath).toString("base64")}`;
73
+ // Copy asset to outDir/assets instead of base64 encoding
74
+ const outDir = options.outDir ?? "./out";
75
+ const assetsDir = path.join(outDir, "assets");
76
+ if (!fs.existsSync(assetsDir)) {
77
+ fs.mkdirSync(assetsDir, { recursive: true });
78
+ }
79
+ // Create a safe filename with hash to avoid collisions
80
+ const hash = crypto.createHash("md5").update(filePath).digest("hex").substring(0, 8);
81
+ const ext = path.extname(filePath);
82
+ const filename = `${path.basename(filePath, ext)}-${hash}${ext}`;
83
+ const outPath = path.join(assetsDir, filename);
84
+ if (!fs.existsSync(outPath)) {
85
+ fs.copyFileSync(filePath, outPath);
86
+ }
87
+ return `assets/${filename}`;
81
88
  }
82
89
  export { resolveAssetSrc, resolveContent };