@thanh01.pmt/presentation-kit 0.2.14 → 0.2.16

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.
@@ -1276,9 +1276,9 @@ var HTML_SLIDE_PRESETS = {
1276
1276
 
1277
1277
  // src/html-engine/shell.ts
1278
1278
  function generateHtmlShell(options) {
1279
- const { title, theme, slidesHtml, slideCount, customHeadTags = "" } = options;
1279
+ const { title, theme, slidesHtml, slideCount, customHeadTags = "", deckId = "" } = options;
1280
1280
  return `<!DOCTYPE html>
1281
- <html lang="en" class="${theme.scheme}">
1281
+ <html lang="en" class="${theme.scheme}"${deckId ? ` data-deck-id="${deckId}"` : ""}>
1282
1282
  <head>
1283
1283
  <meta charset="UTF-8">
1284
1284
  <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
@@ -1292,6 +1292,7 @@ function generateHtmlShell(options) {
1292
1292
  <!-- Core Presentation Styling & 1920x1080 Scaled Tailwind Configuration -->
1293
1293
  <script src="https://cdn.tailwindcss.com"></script>
1294
1294
  <script>
1295
+ window.tailwind = window.tailwind || {};
1295
1296
  tailwind.config = {
1296
1297
  darkMode: 'class',
1297
1298
  theme: {
@@ -1415,6 +1416,7 @@ function generateHtmlShell(options) {
1415
1416
  left: 50%;
1416
1417
  width: 1920px;
1417
1418
  height: 1080px;
1419
+ transform: translate(-50%, -50%);
1418
1420
  transform-origin: center center;
1419
1421
  background-color: var(--bg);
1420
1422
  overflow: hidden;
@@ -2415,10 +2417,21 @@ function generateHtmlShell(options) {
2415
2417
  const TOTAL_SLIDES = ${slideCount};
2416
2418
  let currentSlide = 0;
2417
2419
  let isEditMode = false;
2418
- const STORAGE_KEY = 'deck_edit_' + encodeURIComponent(document.title.trim() || 'default');
2419
- const originalCanvasHtml = document.getElementById('canvas').innerHTML;
2420
-
2421
2420
  const canvas = document.getElementById('canvas');
2421
+ const originalCanvasHtml = canvas ? canvas.innerHTML : '';
2422
+
2423
+ function computeDeckContentHash(str) {
2424
+ let h = 0;
2425
+ for (let i = 0; i < str.length; i++) {
2426
+ h = (Math.imul(31, h) + str.charCodeAt(i)) | 0;
2427
+ }
2428
+ return Math.abs(h).toString(36);
2429
+ }
2430
+
2431
+ const deckIdAttr = document.documentElement.getAttribute('data-deck-id');
2432
+ const contentHash = computeDeckContentHash(originalCanvasHtml);
2433
+ const deckKeyPrefix = deckIdAttr ? encodeURIComponent(deckIdAttr) : (encodeURIComponent(document.title.trim() || 'deck') + '_' + contentHash);
2434
+ const STORAGE_KEY = 'deck_edit_' + deckKeyPrefix;
2422
2435
  const slides = document.querySelectorAll('.slide-page');
2423
2436
  const progressBar = document.getElementById('progress-bar');
2424
2437
  const counter = document.getElementById('slide-counter');
@@ -2478,7 +2491,7 @@ function generateHtmlShell(options) {
2478
2491
  let timerInterval = null;
2479
2492
  let isTimerRunning = false;
2480
2493
 
2481
- const SCALE_STORAGE_KEY = 'deck_scale_' + encodeURIComponent(document.title.trim() || 'default');
2494
+ const SCALE_STORAGE_KEY = 'deck_scale_' + deckKeyPrefix;
2482
2495
  let scaleState = {
2483
2496
  mode: 'unified', // 'unified' | 'independent'
2484
2497
  factor: 1.0,
@@ -2502,8 +2515,9 @@ function generateHtmlShell(options) {
2502
2515
  // Auto-scale 1920x1080 canvas to fit window
2503
2516
  function resizeCanvas() {
2504
2517
  if (isDeckMode) return;
2505
- const vw = window.innerWidth;
2506
- const vh = window.innerHeight;
2518
+ const vw = window.innerWidth || document.documentElement.clientWidth || 1920;
2519
+ const vh = window.innerHeight || document.documentElement.clientHeight || 1080;
2520
+ if (vw <= 0 || vh <= 0) return;
2507
2521
  const scaleX = vw / 1920;
2508
2522
  const scaleY = vh / 1080;
2509
2523
  const scale = Math.min(scaleX, scaleY);
@@ -2511,7 +2525,10 @@ function generateHtmlShell(options) {
2511
2525
  }
2512
2526
 
2513
2527
  window.addEventListener('resize', resizeCanvas);
2528
+ window.addEventListener('load', resizeCanvas);
2529
+ document.addEventListener('DOMContentLoaded', resizeCanvas);
2514
2530
  resizeCanvas();
2531
+ requestAnimationFrame(resizeCanvas);
2515
2532
 
2516
2533
  function getSlideNotes(slideIndex) {
2517
2534
  if (slideIndex < 0 || slideIndex >= slides.length) return '';
@@ -2684,8 +2701,7 @@ function generateHtmlShell(options) {
2684
2701
  if (!presenterNotesText) return;
2685
2702
  const notes = getSlideNotes(currentSlide);
2686
2703
  if (notes && notes.trim()) {
2687
- presenterNotesText.innerHTML = notes.replace(/
2688
- /g, '<br/>');
2704
+ presenterNotesText.innerHTML = notes.split('\\n').join('<br/>');
2689
2705
  } else {
2690
2706
  presenterNotesText.innerHTML = '<span style="opacity: 0.5; font-style: italic;">No speaker notes for Slide ' + (currentSlide + 1) + '.</span>';
2691
2707
  }
@@ -2702,7 +2718,7 @@ function generateHtmlShell(options) {
2702
2718
 
2703
2719
  function openPresenterConsole() {
2704
2720
  startTimer();
2705
- const popupName = 'PresenterConsole_' + encodeURIComponent(document.title.replace(/W/g, ''));
2721
+ const popupName = 'PresenterConsole_' + encodeURIComponent(document.title.replace(/[^a-zA-Z0-9_]/g, ''));
2706
2722
  const win = window.open('', popupName, 'width=1100,height=720,menubar=no,toolbar=no,location=no,status=no');
2707
2723
  if (!win) {
2708
2724
  togglePresenterDrawer();
@@ -2713,151 +2729,84 @@ function generateHtmlShell(options) {
2713
2729
  const currentNotes = getSlideNotes(currentSlide);
2714
2730
  const curSlideTitle = 'Slide ' + (currentSlide + 1) + ' / ' + TOTAL_SLIDES;
2715
2731
 
2716
- const consoleHtml = '<!DOCTYPE html>
2717
- <html>
2718
- <head>
2719
- ' +
2720
- '<meta charset="UTF-8"><title>Presenter Console \u2014 ' + (document.title || 'Slides') + '</title>
2721
- ' +
2722
- '<style>
2723
- ' +
2724
- '* { box-sizing: border-box; margin: 0; padding: 0; }
2725
- ' +
2726
- 'body { background: #0c0d14; color: #f4f4f5; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; display: flex; flex-direction: column; height: 100vh; overflow: hidden; user-select: none; }
2727
- ' +
2728
- '.top-bar { display: flex; align-items: center; justify-content: space-between; padding: 12px 24px; background: #141522; border-bottom: 1px solid rgba(255,255,255,0.12); }
2729
- ' +
2730
- '.timer-badge { font-family: monospace; font-size: 20px; font-weight: 700; color: #38bdf8; background: rgba(56,189,248,0.15); border: 1px solid rgba(56,189,248,0.3); padding: 4px 14px; border-radius: 8px; display: flex; align-items: center; gap: 8px; }
2731
- ' +
2732
- '.grid-container { flex: 1; display: grid; grid-template-columns: 1fr 1fr; gap: 20px; padding: 20px 24px; overflow: hidden; }
2733
- ' +
2734
- '.panel-card { background: #181926; border: 1px solid rgba(255,255,255,0.1); border-radius: 12px; display: flex; flex-direction: column; overflow: hidden; }
2735
- ' +
2736
- '.card-header { padding: 10px 16px; background: rgba(255,255,255,0.04); border-bottom: 1px solid rgba(255,255,255,0.08); font-size: 12px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.05em; color: #a1a1aa; display: flex; justify-content: space-between; }
2737
- ' +
2738
- '.notes-content { flex: 1; padding: 20px; font-size: 19px; line-height: 1.7; overflow-y: auto; color: #f1f5f9; user-select: text; white-space: pre-wrap; }
2739
- ' +
2740
- '.preview-box { flex: 1; display: flex; align-items: center; justify-content: center; background: #000; overflow: hidden; padding: 16px; }
2741
- ' +
2742
- '.btn { background: rgba(255,255,255,0.1); border: 1px solid rgba(255,255,255,0.15); color: #fff; padding: 6px 14px; border-radius: 6px; cursor: pointer; font-size: 13px; font-weight: 600; }
2743
- ' +
2744
- '.btn:hover { background: rgba(255,255,255,0.2); }
2745
- ' +
2746
- '.btn-nav { font-size: 16px; padding: 10px 24px; border-radius: 8px; }
2747
- ' +
2748
- '.bottom-nav { display: flex; align-items: center; justify-content: space-between; padding: 14px 24px; background: #141522; border-top: 1px solid rgba(255,255,255,0.12); }
2749
- ' +
2750
- '</style>
2751
- </head>
2752
- <body>
2753
- ' +
2754
- '<div class="top-bar">
2755
- ' +
2756
- ' <div style="font-weight: 700; font-size: 16px;">' + (document.title || 'Slide Deck') + '</div>
2757
- ' +
2758
- ' <div style="display: flex; align-items: center; gap: 12px;">
2759
- ' +
2760
- ' <div class="timer-badge">\u23F1 <span id="timer-val">00:00</span></div>
2761
- ' +
2762
- ' <button class="btn" id="btn-toggle-t">Pause</button>
2763
- ' +
2764
- ' <button class="btn" id="btn-reset-t">Reset</button>
2765
- ' +
2766
- ' </div>
2767
- ' +
2768
- '</div>
2769
- ' +
2770
- '<div class="grid-container">
2771
- ' +
2772
- ' <div class="panel-card">
2773
- ' +
2774
- ' <div class="card-header"><span>CURRENT SLIDE</span><span id="slide-num">' + curSlideTitle + '</span></div>
2775
- ' +
2776
- ' <div class="preview-box"><div style="font-size: 22px; color: #94a3b8; text-align: center;">Active Slide On Screen<br/><span style="font-size: 14px; color: #38bdf8;">(Broadcasting Synced)</span></div></div>
2777
- ' +
2778
- ' <div class="card-header" style="border-top: 1px solid rgba(255,255,255,0.08);"><span>NEXT SLIDE PREVIEW</span></div>
2779
- ' +
2780
- ' <div style="padding: 12px 16px; font-size: 14px; color: #94a3b8; background: rgba(0,0,0,0.3); min-height: 80px;" id="next-preview-text">Upcoming: Slide ' + (currentSlide + 2) + '</div>
2781
- ' +
2782
- ' </div>
2783
- ' +
2784
- ' <div class="panel-card">
2785
- ' +
2786
- ' <div class="card-header"><span>SPEAKER NOTES / TALK TRACK</span><div><button class="btn" id="font-dn" style="padding: 2px 8px;">A-</button> <button class="btn" id="font-up" style="padding: 2px 8px;">A+</button></div></div>
2787
- ' +
2788
- ' <div class="notes-content" id="speaker-notes">' + (currentNotes ? currentNotes : '<span style="opacity: 0.4;">No speaker notes for this slide.</span>') + '</div>
2789
- ' +
2790
- ' </div>
2791
- ' +
2792
- '</div>
2793
- ' +
2794
- '<div class="bottom-nav">
2795
- ' +
2796
- ' <button class="btn btn-nav" id="btn-prev-p">\u2039 Previous (\u2190)</button>
2797
- ' +
2798
- ' <span style="font-size: 14px; color: #a1a1aa;">Use Arrow Keys or Space to advance</span>
2799
- ' +
2800
- ' <button class="btn btn-nav" id="btn-next-p" style="background: #0284c7; border-color: #0284c7;">Next (\u2192 / Space) \u203A</button>
2801
- ' +
2802
- '</div>
2803
- ' +
2804
- '<script>
2805
- ' +
2806
- ' const bc = new BroadcastChannel("' + SYNC_CHANNEL_NAME + '");
2807
- ' +
2808
- ' let curSecs = ' + timerSeconds + ';
2809
- ' +
2810
- ' function sendCmd(action, idx) { bc.postMessage({ type: "COMMAND", action: action, index: idx }); }
2811
- ' +
2812
- ' document.getElementById("btn-prev-p").onclick = () => sendCmd("PREV");
2813
- ' +
2814
- ' document.getElementById("btn-next-p").onclick = () => sendCmd("NEXT");
2815
- ' +
2816
- ' document.getElementById("btn-reset-t").onclick = () => sendCmd("RESET_TIMER");
2817
- ' +
2818
- ' document.getElementById("btn-toggle-t").onclick = () => sendCmd("TOGGLE_TIMER");
2819
- ' +
2820
- ' let curFontSize = 19;
2821
- ' +
2822
- ' document.getElementById("font-up").onclick = () => { curFontSize += 2; document.getElementById("speaker-notes").style.fontSize = curFontSize + "px"; };
2823
- ' +
2824
- ' document.getElementById("font-dn").onclick = () => { curFontSize = Math.max(12, curFontSize - 2); document.getElementById("speaker-notes").style.fontSize = curFontSize + "px"; };
2825
- ' +
2826
- ' window.onkeydown = (e) => {
2827
- ' +
2828
- ' if (["ArrowRight", "Space", "PageDown"].includes(e.code)) { e.preventDefault(); sendCmd("NEXT"); }
2829
- ' +
2830
- ' else if (["ArrowLeft", "PageUp"].includes(e.code)) { e.preventDefault(); sendCmd("PREV"); }
2831
- ' +
2832
- ' };
2833
- ' +
2834
- ' bc.onmessage = (e) => {
2835
- ' +
2836
- ' if (!e.data || e.data.type !== "STATE_UPDATE") return;
2837
- ' +
2838
- ' document.getElementById("slide-num").textContent = "Slide " + (e.data.currentSlide + 1) + " / " + e.data.totalSlides;
2839
- ' +
2840
- ' document.getElementById("speaker-notes").textContent = e.data.notes || "No speaker notes for this slide.";
2841
- ' +
2842
- ' document.getElementById("next-preview-text").textContent = e.data.nextNotes ? "Next Notes: " + e.data.nextNotes : "Next: Slide " + (e.data.currentSlide + 2);
2843
- ' +
2844
- ' curSecs = e.data.timerSeconds || 0;
2845
- ' +
2846
- ' const m = String(Math.floor(curSecs / 60)).padStart(2, "0");
2847
- ' +
2848
- ' const s = String(curSecs % 60).padStart(2, "0");
2849
- ' +
2850
- ' document.getElementById("timer-val").textContent = m + ":" + s;
2851
- ' +
2852
- ' document.getElementById("btn-toggle-t").textContent = e.data.timerRunning ? "Pause" : "Start";
2853
- ' +
2854
- ' };
2855
- ' +
2856
- ' bc.postMessage({ type: "REQUEST_STATE" });
2857
- ' +
2858
- '<' + '/script>
2859
- </body>
2860
- </html>';
2732
+ const consoleHtml = [
2733
+ '<!DOCTYPE html>',
2734
+ '<html>',
2735
+ '<head>',
2736
+ '<meta charset="UTF-8"><title>Presenter Console \u2014 ' + (document.title || 'Slides') + '</title>',
2737
+ '<style>',
2738
+ '* { box-sizing: border-box; margin: 0; padding: 0; }',
2739
+ 'body { background: #0c0d14; color: #f4f4f5; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; display: flex; flex-direction: column; height: 100vh; overflow: hidden; user-select: none; }',
2740
+ '.top-bar { display: flex; align-items: center; justify-content: space-between; padding: 12px 24px; background: #141522; border-bottom: 1px solid rgba(255,255,255,0.12); }',
2741
+ '.timer-badge { font-family: monospace; font-size: 20px; font-weight: 700; color: #38bdf8; background: rgba(56,189,248,0.15); border: 1px solid rgba(56,189,248,0.3); padding: 4px 14px; border-radius: 8px; display: flex; align-items: center; gap: 8px; }',
2742
+ '.grid-container { flex: 1; display: grid; grid-template-columns: 1fr 1fr; gap: 20px; padding: 20px 24px; overflow: hidden; }',
2743
+ '.panel-card { background: #181926; border: 1px solid rgba(255,255,255,0.1); border-radius: 12px; display: flex; flex-direction: column; overflow: hidden; }',
2744
+ '.card-header { padding: 10px 16px; background: rgba(255,255,255,0.04); border-bottom: 1px solid rgba(255,255,255,0.08); font-size: 12px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.05em; color: #a1a1aa; display: flex; justify-content: space-between; }',
2745
+ '.notes-content { flex: 1; padding: 20px; font-size: 19px; line-height: 1.7; overflow-y: auto; color: #f1f5f9; user-select: text; white-space: pre-wrap; }',
2746
+ '.preview-box { flex: 1; display: flex; align-items: center; justify-content: center; background: #000; overflow: hidden; padding: 16px; }',
2747
+ '.btn { background: rgba(255,255,255,0.1); border: 1px solid rgba(255,255,255,0.15); color: #fff; padding: 6px 14px; border-radius: 6px; cursor: pointer; font-size: 13px; font-weight: 600; }',
2748
+ '.btn:hover { background: rgba(255,255,255,0.2); }',
2749
+ '.btn-nav { font-size: 16px; padding: 10px 24px; border-radius: 8px; }',
2750
+ '.bottom-nav { display: flex; align-items: center; justify-content: space-between; padding: 14px 24px; background: #141522; border-top: 1px solid rgba(255,255,255,0.12); }',
2751
+ '</style>',
2752
+ '</head>',
2753
+ '<body>',
2754
+ '<div class="top-bar">',
2755
+ ' <div style="font-weight: 700; font-size: 16px;">' + (document.title || 'Slide Deck') + '</div>',
2756
+ ' <div style="display: flex; align-items: center; gap: 12px;">',
2757
+ ' <div class="timer-badge">\u23F1 <span id="timer-val">00:00</span></div>',
2758
+ ' <button class="btn" id="btn-toggle-t">Pause</button>',
2759
+ ' <button class="btn" id="btn-reset-t">Reset</button>',
2760
+ ' </div>',
2761
+ '</div>',
2762
+ '<div class="grid-container">',
2763
+ ' <div class="panel-card">',
2764
+ ' <div class="card-header"><span>CURRENT SLIDE</span><span id="slide-num">' + curSlideTitle + '</span></div>',
2765
+ ' <div class="preview-box"><div style="font-size: 22px; color: #94a3b8; text-align: center;">Active Slide On Screen<br/><span style="font-size: 14px; color: #38bdf8;">(Broadcasting Synced)</span></div></div>',
2766
+ ' <div class="card-header" style="border-top: 1px solid rgba(255,255,255,0.08);"><span>NEXT SLIDE PREVIEW</span></div>',
2767
+ ' <div style="padding: 12px 16px; font-size: 14px; color: #94a3b8; background: rgba(0,0,0,0.3); min-height: 80px;" id="next-preview-text">Upcoming: Slide ' + (currentSlide + 2) + '</div>',
2768
+ ' </div>',
2769
+ ' <div class="panel-card">',
2770
+ ' <div class="card-header"><span>SPEAKER NOTES / TALK TRACK</span><div><button class="btn" id="font-dn" style="padding: 2px 8px;">A-</button> <button class="btn" id="font-up" style="padding: 2px 8px;">A+</button></div></div>',
2771
+ ' <div class="notes-content" id="speaker-notes">' + (currentNotes ? currentNotes : '<span style="opacity: 0.4;">No speaker notes for this slide.</span>') + '</div>',
2772
+ ' </div>',
2773
+ '</div>',
2774
+ '<div class="bottom-nav">',
2775
+ ' <button class="btn btn-nav" id="btn-prev-p">\u2039 Previous (\u2190)</button>',
2776
+ ' <span style="font-size: 14px; color: #a1a1aa;">Use Arrow Keys or Space to advance</span>',
2777
+ ' <button class="btn btn-nav" id="btn-next-p" style="background: #0284c7; border-color: #0284c7;">Next (\u2192 / Space) \u203A</button>',
2778
+ '</div>',
2779
+ '<script>',
2780
+ ' const bc = new BroadcastChannel("' + SYNC_CHANNEL_NAME + '");',
2781
+ ' let curSecs = ' + timerSeconds + ';',
2782
+ ' function sendCmd(action, idx) { bc.postMessage({ type: "COMMAND", action: action, index: idx }); }',
2783
+ ' document.getElementById("btn-prev-p").onclick = () => sendCmd("PREV");',
2784
+ ' document.getElementById("btn-next-p").onclick = () => sendCmd("NEXT");',
2785
+ ' document.getElementById("btn-reset-t").onclick = () => sendCmd("RESET_TIMER");',
2786
+ ' document.getElementById("btn-toggle-t").onclick = () => sendCmd("TOGGLE_TIMER");',
2787
+ ' let curFontSize = 19;',
2788
+ ' document.getElementById("font-up").onclick = () => { curFontSize += 2; document.getElementById("speaker-notes").style.fontSize = curFontSize + "px"; };',
2789
+ ' document.getElementById("font-dn").onclick = () => { curFontSize = Math.max(12, curFontSize - 2); document.getElementById("speaker-notes").style.fontSize = curFontSize + "px"; };',
2790
+ ' window.onkeydown = (e) => {',
2791
+ ' if (["ArrowRight", "Space", "PageDown"].includes(e.code)) { e.preventDefault(); sendCmd("NEXT"); }',
2792
+ ' else if (["ArrowLeft", "PageUp"].includes(e.code)) { e.preventDefault(); sendCmd("PREV"); }',
2793
+ ' };',
2794
+ ' bc.onmessage = (e) => {',
2795
+ ' if (!e.data || e.data.type !== "STATE_UPDATE") return;',
2796
+ ' document.getElementById("slide-num").textContent = "Slide " + (e.data.currentSlide + 1) + " / " + e.data.totalSlides;',
2797
+ ' document.getElementById("speaker-notes").textContent = e.data.notes || "No speaker notes for this slide.";',
2798
+ ' document.getElementById("next-preview-text").textContent = e.data.nextNotes ? "Next Notes: " + e.data.nextNotes : "Next: Slide " + (e.data.currentSlide + 2);',
2799
+ ' curSecs = e.data.timerSeconds || 0;',
2800
+ ' const m = String(Math.floor(curSecs / 60)).padStart(2, "0");',
2801
+ ' const s = String(curSecs % 60).padStart(2, "0");',
2802
+ ' document.getElementById("timer-val").textContent = m + ":" + s;',
2803
+ ' document.getElementById("btn-toggle-t").textContent = e.data.timerRunning ? "Pause" : "Start";',
2804
+ ' };',
2805
+ ' bc.postMessage({ type: "REQUEST_STATE" });',
2806
+ '<' + '/script>',
2807
+ '</body>',
2808
+ '</html>'
2809
+ ].join('\\n');
2861
2810
 
2862
2811
  win.document.open();
2863
2812
  win.document.write(consoleHtml);
@@ -2873,8 +2822,7 @@ function generateHtmlShell(options) {
2873
2822
  const card = document.createElement('div');
2874
2823
  card.className = 'deck-notes-card';
2875
2824
  card.setAttribute('data-for-slide', String(idx));
2876
- card.innerHTML = '<div class="deck-notes-card-header"><span>\u{1F4DD}</span><span>Presenter Notes (Slide ' + (idx + 1) + ')</span></div><div>' + notes.replace(/
2877
- /g, '<br/>') + '</div>';
2825
+ card.innerHTML = '<div class="deck-notes-card-header"><span>\u{1F4DD}</span><span>Presenter Notes (Slide ' + (idx + 1) + ')</span></div><div>' + notes.split('\\n').join('<br/>') + '</div>';
2878
2826
  slide.after(card);
2879
2827
  }
2880
2828
  });
@@ -3214,8 +3162,7 @@ function generateHtmlShell(options) {
3214
3162
  if (scalePanel) scalePanel.style.display = 'none';
3215
3163
 
3216
3164
  // Take snapshot of full HTML
3217
- let cleanHtml = '<!DOCTYPE html>
3218
- ' + document.documentElement.outerHTML;
3165
+ let cleanHtml = '<!DOCTYPE html>' + '\\n' + document.documentElement.outerHTML;
3219
3166
 
3220
3167
  // Clean any temporary runtime inline styles or attributes
3221
3168
  cleanHtml = cleanHtml
@@ -3225,28 +3172,20 @@ function generateHtmlShell(options) {
3225
3172
  .replace(/class="edit-toggle[^"]*"/g, 'class="edit-toggle"');
3226
3173
 
3227
3174
  // Embed current scale settings into :root style definition so exported HTML is permanently styled with chosen scales
3228
- const scaleVarsReplacement =
3229
- '--scale-mode: ' + scaleState.mode + ';
3230
- ' +
3231
- ' --scale-factor: ' + scaleState.factor + ';
3232
- ' +
3233
- ' --scale-headings: ' + scaleState.headings + ';
3234
- ' +
3235
- ' --scale-body: ' + scaleState.body + ';
3236
- ' +
3237
- ' --scale-code: ' + scaleState.code + ';
3238
- ' +
3239
- ' --scale-spacing: ' + scaleState.spacing + ';
3240
- ' +
3241
- ' --scale-headings-eff: ' + (scaleState.mode === 'unified' ? scaleState.factor : scaleState.headings) + ';
3242
- ' +
3243
- ' --scale-body-eff: ' + (scaleState.mode === 'unified' ? scaleState.factor : scaleState.body) + ';
3244
- ' +
3245
- ' --scale-code-eff: ' + (scaleState.mode === 'unified' ? scaleState.factor : scaleState.code) + ';
3246
- ' +
3247
- ' --scale-spacing-eff: ' + (scaleState.mode === 'unified' ? scaleState.factor : scaleState.spacing) + ';';
3248
-
3249
- cleanHtml = cleanHtml.replace(/--scale-mode:s*unified;[sS]*?--scale-spacing-eff:s*1.0;/, scaleVarsReplacement);
3175
+ const scaleVarsReplacement = [
3176
+ '--scale-mode: ' + scaleState.mode + ';',
3177
+ ' --scale-factor: ' + scaleState.factor + ';',
3178
+ ' --scale-headings: ' + scaleState.headings + ';',
3179
+ ' --scale-body: ' + scaleState.body + ';',
3180
+ ' --scale-code: ' + scaleState.code + ';',
3181
+ ' --scale-spacing: ' + scaleState.spacing + ';',
3182
+ ' --scale-headings-eff: ' + (scaleState.mode === 'unified' ? scaleState.factor : scaleState.headings) + ';',
3183
+ ' --scale-body-eff: ' + (scaleState.mode === 'unified' ? scaleState.factor : scaleState.body) + ';',
3184
+ ' --scale-code-eff: ' + (scaleState.mode === 'unified' ? scaleState.factor : scaleState.code) + ';',
3185
+ ' --scale-spacing-eff: ' + (scaleState.mode === 'unified' ? scaleState.factor : scaleState.spacing) + ';'
3186
+ ].join('\\n');
3187
+
3188
+ cleanHtml = cleanHtml.replace(/--scale-mode:\\s*[^;]+;[\\s\\S]*?--scale-spacing-eff:\\s*[^;]+;/, scaleVarsReplacement);
3250
3189
 
3251
3190
  // Create Blob and trigger download
3252
3191
  const blob = new Blob([cleanHtml], { type: 'text/html;charset=utf-8' });
@@ -3415,6 +3354,8 @@ function generateHtmlShell(options) {
3415
3354
  applyScales();
3416
3355
  } else if (e.data.type === 'TOGGLE_SCALE_PANEL') {
3417
3356
  toggleScalePanel();
3357
+ } else if (e.data.type === 'RESIZE') {
3358
+ resizeCanvas();
3418
3359
  }
3419
3360
  });
3420
3361
 
@@ -3428,6 +3369,219 @@ function generateHtmlShell(options) {
3428
3369
  </html>`;
3429
3370
  }
3430
3371
 
3372
+ // src/html-engine/normalizer.ts
3373
+ function normalizeSlideSlots(slide) {
3374
+ if (!slide || typeof slide !== "object") {
3375
+ return { title: "Slide" };
3376
+ }
3377
+ const slots = {
3378
+ ...slide,
3379
+ ...slide.slots || {}
3380
+ };
3381
+ delete slots.slots;
3382
+ slots.title = String(slots.title || slide.title || slide.heading || slide.name || "").trim();
3383
+ slots.tag = String(slots.tag || slide.tag || slide.badge || "").trim();
3384
+ const rawContent = typeof slots.content === "string" && slots.content.trim() ? slots.content.trim() : typeof slide.content === "string" && slide.content.trim() ? slide.content.trim() : "";
3385
+ const layoutId = slide.layoutId || "hero-cover";
3386
+ switch (layoutId) {
3387
+ case "hero-cover": {
3388
+ slots.subtitle = slots.subtitle || slide.subtitle || slots.desc || rawContent || "";
3389
+ slots.author = slots.author || slide.author || "";
3390
+ slots.date = slots.date || slide.date || "";
3391
+ break;
3392
+ }
3393
+ case "split-concept-code": {
3394
+ if (!slots.code) {
3395
+ if (slide.code) {
3396
+ slots.code = slide.code;
3397
+ } else if (rawContent) {
3398
+ const fenceMatch = rawContent.match(/```(?:\w+)?\s*\n?([\s\S]*?)\n?```/);
3399
+ if (fenceMatch) {
3400
+ slots.code = fenceMatch[1].trim();
3401
+ } else if (/Code(?:\s+snippet)?:\s*/i.test(rawContent)) {
3402
+ const parts = rawContent.split(/Code(?:\s+snippet)?:\s*/i);
3403
+ if (parts[1] && parts[1].trim()) {
3404
+ slots.code = parts[1].trim();
3405
+ }
3406
+ }
3407
+ }
3408
+ }
3409
+ if (!Array.isArray(slots.points) || slots.points.length === 0) {
3410
+ if (Array.isArray(slide.points) && slide.points.length > 0) {
3411
+ slots.points = slide.points;
3412
+ } else if (rawContent) {
3413
+ const textOnly = rawContent.replace(/```[\s\S]*?```/g, "").replace(/Code(?:\s+snippet)?:\s*[\s\S]*/i, "").trim();
3414
+ const lines = textOnly.split(/\n|\.\s+/).map((l) => l.replace(/^(?:Concept:\s*|[-*•]|\d+\.)\s*/i, "").trim()).filter((l) => l.length > 3);
3415
+ slots.points = lines.length > 0 ? lines.slice(0, 4) : [textOnly || slots.title || "Core Concept"];
3416
+ }
3417
+ }
3418
+ if (!slots.code || slots.code === "...") {
3419
+ const lang = (slots.language || "code").toLowerCase();
3420
+ if (lang.includes("swift")) {
3421
+ slots.code = `// ${slots.title || "Swift Implementation"}
3422
+ import SwiftUI
3423
+
3424
+ struct PreviewView: View {
3425
+ var body: some View {
3426
+ Text("${slots.title || "Hello, World!"}")
3427
+ .padding()
3428
+ }
3429
+ }`;
3430
+ slots.language = slots.language || "swift";
3431
+ } else if (lang.includes("py")) {
3432
+ slots.code = `# ${slots.title || "Python Implementation"}
3433
+ def execute():
3434
+ print("${slots.title || "Ready"}")
3435
+
3436
+ execute()`;
3437
+ slots.language = slots.language || "python";
3438
+ } else {
3439
+ slots.code = `// ${slots.title || "Code Example"}
3440
+ function main() {
3441
+ console.log("${slots.title || "Executing"}");
3442
+ }
3443
+ main();`;
3444
+ slots.language = slots.language || "typescript";
3445
+ }
3446
+ }
3447
+ break;
3448
+ }
3449
+ case "two-columns-compare": {
3450
+ slots.col1Title = slots.col1Title || slide.col1Title || "Approach A";
3451
+ slots.col2Title = slots.col2Title || slide.col2Title || "Approach B";
3452
+ if (!Array.isArray(slots.col1Items) || slots.col1Items.length === 0) {
3453
+ if (Array.isArray(slide.col1Items) && slide.col1Items.length > 0) {
3454
+ slots.col1Items = slide.col1Items;
3455
+ } else if (rawContent) {
3456
+ const items = rawContent.split(/\n|\.\s+/).map((l) => l.replace(/^(?:[-*•]|\d+\.)\s*/, "").trim()).filter((l) => l.length > 3);
3457
+ const half = Math.max(1, Math.ceil(items.length / 2));
3458
+ slots.col1Items = items.slice(0, half);
3459
+ if (!slots.col2Items || !Array.isArray(slots.col2Items)) {
3460
+ slots.col2Items = items.slice(half);
3461
+ }
3462
+ }
3463
+ }
3464
+ slots.col1Items = Array.isArray(slots.col1Items) && slots.col1Items.length > 0 ? slots.col1Items : ["Primary Characteristics", "Standard Practice"];
3465
+ slots.col2Items = Array.isArray(slots.col2Items) && slots.col2Items.length > 0 ? slots.col2Items : Array.isArray(slide.col2Items) ? slide.col2Items : ["Alternative Paradigm", "Trade-offs & Edge Cases"];
3466
+ break;
3467
+ }
3468
+ case "three-cards-grid": {
3469
+ if (!Array.isArray(slots.cards) || slots.cards.length === 0) {
3470
+ if (Array.isArray(slide.cards) && slide.cards.length > 0) {
3471
+ slots.cards = slide.cards;
3472
+ } else if (rawContent) {
3473
+ const cardTokens = rawContent.split(/Card\s*\d+:\s*|(?:\r?\n){2,}|\.\s+/i).map((c) => c.trim()).filter(Boolean);
3474
+ if (cardTokens.length >= 2) {
3475
+ slots.cards = cardTokens.slice(0, 3).map((txt, idx) => {
3476
+ const colonIdx = txt.indexOf(":");
3477
+ const cardTitle = colonIdx > 0 && colonIdx < 30 ? txt.slice(0, colonIdx).trim() : `Pillar ${idx + 1}`;
3478
+ const cardDesc = colonIdx > 0 && colonIdx < 30 ? txt.slice(colonIdx + 1).trim() : txt;
3479
+ return { badge: `0${idx + 1}`, title: cardTitle, desc: cardDesc };
3480
+ });
3481
+ } else {
3482
+ const lines = rawContent.split(/\n|\.\s+/).map((l) => l.replace(/^(?:[-*•]|\d+\.)\s*/, "").trim()).filter((l) => l.length > 3);
3483
+ slots.cards = lines.slice(0, 3).map((line, idx) => ({
3484
+ badge: `0${idx + 1}`,
3485
+ title: line.split(":")[0]?.slice(0, 30) || `Pillar 0${idx + 1}`,
3486
+ desc: line
3487
+ }));
3488
+ }
3489
+ }
3490
+ }
3491
+ if (!Array.isArray(slots.cards) || slots.cards.length === 0) {
3492
+ slots.cards = [
3493
+ { badge: "01", title: "Foundation", desc: rawContent || "Core fundamentals and design principles" },
3494
+ { badge: "02", title: "Architecture", desc: "Component relationships and data flow patterns" },
3495
+ { badge: "03", title: "Practice", desc: "Real-world application and hands-on synthesis" }
3496
+ ];
3497
+ }
3498
+ break;
3499
+ }
3500
+ case "timeline-steps": {
3501
+ if (!Array.isArray(slots.steps) || slots.steps.length === 0) {
3502
+ if (Array.isArray(slide.steps) && slide.steps.length > 0) {
3503
+ slots.steps = slide.steps;
3504
+ } else if (rawContent) {
3505
+ const stepMatches = rawContent.split(/(?:\d+\.|\bStep\s*\d+:)\s*/i).map((s) => s.trim()).filter(Boolean);
3506
+ if (stepMatches.length > 0) {
3507
+ slots.steps = stepMatches.slice(0, 5).map((txt, idx) => {
3508
+ const periodIdx = txt.indexOf(".");
3509
+ const stepTitle = periodIdx > 0 && periodIdx < 35 ? txt.slice(0, periodIdx).trim() : txt.slice(0, 30);
3510
+ const stepDesc = periodIdx > 0 && periodIdx < 35 ? txt.slice(periodIdx + 1).trim() : txt;
3511
+ return { stepNum: idx + 1, title: stepTitle || `Step ${idx + 1}`, desc: stepDesc || stepTitle };
3512
+ });
3513
+ }
3514
+ }
3515
+ }
3516
+ if (!Array.isArray(slots.steps) || slots.steps.length === 0) {
3517
+ slots.steps = [
3518
+ { stepNum: 1, title: "Analyze", desc: "Deconstruct requirements and constraints" },
3519
+ { stepNum: 2, title: "Implement", desc: rawContent || "Build component logic and structure" },
3520
+ { stepNum: 3, title: "Verify", desc: "Test behavior and validate outcomes" }
3521
+ ];
3522
+ }
3523
+ break;
3524
+ }
3525
+ case "metric-callout": {
3526
+ slots.desc = slots.desc || slide.desc || rawContent || "";
3527
+ slots.metricLabel = slots.metricLabel || slide.metricLabel || "KEY METRIC";
3528
+ if (!slots.metric) {
3529
+ const numMatch = (slots.desc || slots.title).match(/(\d+(?:\.\d+)?%?|O\([^)]+\)|\d+x|\b[A-Z0-9_-]{2,}\b)/i);
3530
+ slots.metric = numMatch ? numMatch[1] : "100%";
3531
+ }
3532
+ break;
3533
+ }
3534
+ case "checkpoint-quiz": {
3535
+ slots.question = slots.question || slide.question || slots.title || "Check Your Understanding";
3536
+ if (!Array.isArray(slots.options) || slots.options.length === 0) {
3537
+ if (Array.isArray(slide.options) && slide.options.length > 0) {
3538
+ slots.options = slide.options;
3539
+ } else if (rawContent) {
3540
+ const items = rawContent.split(/(?:\d+\.|\b[A-D]\))\s*/i).map((l) => l.trim()).filter((l) => l.length > 3);
3541
+ if (items.length >= 2) {
3542
+ slots.options = items.slice(0, 4).map((text, idx) => ({
3543
+ label: String.fromCharCode(65 + idx),
3544
+ text
3545
+ }));
3546
+ }
3547
+ }
3548
+ }
3549
+ if (!Array.isArray(slots.options) || slots.options.length === 0) {
3550
+ slots.options = [
3551
+ { label: "A", text: "Option A (Core Concept)" },
3552
+ { label: "B", text: "Option B (Alternative Approach)" },
3553
+ { label: "C", text: "Option C (Edge Case)" }
3554
+ ];
3555
+ }
3556
+ slots.explanation = slots.explanation || slide.explanation || "";
3557
+ break;
3558
+ }
3559
+ case "summary-takeaways": {
3560
+ if (!Array.isArray(slots.takeaways) || slots.takeaways.length === 0) {
3561
+ if (Array.isArray(slide.takeaways) && slide.takeaways.length > 0) {
3562
+ slots.takeaways = slide.takeaways;
3563
+ } else if (rawContent) {
3564
+ const lines = rawContent.split(/\n|\.\s+/).map((l) => l.replace(/^(?:[-*•]|\d+\.)\s*/, "").trim()).filter((l) => l.length > 3);
3565
+ slots.takeaways = lines.length > 0 ? lines.slice(0, 4) : [rawContent];
3566
+ }
3567
+ }
3568
+ if (!Array.isArray(slots.takeaways) || slots.takeaways.length === 0) {
3569
+ slots.takeaways = [
3570
+ slots.title || "Key lesson principles mastered",
3571
+ "Applied hands-on practice through guided examples",
3572
+ "Ready for independent challenge and synthesis"
3573
+ ];
3574
+ }
3575
+ slots.nextStep = slots.nextStep || slide.nextStep || "Next: Practical Hands-On Lab";
3576
+ break;
3577
+ }
3578
+ }
3579
+ if (rawContent && !slots.rawContent) {
3580
+ slots.rawContent = rawContent;
3581
+ }
3582
+ return slots;
3583
+ }
3584
+
3431
3585
  // src/html-engine/compiler.ts
