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.

72

Quality

90%

Does it follow best practices?

Run evals on this skill

Adds up to 20 points to the overall score

View guide

SecuritybySnyk

Passed

No findings from the security scan

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