images and slideshows
Lightbox still doesn’t close when clicking background
This commit is contained in:
parent
f970913cc8
commit
4a24fbd3b7
3 changed files with 360 additions and 30 deletions
|
|
@ -25,6 +25,7 @@ $unit-20x: $unit * 20;
|
|||
/* Page properties
|
||||
* -------------------------------------------------------------------------- */
|
||||
$page-corner-radius: $unit;
|
||||
$image-corner-radius: $unit-2x;
|
||||
$card-corner-radius: $unit-3x;
|
||||
|
||||
$page-top-margin: $unit-6x;
|
||||
|
|
|
|||
|
|
@ -1,4 +1,6 @@
|
|||
<script lang="ts">
|
||||
import Lightbox from './Lightbox.svelte'
|
||||
|
||||
let {
|
||||
images = [],
|
||||
alt = ''
|
||||
|
|
@ -8,46 +10,98 @@
|
|||
} = $props()
|
||||
|
||||
let selectedIndex = $state(0)
|
||||
let lightboxOpen = $state(false)
|
||||
let windowWidth = $state(0)
|
||||
|
||||
// Calculate columns based on breakpoints
|
||||
const columnsPerRow = $derived(windowWidth <= 400 ? 3 : windowWidth <= 600 ? 4 : 6)
|
||||
const totalSlots = $derived(Math.ceil(images.length / columnsPerRow) * columnsPerRow)
|
||||
|
||||
const selectImage = (index: number) => {
|
||||
selectedIndex = index
|
||||
}
|
||||
|
||||
const openLightbox = (index?: number) => {
|
||||
if (index !== undefined) {
|
||||
selectedIndex = index
|
||||
}
|
||||
lightboxOpen = true
|
||||
}
|
||||
|
||||
// Track window width for responsive columns
|
||||
$effect(() => {
|
||||
windowWidth = window.innerWidth
|
||||
|
||||
const handleResize = () => {
|
||||
windowWidth = window.innerWidth
|
||||
}
|
||||
|
||||
window.addEventListener('resize', handleResize)
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('resize', handleResize)
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
{#if images.length === 1}
|
||||
<!-- Single image -->
|
||||
<div class="single-image">
|
||||
<button class="single-image image-button" onclick={() => openLightbox()}>
|
||||
<img src={images[0]} {alt} />
|
||||
</div>
|
||||
</button>
|
||||
{:else if images.length > 1}
|
||||
<!-- Slideshow -->
|
||||
<div class="slideshow">
|
||||
<div class="main-image">
|
||||
<button class="main-image image-button" onclick={() => openLightbox()}>
|
||||
<img src={images[selectedIndex]} alt="{alt} {selectedIndex + 1}" />
|
||||
</div>
|
||||
</button>
|
||||
<div class="thumbnails">
|
||||
{#each images as image, index}
|
||||
<button
|
||||
class="thumbnail"
|
||||
class:active={index === selectedIndex}
|
||||
onclick={() => selectImage(index)}
|
||||
aria-label="View image {index + 1}"
|
||||
>
|
||||
<img src={image} alt="{alt} thumbnail {index + 1}" />
|
||||
</button>
|
||||
{#each Array(totalSlots) as _, index}
|
||||
{#if index < images.length}
|
||||
<button
|
||||
class="thumbnail"
|
||||
class:active={index === selectedIndex}
|
||||
onclick={() => selectImage(index)}
|
||||
aria-label="View image {index + 1}"
|
||||
>
|
||||
<img src={images[index]} alt="{alt} thumbnail {index + 1}" />
|
||||
</button>
|
||||
{:else}
|
||||
<div class="thumbnail placeholder" aria-hidden="true"></div>
|
||||
{/if}
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<Lightbox {images} bind:selectedIndex bind:isOpen={lightboxOpen} {alt} />
|
||||
|
||||
<style lang="scss">
|
||||
.image-button {
|
||||
border: none;
|
||||
padding: 0;
|
||||
background: none;
|
||||
cursor: pointer;
|
||||
display: block;
|
||||
width: 100%;
|
||||
transition: transform 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
transform: scale(0.98);
|
||||
}
|
||||
|
||||
&:focus {
|
||||
outline: 2px solid $red-60;
|
||||
outline-offset: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
.single-image,
|
||||
.main-image {
|
||||
width: 100%;
|
||||
aspect-ratio: 4 / 3;
|
||||
border-radius: $card-corner-radius;
|
||||
border-radius: $image-corner-radius;
|
||||
overflow: hidden;
|
||||
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
|
@ -63,65 +117,74 @@
|
|||
|
||||
.thumbnails {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(calc(25% - 12px), 1fr));
|
||||
grid-template-columns: repeat(6, 1fr);
|
||||
gap: $unit-2x;
|
||||
|
||||
|
||||
@media (max-width: 600px) {
|
||||
grid-template-columns: repeat(auto-fill, minmax(calc(33.33% - 11px), 1fr));
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
}
|
||||
|
||||
|
||||
@media (max-width: 400px) {
|
||||
grid-template-columns: repeat(auto-fill, minmax(calc(50% - 8px), 1fr));
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
}
|
||||
}
|
||||
|
||||
.thumbnail {
|
||||
position: relative;
|
||||
aspect-ratio: 1;
|
||||
border-radius: $card-corner-radius;
|
||||
border-radius: $image-corner-radius;
|
||||
overflow: hidden;
|
||||
border: none;
|
||||
padding: 0;
|
||||
background: none;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
border-radius: $card-corner-radius;
|
||||
border-radius: $image-corner-radius;
|
||||
border: 4px solid transparent;
|
||||
z-index: 2;
|
||||
pointer-events: none;
|
||||
transition: border-color 0.2s ease;
|
||||
}
|
||||
|
||||
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: 4px;
|
||||
border-radius: calc($card-corner-radius - 4px);
|
||||
border-radius: calc($image-corner-radius - 4px);
|
||||
border: 4px solid transparent;
|
||||
z-index: 3;
|
||||
pointer-events: none;
|
||||
transition: border-color 0.2s ease;
|
||||
}
|
||||
|
||||
|
||||
&:hover {
|
||||
transform: scale(0.98);
|
||||
}
|
||||
|
||||
|
||||
&.active {
|
||||
&::before {
|
||||
border-color: $red-60;
|
||||
}
|
||||
|
||||
|
||||
&::after {
|
||||
border-color: $grey-100;
|
||||
}
|
||||
}
|
||||
|
||||
&.placeholder {
|
||||
background: $grey-90;
|
||||
cursor: default;
|
||||
|
||||
&:hover {
|
||||
transform: none;
|
||||
}
|
||||
}
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
|
@ -130,4 +193,4 @@
|
|||
z-index: 1;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
|
|
|||
266
src/lib/components/Lightbox.svelte
Normal file
266
src/lib/components/Lightbox.svelte
Normal file
|
|
@ -0,0 +1,266 @@
|
|||
<script lang="ts">
|
||||
import { onMount } from 'svelte'
|
||||
import { fade, scale } from 'svelte/transition'
|
||||
|
||||
let {
|
||||
images = [],
|
||||
selectedIndex = $bindable(0),
|
||||
isOpen = $bindable(false),
|
||||
alt = ''
|
||||
}: {
|
||||
images: string[]
|
||||
selectedIndex: number
|
||||
isOpen: boolean
|
||||
alt?: string
|
||||
} = $props()
|
||||
|
||||
const close = () => {
|
||||
isOpen = false
|
||||
}
|
||||
|
||||
const selectImage = (index: number) => {
|
||||
selectedIndex = index
|
||||
}
|
||||
|
||||
const handleKeydown = (event: KeyboardEvent) => {
|
||||
if (!isOpen) return
|
||||
|
||||
switch (event.key) {
|
||||
case 'Escape':
|
||||
close()
|
||||
break
|
||||
case 'ArrowLeft':
|
||||
if (selectedIndex > 0) {
|
||||
selectedIndex--
|
||||
}
|
||||
break
|
||||
case 'ArrowRight':
|
||||
if (selectedIndex < images.length - 1) {
|
||||
selectedIndex++
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
const handleBackgroundClick = (event: MouseEvent) => {
|
||||
if (event.target === event.currentTarget) {
|
||||
close()
|
||||
}
|
||||
}
|
||||
|
||||
$effect(() => {
|
||||
if (isOpen) {
|
||||
// Lock scroll when lightbox opens
|
||||
document.body.style.overflow = 'hidden'
|
||||
} else {
|
||||
// Restore scroll when lightbox closes
|
||||
document.body.style.overflow = ''
|
||||
}
|
||||
|
||||
// Cleanup on unmount
|
||||
return () => {
|
||||
document.body.style.overflow = ''
|
||||
}
|
||||
})
|
||||
|
||||
onMount(() => {
|
||||
window.addEventListener('keydown', handleKeydown)
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('keydown', handleKeydown)
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
{#if isOpen}
|
||||
<div
|
||||
class="lightbox-backdrop"
|
||||
onclick={handleBackgroundClick}
|
||||
transition:fade={{ duration: 200 }}
|
||||
role="button"
|
||||
tabindex="-1"
|
||||
>
|
||||
<div class="lightbox-content" onclick={(e) => e.stopPropagation()}>
|
||||
<div class="lightbox-image-container">
|
||||
<img
|
||||
src={images[selectedIndex]}
|
||||
alt="{alt} {selectedIndex + 1}"
|
||||
transition:scale={{ duration: 200, start: 0.9 }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{#if images.length > 1}
|
||||
<div class="lightbox-thumbnails">
|
||||
<div class="thumbnails-inner">
|
||||
{#each images as image, index}
|
||||
<button
|
||||
class="lightbox-thumbnail"
|
||||
class:active={index === selectedIndex}
|
||||
onclick={() => selectImage(index)}
|
||||
aria-label="View image {index + 1}"
|
||||
>
|
||||
<img src={image} alt="{alt} thumbnail {index + 1}" />
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<button class="lightbox-close" onclick={close} aria-label="Close lightbox">
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M18 6L6 18M6 6l12 12" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<style lang="scss">
|
||||
.lightbox-backdrop {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: rgba(0, 0, 0, 0.9);
|
||||
z-index: 1000;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: $unit-4x;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.lightbox-content {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.lightbox-image-container {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
padding-bottom: 120px; // Space for thumbnails
|
||||
|
||||
img {
|
||||
max-width: 90vw;
|
||||
max-height: 80vh;
|
||||
object-fit: contain;
|
||||
user-select: none;
|
||||
-webkit-user-drag: none;
|
||||
}
|
||||
}
|
||||
|
||||
.lightbox-thumbnails {
|
||||
position: fixed;
|
||||
bottom: $unit-3x; // 24px from bottom
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.thumbnails-inner {
|
||||
display: flex;
|
||||
gap: $unit-2x;
|
||||
overflow-x: auto;
|
||||
max-width: 90vw;
|
||||
padding: $unit $unit-2x; // Add vertical padding to prevent clipping
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.lightbox-thumbnail {
|
||||
position: relative;
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
flex-shrink: 0;
|
||||
border-radius: $unit-2x; // 16px
|
||||
overflow: hidden;
|
||||
border: none;
|
||||
padding: 0;
|
||||
background: none;
|
||||
cursor: pointer;
|
||||
opacity: 0.5;
|
||||
transition: all 0.2s ease;
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
border-radius: $unit-2x;
|
||||
border: 2px solid transparent;
|
||||
z-index: 2;
|
||||
pointer-events: none;
|
||||
transition: border-color 0.2s ease;
|
||||
}
|
||||
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: 2px;
|
||||
border-radius: calc($unit-2x - 2px);
|
||||
border: 2px solid transparent;
|
||||
z-index: 3;
|
||||
pointer-events: none;
|
||||
transition: border-color 0.2s ease;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
&.active {
|
||||
opacity: 1;
|
||||
|
||||
&::before {
|
||||
border-color: $red-60;
|
||||
}
|
||||
|
||||
&::after {
|
||||
border-color: $grey-00; // Black inner border
|
||||
}
|
||||
}
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
user-select: none;
|
||||
-webkit-user-drag: none;
|
||||
}
|
||||
}
|
||||
|
||||
.lightbox-close {
|
||||
position: absolute;
|
||||
top: $unit-3x;
|
||||
right: $unit-3x;
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border-radius: 50%;
|
||||
border: none;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
color: $grey-100;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: all 0.2s ease;
|
||||
backdrop-filter: blur(10px);
|
||||
|
||||
&:hover {
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
transform: scale(1.1);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Loading…
Reference in a new issue