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 /* ── Copy code blocks (fences + blob view) ── */58 document.addEventListener('click', function (event) {59 var btn = event.target.closest('[data-code-copy]');60 if (!btn) return;61 var scope = btn.getAttribute('data-code-target');62 var container = scope ? document.querySelector(scope) : btn.closest('.code-block');63 var pre = container ? container.querySelector('pre') : null;64 if (!pre) return;65 // Strip line-number anchors from the copied text.66 var clone = pre.cloneNode(true);67 clone.querySelectorAll('a.ln').forEach(function (a) { a.remove(); });68 navigator.clipboard.writeText(clone.textContent).then(function () {69 btn.classList.add('copied');70 var label = btn.querySelector('.code-copy-label');71 var prev = label ? label.textContent : null;72 if (label) label.textContent = 'Copied!';73 setTimeout(function () {74 btn.classList.remove('copied');75 if (label && prev) label.textContent = prev;76 }, 1400);77 });78 });7980 /* ── Home: filter + sort repo list ────────── */81 var list = document.getElementById('repo-list');82 if (list) {83 var filterInput = document.getElementById('repo-filter');84 var sortSelect = document.getElementById('repo-sort');85 var langSelect = document.getElementById('repo-lang');86 var topicSelect = document.getElementById('repo-topic');87 var emptyNote = document.getElementById('repo-list-empty');8889 var apply = function () {90 var q = (filterInput.value || '').toLowerCase();91 var lang = langSelect.value;92 var topic = topicSelect.value;93 var cards = Array.prototype.slice.call(list.querySelectorAll('.repo-card'));94 var visible = 0;95 cards.forEach(function (card) {96 var name = card.getAttribute('data-name') || '';97 var desc = (card.getAttribute('data-description') || '').toLowerCase();98 var cardLang = card.getAttribute('data-language') || '';99 var topics = (card.getAttribute('data-topics') || '').split(',');100 var show =101 (!q || name.toLowerCase().indexOf(q) !== -1 || desc.indexOf(q) !== -1) &&102 (!lang || cardLang === lang) &&103 (!topic || topics.indexOf(topic) !== -1);104 card.hidden = !show;105 if (show) visible += 1;106 });107 var key = sortSelect.value;108 var attr = key === 'name' ? 'data-name' : key === 'created' ? 'data-created' : 'data-pushed';109 cards110 .sort(function (a, b) {111 var av = a.getAttribute(attr) || '';112 var bv = b.getAttribute(attr) || '';113 return key === 'name' ? av.localeCompare(bv) : bv.localeCompare(av);114 })115 .forEach(function (card) { list.appendChild(card); });116 if (emptyNote) emptyNote.hidden = visible > 0;117 };118 [filterInput, sortSelect, langSelect, topicSelect].forEach(function (el) {119 if (!el) return;120 el.addEventListener('input', apply);121 el.addEventListener('change', apply);122 });123 }124125 /* ── Ref selector navigation ──────────────── */126 var refSelect = document.getElementById('ref-select');127 if (refSelect) {128 refSelect.addEventListener('change', function () {129 var repo = refSelect.getAttribute('data-repo');130 var kind = refSelect.getAttribute('data-kind') || 'tree';131 var path = refSelect.getAttribute('data-path') || '';132 var ref = refSelect.value;133 var url = '/' + repo + '/' + kind + '/' + ref + (path ? '/' + path : '');134 location.href = url;135 });136 }137138 /* ── Line range highlighting (#L10-L20) ───── */139 var codeView = document.getElementById('code-view');140 if (codeView) {141 var applyRange = function () {142 codeView.querySelectorAll('.line.range-hl').forEach(function (el) {143 el.classList.remove('range-hl');144 });145 var match = /^#L(\d+)(?:-L(\d+))?$/.exec(location.hash);146 if (!match) return;147 var start = parseInt(match[1], 10);148 var end = match[2] ? parseInt(match[2], 10) : start;149 if (end < start) { var t = start; start = end; end = t; }150 for (var i = start; i <= end; i += 1) {151 var line = document.getElementById('L' + i);152 if (line) line.classList.add('range-hl');153 }154 var first = document.getElementById('L' + start);155 if (first && match[2]) first.scrollIntoView({ block: 'center' });156 };157 applyRange();158 window.addEventListener('hashchange', applyRange);159 // Shift-click a line number to extend the selection into a range.160 codeView.addEventListener('click', function (event) {161 var anchor = event.target.closest('a.ln');162 if (!anchor || !event.shiftKey) return;163 event.preventDefault();164 var current = /^#L(\d+)/.exec(location.hash);165 var clicked = /^#?L?(\d+)/.exec(anchor.getAttribute('href').slice(1));166 if (current && clicked) {167 history.replaceState(null, '', '#L' + current[1] + '-L' + clicked[1]);168 applyRange();169 }170 });171 }172173 /* ── Mermaid: lazy-load only when present ─── */174 var mermaidBlocks = document.querySelectorAll('.mermaid-block');175 if (mermaidBlocks.length > 0) {176 var script = document.createElement('script');177 script.src = '/assets/vendor/mermaid.min.js';178 script.onload = function () {179 if (!window.mermaid) return;180 var isDark = document.documentElement.getAttribute('data-theme') === 'dark';181 window.mermaid.initialize({ startOnLoad: false, theme: isDark ? 'dark' : 'default', securityLevel: 'strict' });182 mermaidBlocks.forEach(function (block, index) {183 var src = block.querySelector('.mermaid-src');184 var target = block.querySelector('.mermaid-target');185 if (!src || !target) return;186 window.mermaid187 .render('mermaid-svg-' + index, src.textContent)188 .then(function (result) {189 target.innerHTML = result.svg; // mermaid output, securityLevel strict190 block.setAttribute('data-rendered', '1');191 })192 .catch(function () {193 block.setAttribute('data-rendered', '0');194 });195 });196 };197 document.body.appendChild(script);198 }199})();200