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-linear-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}