atagen

malice

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

Blame: web/src/lib/ReactionPicker.svelte

544a170ead2catagen2026-10-20 11:47 UTC1
<script lang="ts">
445bf199a9cfatagen2026-10-25 02:56 UTC2
  import EmojiText from './EmojiText.svelte';
544a170ead2catagen2026-10-20 11:47 UTC3
  import { onMount } from 'svelte';
ecb8598b38afatagen2026-10-22 09:19 UTC4
  import { mediaUrl, messageMediaUrl } from './api';
544a170ead2catagen2026-10-20 11:47 UTC5
  import Emoji from './Emoji.svelte';
544a170ead2catagen2026-10-20 11:47 UTC6
  import { loadEmoji, searchEmoji } from './emoji';
544a170ead2catagen2026-10-20 11:47 UTC7
  import type { EmojiGroup } from './emoji';
544a170ead2catagen2026-10-20 11:47 UTC8
  import { MAX_QUICK_REACTIONS } from './types';
544a170ead2catagen2026-10-20 11:47 UTC9
  import type { ImagePack, PackImage, QuickReaction } from './types';
132eeceeb362atagen2026-10-25 03:34 UTC10
  import { hasUsage } from './packs';
544a170ead2catagen2026-10-20 11:47 UTC11
544a170ead2catagen2026-10-20 11:47 UTC12
  export let packs: ImagePack[];
544a170ead2catagen2026-10-20 11:47 UTC13
  export let quick: QuickReaction[];
544a170ead2catagen2026-10-20 11:47 UTC14
  export let onPick: (key: string, shortcode: string | null) => void;
544a170ead2catagen2026-10-20 11:47 UTC15
  export let onQuickChange: (list: QuickReaction[]) => void;
544a170ead2catagen2026-10-20 11:47 UTC16
  export let onClose: () => void;
df3c0f2b3da4atagen2026-10-22 13:07 UTC17
  export let embedded = false;
544a170ead2catagen2026-10-20 11:47 UTC18
544a170ead2catagen2026-10-20 11:47 UTC19
  let root: HTMLDivElement;
544a170ead2catagen2026-10-20 11:47 UTC20
  let input: HTMLInputElement;
544a170ead2catagen2026-10-20 11:47 UTC21
  let query = '';
df3c0f2b3da4atagen2026-10-22 13:07 UTC22
  let customising = embedded;
544a170ead2catagen2026-10-20 11:47 UTC23
  let groups: EmojiGroup[] | null = null;
544a170ead2catagen2026-10-20 11:47 UTC24
  let loadFailed = false;
544a170ead2catagen2026-10-20 11:47 UTC25
544a170ead2catagen2026-10-20 11:47 UTC26
  onMount(() => {
df3c0f2b3da4atagen2026-10-22 13:07 UTC27
    if (!embedded) input.focus();
544a170ead2catagen2026-10-20 11:47 UTC28
    loadEmoji().then(
544a170ead2catagen2026-10-20 11:47 UTC29
      (g) => (groups = g),
544a170ead2catagen2026-10-20 11:47 UTC30
      () => (loadFailed = true)
544a170ead2catagen2026-10-20 11:47 UTC31
    );
544a170ead2catagen2026-10-20 11:47 UTC32
  });
544a170ead2catagen2026-10-20 11:47 UTC33
544a170ead2catagen2026-10-20 11:47 UTC34
  $: q = query.trim().toLowerCase().replace(/^:|:$/g, '');
544a170ead2catagen2026-10-20 11:47 UTC35
  $: filteredPacks = packs
544a170ead2catagen2026-10-20 11:47 UTC36
    .map((pack) => ({ pack, images: pack.images.filter((i) => imageMatches(i, q)) }))
544a170ead2catagen2026-10-20 11:47 UTC37
    .filter((p) => p.images.length > 0);
544a170ead2catagen2026-10-20 11:47 UTC38
  $: filteredGroups = searchEmoji(groups ?? [], q);
544a170ead2catagen2026-10-20 11:47 UTC39
  $: full = quick.length >= MAX_QUICK_REACTIONS;
544a170ead2catagen2026-10-20 11:47 UTC40
  $: nothingFound = q !== '' && groups !== null && filteredPacks.length === 0 && filteredGroups.length === 0;
