| 1 | <script lang="ts"> |
| 2 | import { tick } from 'svelte'; |
| 3 | import { lightbox } from './lightbox'; |
| 4 | |
| 5 | let closeBtn: HTMLButtonElement | null = null; |
| 6 | let returnFocus: HTMLElement | null = null; |
| 7 | |
| 8 | $: if ($lightbox) void focusClose(); |
| 9 | |
| 10 | async function focusClose(): Promise<void> { |
| 11 | if (!returnFocus) returnFocus = document.activeElement as HTMLElement | null; |
| 12 | await tick(); |
| 13 | closeBtn?.focus(); |
| 14 | } |
| 15 | |
| 16 | function close(): void { |
| 17 | lightbox.set(null); |
| 18 | returnFocus?.focus(); |
| 19 | returnFocus = null; |
| 20 | } |
| 21 | |
| 22 | function onKey(ev: KeyboardEvent): void { |
| 23 | if ($lightbox && ev.key === 'Escape') { |
| 24 | ev.stopPropagation(); |
| 25 | close(); |
| 26 | } |
| 27 | } |
| 28 | </script> |
| 29 | |
| 30 | <svelte:window on:keydown|capture={onKey} /> |
| 31 | |
| 32 | {#if $lightbox} |
| 33 | <!-- svelte-ignore a11y_click_events_have_key_events a11y_no_static_element_interactions --> |
| 34 | <div class="lightbox" role="dialog" tabindex="-1" aria-modal="true" aria-label={$lightbox.filename} on:click|self={close}> |
| 35 | <div class="lightbox-bar"> |
| 36 | <span class="lightbox-name">{$lightbox.filename}</span> |
| 37 | <a class="ghost-btn" href={$lightbox.src} target="_blank" rel="noopener noreferrer">Open original</a> |
| 38 | <a class="ghost-btn" href={$lightbox.src} download={$lightbox.filename}>Download</a> |
| 39 | <button type="button" class="icon-btn" aria-label="Close image" title="Close (Esc)" bind:this={closeBtn} on:click={close}> |
| 40 | <span aria-hidden="true">✕</span> |
| 41 | </button> |
| 42 | </div> |
| 43 | <!-- svelte-ignore a11y_click_events_have_key_events a11y_no_noninteractive_element_interactions --> |
| 44 | <img class="lightbox-image" src={$lightbox.src} alt={$lightbox.alt} on:click={close} /> |
| 45 | </div> |
| 46 | {/if} |
| 47 | |
| 48 | <style> |
| 49 | .lightbox { |
| 50 | position: fixed; |
| 51 | inset: 0; |
| 52 | z-index: 100; |
| 53 | display: flex; |
| 54 | flex-direction: column; |
| 55 | align-items: center; |
| 56 | justify-content: center; |
| 57 | gap: 12px; |
| 58 | padding: 56px 24px 24px; |
| 59 | background: rgb(0 0 0 / 0.82); |
| 60 | backdrop-filter: blur(4px); |
| 61 | } |
| 62 | .lightbox-bar { |
| 63 | position: absolute; |
| 64 | top: 0; |
| 65 | left: 0; |
| 66 | right: 0; |
| 67 | display: flex; |
| 68 | align-items: center; |
| 69 | gap: 8px; |
| 70 | padding: 10px 16px; |
| 71 | color: #fff; |
| 72 | } |
| 73 | .lightbox-name { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 13px; } |
| 74 | .lightbox-bar a { text-decoration: none; } |
| 75 | .lightbox-image { |
| 76 | max-width: 100%; |
| 77 | max-height: 100%; |
| 78 | object-fit: contain; |
| 79 | border-radius: 6px; |
| 80 | box-shadow: 0 12px 40px rgb(0 0 0 / 0.5); |
| 81 | cursor: zoom-out; |
| 82 | } |
| 83 | </style> |