SPB Git

spb/spbgit Public MIT

SPB Git — the platform hosting itself

JavaScript 73.9% CSS 11.7% Nunjucks 11.6% Shell 2.7%
7.3 KB · 177 lines javascript
Raw Blame History
1/**2 * ─────────────────────────────────────────────3 *  SPB Git — Personal Git Platform4 * ─────────────────────────────────────────────5 *  Author  : Simon-Pierre Boucher6 *  Contact : contact@spboucher.ai7 *  File    : src/web/assets/js/app.js8 *  Purpose : Vanilla JS layer — theme, copy, filters, anchors, mermaid9 *  License : MIT © Simon-Pierre Boucher10 * ─────────────────────────────────────────────11 */1213(function () {14  'use strict';1516  /* ── Theme toggle ─────────────────────────── */17  var toggle = document.getElementById('theme-toggle');18  if (toggle) {19    toggle.addEventListener('click', function () {20      var html = document.documentElement;21      var next = html.getAttribute('data-theme') === 'dark' ? 'light' : 'dark';22      html.setAttribute('data-theme', next);23      try { localStorage.setItem('spbgit-theme', next); } catch (e) { /* ignore */ }24    });25  }2627  /* ── "/" focuses the global search ────────── */28  document.addEventListener('keydown', function (event) {29    if (event.key !== '/' || event.metaKey || event.ctrlKey || event.altKey) return;30    var target = event.target;31    if (target && (target.tagName === 'INPUT' || target.tagName === 'TEXTAREA' || target.isContentEditable)) return;32    var input = document.getElementById('global-search');33    if (input) {34      event.preventDefault();35      input.focus();36      input.select();37    }38  });3940  /* ── Copy buttons ─────────────────────────── */41  document.addEventListener('click', function (event) {42    var btn = event.target.closest('.copy-btn');43    if (!btn) return;44    var text = btn.getAttribute('data-copy');45    if (!text) return;46    navigator.clipboard.writeText(text).then(function () {47      btn.classList.add('copied');48      var prev = btn.textContent;49      btn.textContent = '✓';50      setTimeout(function () {51        btn.classList.remove('copied');52        btn.textContent = prev;53      }, 1200);54    });55  });5657  /* ── Home: filter + sort repo list ────────── */58  var list = document.getElementById('repo-list');59  if (list) {60    var filterInput = document.getElementById('repo-filter');61    var sortSelect = document.getElementById('repo-sort');62    var langSelect = document.getElementById('repo-lang');63    var topicSelect = document.getElementById('repo-topic');64    var emptyNote = document.getElementById('repo-list-empty');6566    var apply = function () {67      var q = (filterInput.value || '').toLowerCase();68      var lang = langSelect.value;69      var topic = topicSelect.value;70      var cards = Array.prototype.slice.call(list.querySelectorAll('.repo-card'));71      var visible = 0;72      cards.forEach(function (card) {73        var name = card.getAttribute('data-name') || '';74        var desc = (card.getAttribute('data-description') || '').toLowerCase();75        var cardLang = card.getAttribute('data-language') || '';76        var topics = (card.getAttribute('data-topics') || '').split(',');77        var show =78          (!q || name.toLowerCase().indexOf(q) !== -1 || desc.indexOf(q) !== -1) &&79          (!lang || cardLang === lang) &&80          (!topic || topics.indexOf(topic) !== -1);81        card.hidden = !show;82        if (show) visible += 1;83      });84      var key = sortSelect.value;85      var attr = key === 'name' ? 'data-name' : key === 'created' ? 'data-created' : 'data-pushed';86      cards87        .sort(function (a, b) {88          var av = a.getAttribute(attr) || '';89          var bv = b.getAttribute(attr) || '';90          return key === 'name' ? av.localeCompare(bv) : bv.localeCompare(av);91        })92        .forEach(function (card) { list.appendChild(card); });93      if (emptyNote) emptyNote.hidden = visible > 0;94    };95    [filterInput, sortSelect, langSelect, topicSelect].forEach(function (el) {96      if (!el) return;97      el.addEventListener('input', apply);98      el.addEventListener('change', apply);99    });100  }101102  /* ── Ref selector navigation ──────────────── */103  var refSelect = document.getElementById('ref-select');104  if (refSelect) {105    refSelect.addEventListener('change', function () {106      var repo = refSelect.getAttribute('data-repo');107      var kind = refSelect.getAttribute('data-kind') || 'tree';108      var path = refSelect.getAttribute('data-path') || '';109      var ref = refSelect.value;110      var url = '/' + repo + '/' + kind + '/' + ref + (path ? '/' + path : '');111      location.href = url;112    });113  }114115  /* ── Line range highlighting (#L10-L20) ───── */116  var codeView = document.getElementById('code-view');117  if (codeView) {118    var applyRange = function () {119      codeView.querySelectorAll('.line.range-hl').forEach(function (el) {120        el.classList.remove('range-hl');121      });122      var match = /^#L(\d+)(?:-L(\d+))?$/.exec(location.hash);123      if (!match) return;124      var start = parseInt(match[1], 10);125      var end = match[2] ? parseInt(match[2], 10) : start;126      if (end < start) { var t = start; start = end; end = t; }127      for (var i = start; i <= end; i += 1) {128        var line = document.getElementById('L' + i);129        if (line) line.classList.add('range-hl');130      }131      var first = document.getElementById('L' + start);132      if (first && match[2]) first.scrollIntoView({ block: 'center' });133    };134    applyRange();135    window.addEventListener('hashchange', applyRange);136    // Shift-click a line number to extend the selection into a range.137    codeView.addEventListener('click', function (event) {138      var anchor = event.target.closest('a.ln');139      if (!anchor || !event.shiftKey) return;140      event.preventDefault();141      var current = /^#L(\d+)/.exec(location.hash);142      var clicked = /^#?L?(\d+)/.exec(anchor.getAttribute('href').slice(1));143      if (current && clicked) {144        history.replaceState(null, '', '#L' + current[1] + '-L' + clicked[1]);145        applyRange();146      }147    });148  }149150  /* ── Mermaid: lazy-load only when present ─── */151  var mermaidBlocks = document.querySelectorAll('.mermaid-block');152  if (mermaidBlocks.length > 0) {153    var script = document.createElement('script');154    script.src = '/assets/vendor/mermaid.min.js';155    script.onload = function () {156      if (!window.mermaid) return;157      var isDark = document.documentElement.getAttribute('data-theme') === 'dark';158      window.mermaid.initialize({ startOnLoad: false, theme: isDark ? 'dark' : 'default', securityLevel: 'strict' });159      mermaidBlocks.forEach(function (block, index) {160        var src = block.querySelector('.mermaid-src');161        var target = block.querySelector('.mermaid-target');162        if (!src || !target) return;163        window.mermaid164          .render('mermaid-svg-' + index, src.textContent)165          .then(function (result) {166            target.innerHTML = result.svg; // mermaid output, securityLevel strict167            block.setAttribute('data-rendered', '1');168          })169          .catch(function () {170            block.setAttribute('data-rendered', '0');171          });172      });173    };174    document.body.appendChild(script);175  }176})();177