3432
3586
  function compileHtmlDeck(deckData, options) {
3433
3587
  const theme = options?.themeOverride || resolveHtmlTheme(deckData.theme);
@@ -3435,7 +3589,8 @@ function compileHtmlDeck(deckData, options) {
3435
3589
  const slideCount = Math.max(slides.length, 1);
3436
3590
  const renderedSlides = slides.map((slide, index) => {
3437
3591
  const preset = HTML_SLIDE_PRESETS[slide.layoutId] || HTML_SLIDE_PRESETS["hero-cover"];
3438
- const innerHtml = preset.template(slide.slots || {}, theme);
3592
+ const normalizedSlots = normalizeSlideSlots(slide);
3593
+ const innerHtml = preset.template(normalizedSlots, theme);
3439
3594
  const notesAttr = slide.notes ? ` data-notes="${encodeURIComponent(slide.notes)}"` : "";
3440
3595
  const customClass = slide.customClass ? ` ${slide.customClass}` : "";
3441
3596
  return `
@@ -3451,7 +3606,8 @@ function compileHtmlDeck(deckData, options) {
3451
3606
  theme,
3452
3607
  slidesHtml,
3453
3608
  slideCount,
3454
- customHeadTags: options?.customHeadTags
3609
+ customHeadTags: options?.customHeadTags,
3610
+ deckId: deckData?.id || options?.deckId
3455
3611
  });
3456
3612
  return {
3457
3613
  html: fullHtml,
@@ -3461,6 +3617,6 @@ function compileHtmlDeck(deckData, options) {
3461
3617
  };
3462
3618
  }
3463
3619
 
3464
- export { HTML_SLIDE_PRESETS, HTML_THEMES, SLIDE_LAYOUT_PRESETS, compileHtmlDeck, generateHtmlShell, getSlideLayoutPresetById, getSlideLayoutPresetsByCategory, removeSlideElementFromMarkdown, resolveHtmlTheme, serializeLayoutToComment, splitMarkdownSlides, updateSlideLayoutInMarkdown, updateSlideTextInMarkdown };
3465
- //# sourceMappingURL=chunk-A7T5Y2G4.js.map
3466
- //# sourceMappingURL=chunk-A7T5Y2G4.js.map
3620
+ export { HTML_SLIDE_PRESETS, HTML_THEMES, SLIDE_LAYOUT_PRESETS, compileHtmlDeck, generateHtmlShell, getSlideLayoutPresetById, getSlideLayoutPresetsByCategory, normalizeSlideSlots, removeSlideElementFromMarkdown, resolveHtmlTheme, serializeLayoutToComment, splitMarkdownSlides, updateSlideLayoutInMarkdown, updateSlideTextInMarkdown };
3621
+ //# sourceMappingURL=chunk-7QHDWDSB.js.map
3622
+ //# sourceMappingURL=chunk-7QHDWDSB.js.map