mirror of
https://tangled.org/evan.jarrett.net/at-container-registry
synced 2026-09-20 17:24:16 +00:00
impeccable fixes, scanner fixes
This commit is contained in:
@@ -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
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user