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 properties
|
||||||
* -------------------------------------------------------------------------- */
|
* -------------------------------------------------------------------------- */
|
||||||
$page-corner-radius: $unit;
|
$page-corner-radius: $unit;
|
||||||
|
$image-corner-radius: $unit-2x;
|
||||||
$card-corner-radius: $unit-3x;
|
$card-corner-radius: $unit-3x;
|
||||||
|
|
||||||
$page-top-margin: $unit-6x;
|
$page-top-margin: $unit-6x;
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,6 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import Lightbox from './Lightbox.svelte'
|
||||||
|
|
||||||
let {
|
let {
|
||||||
images = [],
|
images = [],
|
||||||
alt = ''
|
alt = ''
|
||||||
|
|
@ -8,44 +10,96 @@
|
||||||
} = $props()
|
} = $props()
|
||||||
|
|
||||||
let selectedIndex = $state(0)
|
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) => {
|
const selectImage = (index: number) => {
|
||||||
selectedIndex = index
|
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>
|
</script>
|
||||||
|
|
||||||
{#if images.length === 1}
|
{#if images.length === 1}
|
||||||
<!-- Single image -->
|
<!-- Single image -->
|
||||||
<div class="single-image">
|
<button class="single-image image-button" onclick={() => openLightbox()}>
|
||||||
<img src={images[0]} {alt} />
|
<img src={images[0]} {alt} />
|
||||||
</div>
|
</button>
|
||||||
{:else if images.length > 1}
|
{:else if images.length > 1}
|
||||||
<!-- Slideshow -->
|
<!-- Slideshow -->
|
||||||
<div class="slideshow">
|
<div class="slideshow">
|
||||||
<div class="main-image">
|
<button class="main-image image-button" onclick={() => openLightbox()}>
|
||||||
<img src={images[selectedIndex]} alt="{alt} {selectedIndex + 1}" />
|
<img src={images[selectedIndex]} alt="{alt} {selectedIndex + 1}" />
|
||||||
</div>
|
</button>
|
||||||
<div class="thumbnails">
|
<div class="thumbnails">
|
||||||
{#each images as image, index}
|
{#each Array(totalSlots) as _, index}
|
||||||
<button
|
{#if index < images.length}
|
||||||
class="thumbnail"
|
<button
|
||||||
class:active={index === selectedIndex}
|
class="thumbnail"
|
||||||
onclick={() => selectImage(index)}
|
class:active={index === selectedIndex}
|
||||||
aria-label="View image {index + 1}"
|
onclick={() => selectImage(index)}
|
||||||
>
|
aria-label="View image {index + 1}"
|
||||||
<img src={image} alt="{alt} thumbnail {index + 1}" />
|
>
|
||||||
</button>
|
<img src={images[index]} alt="{alt} thumbnail {index + 1}" />
|
||||||
|
</button>
|
||||||
|
{:else}
|
||||||
|
<div class="thumbnail placeholder" aria-hidden="true"></div>
|
||||||
|
{/if}
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
|
<Lightbox {images} bind:selectedIndex bind:isOpen={lightboxOpen} {alt} />
|
||||||
|
|
||||||
<style lang="scss">
|
<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,
|
.single-image,
|
||||||
.main-image {
|
.main-image {
|
||||||
width: 100%;
|
|
||||||
aspect-ratio: 4 / 3;
|
aspect-ratio: 4 / 3;
|
||||||
border-radius: $card-corner-radius;
|
border-radius: $image-corner-radius;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
|
||||||
img {
|
img {
|
||||||
|
|
@ -63,22 +117,22 @@
|
||||||
|
|
||||||
.thumbnails {
|
.thumbnails {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(auto-fill, minmax(calc(25% - 12px), 1fr));
|
grid-template-columns: repeat(6, 1fr);
|
||||||
gap: $unit-2x;
|
gap: $unit-2x;
|
||||||
|
|
||||||
@media (max-width: 600px) {
|
@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) {
|
@media (max-width: 400px) {
|
||||||
grid-template-columns: repeat(auto-fill, minmax(calc(50% - 8px), 1fr));
|
grid-template-columns: repeat(3, 1fr);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.thumbnail {
|
.thumbnail {
|
||||||
position: relative;
|
position: relative;
|
||||||
aspect-ratio: 1;
|
aspect-ratio: 1;
|
||||||
border-radius: $card-corner-radius;
|
border-radius: $image-corner-radius;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
border: none;
|
border: none;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
|
|
@ -90,7 +144,7 @@
|
||||||
content: '';
|
content: '';
|
||||||
position: absolute;
|
position: absolute;
|
||||||
inset: 0;
|
inset: 0;
|
||||||
border-radius: $card-corner-radius;
|
border-radius: $image-corner-radius;
|
||||||
border: 4px solid transparent;
|
border: 4px solid transparent;
|
||||||
z-index: 2;
|
z-index: 2;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
|
|
@ -101,7 +155,7 @@
|
||||||
content: '';
|
content: '';
|
||||||
position: absolute;
|
position: absolute;
|
||||||
inset: 4px;
|
inset: 4px;
|
||||||
border-radius: calc($card-corner-radius - 4px);
|
border-radius: calc($image-corner-radius - 4px);
|
||||||
border: 4px solid transparent;
|
border: 4px solid transparent;
|
||||||
z-index: 3;
|
z-index: 3;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
|
|
@ -122,6 +176,15 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.placeholder {
|
||||||
|
background: $grey-90;
|
||||||
|
cursor: default;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
transform: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
img {
|
img {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
|
|
||||||
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