atagen

malice

public
Star0
Watch0
85d71b94ff8e857ef07ee98c2c9948850574da61
2
ssh://brewery@pls.lobotomise.me:33/atagen/malice.git

Blame: web/src/lib/StickerPicker.svelte

d095131eec0eatagen2026-10-24 08:39 UTC1
<script lang="ts">
d095131eec0eatagen2026-10-24 08:39 UTC2
  import { onMount } from 'svelte';
d095131eec0eatagen2026-10-24 08:39 UTC3
  import { messageMediaUrl } from './api';
d095131eec0eatagen2026-10-24 08:39 UTC4
  import MxcAvatar from './MxcAvatar.svelte';
445bf199a9cfatagen2026-10-25 02:56 UTC5
  import EmojiText from './EmojiText.svelte';
d095131eec0eatagen2026-10-24 08:39 UTC6
  import { packLabel, stickersOf } from './packs';
d095131eec0eatagen2026-10-24 08:39 UTC7
  import type { ImagePack, PackImage } from './types';
d095131eec0eatagen2026-10-24 08:39 UTC8
d095131eec0eatagen2026-10-24 08:39 UTC9
  export let packs: ImagePack[];
d095131eec0eatagen2026-10-24 08:39 UTC10
  export let roomName: (roomId: string) => string;
d095131eec0eatagen2026-10-24 08:39 UTC11
  export let onSend: (pack: ImagePack, image: PackImage) => void;
d095131eec0eatagen2026-10-24 08:39 UTC12
  export let onClone: (pack: ImagePack, image: PackImage, x: number, y: number) => void;
d095131eec0eatagen2026-10-24 08:39 UTC13
  export let onClonePack: (pack: ImagePack, x: number, y: number) => void;
d095131eec0eatagen2026-10-24 08:39 UTC14
  export let onManage: () => void;
d095131eec0eatagen2026-10-24 08:39 UTC15
  export let onClose: () => void;
d095131eec0eatagen2026-10-24 08:39 UTC16
d095131eec0eatagen2026-10-24 08:39 UTC17
  let root: HTMLDivElement;
d095131eec0eatagen2026-10-24 08:39 UTC18
  let search: HTMLInputElement;
d095131eec0eatagen2026-10-24 08:39 UTC19
  let query = '';
d095131eec0eatagen2026-10-24 08:39 UTC20
  let activeId: string | null = null;
d095131eec0eatagen2026-10-24 08:39 UTC21
d095131eec0eatagen2026-10-24 08:39 UTC22
  $: withStickers = packs.filter((p) => stickersOf(p).length > 0);
d095131eec0eatagen2026-10-24 08:39 UTC23
  $: q = query.trim().toLowerCase();
d095131eec0eatagen2026-10-24 08:39 UTC24
  $: shown = withStickers
d095131eec0eatagen2026-10-24 08:39 UTC25
    .map((pack) => ({
d095131eec0eatagen2026-10-24 08:39 UTC26
      pack,
d095131eec0eatagen2026-10-24 08:39 UTC27
      images: stickersOf(pack).filter(
d095131eec0eatagen2026-10-24 08:39 UTC28
        (i) => !q || i.shortcode.toLowerCase().includes(q) || (i.body ?? '').toLowerCase().includes(q)
d095131eec0eatagen2026-10-24 08:39 UTC29
      )
d095131eec0eatagen2026-10-24 08:39 UTC30
    }))
d095131eec0eatagen2026-10-24 08:39 UTC31
    .filter((g) => g.images.length > 0 && (q || activeId === null || g.pack.id === activeId));
d095131eec0eatagen2026-10-24 08:39 UTC32
d095131eec0eatagen2026-10-24 08:39 UTC33
  onMount(() => search?.focus());
d095131eec0eatagen2026-10-24 08:39 UTC34
d095131eec0eatagen2026-10-24 08:39 UTC35
  function outside(ev: PointerEvent): void {
d095131eec0eatagen2026-10-24 08:39 UTC36
    if (!root.contains(ev.target as Node) && !(ev.target as Element).closest?.('.context-menu, .sticker-btn')) onClose();
d095131eec0eatagen2026-10-24 08:39 UTC37
  }
d095131eec0eatagen2026-10-24 08:39 UTC38
d095131eec0eatagen2026-10-24 08:39 UTC39
  function key(ev: KeyboardEvent): void {
d095131eec0eatagen2026-10-24 08:39 UTC40
    if (ev.key === 'Escape' && !document.querySelector('.context-menu')) {
d095131eec0eatagen2026-10-24 08:39 UTC41
      ev.stopPropagation();
d095131eec0eatagen2026-10-24 08:39 UTC42
      onClose();
d095131eec0eatagen2026-10-24 08:39 UTC43
    }
d095131eec0eatagen2026-10-24 08:39 UTC44
  }
