CtrlK
BlogDocsLog inGet started
Tessl Logo

metis-strategy/metis-html-slides

Generate interactive HTML presentations with sidebar navigation, scrollable sections, and 40+ typography-driven components. Supports Metis branding with auto-embedded logo and client brand extraction from PPTX templates. Output is a self-contained HTML file viewable in any browser.

68

Quality

85%

Does it follow best practices?

Impact

No eval scenarios have been run

SecuritybySnyk

Passed

No known issues

Overview
Quality
Evals
Security
Files

base.htmltemplates/

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>{{TITLE}}</title>
  <style>
{{THEME_CSS}}
{{COMPONENTS_CSS}}
  </style>
</head>
<body>
  <div class="presentation">

    <!-- ── Sidebar ── -->
    <nav class="sidebar" id="sidebar">
      <div class="sidebar-header">
        {{LOGO}}
        <div class="sidebar-deck-title">{{SUBTITLE}}</div>
      </div>
      <div class="sidebar-nav" id="sidebarNav">
        {{SIDEBAR_NAV}}
      </div>
      <div class="sidebar-footer">
        <div class="progress-bar"><div class="progress-fill" id="progressFill"></div></div>
        <div class="progress-label" id="progressLabel">PROGRESS  1 / 1</div>
      </div>
    </nav>

    <!-- ── Content Area ── -->
    <main class="content-area">
      <header class="section-header">
        <div class="section-marker" id="sectionMarker">
          <span class="section-symbol">&sect;</span> <span id="sectionText"></span>
        </div>
        <div class="slide-pagination">
          <span class="key-hint">&larr; &rarr; keys</span>
          <button class="nav-arrow" id="navPrev" aria-label="Previous slide">&lsaquo;</button>
          <span class="page-count" id="pageCount">1 / 1</span>
          <button class="nav-arrow" id="navNext" aria-label="Next slide">&rsaquo;</button>
        </div>
      </header>

      <div class="deck" id="deck">
{{SLIDES}}
      </div>
    </main>

  </div>

  <script>
  (function() {
    var deck = document.getElementById('deck');
    var slides = deck.querySelectorAll('.slide');
    var pageCount = document.getElementById('pageCount');
    var progressFill = document.getElementById('progressFill');
    var progressLabel = document.getElementById('progressLabel');
    var sectionText = document.getElementById('sectionText');
    var sidebarNav = document.getElementById('sidebarNav');
    var current = 0;

    // ── Build section map from slide data attributes ──
    var sections = [];
    var sectionMap = {};  // sectionNum -> { title, slides: [indices] }
    slides.forEach(function(slide, i) {
      var secNum = slide.dataset.sectionNum || '';
      var secTitle = slide.dataset.sectionTitle || '';
      var subLabel = slide.dataset.subLabel || '';
      if (secNum && !sectionMap[secNum]) {
        sectionMap[secNum] = { title: secTitle, num: secNum, slides: [], subLabels: [] };
        sections.push(sectionMap[secNum]);
      }
      if (secNum) {
        sectionMap[secNum].slides.push(i);
        sectionMap[secNum].subLabels.push(subLabel);
      }
    });

    // ── Show slide ──
    function showSlide(n) {
      if (n < 0 || n >= slides.length) return;
      slides[current].classList.remove('active');
      current = n;
      slides[current].classList.add('active');

      // Reset scroll position
      slides[current].scrollTop = 0;

      // Update page counter
      pageCount.textContent = (current + 1) + ' / ' + slides.length;

      // Update progress
      var pct = slides.length > 1 ? ((current) / (slides.length - 1)) * 100 : 100;
      progressFill.style.width = pct + '%';
      progressLabel.textContent = 'PROGRESS  ' + (current + 1) + ' / ' + slides.length;

      // Update section marker
      var secNum = slides[current].dataset.sectionNum || '';
      var secTitle = slides[current].dataset.sectionTitle || '';
      if (secNum) {
        sectionText.textContent = secNum + '  \u00B7  ' + secTitle;
      } else {
        sectionText.textContent = '';
      }

      // Update sidebar active states
      updateSidebar();

      // Trigger count-up animations
      animateCountUps();
    }

    function next() { showSlide(current + 1); }
    function prev() { showSlide(current - 1); }

    // ── Update sidebar highlighting ──
    function updateSidebar() {
      var curSecNum = slides[current].dataset.sectionNum || '';
      var curSubLabel = slides[current].dataset.subLabel || '';

      // Update section active states
      var navSections = sidebarNav.querySelectorAll('.nav-section');
      navSections.forEach(function(sec) {
        var secNum = sec.dataset.section;
        sec.classList.remove('active');
        if (secNum === curSecNum) {
          sec.classList.add('active');
        }
        // Mark visited
        if (sectionMap[secNum]) {
          var firstSlide = sectionMap[secNum].slides[0];
          if (firstSlide < current) {
            sec.classList.add('visited');
          }
        }
      });

      // Update sub-item active states
      var subItems = sidebarNav.querySelectorAll('.nav-sub-item');
      subItems.forEach(function(sub) {
        sub.classList.remove('active');
        if (sub.dataset.slideIndex == current) {
          sub.classList.add('active');
        }
      });

      // Scroll active item into view
      var activeSec = sidebarNav.querySelector('.nav-section.active');
      if (activeSec) {
        activeSec.scrollIntoView({ block: 'nearest', behavior: 'smooth' });
      }
    }

    // ── Sidebar click handlers ──
    sidebarNav.addEventListener('click', function(e) {
      // Section header click
      var header = e.target.closest('.nav-section-header');
      if (header) {
        var slideIdx = parseInt(header.dataset.slide, 10);
        if (!isNaN(slideIdx)) showSlide(slideIdx);
        return;
      }
      // Sub-item click
      var subItem = e.target.closest('.nav-sub-item');
      if (subItem) {
        var slideIdx = parseInt(subItem.dataset.slideIndex, 10);
        if (!isNaN(slideIdx)) showSlide(slideIdx);
        return;
      }
    });

    // ── Pagination button clicks ──
    document.getElementById('navPrev').addEventListener('click', function(e) {
      e.stopPropagation();
      prev();
    });
    document.getElementById('navNext').addEventListener('click', function(e) {
      e.stopPropagation();
      next();
    });

    // ── Keyboard navigation ──
    document.addEventListener('keydown', function(e) {
      switch(e.key) {
        case 'ArrowRight': case 'ArrowDown': case ' ': case 'PageDown':
          e.preventDefault(); next(); break;
        case 'ArrowLeft': case 'ArrowUp': case 'PageUp':
          e.preventDefault(); prev(); break;
        case 'Home': e.preventDefault(); showSlide(0); break;
        case 'End': e.preventDefault(); showSlide(slides.length - 1); break;
        case 'f': case 'F':
          if (!e.ctrlKey && !e.metaKey) {
            e.preventDefault();
            if (document.fullscreenElement) document.exitFullscreen();
            else document.documentElement.requestFullscreen();
          }
          break;
        case 'n': case 'N':
          if (!e.ctrlKey && !e.metaKey) {
            document.querySelectorAll('.notes').forEach(function(el) {
              el.style.display = el.style.display === 'block' ? 'none' : 'block';
            });
          }
          break;
      }
    });

    // ── Touch/swipe support on deck ──
    var touchStartX = 0;
    deck.addEventListener('touchstart', function(e) { touchStartX = e.touches[0].clientX; });
    deck.addEventListener('touchend', function(e) {
      var dx = e.changedTouches[0].clientX - touchStartX;
      if (Math.abs(dx) > 50) { dx < 0 ? next() : prev(); }
    });

    // ── Click navigation on deck (right 70% = next, left 30% = prev) ──
    deck.addEventListener('click', function(e) {
      if (e.target.closest('a, button, input, .tab-btn, .clickable-reveal, .accordion-trigger, .nav-arrow')) return;
      var deckRect = deck.getBoundingClientRect();
      var relX = e.clientX - deckRect.left;
      (relX < deckRect.width * 0.3) ? prev() : next();
    });

    // ── Clickable reveal ──
    document.querySelectorAll('.clickable-reveal').forEach(function(el) {
      el.addEventListener('click', function(e) {
        if (e.target.closest('a, button, input, .tab-btn')) return;
        el.classList.toggle('open');
        e.stopPropagation();
      });
    });

    // ── Tabs ──
    document.querySelectorAll('.tab-group').forEach(function(group) {
      var btns = group.querySelectorAll('.tab-btn');
      var panels = group.querySelectorAll('.tab-panel');
      btns.forEach(function(btn, i) {
        btn.addEventListener('click', function(e) {
          e.stopPropagation();
          btns.forEach(function(b) { b.classList.remove('active'); });
          panels.forEach(function(p) { p.classList.remove('active'); });
          btn.classList.add('active');
          if (panels[i]) panels[i].classList.add('active');
        });
      });
    });

    // ── Accordion ──
    document.querySelectorAll('.accordion-trigger').forEach(function(trigger) {
      trigger.addEventListener('click', function(e) {
        e.stopPropagation();
        var item = trigger.closest('.accordion-item');
        item.classList.toggle('open');
      });
    });

    // ── Count-up animation ──
    function animateCountUps() {
      document.querySelectorAll('.slide.active .count-up').forEach(function(el) {
        if (el.dataset.counted) return;
        el.dataset.counted = 'true';
        var target = parseFloat(el.textContent.replace(/[^0-9.]/g, ''));
        var prefix = el.textContent.match(/^[^0-9]*/)[0];
        var suffix = el.textContent.match(/[^0-9]*$/)[0];
        var duration = 1200;
        var start = performance.now();
        function step(now) {
          var progress = Math.min((now - start) / duration, 1);
          var eased = 1 - Math.pow(1 - progress, 3);
          var val = Math.round(target * eased);
          el.textContent = prefix + val.toLocaleString() + suffix;
          if (progress < 1) requestAnimationFrame(step);
        }
        requestAnimationFrame(step);
      });
    }

    // Reset count-up on slide change
    var _showSlide = showSlide;
    showSlide = function(n) {
      slides[current].querySelectorAll('.count-up').forEach(function(el) {
        delete el.dataset.counted;
      });
      _showSlide(n);
    };

    // ── Initialize ──
    if (slides.length > 0) {
      slides[0].classList.add('active');
      pageCount.textContent = '1 / ' + slides.length;
      var pct0 = slides.length > 1 ? 0 : 100;
      progressFill.style.width = pct0 + '%';
      progressLabel.textContent = 'PROGRESS  1 / ' + slides.length;

      var sec0Num = slides[0].dataset.sectionNum || '';
      var sec0Title = slides[0].dataset.sectionTitle || '';
      if (sec0Num) {
        sectionText.textContent = sec0Num + '  \u00B7  ' + sec0Title;
      }

      updateSidebar();
      animateCountUps();
    }
  })();
  </script>
</body>
</html>

templates

SKILL.md

tile.json