+42
-81
src/components/AccountSelector.svelte
+42
-81
src/components/AccountSelector.svelte
···
4
4
import type { Handle } from '@atcute/lexicons';
5
5
import ProfilePicture from './ProfilePicture.svelte';
6
6
import PfpPlaceholder from './PfpPlaceholder.svelte';
7
+
import Popup from './Popup.svelte';
7
8
import { flow } from '$lib/at/oauth';
8
9
import { isHandle, type AtprotoDid } from '@atcute/lexicons/syntax';
9
10
import Icon from '@iconify/svelte';
···
45
46
isDropdownOpen = false;
46
47
loginHandle = '';
47
48
loginError = '';
49
+
// HACK: i hate this but it works so it doesnt really matter
50
+
setTimeout(() => document.getElementById('handle')?.focus(), 100);
48
51
};
49
52
50
53
const closeLoginModal = () => {
54
+
document.getElementById('handle')?.blur();
51
55
isLoginModalOpen = false;
52
56
loginHandle = '';
53
57
loginError = '';
···
79
83
};
80
84
81
85
const handleKeydown = (event: KeyboardEvent) => {
82
-
if (event.key === 'Escape') {
83
-
closeLoginModal();
84
-
} else if (event.key === 'Enter' && !isLoggingIn) {
85
-
handleLogin();
86
-
}
86
+
if (event.key === 'Enter' && !isLoggingIn) handleLogin();
87
87
};
88
88
89
89
const closeDropdown = () => {
···
182
182
{/if}
183
183
</div>
184
184
185
-
{#if isLoginModalOpen}
186
-
<div
187
-
class="fixed inset-0 z-50 flex items-center justify-center bg-(--nucleus-bg)/80 backdrop-blur-sm"
188
-
onclick={closeLoginModal}
189
-
onkeydown={handleKeydown}
190
-
role="button"
191
-
tabindex="-1"
192
-
>
193
-
<!-- svelte-ignore a11y_interactive_supports_focus -->
194
-
<!-- svelte-ignore a11y_click_events_have_key_events -->
195
-
<div
196
-
class="w-full max-w-md animate-fade-in-scale rounded-sm border-2 border-(--nucleus-accent) bg-(--nucleus-bg) p-4 shadow-2xl transition-all"
197
-
onclick={(e) => e.stopPropagation()}
198
-
role="dialog"
199
-
>
200
-
<div class="mb-6 flex items-center justify-between">
201
-
<div>
202
-
<h2 class="text-2xl font-bold">add account</h2>
203
-
<div class="mt-2 flex gap-2">
204
-
<div class="h-1 w-10 rounded-full bg-(--nucleus-accent)"></div>
205
-
<div class="h-1 w-9 rounded-full bg-(--nucleus-accent2)"></div>
206
-
</div>
207
-
</div>
208
-
<!-- svelte-ignore a11y_consider_explicit_label -->
209
-
<button
210
-
onclick={closeLoginModal}
211
-
class="rounded-xl p-2 text-(--nucleus-fg)/40 transition-all hover:scale-110 hover:text-(--nucleus-fg)"
212
-
>
213
-
<svg class="h-6 w-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
214
-
<path
215
-
stroke-linecap="round"
216
-
stroke-linejoin="round"
217
-
stroke-width="2.5"
218
-
d="M6 18L18 6M6 6l12 12"
219
-
/>
220
-
</svg>
221
-
</button>
185
+
<Popup bind:isOpen={isLoginModalOpen} onClose={closeLoginModal} title="add account">
186
+
<!-- svelte-ignore a11y_no_static_element_interactions -->
187
+
<div class="space-y-2" onkeydown={handleKeydown}>
188
+
<div>
189
+
<label for="handle" class="mb-2 block text-sm font-semibold text-(--nucleus-fg)/80">
190
+
account handle
191
+
</label>
192
+
<input
193
+
id="handle"
194
+
type="text"
195
+
bind:value={loginHandle}
196
+
placeholder="example.bsky.social"
197
+
class="single-line-input border-(--nucleus-accent)/40 bg-(--nucleus-accent)/3"
198
+
disabled={isLoggingIn}
199
+
/>
200
+
</div>
201
+
202
+
{#if loginError}
203
+
<div class="error-disclaimer">
204
+
<p>
205
+
<Icon class="inline h-10 w-10" icon="heroicons:exclamation-triangle-16-solid" />
206
+
{loginError}
207
+
</p>
222
208
</div>
209
+
{/if}
223
210
224
-
<div class="space-y-5">
225
-
<div>
226
-
<label for="handle" class="mb-2 block text-sm font-semibold text-(--nucleus-fg)/80">
227
-
handle
228
-
</label>
229
-
<input
230
-
id="handle"
231
-
type="text"
232
-
bind:value={loginHandle}
233
-
placeholder="example.bsky.social"
234
-
class="single-line-input border-(--nucleus-accent)/40 bg-(--nucleus-accent)/3"
235
-
disabled={isLoggingIn}
236
-
/>
237
-
</div>
238
-
239
-
{#if loginError}
240
-
<div class="error-disclaimer">
241
-
<p>
242
-
<Icon class="inline h-10 w-10" icon="heroicons:exclamation-triangle-16-solid" />
243
-
{loginError}
244
-
</p>
245
-
</div>
246
-
{/if}
247
-
248
-
<div class="flex gap-3 pt-3">
249
-
<button onclick={closeLoginModal} class="flex-1 action-button" disabled={isLoggingIn}>
250
-
cancel
251
-
</button>
252
-
<button
253
-
onclick={handleLogin}
254
-
class="flex-1 action-button border-transparent text-(--nucleus-fg)"
255
-
style="background: linear-gradient(135deg, var(--nucleus-accent), var(--nucleus-accent2));"
256
-
disabled={isLoggingIn}
257
-
>
258
-
{isLoggingIn ? 'logging in...' : 'login'}
259
-
</button>
260
-
</div>
261
-
</div>
211
+
<div class="flex gap-3 pt-3">
212
+
<button onclick={closeLoginModal} class="flex-1 action-button" disabled={isLoggingIn}>
213
+
cancel
214
+
</button>
215
+
<button
216
+
onclick={handleLogin}
217
+
class="flex-1 action-button border-transparent text-(--nucleus-fg)"
218
+
style="background: linear-gradient(135deg, var(--nucleus-accent), var(--nucleus-accent2));"
219
+
disabled={isLoggingIn}
220
+
>
221
+
{isLoggingIn ? 'logging in...' : 'login'}
222
+
</button>
262
223
</div>
263
224
</div>
264
-
{/if}
225
+
</Popup>
+102
src/components/Popup.svelte
+102
src/components/Popup.svelte
···
1
+
<script lang="ts">
2
+
import type { Snippet } from 'svelte';
3
+
4
+
interface Props {
5
+
isOpen: boolean;
6
+
onClose?: () => void;
7
+
title: string;
8
+
width?: string;
9
+
height?: string;
10
+
padding?: string;
11
+
showHeaderDivider?: boolean;
12
+
headerActions?: Snippet;
13
+
children: Snippet;
14
+
footer?: Snippet;
15
+
}
16
+
17
+
let {
18
+
isOpen = $bindable(false),
19
+
onClose = () => (isOpen = false),
20
+
title,
21
+
width = 'w-full max-w-md',
22
+
height = 'auto',
23
+
padding = 'p-4',
24
+
showHeaderDivider = false,
25
+
headerActions,
26
+
children,
27
+
footer
28
+
}: Props = $props();
29
+
30
+
const handleKeydown = (event: KeyboardEvent) => {
31
+
if (event.key === 'Escape') onClose();
32
+
};
33
+
</script>
34
+
35
+
{#if isOpen}
36
+
<div
37
+
class="fixed inset-0 z-50 flex items-center justify-center bg-(--nucleus-bg)/80 backdrop-blur-sm"
38
+
onclick={onClose}
39
+
onkeydown={handleKeydown}
40
+
role="button"
41
+
tabindex="-1"
42
+
>
43
+
<!-- svelte-ignore a11y_interactive_supports_focus -->
44
+
<!-- svelte-ignore a11y_click_events_have_key_events -->
45
+
<div
46
+
class="flex {height === 'auto'
47
+
? ''
48
+
: 'h-[' +
49
+
height +
50
+
']'} {width} shrink animate-fade-in-scale flex-col rounded-sm border-2 border-(--nucleus-accent) bg-(--nucleus-bg) shadow-2xl transition-all"
51
+
style={height !== 'auto' ? `height: ${height}` : ''}
52
+
onclick={(e) => e.stopPropagation()}
53
+
role="dialog"
54
+
>
55
+
<!-- Header -->
56
+
<div
57
+
class="flex items-center gap-4 {showHeaderDivider
58
+
? 'border-b-2 border-(--nucleus-accent)/20'
59
+
: ''} {padding}"
60
+
>
61
+
<div>
62
+
<h2 class="text-2xl font-bold">{title}</h2>
63
+
<div class="mt-2 flex gap-2">
64
+
<div class="h-1 w-8 rounded-full bg-(--nucleus-accent)"></div>
65
+
<div class="h-1 w-9.5 rounded-full bg-(--nucleus-accent2)"></div>
66
+
</div>
67
+
</div>
68
+
69
+
{#if headerActions}
70
+
{@render headerActions()}
71
+
{/if}
72
+
73
+
<div class="grow"></div>
74
+
75
+
<!-- svelte-ignore a11y_consider_explicit_label -->
76
+
<button
77
+
onclick={onClose}
78
+
class="rounded-xl p-2 text-(--nucleus-fg)/40 transition-all hover:scale-110 hover:text-(--nucleus-fg)"
79
+
>
80
+
<svg class="h-6 w-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
81
+
<path
82
+
stroke-linecap="round"
83
+
stroke-linejoin="round"
84
+
stroke-width="2.5"
85
+
d="M6 18L18 6M6 6l12 12"
86
+
/>
87
+
</svg>
88
+
</button>
89
+
</div>
90
+
91
+
<!-- Content -->
92
+
<div class="{height === 'auto' ? '' : 'flex-1 overflow-y-auto'} {padding}">
93
+
{@render children()}
94
+
</div>
95
+
96
+
<!-- Footer -->
97
+
{#if footer}
98
+
{@render footer()}
99
+
{/if}
100
+
</div>
101
+
</div>
102
+
{/if}
+35
-83
src/components/SettingsPopup.svelte
+35
-83
src/components/SettingsPopup.svelte
···
3
3
import { handleCache, didDocCache, recordCache } from '$lib/at/client';
4
4
import { get } from 'svelte/store';
5
5
import ColorPicker from 'svelte-awesome-color-picker';
6
+
import Popup from './Popup.svelte';
7
+
import Tabs from './Tabs.svelte';
6
8
7
9
interface Props {
8
10
isOpen: boolean;
···
11
13
12
14
let { isOpen = $bindable(false), onClose }: Props = $props();
13
15
14
-
type Tab = 'advanced' | 'moderation' | 'style';
16
+
type Tab = 'style' | 'moderation' | 'advanced';
15
17
let activeTab = $state<Tab>('advanced');
16
18
17
19
let localSettings = $state(get(settings));
···
32
34
33
35
const handleSave = () => {
34
36
settings.set(localSettings);
35
-
// reload to update api endpoints
36
37
window.location.reload();
37
38
};
38
39
···
49
50
recordCache.clear();
50
51
alert('cache cleared!');
51
52
};
52
-
53
-
const handleKeydown = (event: KeyboardEvent) => {
54
-
if (event.key === 'Escape') handleClose();
55
-
};
56
53
</script>
57
54
58
55
{#snippet divider()}
···
74
71
<label for={name} class="mb-2 block text-sm font-semibold text-(--nucleus-fg)/80">
75
72
{desc}
76
73
</label>
77
-
<!-- todo: add validation for url -->
78
74
<input
79
75
id={name}
80
76
type="url"
···
143
139
</div>
144
140
{/snippet}
145
141
146
-
{#if isOpen}
147
-
<div
148
-
class="fixed inset-0 z-50 flex items-center justify-center bg-(--nucleus-bg)/80 p-12 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-[60vh] w-[42vmax] max-w-2xl shrink 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>
142
+
<Popup
143
+
bind:isOpen
144
+
onClose={handleClose}
145
+
title="settings"
146
+
width="w-[42vmax] max-w-2xl"
147
+
height="60vh"
148
+
showHeaderDivider={true}
149
+
>
150
+
{#snippet headerActions()}
151
+
{#if hasReloadChanges}
152
+
<button onclick={handleSave} class="shrink-0 action-button"> save & reload </button>
153
+
{/if}
154
+
{/snippet}
203
155
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>
156
+
{#if activeTab === 'advanced'}
157
+
{@render advancedTab()}
158
+
{:else if activeTab === 'moderation'}
159
+
<div class="flex h-full items-center justify-center">
160
+
<div class="text-center">
161
+
<div class="mb-4 text-6xl opacity-50">🚧</div>
162
+
<h3 class="text-xl font-bold opacity-80">todo</h3>
221
163
</div>
222
164
</div>
223
-
</div>
224
-
{/if}
165
+
{:else if activeTab === 'style'}
166
+
{@render styleTab()}
167
+
{/if}
168
+
169
+
{#snippet footer()}
170
+
<Tabs
171
+
tabs={['style', 'moderation', 'advanced']}
172
+
bind:activeTab
173
+
onTabChange={(tab) => (activeTab = tab)}
174
+
/>
175
+
{/snippet}
176
+
</Popup>
+23
src/components/Tabs.svelte
+23
src/components/Tabs.svelte
···
1
+
<script lang="ts" generics="T extends string">
2
+
interface Props {
3
+
tabs: T[];
4
+
activeTab: T;
5
+
onTabChange: (tab: T) => void;
6
+
}
7
+
8
+
let { tabs, activeTab = $bindable(), onTabChange }: Props = $props();
9
+
</script>
10
+
11
+
<div class="flex">
12
+
{#each tabs as tab (tab)}
13
+
{@const isActive = activeTab === tab}
14
+
<button
15
+
onclick={() => onTabChange(tab)}
16
+
class="flex-1 border-t-3 px-4 py-3 font-semibold transition-colors hover:cursor-pointer {isActive
17
+
? 'border-(--nucleus-accent) bg-(--nucleus-accent)/20 text-(--nucleus-accent)'
18
+
: 'border-(--nucleus-accent)/20 bg-transparent text-(--nucleus-fg)/60 hover:bg-(--nucleus-accent)/10'}"
19
+
>
20
+
{tab}
21
+
</button>
22
+
{/each}
23
+
</div>