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
+63 -2
View File
@@ -97,6 +97,27 @@
:root {
--shadow-card-hover:
0 8px 25px oklch(67.1% 0.05 145 / 0.25), 0 4px 12px oklch(0% 0 0 / 0.1);
/* Dedicated star/favorite color — semantically distinct from `warning`
even if values currently coincide. Used by .text-star/.fill-star/etc. */
--color-star: oklch(82% 0.189 84.429);
}
/* ========================================
STAR / FAVORITE UTILITIES
Tailwind 4 @utility so the `!` important modifier and
variants (hover:, group-hover:) still work on these classes.
======================================== */
@utility text-star {
color: var(--color-star);
}
@utility stroke-star {
stroke: var(--color-star);
}
@utility fill-star {
fill: var(--color-star);
}
@utility border-star {
border-color: var(--color-star);
}
[data-theme="dark"] {
@@ -198,6 +219,10 @@
.icon.size-5 { width: 1.25rem; height: 1.25rem; }
.icon.size-6 { width: 1.5rem; height: 1.5rem; }
.icon.size-8 { width: 2rem; height: 2rem; }
.icon.size-10 { width: 2.5rem; height: 2.5rem; }
.icon.size-12 { width: 3rem; height: 3rem; }
.icon.size-16 { width: 4rem; height: 4rem; }
.icon.size-20 { width: 5rem; height: 5rem; }
/* Special size for slightly larger than 1rem (used in star/pull count) */
.icon.size-\[1\.1rem\] { width: 1.1rem; height: 1.1rem; }
@@ -430,6 +455,42 @@
display: block;
}
/* ----------------------------------------
RESPONSIVE TABLE — STACK ON MOBILE
Below md, each row becomes a labeled block.
Add data-label="..." to each <td>.
---------------------------------------- */
@media (max-width: 47.999rem) {
.table-stack-mobile thead {
@apply sr-only;
}
.table-stack-mobile,
.table-stack-mobile tbody,
.table-stack-mobile tr,
.table-stack-mobile td {
display: block;
width: 100%;
}
.table-stack-mobile tr {
@apply border border-base-300 rounded-md mb-3 p-3 bg-base-100;
}
.table-stack-mobile tr:nth-child(even) {
@apply bg-base-200;
}
.table-stack-mobile td {
@apply py-1;
border: 0;
}
.table-stack-mobile td::before {
content: attr(data-label);
@apply block text-xs font-semibold uppercase tracking-wide text-base-content/60 mb-0.5;
}
.table-stack-mobile td[data-label=""]::before,
.table-stack-mobile td:not([data-label])::before {
content: none;
}
}
/* ----------------------------------------
VULNERABILITY SEVERITY BOX STRIP
Docker Hub-style connected severity boxes
@@ -442,8 +503,8 @@
}
.vuln-strip > span:first-child { @apply rounded-l-sm; }
.vuln-strip > span:last-child { @apply rounded-r-sm; }
.vuln-box-critical { background-color: oklch(45% 0.16 20); color: oklch(97% 0.01 20); }
.vuln-box-high { background-color: oklch(58% 0.18 35); color: oklch(97% 0.01 35); }
.vuln-box-critical { background-color: oklch(45% 0.19 25); color: oklch(97% 0.01 25); }
.vuln-box-high { background-color: oklch(56% 0.19 50); color: oklch(97% 0.01 50); }
.vuln-box-medium { background-color: oklch(72% 0.15 70); color: oklch(25% 0.05 70); }
.vuln-box-low { background-color: oklch(80% 0.1 85); color: oklch(25% 0.05 85); }
}
+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