mr-md 1.0.2 → 1.0.4

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.
@@ -1,4 +1,4 @@
1
- import type { BuildOptions, Chapter, Lesson } from "../types.js";
1
+ import type { BuildOptions, Chapter, Course, 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";
@@ -399,3 +399,383 @@ ${clientScript()}
399
399
  </body>
400
400
  </html>`;
401
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
+ <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>
746
+ </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>
749
+ </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>
752
+ </button>
753
+ </div>
754
+ </div>
755
+ </div>
756
+ </div>
757
+ </div>
758
+ </aside>
759
+ <button class="bk-sidebar-collapse-floating" id="bk-sidebar-collapse" aria-label="Collapse sidebar">
760
+ <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>
761
+ </button>
762
+ <main class="bk-main">
763
+ <button class="bk-sidebar-expand" id="bk-sidebar-expand" type="button" aria-label="Expand sidebar">
764
+ <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>
765
+ </button>
766
+ <article class="bk-content" style="max-width: 1000px; margin: 0 auto;">
767
+ <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>` : ""}
771
+ </header>
772
+ ${timelineHtml}
773
+ </article>
774
+ </main>
775
+ </div>
776
+ <script>
777
+ ${clientScript()}
778
+ </script>
779
+ </body>
780
+ </html>`;
781
+ }
@@ -30,10 +30,17 @@ function resolveContent(
30
30
  src.startsWith("./") ||
31
31
  src.startsWith("../");
32
32
 
33
- const filePath = path.isAbsolute(src)
33
+ let filePath = path.isAbsolute(src)
34
34
  ? src
35
35
  : path.resolve(options.contentBase ?? ".", src);
36
36
 
37
+ if (!fs.existsSync(filePath) && !path.isAbsolute(src)) {
38
+ const cwdFilePath = path.resolve(process.cwd(), src);
39
+ if (fs.existsSync(cwdFilePath)) {
40
+ filePath = cwdFilePath;
41
+ }
42
+ }
43
+
37
44
  if (fs.existsSync(filePath)) {
38
45
  const stat = fs.statSync(filePath);
39
46
  if (stat.isFile()) {
@@ -57,9 +64,17 @@ function resolveAssetSrc(src: string, options: BuildOptions): string {
57
64
  const isWebAbsolute = src.startsWith("/") && !fs.existsSync(src);
58
65
  if (isWebAbsolute) return src;
59
66
 
60
- const filePath = path.isAbsolute(src)
67
+ let filePath = path.isAbsolute(src)
61
68
  ? src
62
69
  : path.resolve(options.contentBase ?? ".", src);
70
+
71
+ if (!fs.existsSync(filePath) && !path.isAbsolute(src)) {
72
+ const cwdFilePath = path.resolve(process.cwd(), src);
73
+ if (fs.existsSync(cwdFilePath)) {
74
+ filePath = cwdFilePath;
75
+ }
76
+ }
77
+
63
78
  if (!fs.existsSync(filePath)) {
64
79
  if (options.strict !== false)
65
80
  throw new Error(`Missing media asset: ${filePath}`);
package/src/types.ts CHANGED
@@ -286,3 +286,14 @@ export interface Chapter {
286
286
  meta: ChapterMeta;
287
287
  lessons: Lesson[];
288
288
  }
289
+
290
+ export interface CourseMeta {
291
+ title: string;
292
+ slug: string;
293
+ description?: string;
294
+ }
295
+
296
+ export interface Course {
297
+ meta: CourseMeta;
298
+ chapters: Chapter[];
299
+ }