544a170ead2catagen2026-10-20 11:47 UTC41
544a170ead2catagen2026-10-20 11:47 UTC42
  function imageMatches(image: PackImage, needle: string): boolean {
132eeceeb362atagen2026-10-25 03:34 UTC43
    if (!hasUsage(image, 'Emoticon')) return false;
544a170ead2catagen2026-10-20 11:47 UTC44
    if (!needle) return true;
544a170ead2catagen2026-10-20 11:47 UTC45
    return image.shortcode.toLowerCase().includes(needle) || (image.body ?? '').toLowerCase().includes(needle);
544a170ead2catagen2026-10-20 11:47 UTC46
  }
544a170ead2catagen2026-10-20 11:47 UTC47
ecb8598b38afatagen2026-10-22 09:19 UTC48
  function packName(pack: ImagePack): string {
ecb8598b38afatagen2026-10-22 09:19 UTC49
    return pack.display_name ?? 'Emoji pack';
ecb8598b38afatagen2026-10-22 09:19 UTC50
  }
ecb8598b38afatagen2026-10-22 09:19 UTC51
544a170ead2catagen2026-10-20 11:47 UTC52
  function isQuick(key: string): boolean {
544a170ead2catagen2026-10-20 11:47 UTC53
    return quick.some((r) => r.key === key);
544a170ead2catagen2026-10-20 11:47 UTC54
  }
544a170ead2catagen2026-10-20 11:47 UTC55
544a170ead2catagen2026-10-20 11:47 UTC56
  function choose(key: string, shortcode: string | null): void {
544a170ead2catagen2026-10-20 11:47 UTC57
    if (!customising) {
544a170ead2catagen2026-10-20 11:47 UTC58
      onPick(key, shortcode);
544a170ead2catagen2026-10-20 11:47 UTC59
      return;
544a170ead2catagen2026-10-20 11:47 UTC60
    }
544a170ead2catagen2026-10-20 11:47 UTC61
    if (isQuick(key)) onQuickChange(quick.filter((r) => r.key !== key));
544a170ead2catagen2026-10-20 11:47 UTC62
    else if (!full) onQuickChange([...quick, { key, shortcode }]);
544a170ead2catagen2026-10-20 11:47 UTC63
  }
544a170ead2catagen2026-10-20 11:47 UTC64
544a170ead2catagen2026-10-20 11:47 UTC65
  function quickLabel(r: QuickReaction): string {
544a170ead2catagen2026-10-20 11:47 UTC66
    return r.shortcode ? `:${r.shortcode}:` : r.key;
544a170ead2catagen2026-10-20 11:47 UTC67
  }
544a170ead2catagen2026-10-20 11:47 UTC68
544a170ead2catagen2026-10-20 11:47 UTC69
  function onKeydown(e: KeyboardEvent): void {
544a170ead2catagen2026-10-20 11:47 UTC70
    if (e.key !== 'Escape') return;
544a170ead2catagen2026-10-20 11:47 UTC71
    e.stopPropagation();
544a170ead2catagen2026-10-20 11:47 UTC72
    onClose();
544a170ead2catagen2026-10-20 11:47 UTC73
  }
544a170ead2catagen2026-10-20 11:47 UTC74
544a170ead2catagen2026-10-20 11:47 UTC75
  function closeOutside(e: PointerEvent): void {
df3c0f2b3da4atagen2026-10-22 13:07 UTC76
    if (!embedded && !root.contains(e.target as Node)) onClose();
544a170ead2catagen2026-10-20 11:47 UTC77
  }
544a170ead2catagen2026-10-20 11:47 UTC78
</script>
544a170ead2catagen2026-10-20 11:47 UTC79
544a170ead2catagen2026-10-20 11:47 UTC80
<svelte:window on:pointerdown={closeOutside} />
544a170ead2catagen2026-10-20 11:47 UTC81
544a170ead2catagen2026-10-20 11:47 UTC82
<div
544a170ead2catagen2026-10-20 11:47 UTC83
  class="picker"
df3c0f2b3da4atagen2026-10-22 13:07 UTC84
  class:embedded
544a170ead2catagen2026-10-20 11:47 UTC85
  role="dialog"
544a170ead2catagen2026-10-20 11:47 UTC86
  tabindex="-1"
544a170ead2catagen2026-10-20 11:47 UTC87
  aria-label={customising ? 'Customise quick reactions' : 'Pick a reaction'}
