music on atproto
plyr.fm
1<script lang="ts">
2 import { onMount } from 'svelte';
3 import { preferences } from '$lib/preferences.svelte';
4
5 let showSettings = $state(false);
6
7 const presetColors = [
8 { name: 'blue', value: '#6a9fff' },
9 { name: 'purple', value: '#a78bfa' },
10 { name: 'pink', value: '#f472b6' },
11 { name: 'green', value: '#4ade80' },
12 { name: 'orange', value: '#fb923c' },
13 { name: 'red', value: '#ef4444' }
14 ];
15
16 // derive from preferences store
17 let currentColor = $derived(preferences.accentColor ?? '#6a9fff');
18
19 // apply color when it changes
20 $effect(() => {
21 if (currentColor) {
22 applyColorLocally(currentColor);
23 }
24 });
25
26 onMount(() => {
27 // apply initial color from localStorage while waiting for preferences
28 const saved = localStorage.getItem('accentColor');
29 if (saved) {
30 applyColorLocally(saved);
31 }
32 });
33
34 function applyColorLocally(color: string) {
35 document.documentElement.style.setProperty('--accent', color);
36
37 // calculate hover color
38 const r = parseInt(color.slice(1, 3), 16);
39 const g = parseInt(color.slice(3, 5), 16);
40 const b = parseInt(color.slice(5, 7), 16);
41 const hover = `rgb(${Math.min(255, r + 30)}, ${Math.min(255, g + 30)}, ${Math.min(255, b + 30)})`;
42 document.documentElement.style.setProperty('--accent-hover', hover);
43 }
44
45 async function applyColor(color: string) {
46 applyColorLocally(color);
47 localStorage.setItem('accentColor', color);
48 await preferences.update({ accent_color: color });
49 }
50
51 function handleColorInput(e: Event) {
52 const input = e.target as HTMLInputElement;
53 applyColor(input.value);
54 }
55
56 function selectPreset(color: string) {
57 applyColor(color);
58 }
59
60 function toggleSettings() {
61 showSettings = !showSettings;
62 }
63</script>
64
65<div class="color-settings">
66 <button class="settings-toggle" onclick={toggleSettings} title="customize accent color">
67 <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
68 <path d="M12.22 2h-.44a2 2 0 0 0-2 2v.18a2 2 0 0 1-1 1.73l-.43.25a2 2 0 0 1-2 0l-.15-.08a2 2 0 0 0-2.73.73l-.22.38a2 2 0 0 0 .73 2.73l.15.1a2 2 0 0 1 1 1.72v.51a2 2 0 0 1-1 1.74l-.15.09a2 2 0 0 0-.73 2.73l.22.38a2 2 0 0 0 2.73.73l.15-.08a2 2 0 0 1 2 0l.43.25a2 2 0 0 1 1 1.73V20a2 2 0 0 0 2 2h.44a2 2 0 0 0 2-2v-.18a2 2 0 0 1 1-1.73l.43-.25a2 2 0 0 1 2 0l.15.08a2 2 0 0 0 2.73-.73l.22-.39a2 2 0 0 0-.73-2.73l-.15-.08a2 2 0 0 1-1-1.74v-.5a2 2 0 0 1 1-1.74l.15-.09a2 2 0 0 0 .73-2.73l-.22-.38a2 2 0 0 0-2.73-.73l-.15.08a2 2 0 0 1-2 0l-.43-.25a2 2 0 0 1-1-1.73V4a2 2 0 0 0-2-2z"></path>
69 <circle cx="12" cy="12" r="3"></circle>
70 </svg>
71 </button>
72
73 {#if showSettings}
74 <div class="settings-panel">
75 <div class="panel-header">
76 <span>accent color</span>
77 <button class="close-btn" onclick={toggleSettings}>×</button>
78 </div>
79
80 <div class="color-picker-row">
81 <input
82 type="color"
83 value={currentColor}
84 oninput={handleColorInput}
85 class="color-input"
86 />
87 <span class="color-value">{currentColor}</span>
88 </div>
89
90 <div class="presets">
91 <span class="presets-label">presets</span>
92 <div class="preset-grid">
93 {#each presetColors as preset}
94 <button
95 class="preset-btn"
96 class:active={currentColor.toLowerCase() === preset.value.toLowerCase()}
97 style="background: {preset.value}"
98 onclick={() => selectPreset(preset.value)}
99 title={preset.name}
100 ></button>
101 {/each}
102 </div>
103 </div>
104 </div>
105 {/if}
106</div>
107
108<style>
109 .color-settings {
110 position: relative;
111 }
112
113 .settings-toggle {
114 background: transparent;
115 border: 1px solid var(--border-default);
116 color: var(--text-secondary);
117 padding: 0.5rem;
118 border-radius: var(--radius-sm);
119 cursor: pointer;
120 transition: all 0.2s;
121 display: flex;
122 align-items: center;
123 justify-content: center;
124 }
125
126 .settings-toggle:hover {
127 color: var(--accent);
128 border-color: var(--accent);
129 }
130
131 .settings-panel {
132 position: absolute;
133 top: calc(100% + 0.5rem);
134 right: 0;
135 background: var(--bg-secondary);
136 border: 1px solid var(--border-default);
137 border-radius: var(--radius-base);
138 padding: 1rem;
139 min-width: 240px;
140 box-shadow: 0 4px 12px rgba(0, 0, 0, 0.4);
141 z-index: 1000;
142 }
143
144 .panel-header {
145 display: flex;
146 justify-content: space-between;
147 align-items: center;
148 margin-bottom: 1rem;
149 color: var(--text-primary);
150 font-size: var(--text-base);
151 }
152
153 .close-btn {
154 background: transparent;
155 border: none;
156 color: var(--text-secondary);
157 font-size: var(--text-3xl);
158 cursor: pointer;
159 padding: 0;
160 width: 24px;
161 height: 24px;
162 display: flex;
163 align-items: center;
164 justify-content: center;
165 transition: color 0.2s;
166 }
167
168 .close-btn:hover {
169 color: var(--text-primary);
170 }
171
172 .color-picker-row {
173 display: flex;
174 align-items: center;
175 gap: 0.75rem;
176 margin-bottom: 1rem;
177 padding-bottom: 1rem;
178 border-bottom: 1px solid var(--border-subtle);
179 }
180
181 .color-input {
182 width: 48px;
183 height: 32px;
184 border: 1px solid var(--border-default);
185 border-radius: var(--radius-sm);
186 cursor: pointer;
187 background: transparent;
188 }
189
190 .color-input::-webkit-color-swatch-wrapper {
191 padding: 2px;
192 }
193
194 .color-input::-webkit-color-swatch {
195 border: none;
196 border-radius: 2px;
197 }
198
199 .color-value {
200 font-family: monospace;
201 font-size: var(--text-sm);
202 color: var(--text-secondary);
203 }
204
205 .presets {
206 display: flex;
207 flex-direction: column;
208 gap: 0.5rem;
209 }
210
211 .presets-label {
212 font-size: var(--text-sm);
213 color: var(--text-tertiary);
214 }
215
216 .preset-grid {
217 display: grid;
218 grid-template-columns: repeat(6, 1fr);
219 gap: 0.5rem;
220 }
221
222 .preset-btn {
223 width: 32px;
224 height: 32px;
225 border-radius: var(--radius-sm);
226 border: 2px solid transparent;
227 cursor: pointer;
228 transition: all 0.2s;
229 padding: 0;
230 }
231
232 .preset-btn:hover {
233 transform: scale(1.1);
234 box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
235 }
236
237 .preset-btn.active {
238 border-color: var(--text-primary);
239 box-shadow: 0 0 0 1px var(--bg-secondary), 0 2px 8px rgba(0, 0, 0, 0.3);
240 }
241</style>