d095131eec0e | atagen | 2026-10-24 08:39 UTC | 1 | <script lang="ts"> |
d095131eec0e | atagen | 2026-10-24 08:39 UTC | 2 | import { onMount } from 'svelte'; |
d095131eec0e | atagen | 2026-10-24 08:39 UTC | 3 | import { messageMediaUrl } from './api'; |
d095131eec0e | atagen | 2026-10-24 08:39 UTC | 4 | import MxcAvatar from './MxcAvatar.svelte'; |
445bf199a9cf | atagen | 2026-10-25 02:56 UTC | 5 | import EmojiText from './EmojiText.svelte'; |
d095131eec0e | atagen | 2026-10-24 08:39 UTC | 6 | import { packLabel, stickersOf } from './packs'; |
d095131eec0e | atagen | 2026-10-24 08:39 UTC | 7 | import type { ImagePack, PackImage } from './types'; |
d095131eec0e | atagen | 2026-10-24 08:39 UTC | 8 | |
d095131eec0e | atagen | 2026-10-24 08:39 UTC | 9 | export let packs: ImagePack[]; |
d095131eec0e | atagen | 2026-10-24 08:39 UTC | 10 | export let roomName: (roomId: string) => string; |
d095131eec0e | atagen | 2026-10-24 08:39 UTC | 11 | export let onSend: (pack: ImagePack, image: PackImage) => void; |
d095131eec0e | atagen | 2026-10-24 08:39 UTC | 12 | export let onClone: (pack: ImagePack, image: PackImage, x: number, y: number) => void; |
d095131eec0e | atagen | 2026-10-24 08:39 UTC | 13 | export let onClonePack: (pack: ImagePack, x: number, y: number) => void; |
d095131eec0e | atagen | 2026-10-24 08:39 UTC | 14 | export let onManage: () => void; |
d095131eec0e | atagen | 2026-10-24 08:39 UTC | 15 | export let onClose: () => void; |
d095131eec0e | atagen | 2026-10-24 08:39 UTC | 16 | |
d095131eec0e | atagen | 2026-10-24 08:39 UTC | 17 | let root: HTMLDivElement; |
d095131eec0e | atagen | 2026-10-24 08:39 UTC | 18 | let search: HTMLInputElement; |
d095131eec0e | atagen | 2026-10-24 08:39 UTC | 19 | let query = ''; |
d095131eec0e | atagen | 2026-10-24 08:39 UTC | 20 | let activeId: string | null = null; |
d095131eec0e | atagen | 2026-10-24 08:39 UTC | 21 | |
d095131eec0e | atagen | 2026-10-24 08:39 UTC | 22 | $: withStickers = packs.filter((p) => stickersOf(p).length > 0); |
d095131eec0e | atagen | 2026-10-24 08:39 UTC | 23 | $: q = query.trim().toLowerCase(); |
d095131eec0e | atagen | 2026-10-24 08:39 UTC | 24 | $: shown = withStickers |
d095131eec0e | atagen | 2026-10-24 08:39 UTC | 25 | .map((pack) => ({ |
d095131eec0e | atagen | 2026-10-24 08:39 UTC | 26 | pack, |
d095131eec0e | atagen | 2026-10-24 08:39 UTC | 27 | images: stickersOf(pack).filter( |
d095131eec0e | atagen | 2026-10-24 08:39 UTC | 28 | (i) => !q || i.shortcode.toLowerCase().includes(q) || (i.body ?? '').toLowerCase().includes(q) |
d095131eec0e | atagen | 2026-10-24 08:39 UTC | 29 | ) |
d095131eec0e | atagen | 2026-10-24 08:39 UTC | 30 | })) |
d095131eec0e | atagen | 2026-10-24 08:39 UTC | 31 | .filter((g) => g.images.length > 0 && (q || activeId === null || g.pack.id === activeId)); |
d095131eec0e | atagen | 2026-10-24 08:39 UTC | 32 | |
d095131eec0e | atagen | 2026-10-24 08:39 UTC | 33 | onMount(() => search?.focus()); |
d095131eec0e | atagen | 2026-10-24 08:39 UTC | 34 | |
d095131eec0e | atagen | 2026-10-24 08:39 UTC | 35 | function outside(ev: PointerEvent): void { |
d095131eec0e | atagen | 2026-10-24 08:39 UTC | 36 | if (!root.contains(ev.target as Node) && !(ev.target as Element).closest?.('.context-menu, .sticker-btn')) onClose(); |
d095131eec0e | atagen | 2026-10-24 08:39 UTC | 37 | } |
d095131eec0e | atagen | 2026-10-24 08:39 UTC | 38 | |
d095131eec0e | atagen | 2026-10-24 08:39 UTC | 39 | function key(ev: KeyboardEvent): void { |
d095131eec0e | atagen | 2026-10-24 08:39 UTC | 40 | if (ev.key === 'Escape' && !document.querySelector('.context-menu')) { |
d095131eec0e | atagen | 2026-10-24 08:39 UTC | 41 | ev.stopPropagation(); |
d095131eec0e | atagen | 2026-10-24 08:39 UTC | 42 | onClose(); |
d095131eec0e | atagen | 2026-10-24 08:39 UTC | 43 | } |
d095131eec0e | atagen | 2026-10-24 08:39 UTC | 44 | } |
d095131eec0e | atagen | 2026-10-24 08:39 UTC | 45 | </script> |
d095131eec0e | atagen | 2026-10-24 08:39 UTC | 46 | |
d095131eec0e | atagen | 2026-10-24 08:39 UTC | 47 | <svelte:window on:pointerdown|capture={outside} on:keydown|capture={key} /> |
d095131eec0e | atagen | 2026-10-24 08:39 UTC | 48 | |
d095131eec0e | atagen | 2026-10-24 08:39 UTC | 49 | <div class="sticker-picker" role="dialog" aria-label="Stickers" bind:this={root}> |
d095131eec0e | atagen | 2026-10-24 08:39 UTC | 50 | <div class="top"> |
d095131eec0e | atagen | 2026-10-24 08:39 UTC | 51 | <input class="search" type="search" placeholder="Search stickers" bind:value={query} bind:this={search} /> |
d095131eec0e | atagen | 2026-10-24 08:39 UTC | 52 | <button type="button" class="icon-btn" title="Manage packs" aria-label="Manage packs" on:click={onManage}> |
05a48e9e7942 | atagen | 2026-10-24 09:32 UTC | 53 | <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> |
d095131eec0e | atagen | 2026-10-24 08:39 UTC | 54 | </button> |
d095131eec0e | atagen | 2026-10-24 08:39 UTC | 55 | </div> |
d095131eec0e | atagen | 2026-10-24 08:39 UTC | 56 | {#if withStickers.length > 1} |
d095131eec0e | atagen | 2026-10-24 08:39 UTC | 57 | <div class="tabs" role="tablist" aria-label="Packs"> |
d095131eec0e | atagen | 2026-10-24 08:39 UTC | 58 | <button type="button" role="tab" class="tab all" aria-selected={activeId === null} on:click={() => (activeId = null)}>All</button> |
d095131eec0e | atagen | 2026-10-24 08:39 UTC | 59 | {#each withStickers as pack (pack.id)} |
d095131eec0e | atagen | 2026-10-24 08:39 UTC | 60 | <button |
d095131eec0e | atagen | 2026-10-24 08:39 UTC | 61 | type="button" |
d095131eec0e | atagen | 2026-10-24 08:39 UTC | 62 | role="tab" |
d095131eec0e | atagen | 2026-10-24 08:39 UTC | 63 | class="tab" |
d095131eec0e | atagen | 2026-10-24 08:39 UTC | 64 | aria-selected={activeId === pack.id} |
d095131eec0e | atagen | 2026-10-24 08:39 UTC | 65 | title={packLabel(pack, roomName)} |
d095131eec0e | atagen | 2026-10-24 08:39 UTC | 66 | on:click={() => (activeId = pack.id)} |
d095131eec0e | atagen | 2026-10-24 08:39 UTC | 67 | > |
d095131eec0e | atagen | 2026-10-24 08:39 UTC | 68 | {#if pack.avatar} |
d095131eec0e | atagen | 2026-10-24 08:39 UTC | 69 | <MxcAvatar mxc={pack.avatar} label={packLabel(pack, roomName)} size={24} shape="rounded" /> |
d095131eec0e | atagen | 2026-10-24 08:39 UTC | 70 | {:else} |
d095131eec0e | atagen | 2026-10-24 08:39 UTC | 71 | <img src={messageMediaUrl(stickersOf(pack)[0].media_id, 'Thumbnail')} alt={packLabel(pack, roomName)} /> |
d095131eec0e | atagen | 2026-10-24 08:39 UTC | 72 | {/if} |
d095131eec0e | atagen | 2026-10-24 08:39 UTC | 73 | </button> |
d095131eec0e | atagen | 2026-10-24 08:39 UTC | 74 | {/each} |
d095131eec0e | atagen | 2026-10-24 08:39 UTC | 75 | </div> |
d095131eec0e | atagen | 2026-10-24 08:39 UTC | 76 | {/if} |
d095131eec0e | atagen | 2026-10-24 08:39 UTC | 77 | <div class="scroll"> |
d095131eec0e | atagen | 2026-10-24 08:39 UTC | 78 | {#each shown as g (g.pack.id)} |
d095131eec0e | atagen | 2026-10-24 08:39 UTC | 79 | <section class="group" aria-label={packLabel(g.pack, roomName)}> |
d095131eec0e | atagen | 2026-10-24 08:39 UTC | 80 | <div class="group-head"> |
445bf199a9cf | atagen | 2026-10-25 02:56 UTC | 81 | <h3><EmojiText text={packLabel(g.pack, roomName)} /></h3> |
d095131eec0e | atagen | 2026-10-24 08:39 UTC | 82 | {#if !g.pack.editable} |
d095131eec0e | atagen | 2026-10-24 08:39 UTC | 83 | <button type="button" class="clone-pack" on:click={(ev) => onClonePack(g.pack, ev.clientX, ev.clientY)}>Clone pack</button> |
d095131eec0e | atagen | 2026-10-24 08:39 UTC | 84 | {/if} |
d095131eec0e | atagen | 2026-10-24 08:39 UTC | 85 | </div> |
d095131eec0e | atagen | 2026-10-24 08:39 UTC | 86 | <div class="grid"> |
d095131eec0e | atagen | 2026-10-24 08:39 UTC | 87 | {#each g.images as image (image.shortcode)} |
d095131eec0e | atagen | 2026-10-24 08:39 UTC | 88 | <button |
d095131eec0e | atagen | 2026-10-24 08:39 UTC | 89 | type="button" |
d095131eec0e | atagen | 2026-10-24 08:39 UTC | 90 | class="cell" |
d095131eec0e | atagen | 2026-10-24 08:39 UTC | 91 | title={image.body ?? image.shortcode} |
d095131eec0e | atagen | 2026-10-24 08:39 UTC | 92 | aria-label={image.body ?? image.shortcode} |
d095131eec0e | atagen | 2026-10-24 08:39 UTC | 93 | on:click={() => onSend(g.pack, image)} |
d095131eec0e | atagen | 2026-10-24 08:39 UTC | 94 | on:contextmenu|preventDefault={(ev) => onClone(g.pack, image, ev.clientX, ev.clientY)} |
d095131eec0e | atagen | 2026-10-24 08:39 UTC | 95 | > |
d095131eec0e | atagen | 2026-10-24 08:39 UTC | 96 | <img src={messageMediaUrl(image.media_id, 'Thumbnail')} alt="" loading="lazy" /> |
d095131eec0e | atagen | 2026-10-24 08:39 UTC | 97 | </button> |
d095131eec0e | atagen | 2026-10-24 08:39 UTC | 98 | {/each} |
d095131eec0e | atagen | 2026-10-24 08:39 UTC | 99 | </div> |
d095131eec0e | atagen | 2026-10-24 08:39 UTC | 100 | </section> |
d095131eec0e | atagen | 2026-10-24 08:39 UTC | 101 | {:else} |
05a48e9e7942 | atagen | 2026-10-24 09:32 UTC | 102 | {#if withStickers.length === 0} |
05a48e9e7942 | atagen | 2026-10-24 09:32 UTC | 103 | <div class="empty"> |
05a48e9e7942 | atagen | 2026-10-24 09:32 UTC | 104 | <p class="status">No stickers yet</p> |
05a48e9e7942 | atagen | 2026-10-24 09:32 UTC | 105 | <button type="button" class="primary-btn" on:click={onManage}>Add a pack</button> |
05a48e9e7942 | atagen | 2026-10-24 09:32 UTC | 106 | </div> |
05a48e9e7942 | atagen | 2026-10-24 09:32 UTC | 107 | {:else} |
445bf199a9cf | atagen | 2026-10-25 02:56 UTC | 108 | <p class="status">No stickers match "<EmojiText text={query.trim()} />"</p> |
05a48e9e7942 | atagen | 2026-10-24 09:32 UTC | 109 | {/if} |
d095131eec0e | atagen | 2026-10-24 08:39 UTC | 110 | {/each} |
d095131eec0e | atagen | 2026-10-24 08:39 UTC | 111 | </div> |
d095131eec0e | atagen | 2026-10-24 08:39 UTC | 112 | </div> |
d095131eec0e | atagen | 2026-10-24 08:39 UTC | 113 | |
d095131eec0e | atagen | 2026-10-24 08:39 UTC | 114 | <style> |
d095131eec0e | atagen | 2026-10-24 08:39 UTC | 115 | .sticker-picker { |
d095131eec0e | atagen | 2026-10-24 08:39 UTC | 116 | display: flex; |
d095131eec0e | atagen | 2026-10-24 08:39 UTC | 117 | flex-direction: column; |
d095131eec0e | atagen | 2026-10-24 08:39 UTC | 118 | gap: 6px; |
d095131eec0e | atagen | 2026-10-24 08:39 UTC | 119 | width: 360px; |
05a48e9e7942 | atagen | 2026-10-24 09:32 UTC | 120 | height: auto; |
05a48e9e7942 | atagen | 2026-10-24 09:32 UTC | 121 | max-height: min(420px, calc(100vh - 120px)); |
d095131eec0e | atagen | 2026-10-24 08:39 UTC | 122 | max-width: calc(100vw - 16px); |
d095131eec0e | atagen | 2026-10-24 08:39 UTC | 123 | padding: 8px; |
d095131eec0e | atagen | 2026-10-24 08:39 UTC | 124 | background: var(--panel); |
d095131eec0e | atagen | 2026-10-24 08:39 UTC | 125 | border: 1px solid var(--border); |
05a48e9e7942 | atagen | 2026-10-24 09:32 UTC | 126 | border-radius: var(--r-md); |
d095131eec0e | atagen | 2026-10-24 08:39 UTC | 127 | box-shadow: 0 6px 24px rgb(0 0 0 / 0.5); |
d095131eec0e | atagen | 2026-10-24 08:39 UTC | 128 | } |
d095131eec0e | atagen | 2026-10-24 08:39 UTC | 129 | .top { display: flex; gap: 6px; } |
d095131eec0e | atagen | 2026-10-24 08:39 UTC | 130 | .search { |
d095131eec0e | atagen | 2026-10-24 08:39 UTC | 131 | flex: 1; |
d095131eec0e | atagen | 2026-10-24 08:39 UTC | 132 | background: var(--bg-soft); |
d095131eec0e | atagen | 2026-10-24 08:39 UTC | 133 | border: 1px solid var(--border); |
05a48e9e7942 | atagen | 2026-10-24 09:32 UTC | 134 | border-radius: var(--r-md); |
d095131eec0e | atagen | 2026-10-24 08:39 UTC | 135 | color: var(--text); |
d095131eec0e | atagen | 2026-10-24 08:39 UTC | 136 | padding: 6px 10px; |
d095131eec0e | atagen | 2026-10-24 08:39 UTC | 137 | font-size: 13px; |
d095131eec0e | atagen | 2026-10-24 08:39 UTC | 138 | } |
d095131eec0e | atagen | 2026-10-24 08:39 UTC | 139 | .search:focus-visible { outline: none; border-color: var(--accent); } |
d095131eec0e | atagen | 2026-10-24 08:39 UTC | 140 | .tabs { display: flex; gap: 4px; overflow-x: auto; padding-bottom: 2px; } |
d095131eec0e | atagen | 2026-10-24 08:39 UTC | 141 | .tab { |
d095131eec0e | atagen | 2026-10-24 08:39 UTC | 142 | flex: none; |
d095131eec0e | atagen | 2026-10-24 08:39 UTC | 143 | display: grid; |
d095131eec0e | atagen | 2026-10-24 08:39 UTC | 144 | place-items: center; |
d095131eec0e | atagen | 2026-10-24 08:39 UTC | 145 | min-width: 32px; |
d095131eec0e | atagen | 2026-10-24 08:39 UTC | 146 | height: 32px; |
d095131eec0e | atagen | 2026-10-24 08:39 UTC | 147 | padding: 2px 6px; |
d095131eec0e | atagen | 2026-10-24 08:39 UTC | 148 | border: 1px solid transparent; |
05a48e9e7942 | atagen | 2026-10-24 09:32 UTC | 149 | border-radius: var(--r-md); |
d095131eec0e | atagen | 2026-10-24 08:39 UTC | 150 | background: transparent; |
d095131eec0e | atagen | 2026-10-24 08:39 UTC | 151 | color: var(--muted); |
d095131eec0e | atagen | 2026-10-24 08:39 UTC | 152 | font-size: 12px; |
d095131eec0e | atagen | 2026-10-24 08:39 UTC | 153 | cursor: pointer; |
d095131eec0e | atagen | 2026-10-24 08:39 UTC | 154 | } |
d095131eec0e | atagen | 2026-10-24 08:39 UTC | 155 | .tab img { width: 24px; height: 24px; object-fit: contain; } |
d095131eec0e | atagen | 2026-10-24 08:39 UTC | 156 | .tab[aria-selected='true'] { border-color: var(--accent); color: var(--text); } |
d095131eec0e | atagen | 2026-10-24 08:39 UTC | 157 | .scroll { flex: 1; overflow-y: auto; min-height: 0; } |
d095131eec0e | atagen | 2026-10-24 08:39 UTC | 158 | .group-head { display: flex; align-items: center; justify-content: space-between; margin: 6px 2px 4px; } |
d095131eec0e | atagen | 2026-10-24 08:39 UTC | 159 | h3 { margin: 0; font-size: 12px; font-weight: 650; color: var(--muted); } |
d095131eec0e | atagen | 2026-10-24 08:39 UTC | 160 | .clone-pack { |
d095131eec0e | atagen | 2026-10-24 08:39 UTC | 161 | border: none; |
d095131eec0e | atagen | 2026-10-24 08:39 UTC | 162 | background: none; |
d095131eec0e | atagen | 2026-10-24 08:39 UTC | 163 | color: var(--accent); |
d095131eec0e | atagen | 2026-10-24 08:39 UTC | 164 | font-size: 12px; |
d095131eec0e | atagen | 2026-10-24 08:39 UTC | 165 | cursor: pointer; |
d095131eec0e | atagen | 2026-10-24 08:39 UTC | 166 | } |
d095131eec0e | atagen | 2026-10-24 08:39 UTC | 167 | .grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(76px, 1fr)); gap: 4px; } |
d095131eec0e | atagen | 2026-10-24 08:39 UTC | 168 | .cell { |
d095131eec0e | atagen | 2026-10-24 08:39 UTC | 169 | aspect-ratio: 1; |
d095131eec0e | atagen | 2026-10-24 08:39 UTC | 170 | padding: 4px; |
d095131eec0e | atagen | 2026-10-24 08:39 UTC | 171 | border: 1px solid transparent; |
05a48e9e7942 | atagen | 2026-10-24 09:32 UTC | 172 | border-radius: var(--r-md); |
d095131eec0e | atagen | 2026-10-24 08:39 UTC | 173 | background: transparent; |
d095131eec0e | atagen | 2026-10-24 08:39 UTC | 174 | cursor: pointer; |
d095131eec0e | atagen | 2026-10-24 08:39 UTC | 175 | } |
d095131eec0e | atagen | 2026-10-24 08:39 UTC | 176 | .cell:hover, .cell:focus-visible { background: var(--hover); border-color: var(--border); outline: none; } |
d095131eec0e | atagen | 2026-10-24 08:39 UTC | 177 | .cell img { width: 100%; height: 100%; object-fit: contain; } |
05a48e9e7942 | atagen | 2026-10-24 09:32 UTC | 178 | .status { color: var(--muted); font-size: 13px; text-align: center; margin: 16px 0 10px; } |
05a48e9e7942 | atagen | 2026-10-24 09:32 UTC | 179 | .empty { display: flex; flex-direction: column; align-items: center; padding-bottom: 12px; } |
d095131eec0e | atagen | 2026-10-24 08:39 UTC | 180 | </style> |