544a170ead2catagen2026-10-20 11:47 UTC88
  bind:this={root}
544a170ead2catagen2026-10-20 11:47 UTC89
  on:keydown={onKeydown}
544a170ead2catagen2026-10-20 11:47 UTC90
>
544a170ead2catagen2026-10-20 11:47 UTC91
  <input
544a170ead2catagen2026-10-20 11:47 UTC92
    class="search"
544a170ead2catagen2026-10-20 11:47 UTC93
    type="search"
544a170ead2catagen2026-10-20 11:47 UTC94
    placeholder="Search emoji"
544a170ead2catagen2026-10-20 11:47 UTC95
    aria-label="Search emoji"
544a170ead2catagen2026-10-20 11:47 UTC96
    autocomplete="off"
544a170ead2catagen2026-10-20 11:47 UTC97
    bind:this={input}
544a170ead2catagen2026-10-20 11:47 UTC98
    bind:value={query}
544a170ead2catagen2026-10-20 11:47 UTC99
  />
544a170ead2catagen2026-10-20 11:47 UTC100
544a170ead2catagen2026-10-20 11:47 UTC101
  {#if customising}
544a170ead2catagen2026-10-20 11:47 UTC102
    <div class="quick" role="group" aria-label="Quick reactions">
544a170ead2catagen2026-10-20 11:47 UTC103
      <div class="quick-head">
544a170ead2catagen2026-10-20 11:47 UTC104
        <span>Quick reactions</span>
544a170ead2catagen2026-10-20 11:47 UTC105
        <span class="muted">{quick.length}/{MAX_QUICK_REACTIONS}</span>
544a170ead2catagen2026-10-20 11:47 UTC106
      </div>
544a170ead2catagen2026-10-20 11:47 UTC107
      <div class="quick-row">
544a170ead2catagen2026-10-20 11:47 UTC108
        {#each quick as r (r.key)}
544a170ead2catagen2026-10-20 11:47 UTC109
          <span class="quick-item">
544a170ead2catagen2026-10-20 11:47 UTC110
            {#if r.key.startsWith('mxc://')}
7643577c76bbatagen2026-10-25 07:37 UTC111
              <img src={mediaUrl(r.key, 64) ?? ''} alt={quickLabel(r)} />
544a170ead2catagen2026-10-20 11:47 UTC112
            {:else}
544a170ead2catagen2026-10-20 11:47 UTC113
              <Emoji emoji={r.key} size={18} />
544a170ead2catagen2026-10-20 11:47 UTC114
            {/if}
544a170ead2catagen2026-10-20 11:47 UTC115
            <button
544a170ead2catagen2026-10-20 11:47 UTC116
              type="button"
544a170ead2catagen2026-10-20 11:47 UTC117
              class="remove"
544a170ead2catagen2026-10-20 11:47 UTC118
              aria-label={`Remove ${quickLabel(r)} from quick reactions`}
544a170ead2catagen2026-10-20 11:47 UTC119
              on:click={() => onQuickChange(quick.filter((x) => x.key !== r.key))}
544a170ead2catagen2026-10-20 11:47 UTC120
            >
544a170ead2catagen2026-10-20 11:47 UTC121
              ×
544a170ead2catagen2026-10-20 11:47 UTC122
            </button>
544a170ead2catagen2026-10-20 11:47 UTC123
          </span>
544a170ead2catagen2026-10-20 11:47 UTC124
        {:else}
544a170ead2catagen2026-10-20 11:47 UTC125
          <span class="muted">None. Pick emoji below to add them.</span>
544a170ead2catagen2026-10-20 11:47 UTC126
        {/each}
544a170ead2catagen2026-10-20 11:47 UTC127
      </div>
544a170ead2catagen2026-10-20 11:47 UTC128
    </div>
544a170ead2catagen2026-10-20 11:47 UTC129
  {/if}
544a170ead2catagen2026-10-20 11:47 UTC130
544a170ead2catagen2026-10-20 11:47 UTC131
  <div class="scroll" aria-busy={groups === null && !loadFailed}>
544a170ead2catagen2026-10-20 11:47 UTC132
    {#each filteredPacks as { pack, images } (pack.id)}
ecb8598b38afatagen2026-10-22 09:19 UTC133
      <section class="group" aria-label={packName(pack)}>
445bf199a9cfatagen2026-10-25 02:56 UTC134
        <h3><EmojiText text={packName(pack)} /></h3>
544a170ead2catagen2026-10-20 11:47 UTC135
        <div class="grid">
ecb8598b38afatagen2026-10-22 09:19 UTC136
          {#each images as image (image.reaction_key + image.shortcode)}
544a170ead2catagen2026-10-20 11:47 UTC137
            <button
544a170ead2catagen2026-10-20 11:47 UTC138
              type="button"
544a170ead2catagen2026-10-20 11:47 UTC139
              class="cell"
ecb8598b38afatagen2026-10-22 09:19 UTC140
              class:chosen={customising && isQuick(image.reaction_key)}
544a170ead2catagen2026-10-20 11:47 UTC141
              title={`:${image.shortcode}:`}
544a170ead2catagen2026-10-20 11:47 UTC142
              aria-label={`:${image.shortcode}:`}
ecb8598b38afatagen2026-10-22 09:19 UTC143
              aria-pressed={customising ? isQuick(image.reaction_key) : undefined}
ecb8598b38afatagen2026-10-22 09:19 UTC144
              disabled={customising && full && !isQuick(image.reaction_key)}
ecb8598b38afatagen2026-10-22 09:19 UTC145
              on:click={() => choose(image.reaction_key, image.shortcode)}
544a170ead2catagen2026-10-20 11:47 UTC146
            >
ecb8598b38afatagen2026-10-22 09:19 UTC147
              <img src={messageMediaUrl(image.media_id, 'Thumbnail')} alt="" loading="lazy" />
544a170ead2catagen2026-10-20 11:47 UTC148
            </button>
544a170ead2catagen2026-10-20 11:47 UTC149
          {/each}
544a170ead2catagen2026-10-20 11:47 UTC150
        </div>
544a170ead2catagen2026-10-20 11:47 UTC151
      </section>
544a170ead2catagen2026-10-20 11:47 UTC152
    {/each}
544a170ead2catagen2026-10-20 11:47 UTC153
544a170ead2catagen2026-10-20 11:47 UTC154
    {#if loadFailed}
544a170ead2catagen2026-10-20 11:47 UTC155
      <p class="status" role="alert">Could not load the emoji list.</p>
544a170ead2catagen2026-10-20 11:47 UTC156
    {:else if groups === null}
544a170ead2catagen2026-10-20 11:47 UTC157
      <p class="status" role="status">Loading emoji…</p>
544a170ead2catagen2026-10-20 11:47 UTC158
    {:else}
544a170ead2catagen2026-10-20 11:47 UTC159
      {#each filteredGroups as g (g.name)}
544a170ead2catagen2026-10-20 11:47 UTC160
        <section class="group" aria-label={g.name}>
544a170ead2catagen2026-10-20 11:47 UTC161
          <h3>{g.name}</h3>
544a170ead2catagen2026-10-20 11:47 UTC162
          <div class="grid">
544a170ead2catagen2026-10-20 11:47 UTC163
            {#each g.emojis as e (e.emoji)}
544a170ead2catagen2026-10-20 11:47 UTC164
              <button
544a170ead2catagen2026-10-20 11:47 UTC165
                type="button"
544a170ead2catagen2026-10-20 11:47 UTC166
                class="cell"
544a170ead2catagen2026-10-20 11:47 UTC167
                class:chosen={customising && isQuick(e.emoji)}
544a170ead2catagen2026-10-20 11:47 UTC168
                title={e.label}
544a170ead2catagen2026-10-20 11:47 UTC169
                aria-label={e.label}
544a170ead2catagen2026-10-20 11:47 UTC170
                aria-pressed={customising ? isQuick(e.emoji) : undefined}
544a170ead2catagen2026-10-20 11:47 UTC171
                disabled={customising && full && !isQuick(e.emoji)}
544a170ead2catagen2026-10-20 11:47 UTC172
                on:click={() => choose(e.emoji, e.shortcodes[0] ?? null)}
544a170ead2catagen2026-10-20 11:47 UTC173
              >
544a170ead2catagen2026-10-20 11:47 UTC174
                <Emoji emoji={e.emoji} size={24} />
544a170ead2catagen2026-10-20 11:47 UTC175
              </button>
544a170ead2catagen2026-10-20 11:47 UTC176
            {/each}
544a170ead2catagen2026-10-20 11:47 UTC177
          </div>
544a170ead2catagen2026-10-20 11:47 UTC178
        </section>
544a170ead2catagen2026-10-20 11:47 UTC179
      {/each}
544a170ead2catagen2026-10-20 11:47 UTC180
    {/if}
544a170ead2catagen2026-10-20 11:47 UTC181
    {#if nothingFound}
445bf199a9cfatagen2026-10-25 02:56 UTC182
      <p class="status" role="status">No emoji match "<EmojiText text={query.trim()} />".</p>
544a170ead2catagen2026-10-20 11:47 UTC183
    {/if}
544a170ead2catagen2026-10-20 11:47 UTC184
  </div>
544a170ead2catagen2026-10-20 11:47 UTC185
05a48e9e7942atagen2026-10-24 09:32 UTC186
  {#if !embedded}
05a48e9e7942atagen2026-10-24 09:32 UTC187
    <div class="foot">
df3c0f2b3da4atagen2026-10-22 13:07 UTC188
      <button type="button" class="mode" aria-pressed={customising} on:click={() => (customising = !customising)}>
df3c0f2b3da4atagen2026-10-22 13:07 UTC189
        {customising ? 'Done' : 'Customise quick reactions'}
df3c0f2b3da4atagen2026-10-22 13:07 UTC190
      </button>
05a48e9e7942atagen2026-10-24 09:32 UTC191
    </div>
05a48e9e7942atagen2026-10-24 09:32 UTC192
  {/if}
544a170ead2catagen2026-10-20 11:47 UTC193
</div>
544a170ead2catagen2026-10-20 11:47 UTC194
544a170ead2catagen2026-10-20 11:47 UTC195
<style>
df3c0f2b3da4atagen2026-10-22 13:07 UTC196
  .picker.embedded { width: 100%; height: 340px; box-shadow: none; }
544a170ead2catagen2026-10-20 11:47 UTC197
  .picker {
544a170ead2catagen2026-10-20 11:47 UTC198
    outline: none;
544a170ead2catagen2026-10-20 11:47 UTC199
    display: flex; flex-direction: column; gap: 6px;
544a170ead2catagen2026-10-20 11:47 UTC200
    width: 320px; height: 380px; max-width: 100%; max-height: 100%;
544a170ead2catagen2026-10-20 11:47 UTC201
    padding: 8px;
544a170ead2catagen2026-10-20 11:47 UTC202
    background: var(--panel);
544a170ead2catagen2026-10-20 11:47 UTC203
    border: 1px solid var(--border);
05a48e9e7942atagen2026-10-24 09:32 UTC204
    border-radius: var(--r-md);
544a170ead2catagen2026-10-20 11:47 UTC205
    box-shadow: 0 6px 24px rgba(0, 0, 0, 0.5);
544a170ead2catagen2026-10-20 11:47 UTC206
  }
544a170ead2catagen2026-10-20 11:47 UTC207
  .search {
544a170ead2catagen2026-10-20 11:47 UTC208
    background: var(--bg-soft);
544a170ead2catagen2026-10-20 11:47 UTC209
    border: 1px solid var(--border);
05a48e9e7942atagen2026-10-24 09:32 UTC210
    border-radius: var(--r-md);
544a170ead2catagen2026-10-20 11:47 UTC211
    color: var(--text);
544a170ead2catagen2026-10-20 11:47 UTC212
    padding: 6px 10px;
544a170ead2catagen2026-10-20 11:47 UTC213
    font-size: 13px;
544a170ead2catagen2026-10-20 11:47 UTC214
  }
544a170ead2catagen2026-10-20 11:47 UTC215
  .search:focus-visible { outline: none; border-color: var(--accent); }
544a170ead2catagen2026-10-20 11:47 UTC216
  .muted { color: var(--muted); font-size: 12px; }
05a48e9e7942atagen2026-10-24 09:32 UTC217
  .quick { border: 1px solid var(--border); border-radius: var(--r-md); padding: 6px; background: var(--bg-soft); }
544a170ead2catagen2026-10-20 11:47 UTC218
  .quick-head { display: flex; justify-content: space-between; font-size: 12px; font-weight: 650; margin-bottom: 4px; }
544a170ead2catagen2026-10-20 11:47 UTC219
  .quick-row { display: flex; flex-wrap: wrap; gap: 4px; min-height: 28px; align-items: center; }
544a170ead2catagen2026-10-20 11:47 UTC220
  .quick-item {
544a170ead2catagen2026-10-20 11:47 UTC221
    display: inline-flex; align-items: center; gap: 2px;
544a170ead2catagen2026-10-20 11:47 UTC222
    padding: 1px 2px 1px 6px;
05a48e9e7942atagen2026-10-24 09:32 UTC223
    border: 1px solid var(--border); border-radius: var(--r-sm);
544a170ead2catagen2026-10-20 11:47 UTC224
    font-size: 16px;
544a170ead2catagen2026-10-20 11:47 UTC225
  }
544a170ead2catagen2026-10-20 11:47 UTC226
  .quick-item img { width: 18px; height: 18px; object-fit: contain; }
544a170ead2catagen2026-10-20 11:47 UTC227
  .remove {
544a170ead2catagen2026-10-20 11:47 UTC228
    background: transparent; border: none; color: var(--muted);
05a48e9e7942atagen2026-10-24 09:32 UTC229
    cursor: pointer; font-size: 14px; line-height: 1; padding: 2px 5px; border-radius: var(--r-sm);
544a170ead2catagen2026-10-20 11:47 UTC230
  }
544a170ead2catagen2026-10-20 11:47 UTC231
  .remove:hover { color: var(--danger); }
544a170ead2catagen2026-10-20 11:47 UTC232
  .scroll { flex: 1; min-height: 0; overflow-y: auto; }
544a170ead2catagen2026-10-20 11:47 UTC233
  .group h3 {
544a170ead2catagen2026-10-20 11:47 UTC234
    margin: 6px 2px 2px; font-size: 11px; font-weight: 650;
544a170ead2catagen2026-10-20 11:47 UTC235
    text-transform: uppercase; letter-spacing: 0.04em; color: var(--muted);
544a170ead2catagen2026-10-20 11:47 UTC236
  }
544a170ead2catagen2026-10-20 11:47 UTC237
  .grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(32px, 1fr)); gap: 2px; }
544a170ead2catagen2026-10-20 11:47 UTC238
  .cell {
544a170ead2catagen2026-10-20 11:47 UTC239
    display: grid; place-items: center;
544a170ead2catagen2026-10-20 11:47 UTC240
    height: 32px; padding: 0;
05a48e9e7942atagen2026-10-24 09:32 UTC241
    background: transparent; border: 1px solid transparent; border-radius: var(--r-sm);
544a170ead2catagen2026-10-20 11:47 UTC242
    font-size: 20px; cursor: pointer; color: var(--text);
544a170ead2catagen2026-10-20 11:47 UTC243
  }
544a170ead2catagen2026-10-20 11:47 UTC244
  .cell img { width: 24px; height: 24px; object-fit: contain; }
df3c0f2b3da4atagen2026-10-22 13:07 UTC245
  .cell:hover:not(:disabled), .cell:focus-visible { background: var(--hover); border-color: var(--border); outline: none; }
df3c0f2b3da4atagen2026-10-22 13:07 UTC246
  .cell.chosen { border-color: var(--accent); background: color-mix(in srgb, var(--accent) 16%, transparent); }
544a170ead2catagen2026-10-20 11:47 UTC247
  .cell:disabled { opacity: 0.3; cursor: default; }
544a170ead2catagen2026-10-20 11:47 UTC248
  .status { color: var(--muted); font-size: 13px; text-align: center; margin: 16px 0; }
544a170ead2catagen2026-10-20 11:47 UTC249
  .foot { display: flex; align-items: center; justify-content: space-between; gap: 8px; }
544a170ead2catagen2026-10-20 11:47 UTC250
  .mode {
544a170ead2catagen2026-10-20 11:47 UTC251
    margin-left: auto;
05a48e9e7942atagen2026-10-24 09:32 UTC252
    background: transparent; border: 1px solid var(--border); border-radius: var(--r-sm);
544a170ead2catagen2026-10-20 11:47 UTC253
    color: var(--text); font-size: 12px; padding: 3px 8px; cursor: pointer;
544a170ead2catagen2026-10-20 11:47 UTC254
  }
544a170ead2catagen2026-10-20 11:47 UTC255
  .mode:hover, .mode[aria-pressed='true'] { border-color: var(--accent); }
544a170ead2catagen2026-10-20 11:47 UTC256
</style>