/** * ───────────────────────────────────────────── * SPB Git — Personal Git Platform * ───────────────────────────────────────────── * Author : Simon-Pierre Boucher * Contact : contact@spboucher.ai * File : src/web/assets/js/app.js * Purpose : Vanilla JS layer — theme, copy, filters, anchors, mermaid * License : MIT © Simon-Pierre Boucher * ───────────────────────────────────────────── */ (function () { 'use strict'; /* ── Theme toggle ─────────────────────────── */ var toggle = document.getElementById('theme-toggle'); if (toggle) { toggle.addEventListener('click', function () { var html = document.documentElement; var next = html.getAttribute('data-theme') === 'dark' ? 'light' : 'dark'; html.setAttribute('data-theme', next); try { localStorage.setItem('spbgit-theme', next); } catch (e) { /* ignore */ } }); } /* ── "/" focuses the global search ────────── */ document.addEventListener('keydown', function (event) { if (event.key !== '/' || event.metaKey || event.ctrlKey || event.altKey) return; var target = event.target; if (target && (target.tagName === 'INPUT' || target.tagName === 'TEXTAREA' || target.isContentEditable)) return; var input = document.getElementById('global-search'); if (input) { event.preventDefault(); input.focus(); input.select(); } }); /* ── Copy buttons ─────────────────────────── */ document.addEventListener('click', function (event) { var btn = event.target.closest('.copy-btn'); if (!btn) return; var text = btn.getAttribute('data-copy'); if (!text) return; navigator.clipboard.writeText(text).then(function () { btn.classList.add('copied'); var prev = btn.textContent; btn.textContent = '✓'; setTimeout(function () { btn.classList.remove('copied'); btn.textContent = prev; }, 1200); }); }); /* ── Home: filter + sort repo list ────────── */ var list = document.getElementById('repo-list'); if (list) { var filterInput = document.getElementById('repo-filter'); var sortSelect = document.getElementById('repo-sort'); var langSelect = document.getElementById('repo-lang'); var topicSelect = document.getElementById('repo-topic'); var emptyNote = document.getElementById('repo-list-empty'); var apply = function () { var q = (filterInput.value || '').toLowerCase(); var lang = langSelect.value; var topic = topicSelect.value; var cards = Array.prototype.slice.call(list.querySelectorAll('.repo-card')); var visible = 0; cards.forEach(function (card) { var name = card.getAttribute('data-name') || ''; var desc = (card.getAttribute('data-description') || '').toLowerCase(); var cardLang = card.getAttribute('data-language') || ''; var topics = (card.getAttribute('data-topics') || '').split(','); var show = (!q || name.toLowerCase().indexOf(q) !== -1 || desc.indexOf(q) !== -1) && (!lang || cardLang === lang) && (!topic || topics.indexOf(topic) !== -1); card.hidden = !show; if (show) visible += 1; }); var key = sortSelect.value; var attr = key === 'name' ? 'data-name' : key === 'created' ? 'data-created' : 'data-pushed'; cards .sort(function (a, b) { var av = a.getAttribute(attr) || ''; var bv = b.getAttribute(attr) || ''; return key === 'name' ? av.localeCompare(bv) : bv.localeCompare(av); }) .forEach(function (card) { list.appendChild(card); }); if (emptyNote) emptyNote.hidden = visible > 0; }; [filterInput, sortSelect, langSelect, topicSelect].forEach(function (el) { if (!el) return; el.addEventListener('input', apply); el.addEventListener('change', apply); }); } /* ── Ref selector navigation ──────────────── */ var refSelect = document.getElementById('ref-select'); if (refSelect) { refSelect.addEventListener('change', function () { var repo = refSelect.getAttribute('data-repo'); var kind = refSelect.getAttribute('data-kind') || 'tree'; var path = refSelect.getAttribute('data-path') || ''; var ref = refSelect.value; var url = '/' + repo + '/' + kind + '/' + ref + (path ? '/' + path : ''); location.href = url; }); } /* ── Line range highlighting (#L10-L20) ───── */ var codeView = document.getElementById('code-view'); if (codeView) { var applyRange = function () { codeView.querySelectorAll('.line.range-hl').forEach(function (el) { el.classList.remove('range-hl'); }); var match = /^#L(\d+)(?:-L(\d+))?$/.exec(location.hash); if (!match) return; var start = parseInt(match[1], 10); var end = match[2] ? parseInt(match[2], 10) : start; if (end < start) { var t = start; start = end; end = t; } for (var i = start; i <= end; i += 1) { var line = document.getElementById('L' + i); if (line) line.classList.add('range-hl'); } var first = document.getElementById('L' + start); if (first && match[2]) first.scrollIntoView({ block: 'center' }); }; applyRange(); window.addEventListener('hashchange', applyRange); // Shift-click a line number to extend the selection into a range. codeView.addEventListener('click', function (event) { var anchor = event.target.closest('a.ln'); if (!anchor || !event.shiftKey) return; event.preventDefault(); var current = /^#L(\d+)/.exec(location.hash); var clicked = /^#?L?(\d+)/.exec(anchor.getAttribute('href').slice(1)); if (current && clicked) { history.replaceState(null, '', '#L' + current[1] + '-L' + clicked[1]); applyRange(); } }); } /* ── Mermaid: lazy-load only when present ─── */ var mermaidBlocks = document.querySelectorAll('.mermaid-block'); if (mermaidBlocks.length > 0) { var script = document.createElement('script'); script.src = '/assets/vendor/mermaid.min.js'; script.onload = function () { if (!window.mermaid) return; var isDark = document.documentElement.getAttribute('data-theme') === 'dark'; window.mermaid.initialize({ startOnLoad: false, theme: isDark ? 'dark' : 'default', securityLevel: 'strict' }); mermaidBlocks.forEach(function (block, index) { var src = block.querySelector('.mermaid-src'); var target = block.querySelector('.mermaid-target'); if (!src || !target) return; window.mermaid .render('mermaid-svg-' + index, src.textContent) .then(function (result) { target.innerHTML = result.svg; // mermaid output, securityLevel strict block.setAttribute('data-rendered', '1'); }) .catch(function () { block.setAttribute('data-rendered', '0'); }); }); }; document.body.appendChild(script); } })();