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
@@ -1,4 +1,4 @@
1
- import type { BuildOptions, Chapter, Course, Lesson } from "../types.js";
1
+ import type { BuildOptions, Chapter, Lesson } from "../types.js";
2
2
  import { escAttr, escHtml, renderBlock } from "./blocks.js";
3
3
  import { clientScript, pageCSS, renderPage } from "./html.js"; // Used in renderChapter
4
4
  import type { NavItem } from "./utils.js";
@@ -26,13 +26,14 @@ export function renderChapter(
26
26
  chapter: Chapter,
27
27
  opts: BuildOptions = {},
28
28
  ): string {
29
- const theme = opts.theme ?? "auto";
30
- const schemeAttr = theme === "auto" ? "" : `data-theme="${theme}"`;
29
+ const theme = opts.theme ?? "light";
30
+ const schemeAttr = `data-theme="${theme}"`;
31
31
  const preset = opts.preset ?? {};
32
32
  const layout = preset.layout ?? "lesson";
33
33
  const density = preset.density ?? "comfortable";
34
34
  const tone = preset.tone ?? "scholarly";
35
35
  const palette = opts.palette ?? "ink";
36
+ const ui = opts.ui ?? "standard";
36
37
 
37
38
  const navHtml = chapter.lessons
38
39
  .map(
@@ -278,16 +279,16 @@ export function renderChapter(
278
279
  border-bottom-color: var(--ink);
279
280
  }
280
281
 
281
- @media (prefers-color-scheme: dark) {
282
- .bk-timeline-content {
283
- background: var(--panel);
284
- }
285
- .bk-status-unread .bk-timeline-content {
286
- background: transparent;
287
- }
288
- .bk-status-unread:hover .bk-timeline-content {
289
- background: var(--panel);
290
- }
282
+
283
+
284
+ :root[data-theme="dark"] .bk-timeline-content {
285
+ background: var(--panel);
286
+ }
287
+ :root[data-theme="dark"] .bk-status-unread .bk-timeline-content {
288
+ background: transparent;
289
+ }
290
+ :root[data-theme="dark"] .bk-status-unread:hover .bk-timeline-content {
291
+ background: var(--panel);
291
292
  }
292
293
 
293
294
  @media (max-width: 600px) {
@@ -316,9 +317,10 @@ export function renderChapter(
316
317
  `;
317
318
 
318
319
  return `<!DOCTYPE html>
319
- <html lang="en" data-palette="${palette}" ${schemeAttr}>
320
+ <html lang="en" data-palette="${palette}" data-ui="${ui}" ${schemeAttr}>
320
321
  <head>
321
322
  <meta charset="UTF-8">
323
+ <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">
322
324
  <meta name="viewport" content="width=device-width, initial-scale=1">
323
325
  <title>${escHtml(chapter.meta.title)}</title>
324
326
  ${chapter.meta.description ? `<meta name="description" content="${escHtml(chapter.meta.description)}">` : ""}
@@ -350,12 +352,12 @@ ${chapterStyles}
350
352
  <button type="button" class="bk-segment-btn ${theme === "light" ? "active" : ""}" data-theme="light" title="Light" aria-label="Light theme">
351
353
  <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>
352
354
  </button>
355
+ <button type="button" class="bk-segment-btn ${theme === "auto" ? "active" : (!theme ? "active" : "")}" data-theme="auto" title="System" aria-label="System theme">
356
+ <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>
357
+ </button>
353
358
  <button type="button" class="bk-segment-btn ${theme === "dark" ? "active" : ""}" data-theme="dark" title="Dark" aria-label="Dark theme">
354
359
  <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>
355
360
  </button>
356
- <button type="button" class="bk-segment-btn ${theme === "auto" ? "active" : ""}" data-theme="auto" title="System" aria-label="System theme">
357
- <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>
358
- </button>
359
361
  </div>
360
362
  </div>
361
363
  <div class="bk-theme-row">
@@ -372,383 +374,17 @@ ${chapterStyles}
372
374
  </button>
373
375
  </div>
374
376
  </div>
375
- </div>
376
- </div>
377
- </div>
378
- </aside>
379
- <button class="bk-sidebar-collapse-floating" id="bk-sidebar-collapse" aria-label="Collapse sidebar">
380
- <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>
381
- </button>
382
- <main class="bk-main">
383
- <button class="bk-sidebar-expand" id="bk-sidebar-expand" type="button" aria-label="Expand sidebar">
384
- <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>
385
- </button>
386
- <article class="bk-content" style="max-width: 1000px; margin: 0 auto;">
387
- <header class="bk-hero" style="border-bottom: none;">
388
- <p class="bk-eyebrow">Chapter</p>
389
- <h1>${escHtml(chapter.meta.title)}</h1>
390
- ${chapter.meta.description ? `<p class="bk-deck">${escHtml(chapter.meta.description)}</p>` : ""}
391
- </header>
392
- ${timelineHtml}
393
- </article>
394
- </main>
395
- </div>
396
- <script>
397
- ${clientScript()}
398
- </script>
399
- </body>
400
- </html>`;
401
- }
402
-
403
- // ─── Course Rendering ────────────────────────────────────────────────────────
404
-
405
- export function renderCourse(
406
- course: Course,
407
- opts: BuildOptions = {},
408
- ): string {
409
- const theme = opts.theme ?? "auto";
410
- const schemeAttr = theme === "auto" ? "" : `data-theme="${theme}"`;
411
- const preset = opts.preset ?? {};
412
- const layout = preset.layout ?? "lesson";
413
- const density = preset.density ?? "comfortable";
414
- const tone = preset.tone ?? "scholarly";
415
- const palette = opts.palette ?? "ink";
416
-
417
- const navHtml = course.chapters
418
- .map(
419
- (c) =>
420
- `<a href="${escAttr(c.meta.slug)}.html" class="bk-nav-item bk-nav-chapter">${escHtml(c.meta.title)}</a>`,
421
- )
422
- .join("\n");
423
-
424
- const timelineHtml = `
425
- <div class="bk-chapter-timeline-wrapper">
426
- <div class="bk-chapter-timeline">
427
- ${course.chapters
428
- .map(
429
- (chapter, idx) => `
430
- <a href="${escAttr(chapter.meta.slug)}.html" class="bk-timeline-card bk-status-${chapter.meta.status ?? "unread"}">
431
- <div class="bk-timeline-node"></div>
432
- <div class="bk-timeline-content">
433
- <h3 class="bk-timeline-title" style="view-transition-name: title-${escAttr(chapter.meta.slug)}">${escHtml(chapter.meta.title)}</h3>
434
- ${chapter.meta.description ? `<p class="bk-timeline-desc">${escHtml(chapter.meta.description)}</p>` : ""}
435
- <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>
436
- </div>
437
- </a>
438
- `,
439
- )
440
- .join("")}
441
- </div>
442
- </div>
443
- <script>
444
- if (!CSS.supports('(animation-timeline: view()) and (animation-range: entry)')) {
445
- const observer = new IntersectionObserver(
446
- (entries) => {
447
- for (const entry of entries) {
448
- if (entry.isIntersecting) {
449
- entry.target.classList.add('js-visible');
450
- entry.target.classList.remove('js-hidden');
451
- observer.unobserve(entry.target);
452
- }
453
- }
454
- },
455
- { threshold: 0.1 }
456
- );
457
- document.querySelectorAll('.bk-timeline-card').forEach((el) => {
458
- el.classList.add('js-hidden');
459
- observer.observe(el);
460
- });
461
- }
462
- </script>`;
463
-
464
- const chapterStyles = `
465
- .bk-chapter-timeline-wrapper {
466
- position: relative;
467
- width: 100%;
468
- padding: 1rem 0;
469
- z-index: 1;
470
- }
471
-
472
- .bk-chapter-timeline {
473
- display: flex;
474
- flex-direction: column;
475
- gap: 3rem;
476
- padding: 3rem 0;
477
- position: relative;
478
- max-width: 800px;
479
- margin: 0 auto;
480
- }
481
-
482
- /* Minimalist vertical connecting line */
483
- .bk-chapter-timeline::before {
484
- content: '';
485
- position: absolute;
486
- left: 20px;
487
- top: 3rem;
488
- bottom: 3rem;
489
- width: 1px;
490
- background: var(--line);
491
- z-index: 0;
492
- transform: translateX(-50%);
493
- }
494
-
495
- .bk-timeline-card {
496
- display: flex;
497
- align-items: stretch;
498
- gap: 2rem;
499
- text-decoration: none !important;
500
- border: none !important;
501
- color: inherit;
502
- position: relative;
503
- z-index: 1;
504
- opacity: 1;
505
- transform: none;
506
- }
507
-
508
- /* Subtle scroll-driven animations */
509
- @media (prefers-reduced-motion: no-preference) {
510
- @supports ((animation-timeline: view()) and (animation-range: entry)) {
511
- .bk-timeline-card {
512
- animation-name: slide-fade-in;
513
- animation-fill-mode: both;
514
- animation-timing-function: cubic-bezier(0.16, 1, 0.3, 1);
515
- animation-timeline: view(block);
516
- animation-range: entry 5% cover 20%;
517
- }
518
- @keyframes slide-fade-in {
519
- 0% { opacity: 0; transform: translateY(20px); }
520
- 100% { opacity: 1; transform: translateY(0); }
521
- }
522
- }
523
- }
524
-
525
- /* Fallback for browsers without animation-timeline support */
526
- .bk-timeline-card.js-hidden {
527
- opacity: 0;
528
- transform: translateY(20px);
529
- }
530
- .bk-timeline-card.js-visible {
531
- opacity: 1;
532
- transform: translateY(0);
533
- transition: opacity 0.6s cubic-bezier(0.16, 1, 0.3, 1), transform 0.6s cubic-bezier(0.16, 1, 0.3, 1);
534
- }
535
-
536
- .bk-timeline-node {
537
- width: 40px;
538
- height: 40px;
539
- border-radius: 50%;
540
- background: var(--paper);
541
- border: 1px solid var(--accent);
542
- display: flex;
543
- align-items: center;
544
- justify-content: center;
545
- flex-shrink: 0;
546
- transition: all 0.3s ease;
547
- position: relative;
548
- box-shadow: 0 2px 4px rgba(0,0,0,0.02);
549
- }
550
- .bk-timeline-node::after {
551
- content: '';
552
- width: 10px;
553
- height: 10px;
554
- border-radius: 50%;
555
- background: var(--accent);
556
- transition: all 0.3s ease;
557
- }
558
- .bk-timeline-card:hover .bk-timeline-node {
559
- background: var(--accent);
560
- }
561
- .bk-timeline-card:hover .bk-timeline-node::after {
562
- background: var(--paper);
563
- }
564
-
565
- .bk-timeline-content {
566
- background: var(--paper);
567
- padding: 2rem;
568
- border-radius: 12px;
569
- border: 1px solid var(--line);
570
- box-shadow: 0 4px 12px rgba(0, 0, 0, 0.02);
571
- flex: 1;
572
- transition: all 0.3s ease;
573
- display: flex;
574
- flex-direction: column;
575
- justify-content: center;
576
- position: relative;
577
- }
578
- .bk-timeline-card:hover .bk-timeline-content {
579
- border-color: color-mix(in srgb, var(--accent) 30%, var(--line));
580
- box-shadow: 0 8px 24px rgba(0, 0, 0, 0.04);
581
- transform: translateY(-2px);
582
- }
583
-
584
- .bk-timeline-title {
585
- margin: 0 0 0.5rem 0;
586
- font-family: var(--font-display);
587
- font-size: 1.6rem;
588
- color: var(--ink);
589
- width: fit-content;
590
- transition: color 0.2s ease;
591
- }
592
- .bk-timeline-card:hover .bk-timeline-title {
593
- color: var(--accent);
594
- }
595
-
596
- .bk-timeline-desc {
597
- margin: 0 0 1.5rem 0;
598
- color: var(--muted);
599
- line-height: 1.6;
600
- font-size: 1.05rem;
601
- }
602
-
603
- .bk-timeline-action {
604
- font-weight: 500;
605
- color: var(--accent);
606
- display: inline-flex;
607
- align-items: center;
608
- gap: 0.4rem;
609
- font-size: 0.95rem;
610
- align-self: flex-start;
611
- transition: all 0.2s ease;
612
- border-bottom: 1px solid transparent;
613
- }
614
- .bk-timeline-card:hover .bk-timeline-action {
615
- gap: 0.6rem;
616
- border-bottom-color: var(--accent);
617
- }
618
-
619
- .bk-status-unread .bk-timeline-node {
620
- border: 1px solid var(--line-strong);
621
- }
622
- .bk-status-unread .bk-timeline-node::after {
623
- background: var(--line-strong);
624
- transform: scale(0.8);
625
- }
626
- .bk-status-unread .bk-timeline-content {
627
- background: transparent;
628
- box-shadow: none;
629
- border: 1px solid transparent;
630
- }
631
- .bk-status-unread .bk-timeline-title {
632
- color: var(--muted);
633
- }
634
- .bk-status-unread .bk-timeline-desc {
635
- color: color-mix(in srgb, var(--muted) 80%, transparent);
636
- }
637
- .bk-status-unread .bk-timeline-action {
638
- color: var(--muted);
639
- }
640
- .bk-status-unread:hover .bk-timeline-node {
641
- border-color: var(--ink);
642
- background: var(--paper);
643
- }
644
- .bk-status-unread:hover .bk-timeline-node::after {
645
- background: var(--ink);
646
- transform: scale(1);
647
- }
648
- .bk-status-unread:hover .bk-timeline-content {
649
- background: var(--paper);
650
- border-color: var(--line);
651
- box-shadow: 0 4px 12px rgba(0, 0, 0, 0.02);
652
- }
653
- .bk-status-unread:hover .bk-timeline-title {
654
- color: var(--ink);
655
- }
656
- .bk-status-unread:hover .bk-timeline-action {
657
- color: var(--ink);
658
- border-bottom-color: var(--ink);
659
- }
660
-
661
- @media (prefers-color-scheme: dark) {
662
- .bk-timeline-content {
663
- background: var(--panel);
664
- }
665
- .bk-status-unread .bk-timeline-content {
666
- background: transparent;
667
- }
668
- .bk-status-unread:hover .bk-timeline-content {
669
- background: var(--panel);
670
- }
671
- }
672
-
673
- @media (max-width: 600px) {
674
- .bk-chapter-timeline::before {
675
- left: 16px;
676
- }
677
- .bk-timeline-card {
678
- gap: 1.5rem;
679
- }
680
- .bk-timeline-node {
681
- width: 32px;
682
- height: 32px;
683
- }
684
- .bk-timeline-node::after {
685
- width: 8px;
686
- height: 8px;
687
- }
688
- .bk-timeline-content {
689
- padding: 1.5rem;
690
- border-radius: 10px;
691
- }
692
- .bk-timeline-title {
693
- font-size: 1.4rem;
694
- }
695
- }
696
- `;
697
-
698
- return `<!DOCTYPE html>
699
- <html lang="en" data-palette="${palette}" ${schemeAttr}>
700
- <head>
701
- <meta charset="UTF-8">
702
- <meta name="viewport" content="width=device-width, initial-scale=1">
703
- <title>${escHtml(course.meta.title)}</title>
704
- ${course.meta.description ? `<meta name="description" content="${escHtml(course.meta.description)}">` : ""}
705
- ${opts.head ?? ""}
706
- <style>
707
- ${opts.font ? `:root { --font-sans: ${opts.font}, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; }` : ""}
708
- ${pageCSS()}
709
- ${chapterStyles}
710
- </style>
711
- </head>
712
- <body class="bk-layout-${layout} bk-density-${density} bk-tone-${tone}">
713
- <div class="bk-shell">
714
- <aside class="bk-sidebar">
715
- <div class="bk-sidebar-inner">
716
- <div class="bk-sidebar-header">
717
- <div style="margin-top: 8px;"></div>
718
- <div class="bk-sidebar-title">${escHtml(course.meta.title)}</div>
719
- </div>
720
- <nav class="bk-nav">${navHtml}</nav>
721
- <div class="bk-sidebar-footer">
722
- <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">
723
- <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>
724
- <span class="bk-sr-only">Display settings</span>
725
- </button>
726
- <div class="bk-theme-panel" id="bk-theme-panel" aria-label="Display settings" hidden>
727
377
  <div class="bk-theme-row">
728
- <span>Theme</span>
729
- <div class="bk-segmented-control" id="bk-theme-icons">
730
- <button type="button" class="bk-segment-btn ${theme === "light" ? "active" : ""}" data-theme="light" title="Light" aria-label="Light theme">
731
- <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>
732
- </button>
733
- <button type="button" class="bk-segment-btn ${theme === "dark" ? "active" : ""}" data-theme="dark" title="Dark" aria-label="Dark theme">
734
- <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>
735
- </button>
736
- <button type="button" class="bk-segment-btn ${theme === "auto" ? "active" : ""}" data-theme="auto" title="System" aria-label="System theme">
737
- <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>
738
- </button>
739
- </div>
740
- </div>
741
- <div class="bk-theme-row">
742
- <span>Palette</span>
743
- <div class="bk-segmented-control" id="bk-palette-icons">
744
- <button type="button" class="bk-segment-btn ${palette === "ink" ? "active" : ""}" data-palette="ink" title="Ink" aria-label="Ink palette">
745
- <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>
378
+ <span>UI</span>
379
+ <div class="bk-segmented-control" id="bk-ui-icons">
380
+ <button type="button" class="bk-segment-btn ${ui === 'standard' ? 'active' : ''}" data-ui="standard" title="Standard" aria-label="Standard UI">
381
+ <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>
746
382
  </button>
747
- <button type="button" class="bk-segment-btn ${palette === "field" ? "active" : ""}" data-palette="field" title="Field" aria-label="Field palette">
748
- <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>
383
+ <button type="button" class="bk-segment-btn ${ui === 'neo' ? 'active' : ''}" data-ui="neo" title="Neo Brutalist" aria-label="Neo UI">
384
+ <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>
749
385
  </button>
750
- <button type="button" class="bk-segment-btn ${palette === "ember" ? "active" : ""}" data-palette="ember" title="Ember" aria-label="Ember palette">
751
- <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>
386
+ <button type="button" class="bk-segment-btn ${ui === 'playful' ? 'active' : ''}" data-ui="playful" title="Playful" aria-label="Playful UI">
387
+ <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>
752
388
  </button>
753
389
  </div>
754
390
  </div>
@@ -765,9 +401,9 @@ ${chapterStyles}
765
401
  </button>
766
402
  <article class="bk-content" style="max-width: 1000px; margin: 0 auto;">
767
403
  <header class="bk-hero" style="border-bottom: none;">
768
- <p class="bk-eyebrow">Course</p>
769
- <h1>${escHtml(course.meta.title)}</h1>
770
- ${course.meta.description ? `<p class="bk-deck">${escHtml(course.meta.description)}</p>` : ""}
404
+ <p class="bk-eyebrow">Chapter</p>
405
+ <h1>${escHtml(chapter.meta.title)}</h1>
406
+ ${chapter.meta.description ? `<p class="bk-deck">${escHtml(chapter.meta.description)}</p>` : ""}
771
407
  </header>
772
408
  ${timelineHtml}
773
409
  </article>
@@ -61,7 +61,7 @@ function mdToHtml(md: string): { html: string; title: string; headings: { id: st
61
61
  const renderer = new marked.Renderer();
62
62
  renderer.heading = ({ tokens, depth, text }) => {
63
63
  const id = `bk-heading-${headingIdCounter++}`;
64
- if (depth === 2 || depth === 3) {
64
+ if (depth === 1 || depth === 2) {
65
65
  const plainText = text.replace(/<[^>]+>/g, "");
66
66
  headings.push({ id, text: plainText, level: depth });
67
67
  }
@@ -119,8 +119,9 @@ function blockChrome(
119
119
  body: string,
120
120
  accent = "neutral",
121
121
  allowMaximize = true,
122
+ id?: string,
122
123
  ): string {
123
- return `<figure class="bk-object bk-object--${escAttr(accent)}">
124
+ return `<figure ${id ? `id="${escAttr(id)}" ` : ""}class="bk-object bk-object--${escAttr(accent)}">
124
125
  <div class="bk-object-header">
125
126
  <span class="bk-object-kicker">${escHtml(kind)}</span>
126
127
  ${label ? `<span class="bk-object-title">${escHtml(label)}</span>` : ""}
@@ -1,11 +1,13 @@
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
  import type { BuildOptions } from "../types.js";
4
6
 
5
7
  export interface NavItem {
6
8
  id: string;
7
9
  label: string;
8
- kind: "heading" | "section" | "quiz";
10
+ kind: "heading" | "section" | "quiz" | "simulation";
9
11
  }
10
12
 
11
13
  // ─── Smart Content Resolution ────────────────────────────────────────────────
@@ -34,16 +36,27 @@ function resolveContent(
34
36
  ? src
35
37
  : path.resolve(options.contentBase ?? ".", src);
36
38
 
37
- if (!fs.existsSync(filePath) && !path.isAbsolute(src)) {
38
- const cwdFilePath = path.resolve(process.cwd(), src);
39
- if (fs.existsSync(cwdFilePath)) {
40
- filePath = cwdFilePath;
39
+ // Graceful fallback: If it starts with "/" but doesn't exist at the root,
40
+ // the user likely meant it relative to the lesson folder (contentBase).
41
+ if (src.startsWith("/") && !fs.existsSync(filePath)) {
42
+ const fallbackPath = path.resolve(options.contentBase ?? ".", src.slice(1));
43
+ if (fs.existsSync(fallbackPath)) {
44
+ filePath = fallbackPath;
41
45
  }
42
46
  }
43
47
 
44
48
  if (fs.existsSync(filePath)) {
45
49
  const stat = fs.statSync(filePath);
46
50
  if (stat.isFile()) {
51
+ if (expectedType === "js" && (filePath.endsWith(".js") || filePath.endsWith(".ts") || filePath.endsWith(".jsx") || filePath.endsWith(".tsx"))) {
52
+ const out = spawnSync("bun", ["build", "--target=browser", filePath]);
53
+ if (out.status === 0) {
54
+ return out.stdout.toString("utf-8");
55
+ } else {
56
+ console.warn(`\n ⚠ Bun build failed for ${filePath}:\n${out.stderr.toString("utf-8")}`);
57
+ // fallback to reading raw
58
+ }
59
+ }
47
60
  return fs.readFileSync(filePath, "utf-8");
48
61
  }
49
62
  }
@@ -61,51 +74,45 @@ function resolveContent(
61
74
  function resolveAssetSrc(src: string, options: BuildOptions): string {
62
75
  if (/^(https?:|data:)/.test(src)) return src;
63
76
 
64
- const isWebAbsolute = src.startsWith("/") && !fs.existsSync(src);
65
- if (isWebAbsolute) return src;
77
+ let isWebAbsolute = src.startsWith("/") && !fs.existsSync(src);
66
78
 
67
79
  let filePath = path.isAbsolute(src)
68
80
  ? src
69
81
  : path.resolve(options.contentBase ?? ".", src);
70
82
 
71
- if (!fs.existsSync(filePath) && !path.isAbsolute(src)) {
72
- const cwdFilePath = path.resolve(process.cwd(), src);
73
- if (fs.existsSync(cwdFilePath)) {
74
- filePath = cwdFilePath;
83
+ if (src.startsWith("/") && !fs.existsSync(filePath)) {
84
+ const fallbackPath = path.resolve(options.contentBase ?? ".", src.slice(1));
85
+ if (fs.existsSync(fallbackPath)) {
86
+ filePath = fallbackPath;
87
+ isWebAbsolute = false; // We found it locally, so don't treat it as a web URL
75
88
  }
76
89
  }
77
90
 
91
+ if (isWebAbsolute) return src;
78
92
  if (!fs.existsSync(filePath)) {
79
93
  if (options.strict !== false)
80
94
  throw new Error(`Missing media asset: ${filePath}`);
81
95
  return src;
82
96
  }
83
97
 
84
- const ext = path.extname(filePath).toLowerCase();
85
- const mime =
86
- ext === ".svg"
87
- ? "image/svg+xml"
88
- : ext === ".png"
89
- ? "image/png"
90
- : ext === ".jpg" || ext === ".jpeg"
91
- ? "image/jpeg"
92
- : ext === ".webp"
93
- ? "image/webp"
94
- : ext === ".gif"
95
- ? "image/gif"
96
- : ext === ".avif"
97
- ? "image/avif"
98
- : ext === ".mp4"
99
- ? "video/mp4"
100
- : ext === ".webm"
101
- ? "video/webm"
102
- : ext === ".mp3"
103
- ? "audio/mpeg"
104
- : ext === ".wav"
105
- ? "audio/wav"
106
- : "application/octet-stream";
107
-
108
- return `data:${mime};base64,${fs.readFileSync(filePath).toString("base64")}`;
98
+ // Copy asset to outDir/assets instead of base64 encoding
99
+ const outDir = options.outDir ?? "./out";
100
+ const assetsDir = path.join(outDir, "assets");
101
+ if (!fs.existsSync(assetsDir)) {
102
+ fs.mkdirSync(assetsDir, { recursive: true });
103
+ }
104
+
105
+ // Create a safe filename with hash to avoid collisions
106
+ const hash = crypto.createHash("md5").update(filePath).digest("hex").substring(0, 8);
107
+ const ext = path.extname(filePath);
108
+ const filename = `${path.basename(filePath, ext)}-${hash}${ext}`;
109
+ const outPath = path.join(assetsDir, filename);
110
+
111
+ if (!fs.existsSync(outPath)) {
112
+ fs.copyFileSync(filePath, outPath);
113
+ }
114
+
115
+ return `assets/${filename}`;
109
116
  }
110
117
 
111
118
  export { resolveAssetSrc, resolveContent };