impeccable fixes, scanner fixes

This commit is contained in:
Evan Jarrett
2026-04-12 20:36:57 -05:00
parent 25628dad2c
commit 2f30c22f0a
23 changed files with 644 additions and 264 deletions
+48 -66
View File
@@ -130,9 +130,34 @@ function copyToClipboard(text, btn) {
});
}
// Serialize a <table> (thead + tbody) as CSV (RFC 4180 quoting)
function tableToCSV(table) {
const escape = (s) => {
const v = (s == null ? '' : String(s)).replace(/\s+/g, ' ').trim();
return /[",\n\r]/.test(v) ? '"' + v.replace(/"/g, '""') + '"' : v;
};
const rowToCsv = (cells) => Array.from(cells).map((c) => escape(c.textContent)).join(',');
const lines = [];
const headRow = table.querySelector('thead tr');
if (headRow) lines.push(rowToCsv(headRow.querySelectorAll('th,td')));
table.querySelectorAll('tbody tr').forEach((tr) => {
lines.push(rowToCsv(tr.querySelectorAll('td,th')));
});
return lines.join('\n');
}
// Initialize copy buttons with data-cmd attribute and clickable cards with data-href
document.addEventListener('DOMContentLoaded', () => {
document.addEventListener('click', (e) => {
// Handle copy-as-CSV buttons (for vulnerability / SBOM tables)
const csvBtn = e.target.closest('button[data-copy-csv]');
if (csvBtn) {
const section = csvBtn.closest('[data-csv-section]');
const table = section && section.querySelector('table');
if (table) copyToClipboard(tableToCSV(table), csvBtn);
return;
}
// Handle copy buttons
const btn = e.target.closest('button[data-cmd]');
if (btn) {
@@ -453,97 +478,49 @@ document.addEventListener('DOMContentLoaded', () => {
}
});
// Featured carousel - scroll-based with proper wrap-around
// Deferred initialization to avoid blocking main thread during page load
// Featured carousel - relies on DaisyUI's native scroll-snap (.carousel +
// .carousel-item already set scroll-snap-type: x mandatory and snap-start).
// JS only handles next/prev buttons, auto-advance, and wrap-around. The
// browser handles all snapping precision after scrollBy().
function initFeaturedCarousel() {
const carousel = document.getElementById('featured-carousel');
const prevBtn = document.getElementById('carousel-prev');
const nextBtn = document.getElementById('carousel-next');
if (!carousel) return;
const items = Array.from(carousel.querySelectorAll('.carousel-item'));
const items = carousel.querySelectorAll('.carousel-item');
if (items.length === 0) return;
let intervalId = null;
const intervalMs = 5000;
// Cache dimensions to avoid forced reflow on every navigation
let cachedItemWidth = 0;
let cachedContainerWidth = 0;
let cachedScrollWidth = 0;
function updateCachedDimensions() {
const item = items[0];
if (!item) return;
// Batch all geometric reads together to minimize reflow
const style = getComputedStyle(carousel);
const gap = parseFloat(style.gap) || 24;
cachedItemWidth = item.offsetWidth + gap;
cachedContainerWidth = carousel.offsetWidth;
cachedScrollWidth = carousel.scrollWidth;
}
// Initial measurement after layout is stable (double-rAF)
requestAnimationFrame(() => {
requestAnimationFrame(() => {
updateCachedDimensions();
startInterval();
});
});
// Update on resize (debounced to avoid excessive recalculation)
let resizeTimeout;
window.addEventListener('resize', () => {
clearTimeout(resizeTimeout);
resizeTimeout = setTimeout(updateCachedDimensions, 150);
});
function getItemWidth() {
if (!cachedItemWidth) updateCachedDimensions();
return cachedItemWidth;
}
function getVisibleCount() {
if (!cachedContainerWidth || !cachedItemWidth) updateCachedDimensions();
return Math.round(cachedContainerWidth / cachedItemWidth) || 1;
}
function getMaxScroll() {
if (!cachedScrollWidth || !cachedContainerWidth) updateCachedDimensions();
return cachedScrollWidth - cachedContainerWidth;
// Read on demand — at most once per click or per 5s autoplay tick.
function step() {
const first = items[0];
const gap = parseFloat(getComputedStyle(carousel).gap) || 24;
return first.offsetWidth + gap;
}
function advance() {
const itemWidth = getItemWidth();
const maxScroll = getMaxScroll();
const currentScroll = carousel.scrollLeft;
// If we're at or near the end, wrap to start
if (currentScroll >= maxScroll - 10) {
const max = carousel.scrollWidth - carousel.clientWidth;
if (carousel.scrollLeft >= max - 10) {
carousel.scrollTo({ left: 0, behavior: 'smooth' });
} else {
carousel.scrollTo({ left: currentScroll + itemWidth, behavior: 'smooth' });
carousel.scrollBy({ left: step(), behavior: 'smooth' });
}
}
function retreat() {
const itemWidth = getItemWidth();
const maxScroll = getMaxScroll();
const currentScroll = carousel.scrollLeft;
// If we're at or near the start, wrap to end
if (currentScroll <= 10) {
carousel.scrollTo({ left: maxScroll, behavior: 'smooth' });
if (carousel.scrollLeft <= 10) {
carousel.scrollTo({ left: carousel.scrollWidth, behavior: 'smooth' });
} else {
carousel.scrollTo({ left: currentScroll - itemWidth, behavior: 'smooth' });
carousel.scrollBy({ left: -step(), behavior: 'smooth' });
}
}
if (prevBtn) prevBtn.addEventListener('click', () => { stopInterval(); retreat(); startInterval(); });
if (nextBtn) nextBtn.addEventListener('click', () => { stopInterval(); advance(); startInterval(); });
function startInterval() {
if (intervalId || items.length <= getVisibleCount()) return;
if (intervalId) return;
if (carousel.scrollWidth <= carousel.clientWidth + 10) return;
intervalId = setInterval(advance, intervalMs);
}
@@ -551,8 +528,13 @@ function initFeaturedCarousel() {
if (intervalId) { clearInterval(intervalId); intervalId = null; }
}
if (prevBtn) prevBtn.addEventListener('click', () => { stopInterval(); retreat(); startInterval(); });
if (nextBtn) nextBtn.addEventListener('click', () => { stopInterval(); advance(); startInterval(); });
carousel.addEventListener('mouseenter', stopInterval);
carousel.addEventListener('mouseleave', startInterval);
startInterval();
}
// Defer carousel setup to after critical path