A music player that connects to your cloud/distributed storage.
5
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat: settings screen + background customisation blur theme

+675 -18
+1 -1
src/themes/blur/browser/element.css
··· 1 1 :host { 2 - --border-color: color-mix(in oklch, var(--text-color) 6%, transparent); 2 + --border-color: color-mix(in oklch, var(--text-color) 9%, transparent); 3 3 4 4 background: var(--bg-color); 5 5 color: var(--text-color);
+296 -17
src/themes/blur/facet/index.css
··· 1 1 :root { 2 - --facet-bg-color: var(--color-2); 3 - } 4 - 5 - @media (prefers-color-scheme: dark) { 6 - :root { 7 - --facet-bg-color: var(--color-3); 8 - } 2 + --facet-bg-color: #565450; 9 3 } 10 4 11 5 body { 12 - background-color: oklch(from var(--facet-bg-color) calc(l - 0.025) c h); 6 + background-color: var(--facet-bg-color); 13 7 color: var(--text-color); 14 8 display: flex; 15 9 flex-direction: column; ··· 20 14 } 21 15 22 16 /*********************************** 17 + * Background overlay 18 + ***********************************/ 19 + 20 + #bg-overlay { 21 + background-position: center; 22 + background-size: cover; 23 + inset: 0; 24 + mix-blend-mode: luminosity; 25 + opacity: 0; 26 + pointer-events: none; 27 + position: fixed; 28 + transition: opacity 750ms; 29 + z-index: -1; 30 + } 31 + 32 + #bg-overlay.bg-overlay--visible { 33 + opacity: 1; 34 + } 35 + 36 + #bg-overlay.bg-overlay--no-mix { 37 + mix-blend-mode: normal; 38 + } 39 + 40 + /*********************************** 23 41 * Main 24 42 ***********************************/ 25 43 ··· 41 59 42 60 @media (min-width: 63rem) { 43 61 gap: var(--space-sm); 44 - padding: var(--space-md); 62 + padding: var(--space-xl) var(--space-md); 45 63 } 46 64 47 65 @media (min-width: 84rem) { 48 66 gap: var(--space-lg); 49 - padding: var(--space-xl); 67 + padding: var(--space-3xl) var(--space-xl); 50 68 } 51 69 } 52 70 ··· 55 73 ***********************************/ 56 74 57 75 db-artwork-controller, 58 - db-browser { 76 + db-browser, 77 + #settings-panel { 59 78 border-radius: var(--radius-md); 60 79 box-shadow: var(--box-shadow-md); 61 80 justify-self: end; ··· 63 82 width: 100%; 64 83 } 65 84 66 - db-artwork-controller { 85 + db-artwork-controller, 86 + #settings-panel { 67 87 grid-column: 3; 68 88 grid-row: 1; 69 89 min-height: 50vh; ··· 74 94 grid-row: 1 / span 2; 75 95 } 76 96 97 + /*********************************** 98 + * Settings panel 99 + ***********************************/ 100 + 101 + #settings-panel { 102 + display: none; 103 + } 104 + 105 + main.settings-open db-artwork-controller { 106 + display: none; 107 + } 108 + 109 + main.settings-open #settings-panel { 110 + display: flex; 111 + flex-direction: column; 112 + } 113 + 114 + .settings-scroll { 115 + background: var(--bg-color); 116 + color: var(--text-color); 117 + flex: 1; 118 + font-size: var(--fs-sm); 119 + overflow-y: auto; 120 + padding: var(--space-md); 121 + } 122 + 123 + .settings-section { 124 + &:not(:last-child) { 125 + margin-bottom: var(--space-lg); 126 + } 127 + 128 + h2 { 129 + font-size: 65%; 130 + font-weight: 500; 131 + letter-spacing: var(--tracking-wider); 132 + margin: 0 0 var(--space-xs) 0; 133 + opacity: 0.4; 134 + text-transform: uppercase; 135 + } 136 + } 137 + 138 + /* Background image grid */ 139 + 140 + .bg-images { 141 + display: grid; 142 + gap: var(--space-3xs); 143 + grid-template-columns: repeat(5, 1fr); 144 + margin-bottom: var(--space-2xs); 145 + } 146 + 147 + .bg-thumb { 148 + aspect-ratio: 16 / 10; 149 + background: oklch(from var(--text-color) l c h / 0.07); 150 + border: 0; 151 + border-radius: var(--radius-sm); 152 + color: white; 153 + cursor: pointer; 154 + overflow: hidden; 155 + padding: 0; 156 + position: relative; 157 + transition: opacity 150ms; 158 + 159 + img { 160 + display: block; 161 + height: 100%; 162 + object-fit: cover; 163 + width: 100%; 164 + } 165 + 166 + .bg-thumb-check { 167 + display: none; 168 + } 169 + 170 + &[data-selected] .bg-thumb-check { 171 + align-items: center; 172 + background: oklch(from black l c h / 0.4); 173 + display: flex; 174 + inset: 0; 175 + justify-content: center; 176 + position: absolute; 177 + } 178 + 179 + &:hover { 180 + opacity: 0.8; 181 + } 182 + } 183 + 184 + /* Special option buttons */ 185 + 186 + .bg-special-options { 187 + display: flex; 188 + gap: var(--space-2xs); 189 + margin-bottom: var(--space-2xs); 190 + } 191 + 192 + .bg-special-btn { 193 + align-items: center; 194 + background: oklch(from var(--text-color) l c h / 0.07); 195 + border: 2px solid oklch(from var(--text-color) l c h / 0.1); 196 + border-radius: var(--radius-md); 197 + color: oklch(from var(--text-color) l c h / 0.6); 198 + cursor: pointer; 199 + display: flex; 200 + flex: 1; 201 + flex-direction: column; 202 + font-family: inherit; 203 + gap: var(--space-2xs); 204 + justify-content: center; 205 + overflow: hidden; 206 + padding: var(--space-xs) 0; 207 + position: relative; 208 + transition-duration: 150ms; 209 + transition-property: border-color, color; 210 + 211 + i { 212 + font-size: 120%; 213 + line-height: 0.75; 214 + } 215 + 216 + span { 217 + font-size: 70%; 218 + font-weight: 600; 219 + letter-spacing: var(--tracking-wide); 220 + text-box: trim-both cap alphabetic; 221 + text-transform: uppercase; 222 + } 223 + 224 + &[data-selected], 225 + &:hover, 226 + &:focus { 227 + border-color: oklch(from var(--text-color) l c h / 0.4); 228 + color: var(--text-color); 229 + } 230 + 231 + /* Hide the color input; label click still activates it */ 232 + input[type="color"] { 233 + height: 1px; 234 + left: 0; 235 + opacity: 0; 236 + pointer-events: none; 237 + position: absolute; 238 + top: 0; 239 + width: 1px; 240 + } 241 + } 242 + 243 + /* URL input row */ 244 + 245 + .bg-url-row { 246 + display: flex; 247 + gap: var(--space-2xs); 248 + 249 + input { 250 + background: oklch(from var(--text-color) l c h / 0.07); 251 + border: 2px solid oklch(from var(--text-color) l c h / 0.1); 252 + border-radius: var(--radius-md); 253 + color: var(--text-color); 254 + flex: 1; 255 + font-family: inherit; 256 + font-size: inherit; 257 + padding: var(--space-xs) var(--space-sm); 258 + 259 + &::placeholder { 260 + color: oklch(from var(--text-color) l c h / 0.4); 261 + } 262 + 263 + &:focus { 264 + border-color: oklch(from var(--text-color) l c h / 0.4); 265 + outline: none; 266 + } 267 + } 268 + 269 + button { 270 + background: oklch(from var(--text-color) l c h / 0.12); 271 + border: 0; 272 + border-radius: var(--radius-md); 273 + color: var(--text-color); 274 + cursor: pointer; 275 + padding: var(--space-xs) var(--space-sm); 276 + transition: background 150ms; 277 + 278 + &:hover { 279 + background: oklch(from var(--text-color) l c h / 0.22); 280 + } 281 + } 282 + } 283 + 284 + /* Mix toggle */ 285 + 286 + .bg-mix-btn { 287 + align-items: center; 288 + background: oklch(from var(--text-color) l c h / 0.07); 289 + border: 2px solid oklch(from var(--text-color) l c h / 0.1); 290 + border-radius: var(--radius-md); 291 + color: oklch(from var(--text-color) l c h / 0.6); 292 + cursor: pointer; 293 + display: flex; 294 + font-family: inherit; 295 + font-size: 70%; 296 + font-weight: 600; 297 + gap: var(--space-2xs); 298 + letter-spacing: var(--tracking-wide); 299 + margin-top: var(--space-2xs); 300 + padding: var(--space-xs) var(--space-sm); 301 + text-transform: uppercase; 302 + transition-duration: 150ms; 303 + transition-property: border-color, color; 304 + width: 100%; 305 + 306 + &[data-selected], 307 + &:hover, 308 + &:focus { 309 + border-color: oklch(from var(--text-color) l c h / 0.4); 310 + color: var(--text-color); 311 + } 312 + } 313 + 314 + /* Background color section */ 315 + 316 + .bg-color-row { 317 + display: flex; 318 + gap: var(--space-2xs); 319 + } 320 + 321 + .bg-color-swatch { 322 + border: 2px solid oklch(from var(--text-color) l c h / 0.1); 323 + border-radius: var(--radius-md); 324 + cursor: pointer; 325 + display: block; 326 + flex: 1; 327 + min-height: 2.5rem; 328 + overflow: hidden; 329 + position: relative; 330 + transition-duration: 150ms; 331 + transition-property: border-color, background-color; 332 + 333 + input[type="color"] { 334 + height: 1px; 335 + left: 0; 336 + opacity: 0; 337 + pointer-events: none; 338 + position: absolute; 339 + top: 0; 340 + width: 1px; 341 + } 342 + 343 + &[data-selected] { 344 + border-color: oklch(from var(--text-color) l c h / 0.4); 345 + } 346 + } 347 + 348 + /*********************************** 349 + * Shortcuts 350 + ***********************************/ 351 + 77 352 #shortcuts { 353 + --shortcut-color: oklch(100% 0 0); 354 + 78 355 display: flex; 79 356 font-size: var(--fs-sm); 80 357 gap: var(--space-2xs); ··· 83 360 a { 84 361 align-items: center; 85 362 background-color: transparent; 86 - border: 2px dotted oklch(from var(--text-color) l c h / 0.1); 363 + border: 2px dotted oklch(from var(--shortcut-color) l c h / 0.3); 87 364 border-radius: var(--radius-md); 88 - color: oklch(from var(--text-color) l c h / 0.6); 365 + color: oklch(from var(--shortcut-color) l c h / 0.7); 89 366 cursor: pointer; 90 367 display: flex; 91 368 flex: 1; 92 369 gap: var(--space-3xs); 93 370 line-height: 0.75; 94 371 justify-content: center; 372 + mix-blend-mode: difference; 95 373 overflow: hidden; 96 374 padding: var(--space-xs) var(--space-3xs); 97 375 text-box: trim-both cap alphabetic; ··· 102 380 white-space: nowrap; 103 381 104 382 &:focus, 105 - &:hover { 106 - border-color: oklch(from var(--text-color) l c h / 0.3); 107 - color: oklch(from var(--text-color) l c h / 1); 383 + &:hover, 384 + &[data-active="t"] { 385 + border-color: oklch(from var(--shortcut-color) l c h / 0.5); 386 + color: oklch(from var(--shortcut-color) l c h / 1); 108 387 } 109 388 } 110 389
+45
src/themes/blur/facet/index.html
··· 25 25 } 26 26 </style> 27 27 28 + <div id="bg-overlay"></div> 29 + 28 30 <main> 29 31 <db-browser 30 32 output-selector="#output" ··· 42 44 repeat-shuffle-engine-selector="de-repeat-shuffle" 43 45 ></db-artwork-controller> 44 46 47 + <div id="settings-panel"> 48 + <div class="settings-scroll"> 49 + <section class="settings-section"> 50 + <h2>Background Image</h2> 51 + <div class="bg-images" id="bg-images"></div> 52 + <div class="bg-special-options"> 53 + <button class="bg-special-btn" id="bg-none-btn" title="No background image"> 54 + <i class="ph-bold ph-x"></i> 55 + <span>None</span> 56 + </button> 57 + <button class="bg-special-btn" id="bg-custom-btn" title="Custom image URL"> 58 + <i class="ph-bold ph-link"></i> 59 + <span>URL</span> 60 + </button> 61 + </div> 62 + <div class="bg-url-row" id="bg-url-row" hidden> 63 + <input type="url" id="bg-url-input" placeholder="https://..." /> 64 + <button id="bg-url-apply"><i class="ph-bold ph-arrow-right"></i></button> 65 + </div> 66 + </section> 67 + 68 + <section class="settings-section"> 69 + <h2>Background Color</h2> 70 + <div class="bg-color-row"> 71 + <label class="bg-color-swatch" id="bg-color-label" title="Pick a color"> 72 + <input type="color" id="bg-color-picker" value="#000000" /> 73 + </label> 74 + <button class="bg-special-btn" id="bg-color-clear-btn" title="Remove color"> 75 + <i class="ph-bold ph-x"></i> 76 + <span>None</span> 77 + </button> 78 + </div> 79 + <button class="bg-mix-btn" id="bg-mix-btn"> 80 + <i class="ph-bold ph-intersect"></i> 81 + Mix image with color 82 + </button> 83 + </section> 84 + </div> 85 + </div> 86 + 45 87 <div id="shortcuts"> 46 88 <button id="btn-new-deck" title="Open a new deck"> 47 89 <i class="ph-bold ph-circles-three-plus"></i> ··· 53 95 > 54 96 <i class="ph-bold ph-eject"></i> 55 97 </a> 98 + <button id="btn-settings" title="Theme settings"> 99 + <i class="ph-bold ph-gear"></i> 100 + </button> 56 101 </div> 57 102 </main> 58 103
+333
src/themes/blur/facet/index.inline.js
··· 1 1 import foundation from "~/common/foundation.js"; 2 + import { data } from "~/common/output.js"; 3 + 4 + // Move #bg-overlay to document.body so it's not inside #container. 5 + // #container fades in via an opacity transition (0→1), which creates an 6 + // isolated stacking context while opacity < 1. That breaks mix-blend-mode 7 + // on the overlay: it blends against transparent instead of the dark page 8 + // background, causing a flash of regular image colors until opacity reaches 1. 9 + const overlayEl = document.querySelector("#bg-overlay"); 10 + if (overlayEl) document.body.appendChild(overlayEl); 2 11 3 12 // Set doc title 4 13 foundation.setup({ title: "Blur | Diffuse" }); ··· 27 36 document.querySelector("db-browser")?.setAttribute("group", group); 28 37 29 38 //////////////////////////////////////////// 39 + // BACKGROUND SETTINGS 40 + //////////////////////////////////////////// 41 + 42 + const BACKGROUND_KEY = "sh.diffuse.theme.blur.background"; 43 + const BACKGROUND_COLOR_KEY = "sh.diffuse.theme.blur.background-color"; 44 + const BACKGROUND_MIX_KEY = "sh.diffuse.theme.blur.background-mix"; 45 + const BG_IMAGE_COUNT = 30; 46 + 47 + const output = await foundation.orchestrator.output(); 48 + await data(output.settings); 49 + 50 + // Apply stored image and color before fading in (with defaults for first run) 51 + const storedBg = getSettingValue(BACKGROUND_KEY); 52 + const storedBgColor = getSettingValue(BACKGROUND_COLOR_KEY); 53 + const storedBgMix = getSettingValue(BACKGROUND_MIX_KEY); 54 + 55 + const activeBg = storedBg ?? "builtin:13"; 56 + const activeMix = storedBgMix !== null ? storedBgMix === "true" : true; 57 + 58 + applyBackgroundMix(activeMix); 59 + if (storedBgColor) applyBackgroundColor(storedBgColor); 60 + applyBackgroundImage(activeBg); 61 + 62 + //////////////////////////////////////////// 30 63 // SHORTCUTS 31 64 //////////////////////////////////////////// 32 65 ··· 49 82 window.open(url.toString(), "_blank"); 50 83 }); 51 84 85 + document.querySelector("#btn-settings")?.addEventListener("click", () => { 86 + const main = document.querySelector("main"); 87 + const btn = document.querySelector("#btn-settings"); 88 + const isOpen = main?.classList.toggle("settings-open") ?? false; 89 + btn?.setAttribute("data-active", isOpen ? "t" : "f"); 90 + }); 91 + 92 + //////////////////////////////////////////// 93 + // SETTINGS PANEL 94 + //////////////////////////////////////////// 95 + 96 + // Populate background image grid 97 + const bgGrid = document.querySelector("#bg-images"); 98 + 99 + if (bgGrid) { 100 + for (let i = 1; i <= BG_IMAGE_COUNT; i++) { 101 + const btn = document.createElement("button"); 102 + btn.className = "bg-thumb"; 103 + btn.dataset.value = `builtin:${i}`; 104 + btn.title = `Background ${i}`; 105 + 106 + const img = document.createElement("img"); 107 + img.src = `images/background/thumbnails/${i}.jpg`; 108 + img.alt = `Background ${i}`; 109 + img.loading = "lazy"; 110 + 111 + const check = document.createElement("i"); 112 + check.className = "ph-bold ph-check bg-thumb-check"; 113 + 114 + btn.append(img, check); 115 + btn.addEventListener("click", async () => { 116 + const value = `builtin:${i}`; 117 + await saveSetting(BACKGROUND_KEY, value); 118 + await applyBackgroundImage(value); 119 + updateImageSelected(value); 120 + }); 121 + 122 + bgGrid.append(btn); 123 + } 124 + } 125 + 126 + // Reflect current selections in the UI 127 + updateImageSelected(activeBg); 128 + updateColorSelected(storedBgColor); 129 + updateMixSelected(activeMix); 130 + 131 + // Image: None button 132 + document.querySelector("#bg-none-btn")?.addEventListener("click", async () => { 133 + await saveSetting(BACKGROUND_KEY, ""); 134 + await applyBackgroundImage(""); 135 + updateImageSelected(""); 136 + }); 137 + 138 + // Image: URL toggle 139 + document.querySelector("#bg-custom-btn")?.addEventListener("click", () => { 140 + const row = /** @type {HTMLElement | null} */ ( 141 + document.querySelector("#bg-url-row") 142 + ); 143 + if (row) row.hidden = !row.hidden; 144 + document.querySelector("#bg-custom-btn")?.toggleAttribute( 145 + "data-selected", 146 + row ? !row.hidden : false, 147 + ); 148 + }); 149 + 150 + // Image: apply custom URL 151 + document.querySelector("#bg-url-apply")?.addEventListener("click", async () => { 152 + const input = /** @type {HTMLInputElement | null} */ ( 153 + document.querySelector("#bg-url-input") 154 + ); 155 + const url = input?.value?.trim(); 156 + if (!url) return; 157 + const value = `url:${url}`; 158 + await saveSetting(BACKGROUND_KEY, value); 159 + await applyBackgroundImage(value); 160 + updateImageSelected(value); 161 + }); 162 + 163 + // Color: picker — label wraps the input, clicking opens the native picker 164 + document.querySelector("#bg-color-picker")?.addEventListener( 165 + "change", 166 + async (e) => { 167 + const color = /** @type {HTMLInputElement} */ (e.target).value; 168 + await saveSetting(BACKGROUND_COLOR_KEY, color); 169 + applyBackgroundColor(color); 170 + updateColorSelected(color); 171 + }, 172 + ); 173 + 174 + // Color: clear button 175 + document.querySelector("#bg-color-clear-btn")?.addEventListener( 176 + "click", 177 + async () => { 178 + await saveSetting(BACKGROUND_COLOR_KEY, ""); 179 + applyBackgroundColor(""); 180 + updateColorSelected(null); 181 + }, 182 + ); 183 + 184 + // Mix: toggle 185 + document.querySelector("#bg-mix-btn")?.addEventListener("click", async () => { 186 + const isMixed = 187 + !(document.querySelector("#bg-overlay")?.classList.contains("bg-overlay--no-mix") ?? false); 188 + const next = !isMixed; 189 + applyBackgroundMix(next); 190 + updateMixSelected(next); 191 + await saveSetting(BACKGROUND_MIX_KEY, next ? "true" : "false"); 192 + }); 193 + 194 + //////////////////////////////////////////// 195 + // 🚀 196 + //////////////////////////////////////////// 197 + 52 198 foundation.ready(); 199 + 200 + //////////////////////////////////////////// 201 + // 🛠️ HELPERS 202 + //////////////////////////////////////////// 203 + 204 + /** 205 + * Returns the stored value for a settings key, or null if absent. 206 + * @param {string} key 207 + * @returns {string | null} 208 + */ 209 + function getSettingValue(key) { 210 + const col = output.settings.collection(); 211 + if (col.state !== "loaded") return null; 212 + return col.data.find((s) => s.key === key)?.value ?? null; 213 + } 214 + 215 + /** 216 + * Persist a value to a settings key. Pass "" to remove the setting. 217 + * @param {string} key 218 + * @param {string} value 219 + */ 220 + async function saveSetting(key, value) { 221 + const col = output.settings.collection(); 222 + if (col.state !== "loaded") return; 223 + 224 + const settings = col.data; 225 + const existing = settings.find((s) => s.key === key); 226 + 227 + /** @type {import("~/definitions/types.d.ts").Setting[]} */ 228 + let updated; 229 + 230 + if (!value) { 231 + updated = settings.filter((s) => s.key !== key); 232 + } else if (existing) { 233 + updated = settings.map((s) => s.key === key ? { ...s, value } : s); 234 + } else { 235 + updated = [ 236 + ...settings, 237 + { 238 + $type: /** @type {"sh.diffuse.output.setting"} */ ( 239 + "sh.diffuse.output.setting" 240 + ), 241 + id: crypto.randomUUID(), 242 + key, 243 + value, 244 + }, 245 + ]; 246 + } 247 + 248 + await output.settings.save(updated); 249 + } 250 + 251 + /** 252 + * Apply a background image value to #bg-overlay. Preloads before fading in. 253 + * @param {string} value builtin:N | url:... | "" for none 254 + */ 255 + async function applyBackgroundImage(value) { 256 + const overlay = /** @type {HTMLElement | null} */ ( 257 + document.querySelector("#bg-overlay") 258 + ); 259 + 260 + if (!overlay) return; 261 + 262 + const wasVisible = overlay.classList.contains("bg-overlay--visible"); 263 + overlay.classList.remove("bg-overlay--visible"); 264 + 265 + // Wait for the fade-out to finish before swapping the image, so that 266 + // backgroundImage never changes while the overlay is partially opaque 267 + // (which would recreate the GPU layer and drop the blend mode for a frame). 268 + // On initial load the overlay is already transparent so no wait is needed. 269 + if (wasVisible) { 270 + await new Promise((resolve) => { 271 + overlay.addEventListener("transitionend", resolve, { once: true }); 272 + }); 273 + } 274 + 275 + if (!value) { 276 + overlay.style.backgroundImage = ""; 277 + return; 278 + } 279 + 280 + let imageUrl = ""; 281 + if (value.startsWith("builtin:")) { 282 + imageUrl = `images/background/${value.slice(8)}.jpg`; 283 + } else if (value.startsWith("url:")) { 284 + imageUrl = value.slice(4); 285 + } 286 + 287 + if (imageUrl) { 288 + await new Promise((resolve) => { 289 + const img = new Image(); 290 + img.onload = resolve; 291 + img.onerror = resolve; 292 + img.src = imageUrl; 293 + }); 294 + 295 + overlay.style.backgroundImage = `url('${imageUrl.replace(/'/g, "\\'")}')`; 296 + 297 + await new Promise((resolve) => { 298 + requestAnimationFrame(() => resolve(undefined)); 299 + }); 300 + 301 + overlay.classList.add("bg-overlay--visible"); 302 + } 303 + } 304 + 305 + /** 306 + * Apply a background color value as the page background color. 307 + * @param {string | null} color CSS color string, or null/empty to clear 308 + */ 309 + function applyBackgroundColor(color) { 310 + if (color) { 311 + document.documentElement.style.setProperty("--facet-bg-color", color); 312 + } else { 313 + document.documentElement.style.removeProperty("--facet-bg-color"); 314 + } 315 + } 316 + 317 + /** 318 + * Highlight the active image selection in the settings panel. 319 + * @param {string} value 320 + */ 321 + function updateImageSelected(value) { 322 + document.querySelectorAll(".bg-thumb, #bg-none-btn, #bg-custom-btn").forEach( 323 + (el) => el.removeAttribute("data-selected"), 324 + ); 325 + 326 + if (!value) { 327 + document.querySelector("#bg-none-btn")?.setAttribute("data-selected", ""); 328 + } else if (value.startsWith("builtin:")) { 329 + document 330 + .querySelector(`.bg-thumb[data-value="${value}"]`) 331 + ?.setAttribute("data-selected", ""); 332 + } else if (value.startsWith("url:")) { 333 + const input = /** @type {HTMLInputElement | null} */ ( 334 + document.querySelector("#bg-url-input") 335 + ); 336 + if (input) input.value = value.slice(4); 337 + const row = /** @type {HTMLElement | null} */ ( 338 + document.querySelector("#bg-url-row") 339 + ); 340 + if (row) row.hidden = false; 341 + document.querySelector("#bg-custom-btn")?.setAttribute("data-selected", ""); 342 + } 343 + } 344 + 345 + /** 346 + * Toggle mix-blend-mode on #bg-overlay. 347 + * @param {boolean} enabled 348 + */ 349 + function applyBackgroundMix(enabled) { 350 + document.querySelector("#bg-overlay")?.classList.toggle( 351 + "bg-overlay--no-mix", 352 + !enabled, 353 + ); 354 + } 355 + 356 + /** 357 + * Reflect the mix toggle state in the settings panel. 358 + * @param {boolean} enabled 359 + */ 360 + function updateMixSelected(enabled) { 361 + const btn = document.querySelector("#bg-mix-btn"); 362 + btn?.toggleAttribute("data-selected", enabled); 363 + } 364 + 365 + /** 366 + * Highlight the active color selection and update the swatch. 367 + * @param {string | null} color 368 + */ 369 + function updateColorSelected(color) { 370 + const label = /** @type {HTMLElement | null} */ ( 371 + document.querySelector("#bg-color-label") 372 + ); 373 + const picker = /** @type {HTMLInputElement | null} */ ( 374 + document.querySelector("#bg-color-picker") 375 + ); 376 + 377 + if (color) { 378 + label?.setAttribute("data-selected", ""); 379 + if (label) label.style.backgroundColor = color; 380 + if (picker) picker.value = color; 381 + } else { 382 + label?.removeAttribute("data-selected"); 383 + if (label) label.style.backgroundColor = ""; 384 + } 385 + }