replies timeline only, appview-less bluesky client
1<script lang="ts"> 2 import { defaultSettings, needsReload, settings } from '$lib/settings'; 3 import { handleCache, didDocCache, recordCache } from '$lib/at/client'; 4 import { get } from 'svelte/store'; 5 import ColorPicker from 'svelte-awesome-color-picker'; 6 7 interface Props { 8 isOpen: boolean; 9 onClose: () => void; 10 } 11 12 let { isOpen = $bindable(false), onClose }: Props = $props(); 13 14 type Tab = 'advanced' | 'moderation' | 'style'; 15 let activeTab = $state<Tab>('advanced'); 16 17 let localSettings = $state(get(settings)); 18 let hasReloadChanges = $derived(needsReload($settings, localSettings)); 19 20 $effect(() => { 21 $settings.theme = localSettings.theme; 22 }); 23 24 const resetSettingsToSaved = () => { 25 localSettings = $settings; 26 }; 27 28 const handleClose = () => { 29 resetSettingsToSaved(); 30 onClose(); 31 }; 32 33 const handleSave = () => { 34 settings.set(localSettings); 35 // reload to update api endpoints 36 window.location.reload(); 37 }; 38 39 const handleReset = () => { 40 const confirmed = confirm('reset all settings to defaults?'); 41 if (!confirmed) return; 42 settings.reset(); 43 window.location.reload(); 44 }; 45 46 const handleClearCache = () => { 47 handleCache.clear(); 48 didDocCache.clear(); 49 recordCache.clear(); 50 alert('cache cleared!'); 51 }; 52 53 const handleKeydown = (event: KeyboardEvent) => { 54 if (event.key === 'Escape') handleClose(); 55 }; 56</script> 57 58{#snippet divider()} 59 <div class="h-px bg-gradient-to-r from-(--nucleus-accent) to-(--nucleus-accent2)"></div> 60{/snippet} 61 62{#snippet settingHeader(name: string, desc: string)} 63 <h3 class="mb-3 text-lg font-bold">{name}</h3> 64 <p class="mb-4 text-sm opacity-80">{desc}</p> 65{/snippet} 66 67{#snippet advancedTab()} 68 <div class="space-y-5"> 69 <div> 70 <h3 class="mb-3 text-lg font-bold">api endpoints</h3> 71 <div class="space-y-4"> 72 {#snippet _input(name: string, desc: string)} 73 <div> 74 <label for={name} class="mb-2 block text-sm font-semibold text-(--nucleus-fg)/80"> 75 {desc} 76 </label> 77 <!-- todo: add validation for url --> 78 <input 79 id={name} 80 type="url" 81 bind:value={localSettings.endpoints[name]} 82 placeholder={defaultSettings.endpoints[name]} 83 class="single-line-input border-(--nucleus-accent)/40 bg-(--nucleus-accent)/3" 84 /> 85 </div> 86 {/snippet} 87 {@render _input('slingshot', 'slingshot url (for fetching records & resolving identity)')} 88 {@render _input('spacedust', 'spacedust url (for notifications)')} 89 {@render _input('constellation', 'constellation url (for backlinks)')} 90 </div> 91 </div> 92 93 {@render divider()} 94 95 <div> 96 {@render settingHeader( 97 'cache management', 98 'clears cached data (records, DID documents, handles, etc.)' 99 )} 100 <button onclick={handleClearCache} class="action-button"> clear cache </button> 101 </div> 102 103 {@render divider()} 104 105 <div> 106 {@render settingHeader('reset settings', 'resets all settings to their default values')} 107 <button 108 onclick={handleReset} 109 class="action-button border-red-600 text-red-600 hover:bg-red-600/20" 110 > 111 reset to defaults 112 </button> 113 </div> 114 </div> 115{/snippet} 116 117{#snippet styleTab()} 118 <div class="space-y-5"> 119 <div> 120 <h3 class="mb-3 text-lg font-bold">colors</h3> 121 <div class="space-y-4"> 122 {#snippet color(name: string, desc: string)} 123 <div> 124 <label for={name} class="mb-2 block text-sm font-semibold text-(--nucleus-fg)/80"> 125 {desc} 126 </label> 127 <div class="color-picker"> 128 <ColorPicker 129 bind:hex={localSettings.theme[name]} 130 isAlpha={false} 131 position="responsive" 132 label={localSettings.theme[name]} 133 /> 134 </div> 135 </div> 136 {/snippet} 137 {@render color('fg', 'foreground color')} 138 {@render color('bg', 'background color')} 139 {@render color('accent', 'accent color')} 140 {@render color('accent2', 'secondary accent color')} 141 </div> 142 </div> 143 </div> 144{/snippet} 145 146{#if isOpen} 147 <div 148 class="fixed inset-0 z-50 flex items-center justify-center bg-(--nucleus-bg)/80 p-8 backdrop-blur-sm" 149 onclick={handleClose} 150 onkeydown={handleKeydown} 151 role="button" 152 tabindex="-1" 153 > 154 <!-- svelte-ignore a11y_interactive_supports_focus --> 155 <!-- svelte-ignore a11y_click_events_have_key_events --> 156 <div 157 class="flex h-[600px] w-full max-w-2xl animate-fade-in-scale flex-col rounded-sm border-2 border-(--nucleus-accent) bg-(--nucleus-bg) shadow-2xl transition-all" 158 onclick={(e) => e.stopPropagation()} 159 role="dialog" 160 > 161 <div class="flex items-center gap-4 border-b-2 border-(--nucleus-accent)/20 p-4"> 162 <div> 163 <h2 class="text-2xl font-bold">settings</h2> 164 <div class="mt-2 flex gap-2"> 165 <div class="h-1 w-8 rounded-full bg-(--nucleus-accent)"></div> 166 <div class="h-1 w-9.5 rounded-full bg-(--nucleus-accent2)"></div> 167 </div> 168 </div> 169 {#if hasReloadChanges} 170 <button onclick={handleSave} class="shrink-0 action-button px-6"> save & reload </button> 171 {/if} 172 <div class="grow"></div> 173 <!-- svelte-ignore a11y_consider_explicit_label --> 174 <button 175 onclick={handleClose} 176 class="rounded-xl p-2 text-(--nucleus-fg)/40 transition-all hover:scale-110" 177 > 178 <svg class="h-6 w-6" fill="none" stroke="currentColor" viewBox="0 0 24 24"> 179 <path 180 stroke-linecap="round" 181 stroke-linejoin="round" 182 stroke-width="2.5" 183 d="M6 18L18 6M6 6l12 12" 184 /> 185 </svg> 186 </button> 187 </div> 188 189 <div class="flex-1 overflow-y-auto p-4"> 190 {#if activeTab === 'advanced'} 191 {@render advancedTab()} 192 {:else if activeTab === 'moderation'} 193 <div class="flex h-full items-center justify-center"> 194 <div class="text-center"> 195 <div class="mb-4 text-6xl opacity-50">🚧</div> 196 <h3 class="text-xl font-bold opacity-80">todo</h3> 197 </div> 198 </div> 199 {:else if activeTab === 'style'} 200 {@render styleTab()} 201 {/if} 202 </div> 203 204 <div> 205 <div class="flex"> 206 {#snippet tabButton(name: Tab)} 207 {@const isActive = activeTab === name} 208 <button 209 onclick={() => (activeTab = name)} 210 class="flex-1 border-t-3 px-4 py-3 font-semibold transition-colors hover:cursor-pointer {isActive 211 ? 'border-(--nucleus-accent) bg-(--nucleus-accent)/20 text-(--nucleus-accent)' 212 : 'border-(--nucleus-accent)/20 bg-transparent text-(--nucleus-fg)/60 hover:bg-(--nucleus-accent)/10'}" 213 > 214 {name} 215 </button> 216 {/snippet} 217 {#each ['style', 'moderation', 'advanced'] as Tab[] as tabName (tabName)} 218 {@render tabButton(tabName)} 219 {/each} 220 </div> 221 </div> 222 </div> 223 </div> 224{/if}