feat: redesign browse grid with Chevereto-style card layout and hover overlay - Compact grid with 2px gaps and dark image backgrounds (aspect-ratio 16:10) - Image fills card via object-fit: cover with subtle scale-on-hover - Gradient overlay on hover shows filename, dimensions, and truncated tags - Removed rounded corners and info text below cards for cleaner look 💘 Generated with Crush Assisted-by: Crush:deepseek-reasoner
Maxwell Jensen maxwelljensen@posteo.net
Wed, 13 May 2026 15:44:59 +0200
1 files changed,
84 insertions(+),
37 deletions(-)
M
web/src/routes/browse/+page.svelte
→
web/src/routes/browse/+page.svelte
@@ -1,6 +1,6 @@
<script lang="ts"> import { onMount } from 'svelte'; - import { listImages, thumbnailUrl, type Image } from '$lib/api'; + import { listImages, thumbnailUrl, imageDownloadUrl, type Image } from '$lib/api'; import ImageViewer from '$lib/ImageViewer.svelte'; let images = $state<Image[]>([]);@@ -45,6 +45,12 @@ }
function openViewer(index: number) { viewerIndex = index; + } + + function formatSize(bytes: number): string { + if (bytes < 1024) return bytes + ' B'; + if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB'; + return (bytes / (1024 * 1024)).toFixed(1) + ' MB'; } </script>@@ -77,14 +83,24 @@ {:else}
<div class="grid"> {#each images as img, i (img.id)} <button class="card" onclick={() => openViewer(i)}> - {#if img.thumbnail_path} - <img src={thumbnailUrl(img.id)} alt={img.filename} loading="lazy" /> - {:else} - <div class="placeholder">{img.mime_type}</div> - {/if} - <div class="info"> - <span class="name">{img.filename}</span> - <span class="tags">{img.tags.join(', ')}</span> + <div class="card-image"> + {#if img.thumbnail_path} + <img src={thumbnailUrl(img.id)} alt={img.filename} loading="lazy" /> + {:else} + <div class="placeholder">{img.mime_type}</div> + {/if} + </div> + <div class="card-overlay"> + <div class="card-title">{img.filename}</div> + <div class="card-meta"> + {img.width}×{img.height} + {#if img.tags.length > 0} + · {img.tags.slice(0, 3).join(', ')} + {#if img.tags.length > 3} + … + {/if} + {/if} + </div> </div> </button> {/each}@@ -129,67 +145,98 @@ }
.controls button.clear { background: #666; } + .grid { display: grid; - grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); - gap: 1rem; + grid-template-columns: repeat(auto-fill, minmax(240px, 1fr)); + gap: 2px; } + .card { - border: 1px solid #ddd; - border-radius: 8px; - overflow: hidden; - text-decoration: none; - color: inherit; + position: relative; + display: block; + width: 100%; + padding: 0; + border: none; background: none; - padding: 0; font: inherit; + color: inherit; + cursor: pointer; text-align: left; - cursor: pointer; - transition: box-shadow 0.15s; + overflow: hidden; + } + .card:focus-visible { + outline: 2px solid #1a1a2e; + outline-offset: -2px; } - .card:hover { - box-shadow: 0 2px 12px rgba(0,0,0,0.15); + .card-image { + position: relative; + overflow: hidden; + background: #222; + aspect-ratio: 16 / 10; + display: flex; + align-items: center; + justify-content: center; } - .card img, .placeholder { + .card-image img { + display: block; width: 100%; - height: 180px; + height: 100%; object-fit: cover; - display: block; + transition: transform 200ms ease; + } + .card:hover .card-image img { + transform: scale(1.05); } .placeholder { display: flex; align-items: center; justify-content: center; - background: #f0f0f0; - color: #999; - font-size: 0.9rem; + width: 100%; + height: 100%; + color: #666; + font-size: 0.85rem; } - .info { - padding: 0.5rem; - display: flex; - flex-direction: column; - gap: 0.25rem; + + .card-overlay { + position: absolute; + bottom: 0; + left: 0; + right: 0; + background: linear-gradient(transparent, rgba(0, 0, 0, 0.8)); + padding: 2rem 0.75rem 0.6rem; + opacity: 0; + transition: opacity 200ms ease; + pointer-events: none; } - .name { - font-weight: 600; + .card:hover .card-overlay { + opacity: 1; + } + .card-title { + color: #fff; font-size: 0.85rem; + font-weight: 600; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; + text-shadow: 0 1px 3px rgba(0, 0, 0, 0.6); } - .tags { + .card-meta { + color: rgba(255, 255, 255, 0.7); font-size: 0.75rem; - color: #666; + margin-top: 0.1rem; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; + text-shadow: 0 1px 2px rgba(0, 0, 0, 0.5); } + .pagination { display: flex; align-items: center; justify-content: center; gap: 1rem; - margin-top: 1.5rem; + margin-top: 2rem; } .pagination button { padding: 0.5rem 1rem;