| 1 | <script lang="ts"> |
| 2 | import { createEventDispatcher } from 'svelte'; |
| 3 | import { |
| 4 | LINE_HEIGHT_DEFAULT, |
| 5 | LINE_HEIGHT_MAX, |
| 6 | LINE_HEIGHT_MIN, |
| 7 | MESSAGE_SPACING_DEFAULT, |
| 8 | MESSAGE_SPACING_MAX, |
| 9 | MESSAGE_SPACING_MIN, |
| 10 | THEMES, |
| 11 | accent, |
| 12 | lineHeight, |
| 13 | messageSpacing, |
| 14 | theme |
| 15 | } from './appearance'; |
| 16 | import { DEFAULT_QUICK_REACTIONS } from './quick-reactions'; |
| 17 | import ReactionPicker from './ReactionPicker.svelte'; |
| 18 | import type { ImagePack, QuickReaction } from './types'; |
| 19 | |
| 20 | export let quick: QuickReaction[]; |
| 21 | export let packs: ImagePack[] = []; |
| 22 | export let onQuickChange: (list: QuickReaction[]) => void; |
| 23 | |
| 24 | const dispatch = createEventDispatcher<{ close: void; packs: void }>(); |
| 25 | |
| 26 | $: quickIsDefault = |
| 27 | quick.length === DEFAULT_QUICK_REACTIONS.length && quick.every((r, i) => r.key === DEFAULT_QUICK_REACTIONS[i].key); |
| 28 | |
| 29 | function themeAccent(): string { |
| 30 | return getComputedStyle(document.documentElement).getPropertyValue('--accent').trim() || '#5865f2'; |
| 31 | } |
| 32 | </script> |
| 33 | |
| 34 | <div class="modal settings" role="dialog" aria-modal="true" aria-label="Settings"> |
| 35 | <div class="modal-tabs" role="tablist" aria-label="Settings"> |
| 36 | <button type="button" role="tab" aria-selected="true" class="active">Settings</button> |
| 37 | <button type="button" class="icon-btn modal-x" aria-label="Close settings" on:click={() => dispatch('close')}> |
| 38 | <span aria-hidden="true">✕</span> |
| 39 | </button> |
| 40 | </div> |
| 41 | |
| 42 | <section aria-label="Theme"> |
| 43 | <h3 class="sec-h">Theme</h3> |
| 44 | <div class="themes" role="radiogroup" aria-label="Theme"> |
| 45 | {#each THEMES as t (t.id)} |
| 46 | <button |
| 47 | type="button" |
| 48 | role="radio" |
| 49 | aria-checked={$theme === t.id} |
| 50 | class="theme-opt" |
| 51 | class:active={$theme === t.id} |
| 52 | data-swatch={t.id} |
| 53 | on:click={() => theme.set(t.id)} |
| 54 | > |
| 55 | <span class="swatch" aria-hidden="true"></span> |
| 56 | {t.label} |
| 57 | </button> |
| 58 | {/each} |
| 59 | </div> |
| 60 | <div class="range accent-row"> |
| 61 | <label for="accent-colour">Accent colour</label> |
| 62 | <input id="accent-colour" type="color" value={$accent ?? themeAccent()} on:input={(e) => accent.set(e.currentTarget.value)} /> |
| 63 | <button type="button" class="ghost-btn" disabled={$accent === null} on:click={() => accent.set(null)}>Use theme colour</button> |
| 64 | </div> |
| 65 | </section> |
| 66 | |
| 67 | <section aria-label="Message spacing"> |
| 68 | <h3 class="sec-h">Space between messages</h3> |
| 69 | <div class="range"> |
| 70 | <input |
| 71 | type="range" |
| 72 | min={MESSAGE_SPACING_MIN} |
| 73 | max={MESSAGE_SPACING_MAX} |
| 74 | step="1" |
| 75 | bind:value={$messageSpacing} |
| 76 | aria-label="Space between messages in pixels" |
| 77 | /> |
| 78 | <span class="value">{$messageSpacing}px</span> |
| 79 | <button |
| 80 | type="button" |
| 81 | class="ghost-btn" |
| 82 | disabled={$messageSpacing === MESSAGE_SPACING_DEFAULT} |
| 83 | on:click={() => messageSpacing.set(MESSAGE_SPACING_DEFAULT)} |
| 84 | > |
| 85 | Reset |
| 86 | </button> |
| 87 | </div> |
| 88 | <h3 class="sec-h">Line height</h3> |
| 89 | <div class="range"> |
| 90 | <input |
| 91 | type="range" |
| 92 | min={LINE_HEIGHT_MIN} |
| 93 | max={LINE_HEIGHT_MAX} |
| 94 | step="0.05" |
| 95 | bind:value={$lineHeight} |
| 96 | aria-label="Line height within messages" |
| 97 | /> |
| 98 | <span class="value">{$lineHeight.toFixed(2)}</span> |
| 99 | <button |
| 100 | type="button" |
| 101 | class="ghost-btn" |
| 102 | disabled={$lineHeight === LINE_HEIGHT_DEFAULT} |
| 103 | on:click={() => lineHeight.set(LINE_HEIGHT_DEFAULT)} |
| 104 | > |
| 105 | Reset |
| 106 | </button> |
| 107 | </div> |
| 108 | </section> |
| 109 | |
| 110 | <section aria-label="Quick reactions"> |
| 111 | <div class="sec-head"> |
| 112 | <h3 class="sec-h">Quick reactions</h3> |
| 113 | <button type="button" class="ghost-btn" disabled={quickIsDefault} on:click={() => onQuickChange(DEFAULT_QUICK_REACTIONS)}> |
| 114 | Reset |
| 115 | </button> |
| 116 | </div> |
| 117 | <ReactionPicker embedded {quick} {packs} {onQuickChange} onPick={() => {}} onClose={() => dispatch('close')} /> |
| 118 | </section> |
| 119 | |
| 120 | <section aria-label="Packs"> |
| 121 | <div class="sec-head"> |
| 122 | <h3 class="sec-h">Sticker and emoji packs</h3> |
| 123 | <button type="button" class="ghost-btn" on:click={() => dispatch('packs')}>Manage</button> |
| 124 | </div> |
| 125 | </section> |
| 126 | </div> |
| 127 | |
| 128 | <style> |
| 129 | .settings { max-height: calc(100dvh - 32px); overflow-y: auto; } |
| 130 | section + section { margin-top: 18px; } |
| 131 | .sec-head { display: flex; align-items: center; justify-content: space-between; margin-bottom: 6px; } |
| 132 | .sec-head .sec-h { margin: 0; } |
| 133 | .sec-h { font-size: 12px; text-transform: uppercase; letter-spacing: 0.08em; color: var(--muted); margin: 0 0 6px; } |
| 134 | .range + .sec-h { margin-top: 12px; } |
| 135 | .range { display: flex; align-items: center; gap: 10px; } |
| 136 | .range input[type='range'] { flex: 1; accent-color: var(--accent); } |
| 137 | .value { font-size: 13px; min-width: 4ch; text-align: right; font-variant-numeric: tabular-nums; } |
| 138 | .themes { display: grid; grid-template-columns: repeat(auto-fill, minmax(118px, 1fr)); gap: 6px; } |
| 139 | .theme-opt { |
| 140 | display: flex; align-items: center; gap: 8px; |
| 141 | padding: 6px 8px; |
| 142 | background: var(--bg-soft); |
| 143 | border: 1px solid var(--border); |
| 144 | border-radius: 8px; |
| 145 | color: var(--text); |
| 146 | font-size: 13px; |
| 147 | cursor: pointer; |
| 148 | text-align: left; |
| 149 | } |
| 150 | .theme-opt.active { border-color: var(--accent); box-shadow: 0 0 0 1px var(--accent); } |
| 151 | .swatch { flex: none; width: 18px; height: 18px; border-radius: 50%; border: 1px solid var(--border); } |
| 152 | [data-swatch='system'] .swatch { background: linear-gradient(135deg, #14151c 50%, #f7f7fa 50%); } |
| 153 | [data-swatch='dark'] .swatch { background: #14151c; } |
| 154 | [data-swatch='light'] .swatch { background: #f7f7fa; } |
| 155 | [data-swatch='black'] .swatch { background: #000; } |
| 156 | [data-swatch='nord'] .swatch { background: #2e3440; } |
| 157 | .accent-row { margin-top: 10px; font-size: 13px; } |
| 158 | .accent-row label { flex: 1; } |
| 159 | .accent-row input[type='color'] { width: 36px; height: 26px; padding: 0; border: 1px solid var(--border); border-radius: 6px; background: none; cursor: pointer; } |
| 160 | </style> |