atagen

malice

public
Star0
Watch0
294426c7f88fd4348a61b8a8590233237a1e52c0
2
ssh://brewery@pls.lobotomise.me:33/atagen/malice.git
web/src/lib/ImageLightbox.svelteSvelte108 lines3.2 KB
1
<script lang="ts">
2
  import { tick } from 'svelte';
3
  import { lightbox } from './lightbox';
4
  import EmojiText from './EmojiText.svelte';
5
6
  let closeBtn: HTMLButtonElement | null = null;
7
  let copied = false;
8
  let copiedTimer: number | null = null;
9
10
  async function copyMxc(mxc: string): Promise<void> {
11
    try {
12
      await navigator.clipboard.writeText(mxc);
13
    } catch {
14
      return;
15
    }
16
    copied = true;
17
    if (copiedTimer !== null) window.clearTimeout(copiedTimer);
18
    copiedTimer = window.setTimeout(() => (copied = false), 1500);
19
  }
20
  let returnFocus: HTMLElement | null = null;
21
22
  $: if ($lightbox) void focusClose();
23
24
  async function focusClose(): Promise<void> {
25
    if (!returnFocus) returnFocus = document.activeElement as HTMLElement | null;
26
    await tick();
27
    closeBtn?.focus();
28
  }
29
30
  function close(): void {
31
    lightbox.set(null);
32
    returnFocus?.focus();
33
    returnFocus = null;
34
  }
35
36
  function onKey(ev: KeyboardEvent): void {
37
    if ($lightbox && ev.key === 'Escape') {
38
      ev.stopPropagation();
39
      close();
40
    }
41
  }
42
</script>
43
44
<svelte:window on:keydown|capture={onKey} />
45
46
{#if $lightbox}
47
  <!-- svelte-ignore a11y_click_events_have_key_events a11y_no_static_element_interactions -->
48
  <div class="lightbox" role="dialog" tabindex="-1" aria-modal="true" aria-label={$lightbox.filename} on:click|self={close}>
49
    <div class="lightbox-bar">
50
      <span class="lightbox-name"><EmojiText text={$lightbox.filename} /></span>
51
      <button
52
        type="button"
53
        class="ghost-btn"
54
        aria-label="Copy MXC URI"
55
        title={$lightbox.mxc ?? 'MXC URI unavailable for this attachment'}
56
        disabled={!$lightbox.mxc}
57
        on:click={() => $lightbox.mxc && void copyMxc($lightbox.mxc)}
58
      >
59
        {copied ? 'Copied' : 'Copy MXC'}
60
      </button>
61
      <a class="ghost-btn" href={$lightbox.src} target="_blank" rel="noopener noreferrer">Open original</a>
62
      <a class="ghost-btn" href={$lightbox.src} download={$lightbox.filename}>Download</a>
63
      <button type="button" class="icon-btn" aria-label="Close image" title="Close (Esc)" bind:this={closeBtn} on:click={close}>
64
        <span aria-hidden="true">✕</span>
65
      </button>
66
    </div>
67
    <!-- svelte-ignore a11y_click_events_have_key_events a11y_no_noninteractive_element_interactions -->
68
    <img class="lightbox-image" src={$lightbox.src} alt={$lightbox.alt} on:click={close} />
69
  </div>
70
{/if}
71
72
<style>
73
  .lightbox {
74
    animation: fade-in var(--pop);
75
    position: fixed;
76
    inset: 0;
77
    z-index: 100;
78
    display: flex;
79
    flex-direction: column;
80
    align-items: center;
81
    justify-content: center;
82
    gap: 12px;
83
    padding: 56px 24px 24px;
84
    background: rgb(0 0 0 / 0.82);
85
    backdrop-filter: blur(4px);
86
  }
87
  .lightbox-bar {
88
    position: absolute;
89
    top: 0;
90
    left: 0;
91
    right: 0;
92
    display: flex;
93
    align-items: center;
94
    gap: 8px;
95
    padding: 10px 16px;
96
    color: #fff;
97
  }
98
  .lightbox-name { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 13px; }
99
  .lightbox-bar a { text-decoration: none; }
100
  .lightbox-image {
101
    max-width: 100%;
102
    max-height: 100%;
103
    object-fit: contain;
104
    border-radius: var(--r-sm);
105
    box-shadow: 0 12px 40px rgb(0 0 0 / 0.5);
106
    cursor: zoom-out;
107
  }
108
</style>