spb/spbgit Public MIT
SPB Git — the platform hosting itself
JavaScript 73.9%
CSS 11.7%
Nunjucks 11.6%
Shell 2.7%
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