d095131eec0eatagen2026-10-24 08:39 UTC45
</script>
d095131eec0eatagen2026-10-24 08:39 UTC46
d095131eec0eatagen2026-10-24 08:39 UTC47
<svelte:window on:pointerdown|capture={outside} on:keydown|capture={key} />
d095131eec0eatagen2026-10-24 08:39 UTC48
d095131eec0eatagen2026-10-24 08:39 UTC49
<div class="sticker-picker" role="dialog" aria-label="Stickers" bind:this={root}>
d095131eec0eatagen2026-10-24 08:39 UTC50
  <div class="top">
d095131eec0eatagen2026-10-24 08:39 UTC51
    <input class="search" type="search" placeholder="Search stickers" bind:value={query} bind:this={search} />
d095131eec0eatagen2026-10-24 08:39 UTC52
    <button type="button" class="icon-btn" title="Manage packs" aria-label="Manage packs" on:click={onManage}>
05a48e9e7942atagen2026-10-24 09:32 UTC53
      <svg viewBox="0 0 24 24" width="17" height="17" aria-hidden="true"><circle cx="12" cy="12" r="3" fill="none" stroke="currentColor" stroke-width="1.8" /><path d="M12 2v3M12 19v3M2 12h3M19 12h3M4.9 4.9l2.1 2.1M17 17l2.1 2.1M4.9 19.1L7 17M17 7l2.1-2.1" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" /></svg>
d095131eec0eatagen2026-10-24 08:39 UTC54
    </button>
d095131eec0eatagen2026-10-24 08:39 UTC55
  </div>
