The SSH Access page was boxed to max-width 860px and centered, unlike the Overview and System admin pages (.admin-page) which span the full content width. Drop the cap and match .admin-page padding so /admin/tools/ssh-access looks like the rest of the Admin area. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> Signed-off-by: librelad <librelad@digitalangels.vip>
67 lines
3.0 KiB
JavaScript
67 lines
3.0 KiB
JavaScript
// LibrePortal site — interactions (mobile drawer, copy, scrollspy, reveal, app filter)
|
|
(() => {
|
|
// mobile drawer (same behaviour as the dashboard)
|
|
const menuToggle = document.getElementById('mobile-menu-toggle');
|
|
const drawer = document.getElementById('mobile-drawer');
|
|
if (menuToggle && drawer) {
|
|
menuToggle.addEventListener('click', () => {
|
|
const open = drawer.classList.toggle('mobile-open');
|
|
document.body.style.overflow = open ? 'hidden' : '';
|
|
});
|
|
drawer.querySelectorAll('a').forEach((a) =>
|
|
a.addEventListener('click', () => { drawer.classList.remove('mobile-open'); document.body.style.overflow = ''; }));
|
|
}
|
|
|
|
// copy the install command
|
|
const cmdEl = document.querySelector('[data-install-cmd]');
|
|
const CMD = cmdEl ? cmdEl.getAttribute('data-install-cmd') : '';
|
|
document.querySelectorAll('.copy').forEach((btn) => {
|
|
btn.addEventListener('click', async () => {
|
|
try { await navigator.clipboard.writeText(CMD); }
|
|
catch { const t = document.createElement('textarea'); t.value = CMD; document.body.appendChild(t); t.select(); document.execCommand('copy'); t.remove(); }
|
|
btn.classList.add('done');
|
|
const span = btn.querySelector('span'); const prev = span.textContent; span.textContent = 'Copied ✓';
|
|
setTimeout(() => { btn.classList.remove('done'); span.textContent = prev; }, 1700);
|
|
});
|
|
});
|
|
|
|
// scrollspy → light up the active topbar pill
|
|
const spies = [...document.querySelectorAll('.nav-item[data-spy]')];
|
|
if (spies.length) {
|
|
const spyIO = new IntersectionObserver((entries) => {
|
|
entries.forEach((e) => {
|
|
if (e.isIntersecting) spies.forEach((s) => s.classList.toggle('nav-active', s.dataset.spy === e.target.id));
|
|
});
|
|
}, { rootMargin: '-45% 0px -50% 0px', threshold: 0 });
|
|
spies.map((s) => document.getElementById(s.dataset.spy)).filter(Boolean).forEach((s) => spyIO.observe(s));
|
|
}
|
|
|
|
// reveal on scroll
|
|
const io = new IntersectionObserver((entries) => {
|
|
entries.forEach((e) => { if (e.isIntersecting) { e.target.classList.add('in'); io.unobserve(e.target); } });
|
|
}, { threshold: 0.16, rootMargin: '0px 0px -8% 0px' });
|
|
document.querySelectorAll('.reveal, .stagger').forEach((el) => io.observe(el));
|
|
|
|
// app category filter
|
|
const filters = document.querySelector('.app-filters');
|
|
if (filters) {
|
|
const cards = [...document.querySelectorAll('.app-card')];
|
|
const countEl = document.querySelector('.app-count');
|
|
const update = (cat) => {
|
|
let shown = 0;
|
|
cards.forEach((c) => {
|
|
const match = cat === 'all' || (c.dataset.cats || '').split(' ').includes(cat);
|
|
c.classList.toggle('hidden', !match);
|
|
if (match) shown++;
|
|
});
|
|
if (countEl) countEl.textContent = `${shown} app${shown === 1 ? '' : 's'}`;
|
|
};
|
|
filters.addEventListener('click', (e) => {
|
|
const chip = e.target.closest('.chip');
|
|
if (!chip) return;
|
|
filters.querySelectorAll('.chip').forEach((c) => c.classList.toggle('active', c === chip));
|
|
update(chip.dataset.cat);
|
|
});
|
|
}
|
|
})();
|