audio streaming app plyr.fm
38
fork

Configure Feed

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

feat: global font selector in settings and profile menu (#1300)

adds a font picker alongside the existing accent color controls. six
options: mono (default), geist, inter, system, georgia, comic sans.
stored in ui_settings JSONB (no migration needed), cached in
localStorage for flash prevention, applied via --font-family CSS var.

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

authored by

nate nowack
Claude Opus 4.6 (1M context)
and committed by
GitHub
a17efc48 85122f8f

+159 -5
+53 -1
frontend/src/lib/components/ProfileMenu.svelte
··· 3 3 import { onMount } from 'svelte'; 4 4 import { page } from '$app/stores'; 5 5 import { queue } from '$lib/queue.svelte'; 6 - import { preferences, type Theme } from '$lib/preferences.svelte'; 6 + import { preferences, FONT_OPTIONS, type Theme, type FontFamily } from '$lib/preferences.svelte'; 7 7 import { ambient } from '$lib/ambient.svelte'; 8 8 import { API_URL } from '$lib/config'; 9 9 import type { User, LinkedAccount } from '$lib/types'; ··· 44 44 ]; 45 45 46 46 let currentColor = $derived(preferences.accentColor ?? '#6a9fff'); 47 + let currentFont = $derived(preferences.fontFamily); 47 48 let autoAdvance = $derived(preferences.autoAdvance); 48 49 let currentTheme = $derived(preferences.theme); 49 50 ··· 119 120 120 121 function selectPreset(color: string) { 121 122 applyColor(color); 123 + } 124 + 125 + async function selectFont(font: FontFamily) { 126 + preferences.applyFont(font); 127 + localStorage.setItem('fontFamily', font); 128 + await preferences.updateUiSettings({ font_family: font }); 122 129 } 123 130 124 131 async function handleAutoAdvanceToggle(event: Event) { ··· 418 425 ></button> 419 426 {/each} 420 427 </div> 428 + </div> 429 + </section> 430 + 431 + <section class="settings-section"> 432 + <h3>font</h3> 433 + <div class="font-row"> 434 + {#each FONT_OPTIONS as option} 435 + <button 436 + class="font-btn" 437 + class:active={currentFont === option.value} 438 + style="font-family: {option.stack}" 439 + onclick={() => selectFont(option.value)} 440 + > 441 + {option.label} 442 + </button> 443 + {/each} 421 444 </div> 422 445 </section> 423 446 ··· 968 991 .preset-btn.active { 969 992 border-color: var(--text-primary); 970 993 box-shadow: 0 0 0 1px var(--bg-secondary); 994 + } 995 + 996 + .font-row { 997 + display: flex; 998 + flex-wrap: wrap; 999 + gap: 0.4rem; 1000 + } 1001 + 1002 + .font-btn { 1003 + padding: 0.3rem 0.55rem; 1004 + background: var(--bg-primary); 1005 + border: 1px solid var(--border-default); 1006 + border-radius: var(--radius-md); 1007 + color: var(--text-secondary); 1008 + cursor: pointer; 1009 + transition: all 0.15s; 1010 + font-size: var(--text-xs); 1011 + -webkit-tap-highlight-color: transparent; 1012 + } 1013 + 1014 + .font-btn:hover { 1015 + border-color: var(--accent); 1016 + color: var(--accent); 1017 + } 1018 + 1019 + .font-btn.active { 1020 + background: color-mix(in srgb, var(--accent) 15%, transparent); 1021 + border-color: var(--accent); 1022 + color: var(--accent); 971 1023 } 972 1024 973 1025 .toggle {
+29
frontend/src/lib/preferences.svelte.ts
··· 6 6 7 7 export type Theme = 'dark' | 'light' | 'system' | 'live'; 8 8 9 + export type FontFamily = 'mono' | 'geist' | 'inter' | 'comic-sans' | 'georgia' | 'system-ui'; 10 + 11 + export const FONT_OPTIONS: { value: FontFamily; label: string; stack: string }[] = [ 12 + { value: 'mono', label: 'mono', stack: "'SF Mono', 'Monaco', 'Inconsolata', 'Fira Code', 'Consolas', monospace" }, 13 + { value: 'geist', label: 'geist', stack: "'Geist', 'Inter', system-ui, sans-serif" }, 14 + { value: 'inter', label: 'inter', stack: "'Inter', system-ui, sans-serif" }, 15 + { value: 'system-ui', label: 'system', stack: "system-ui, -apple-system, 'Segoe UI', sans-serif" }, 16 + { value: 'georgia', label: 'georgia', stack: "'Georgia', 'Times New Roman', serif" }, 17 + { value: 'comic-sans', label: 'comic sans', stack: "'Comic Sans MS', 'Comic Sans', cursive" }, 18 + ]; 19 + 20 + export const DEFAULT_FONT: FontFamily = 'mono'; 21 + 9 22 export interface UiSettings { 10 23 background_image_url?: string; 11 24 background_tile?: boolean; 12 25 use_playing_artwork_as_background?: boolean; 13 26 pds_audio_uploads_enabled?: boolean; 27 + font_family?: FontFamily; 14 28 } 15 29 16 30 export interface Preferences { ··· 110 124 return this.data?.ui_settings ?? DEFAULT_PREFERENCES.ui_settings; 111 125 } 112 126 127 + get fontFamily(): FontFamily { 128 + return this.data?.ui_settings?.font_family ?? DEFAULT_FONT; 129 + } 130 + 131 + applyFont(font: FontFamily): void { 132 + if (!browser) return; 133 + const option = FONT_OPTIONS.find((f) => f.value === font); 134 + if (option) { 135 + document.documentElement.style.setProperty('--font-family', option.stack); 136 + } 137 + } 138 + 113 139 get autoDownloadLiked(): boolean { 114 140 return this.data?.auto_download_liked ?? DEFAULT_PREFERENCES.auto_download_liked; 115 141 } ··· 225 251 localStorage.setItem('accentColor', this.data.accent_color); 226 252 this.applyAccentColor(this.data.accent_color); 227 253 } 254 + const font = this.data.ui_settings?.font_family ?? DEFAULT_FONT; 255 + localStorage.setItem('fontFamily', font); 256 + this.applyFont(font); 228 257 this.applyTheme(this.data.theme); 229 258 if (this.data.theme === 'live') { 230 259 ambient.activate();
+18 -1
frontend/src/routes/+layout.svelte
··· 387 387 <link rel="icon" href={logo} /> 388 388 <link rel="manifest" href="/manifest.webmanifest" /> 389 389 <meta name="theme-color" content="#0a0a0a" /> 390 + <link rel="preconnect" href="https://fonts.googleapis.com" /> 391 + <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin="anonymous" /> 392 + <link href="https://fonts.googleapis.com/css2?family=Geist:wght@300..900&family=Inter:wght@300..900&display=swap" rel="stylesheet" /> 390 393 391 394 {#if !hasPageMetadata} 392 395 <!-- default meta tags for pages without specific metadata --> ··· 433 436 const b = parseInt(savedAccent.slice(5, 7), 16); 434 437 const hover = `rgb(${Math.min(255, r + 30)}, ${Math.min(255, g + 30)}, ${Math.min(255, b + 30)})`; 435 438 root.style.setProperty('--accent-hover', hover); 439 + } 440 + 441 + // apply font 442 + const savedFont = localStorage.getItem('fontFamily'); 443 + if (savedFont) { 444 + const fonts = { 445 + 'mono': "'SF Mono', 'Monaco', 'Inconsolata', 'Fira Code', 'Consolas', monospace", 446 + 'geist': "'Geist', 'Inter', system-ui, sans-serif", 447 + 'inter': "'Inter', system-ui, sans-serif", 448 + 'system-ui': "system-ui, -apple-system, 'Segoe UI', sans-serif", 449 + 'georgia': "'Georgia', 'Times New Roman', serif", 450 + 'comic-sans': "'Comic Sans MS', 'Comic Sans', cursive", 451 + }; 452 + if (fonts[savedFont]) root.style.setProperty('--font-family', fonts[savedFont]); 436 453 } 437 454 438 455 // apply theme ··· 620 637 :global(body) { 621 638 margin: 0; 622 639 padding: 0; 623 - font-family: 'SF Mono', 'Monaco', 'Inconsolata', 'Fira Code', 'Consolas', monospace; 640 + font-family: var(--font-family, 'SF Mono', 'Monaco', 'Inconsolata', 'Fira Code', 'Consolas', monospace); 624 641 background-color: var(--bg-primary); 625 642 color: var(--text-primary); 626 643 -webkit-font-smoothing: antialiased;
+57 -1
frontend/src/routes/settings/+page.svelte
··· 7 7 import { API_URL } from '$lib/config'; 8 8 import { toast } from '$lib/toast.svelte'; 9 9 import { auth } from '$lib/auth.svelte'; 10 - import { preferences, type Theme } from '$lib/preferences.svelte'; 10 + import { preferences, FONT_OPTIONS, type Theme, type FontFamily } from '$lib/preferences.svelte'; 11 11 import { ambient } from '$lib/ambient.svelte'; 12 12 import { queue } from '$lib/queue.svelte'; 13 13 ··· 21 21 let showLikedOnProfile = $derived(preferences.showLikedOnProfile); 22 22 let currentTheme = $derived(preferences.theme); 23 23 let currentColor = $derived(preferences.accentColor ?? '#6a9fff'); 24 + let currentFont = $derived(preferences.fontFamily); 24 25 let autoAdvance = $derived(preferences.autoAdvance); 25 26 let backgroundImageUrl = $derived(preferences.uiSettings.background_image_url ?? ''); 26 27 let backgroundTile = $derived(preferences.uiSettings.background_tile ?? false); ··· 153 154 154 155 function selectPreset(color: string) { 155 156 applyColor(color); 157 + } 158 + 159 + async function selectFont(font: FontFamily) { 160 + preferences.applyFont(font); 161 + localStorage.setItem('fontFamily', font); 162 + await preferences.updateUiSettings({ font_family: font }); 156 163 } 157 164 158 165 // background image state - initialize once, don't sync reactively ··· 501 508 ></button> 502 509 {/each} 503 510 </div> 511 + </div> 512 + </div> 513 + 514 + <div class="setting-row"> 515 + <div class="setting-info"> 516 + <h3>font</h3> 517 + <p>choose a global font for the app</p> 518 + </div> 519 + <div class="font-controls"> 520 + {#each FONT_OPTIONS as option} 521 + <button 522 + class="font-btn" 523 + class:active={currentFont === option.value} 524 + style="font-family: {option.stack}" 525 + onclick={() => selectFont(option.value)} 526 + > 527 + {option.label} 528 + </button> 529 + {/each} 504 530 </div> 505 531 </div> 506 532 ··· 1077 1103 .preset-btn.active { 1078 1104 border-color: var(--text-primary); 1079 1105 box-shadow: 0 0 0 1px var(--bg-secondary); 1106 + } 1107 + 1108 + /* font controls */ 1109 + .font-controls { 1110 + display: flex; 1111 + flex-wrap: wrap; 1112 + gap: 0.4rem; 1113 + flex-shrink: 0; 1114 + } 1115 + 1116 + .font-btn { 1117 + padding: 0.4rem 0.7rem; 1118 + background: var(--bg-primary); 1119 + border: 1px solid var(--border-default); 1120 + border-radius: var(--radius-md); 1121 + color: var(--text-secondary); 1122 + cursor: pointer; 1123 + transition: all 0.15s; 1124 + font-size: var(--text-sm); 1125 + } 1126 + 1127 + .font-btn:hover { 1128 + border-color: var(--accent); 1129 + color: var(--accent); 1130 + } 1131 + 1132 + .font-btn.active { 1133 + background: color-mix(in srgb, var(--accent) 15%, transparent); 1134 + border-color: var(--accent); 1135 + color: var(--accent); 1080 1136 } 1081 1137 1082 1138 /* background controls */
+2 -2
loq.toml
··· 100 100 101 101 [[rules]] 102 102 path = "frontend/src/lib/components/ProfileMenu.svelte" 103 - max_lines = 1148 103 + max_lines = 1186 104 104 105 105 [[rules]] 106 106 path = "frontend/src/lib/components/Queue.svelte" ··· 132 132 133 133 [[rules]] 134 134 path = "frontend/src/routes/+layout.svelte" 135 - max_lines = 750 135 + max_lines = 767 136 136 137 137 [[rules]] 138 138 path = "frontend/src/routes/costs/+page.svelte"