d095131eec0eatagen2026-10-24 08:39 UTC56
  {#if withStickers.length > 1}
d095131eec0eatagen2026-10-24 08:39 UTC57
    <div class="tabs" role="tablist" aria-label="Packs">
d095131eec0eatagen2026-10-24 08:39 UTC58
      <button type="button" role="tab" class="tab all" aria-selected={activeId === null} on:click={() => (activeId = null)}>All</button>
d095131eec0eatagen2026-10-24 08:39 UTC59
      {#each withStickers as pack (pack.id)}
d095131eec0eatagen2026-10-24 08:39 UTC60
        <button
d095131eec0eatagen2026-10-24 08:39 UTC61
          type="button"
d095131eec0eatagen2026-10-24 08:39 UTC62
          role="tab"
d095131eec0eatagen2026-10-24 08:39 UTC63
          class="tab"
d095131eec0eatagen2026-10-24 08:39 UTC64
          aria-selected={activeId === pack.id}
d095131eec0eatagen2026-10-24 08:39 UTC65
          title={packLabel(pack, roomName)}
d095131eec0eatagen2026-10-24 08:39 UTC66
          on:click={() => (activeId = pack.id)}
d095131eec0eatagen2026-10-24 08:39 UTC67
        >
d095131eec0eatagen2026-10-24 08:39 UTC68
          {#if pack.avatar}
d095131eec0eatagen2026-10-24 08:39 UTC69
            <MxcAvatar mxc={pack.avatar} label={packLabel(pack, roomName)} size={24} shape="rounded" />
d095131eec0eatagen2026-10-24 08:39 UTC70
          {:else}
d095131eec0eatagen2026-10-24 08:39 UTC71
            <img src={messageMediaUrl(stickersOf(pack)[0].media_id, 'Thumbnail')} alt={packLabel(pack, roomName)} />
d095131eec0eatagen2026-10-24 08:39 UTC72
          {/if}
d095131eec0eatagen2026-10-24 08:39 UTC73
        </button>
d095131eec0eatagen2026-10-24 08:39 UTC74
      {/each}
d095131eec0eatagen2026-10-24 08:39 UTC75
    </div>
d095131eec0eatagen2026-10-24 08:39 UTC76
  {/if}
d095131eec0eatagen2026-10-24 08:39 UTC77
  <div class="scroll">
d095131eec0eatagen2026-10-24 08:39 UTC78
    {#each shown as g (g.pack.id)}
d095131eec0eatagen2026-10-24 08:39 UTC79
      <section class="group" aria-label={packLabel(g.pack, roomName)}>
d095131eec0eatagen2026-10-24 08:39 UTC80
        <div class="group-head">
445bf199a9cfatagen2026-10-25 02:56 UTC81
          <h3><EmojiText text={packLabel(g.pack, roomName)} /></h3>
132eeceeb362atagen2026-10-25 03:34 UTC82
          <button type="button" class="clone-pack" on:click={(ev) => onClonePack(g.pack, ev.clientX, ev.clientY)}>Clone pack</button>
d095131eec0eatagen2026-10-24 08:39 UTC83
        </div>
d095131eec0eatagen2026-10-24 08:39 UTC84
        <div class="grid">
d095131eec0eatagen2026-10-24 08:39 UTC85
          {#each g.images as image (image.shortcode)}
d095131eec0eatagen2026-10-24 08:39 UTC86
            <button
d095131eec0eatagen2026-10-24 08:39 UTC87
              type="button"
d095131eec0eatagen2026-10-24 08:39 UTC88
              class="cell"
d095131eec0eatagen2026-10-24 08:39 UTC89
              title={image.body ?? image.shortcode}
d095131eec0eatagen2026-10-24 08:39 UTC90
              aria-label={image.body ?? image.shortcode}
d095131eec0eatagen2026-10-24 08:39 UTC91
              on:click={() => onSend(g.pack, image)}
d095131eec0eatagen2026-10-24 08:39 UTC92
              on:contextmenu|preventDefault={(ev) => onClone(g.pack, image, ev.clientX, ev.clientY)}
d095131eec0eatagen2026-10-24 08:39 UTC93
            >
d095131eec0eatagen2026-10-24 08:39 UTC94
              <img src={messageMediaUrl(image.media_id, 'Thumbnail')} alt="" loading="lazy" />
d095131eec0eatagen2026-10-24 08:39 UTC95
            </button>
d095131eec0eatagen2026-10-24 08:39 UTC96
          {/each}
d095131eec0eatagen2026-10-24 08:39 UTC97
        </div>
d095131eec0eatagen2026-10-24 08:39 UTC98
      </section>
d095131eec0eatagen2026-10-24 08:39 UTC99
    {:else}
05a48e9e7942atagen2026-10-24 09:32 UTC100
      {#if withStickers.length === 0}
05a48e9e7942atagen2026-10-24 09:32 UTC101
        <div class="empty">
05a48e9e7942atagen2026-10-24 09:32 UTC102
          <p class="status">No stickers yet</p>
05a48e9e7942atagen2026-10-24 09:32 UTC103
          <button type="button" class="primary-btn" on:click={onManage}>Add a pack</button>
05a48e9e7942atagen2026-10-24 09:32 UTC104
        </div>
05a48e9e7942atagen2026-10-24 09:32 UTC105
      {:else}
445bf199a9cfatagen2026-10-25 02:56 UTC106
        <p class="status">No stickers match "<EmojiText text={query.trim()} />"</p>
05a48e9e7942atagen2026-10-24 09:32 UTC107
      {/if}
d095131eec0eatagen2026-10-24 08:39 UTC108
    {/each}
d095131eec0eatagen2026-10-24 08:39 UTC109
  </div>
d095131eec0eatagen2026-10-24 08:39 UTC110
</div>
d095131eec0eatagen2026-10-24 08:39 UTC111
d095131eec0eatagen2026-10-24 08:39 UTC112
<style>
d095131eec0eatagen2026-10-24 08:39 UTC113
  .sticker-picker {
d095131eec0eatagen2026-10-24 08:39 UTC114
    display: flex;
d095131eec0eatagen2026-10-24 08:39 UTC115
    flex-direction: column;
d095131eec0eatagen2026-10-24 08:39 UTC116
    gap: 6px;
d095131eec0eatagen2026-10-24 08:39 UTC117
    width: 360px;
05a48e9e7942atagen2026-10-24 09:32 UTC118
    height: auto;
05a48e9e7942atagen2026-10-24 09:32 UTC119
    max-height: min(420px, calc(100vh - 120px));
d095131eec0eatagen2026-10-24 08:39 UTC120
    max-width: calc(100vw - 16px);
d095131eec0eatagen2026-10-24 08:39 UTC121
    padding: 8px;
d095131eec0eatagen2026-10-24 08:39 UTC122
    background: var(--panel);
d095131eec0eatagen2026-10-24 08:39 UTC123
    border: 1px solid var(--border);
05a48e9e7942atagen2026-10-24 09:32 UTC124
    border-radius: var(--r-md);
d095131eec0eatagen2026-10-24 08:39 UTC125
    box-shadow: 0 6px 24px rgb(0 0 0 / 0.5);
d095131eec0eatagen2026-10-24 08:39 UTC126
  }
d095131eec0eatagen2026-10-24 08:39 UTC127
  .top { display: flex; gap: 6px; }
d095131eec0eatagen2026-10-24 08:39 UTC128
  .search {
d095131eec0eatagen2026-10-24 08:39 UTC129
    flex: 1;
d095131eec0eatagen2026-10-24 08:39 UTC130
    background: var(--bg-soft);
d095131eec0eatagen2026-10-24 08:39 UTC131
    border: 1px solid var(--border);
05a48e9e7942atagen2026-10-24 09:32 UTC132
    border-radius: var(--r-md);
d095131eec0eatagen2026-10-24 08:39 UTC133
    color: var(--text);
d095131eec0eatagen2026-10-24 08:39 UTC134
    padding: 6px 10px;
d095131eec0eatagen2026-10-24 08:39 UTC135
    font-size: 13px;
d095131eec0eatagen2026-10-24 08:39 UTC136
  }
d095131eec0eatagen2026-10-24 08:39 UTC137
  .search:focus-visible { outline: none; border-color: var(--accent); }
d095131eec0eatagen2026-10-24 08:39 UTC138
  .tabs { display: flex; gap: 4px; overflow-x: auto; padding-bottom: 2px; }
d095131eec0eatagen2026-10-24 08:39 UTC139
  .tab {
d095131eec0eatagen2026-10-24 08:39 UTC140
    flex: none;
d095131eec0eatagen2026-10-24 08:39 UTC141
    display: grid;
d095131eec0eatagen2026-10-24 08:39 UTC142
    place-items: center;
d095131eec0eatagen2026-10-24 08:39 UTC143
    min-width: 32px;
d095131eec0eatagen2026-10-24 08:39 UTC144
    height: 32px;
d095131eec0eatagen2026-10-24 08:39 UTC145
    padding: 2px 6px;
d095131eec0eatagen2026-10-24 08:39 UTC146
    border: 1px solid transparent;
05a48e9e7942atagen2026-10-24 09:32 UTC147
    border-radius: var(--r-md);
d095131eec0eatagen2026-10-24 08:39 UTC148
    background: transparent;
d095131eec0eatagen2026-10-24 08:39 UTC149
    color: var(--muted);
d095131eec0eatagen2026-10-24 08:39 UTC150
    font-size: 12px;
d095131eec0eatagen2026-10-24 08:39 UTC151
    cursor: pointer;
d095131eec0eatagen2026-10-24 08:39 UTC152
  }
d095131eec0eatagen2026-10-24 08:39 UTC153
  .tab img { width: 24px; height: 24px; object-fit: contain; }
d095131eec0eatagen2026-10-24 08:39 UTC154
  .tab[aria-selected='true'] { border-color: var(--accent); color: var(--text); }
d095131eec0eatagen2026-10-24 08:39 UTC155
  .scroll { flex: 1; overflow-y: auto; min-height: 0; }
d095131eec0eatagen2026-10-24 08:39 UTC156
  .group-head { display: flex; align-items: center; justify-content: space-between; margin: 6px 2px 4px; }
d095131eec0eatagen2026-10-24 08:39 UTC157
  h3 { margin: 0; font-size: 12px; font-weight: 650; color: var(--muted); }
d095131eec0eatagen2026-10-24 08:39 UTC158
  .clone-pack {
d095131eec0eatagen2026-10-24 08:39 UTC159
    border: none;
d095131eec0eatagen2026-10-24 08:39 UTC160
    background: none;
d095131eec0eatagen2026-10-24 08:39 UTC161
    color: var(--accent);
d095131eec0eatagen2026-10-24 08:39 UTC162
    font-size: 12px;
d095131eec0eatagen2026-10-24 08:39 UTC163
    cursor: pointer;
d095131eec0eatagen2026-10-24 08:39 UTC164
  }
d095131eec0eatagen2026-10-24 08:39 UTC165
  .grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(76px, 1fr)); gap: 4px; }
d095131eec0eatagen2026-10-24 08:39 UTC166
  .cell {
d095131eec0eatagen2026-10-24 08:39 UTC167
    aspect-ratio: 1;
d095131eec0eatagen2026-10-24 08:39 UTC168
    padding: 4px;
d095131eec0eatagen2026-10-24 08:39 UTC169
    border: 1px solid transparent;
05a48e9e7942atagen2026-10-24 09:32 UTC170
    border-radius: var(--r-md);
d095131eec0eatagen2026-10-24 08:39 UTC171
    background: transparent;
d095131eec0eatagen2026-10-24 08:39 UTC172
    cursor: pointer;
d095131eec0eatagen2026-10-24 08:39 UTC173
  }
d095131eec0eatagen2026-10-24 08:39 UTC174
  .cell:hover, .cell:focus-visible { background: var(--hover); border-color: var(--border); outline: none; }
d095131eec0eatagen2026-10-24 08:39 UTC175
  .cell img { width: 100%; height: 100%; object-fit: contain; }
05a48e9e7942atagen2026-10-24 09:32 UTC176
  .status { color: var(--muted); font-size: 13px; text-align: center; margin: 16px 0 10px; }
05a48e9e7942atagen2026-10-24 09:32 UTC177
  .empty { display: flex; flex-direction: column; align-items: center; padding-bottom: 12px; }
d095131eec0eatagen2026-10-24 08:39 UTC178
</style>