feat: make browse grid 6 columns and full-width - Grid layout set to repeat(6, 1fr) with responsive breakpoints for smaller screens - Removed layout max-width constraint on browse page via :global(main) override - Header and pagination get their own padding within the full-width layout - Increased per page to 60 for better coverage at 6 columns 💘 Generated with Crush Assisted-by: Crush:deepseek-reasoner
Maxwell Jensen maxwelljensen@posteo.net
Wed, 13 May 2026 15:50:15 +0200
1 files changed,
52 insertions(+),
51 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, imageDownloadUrl, type Image } from '$lib/api'; + import { listImages, thumbnailUrl, type Image } from '$lib/api'; import ImageViewer from '$lib/ImageViewer.svelte'; let images = $state<Image[]>([]);@@ -10,7 +10,7 @@ let error = $state('');
let filterTag = $state(''); let page = $state(1); let viewerIndex = $state(-1); - const perPage = 50; + const perPage = 60; onMount(() => loadImages());@@ -46,38 +46,33 @@
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> -<h1>Browse</h1> - -<div class="controls"> - <input - type="text" - placeholder="Filter by tag..." - bind:value={filterTag} - onkeydown={(e) => { if (e.key === 'Enter') applyFilter(); }} - /> - <button onclick={applyFilter}>Filter</button> - {#if filterTag} - <button class="clear" onclick={() => { filterTag = ''; applyFilter(); }}>Clear</button> - {/if} + <div class="header"> + <h1>Browse</h1> + <div class="controls"> + <input + type="text" + placeholder="Filter by tag..." + bind:value={filterTag} + onkeydown={(e) => { if (e.key === 'Enter') applyFilter(); }} + /> + <button onclick={applyFilter}>Filter</button> + {#if filterTag} + <button class="clear" onclick={() => { filterTag = ''; applyFilter(); }}>Clear</button> + {/if} + </div> </div> {#if loading} - <p>Loading...</p> + <p class="empty-state">Loading...</p> {:else if error} - <p class="error">{error}</p> + <p class="empty-state error">{error}</p> {:else if images.length === 0} {#if filterTag} - <p>No results found for tag “{filterTag}”.</p> + <p class="empty-state">No results found for tag “{filterTag}”.</p> {:else} - <p>No images yet. <a href="/upload">Upload one!</a></p> + <p class="empty-state">No images yet. <a href="/upload">Upload one!</a></p> {/if} {:else} <div class="grid">@@ -92,15 +87,6 @@ {/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}@@ -122,10 +108,20 @@ />
{/if} <style> + :global(main) { + padding: 0 !important; + max-width: none !important; + } + + .header { + padding: 1.5rem 1.5rem 0.75rem; + } + .header h1 { + margin: 0 0 0.5rem; + } .controls { display: flex; gap: 0.5rem; - margin-bottom: 1.5rem; } .controls input { padding: 0.5rem;@@ -148,10 +144,23 @@ }
.grid { display: grid; - grid-template-columns: repeat(auto-fill, minmax(240px, 1fr)); + grid-template-columns: repeat(6, 1fr); gap: 2px; } + @media (max-width: 1200px) { + .grid { grid-template-columns: repeat(5, 1fr); } + } + @media (max-width: 1000px) { + .grid { grid-template-columns: repeat(4, 1fr); } + } + @media (max-width: 750px) { + .grid { grid-template-columns: repeat(3, 1fr); } + } + @media (max-width: 500px) { + .grid { grid-template-columns: repeat(2, 1fr); } + } + .card { position: relative; display: block;@@ -174,9 +183,6 @@ position: relative;
overflow: hidden; background: #222; aspect-ratio: 16 / 10; - display: flex; - align-items: center; - justify-content: center; } .card-image img { display: block;@@ -204,7 +210,7 @@ bottom: 0;
left: 0; right: 0; background: linear-gradient(transparent, rgba(0, 0, 0, 0.8)); - padding: 2rem 0.75rem 0.6rem; + padding: 2rem 0.6rem 0.5rem; opacity: 0; transition: opacity 200ms ease; pointer-events: none;@@ -214,29 +220,20 @@ opacity: 1;
} .card-title { color: #fff; - font-size: 0.85rem; + font-size: 0.8rem; font-weight: 600; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; text-shadow: 0 1px 3px rgba(0, 0, 0, 0.6); } - .card-meta { - color: rgba(255, 255, 255, 0.7); - font-size: 0.75rem; - 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: 2rem; + padding: 2rem 1.5rem; } .pagination button { padding: 0.5rem 1rem;@@ -249,6 +246,10 @@ }
.pagination button:disabled { opacity: 0.4; cursor: default; + } + + .empty-state { + padding: 2rem 1.5rem; } .error { color: #c0392b;